ilk
This commit is contained in:
383
ui/src/pages/UserDetail.vue
Normal file
383
ui/src/pages/UserDetail.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<q-page class="user-detail-page">
|
||||
|
||||
<!-- LOADING -->
|
||||
<q-inner-loading :showing="loading">
|
||||
<q-spinner size="48px" />
|
||||
</q-inner-loading>
|
||||
|
||||
<!-- ================= STICKY HEADER ================= -->
|
||||
<div class="sticky-stack">
|
||||
|
||||
<div class="filter-bar row q-col-gutter-md q-mb-sm">
|
||||
<div class="col-3">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Kullanıcı Kodu</div>
|
||||
<q-input v-model="form.code" dense filled />
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Ad Soyad</div>
|
||||
<q-input v-model="form.full_name" dense filled />
|
||||
</div>
|
||||
|
||||
<div class="col-2 flex items-end">
|
||||
<q-toggle v-model="form.is_active" label="Aktif" color="primary" />
|
||||
</div>
|
||||
<q-badge
|
||||
:color="hasPassword ? 'positive' : 'grey'"
|
||||
class="q-ml-sm"
|
||||
>
|
||||
{{ hasPassword ? 'Parola Var' : 'Parola Yok' }}
|
||||
</q-badge>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="save-toolbar">
|
||||
<div class="text-subtitle2 text-weight-bold">
|
||||
{{ pageTitle }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<q-btn
|
||||
:label="saveLabel"
|
||||
color="primary"
|
||||
icon="save"
|
||||
:loading="saving"
|
||||
@click="onSave"
|
||||
/>
|
||||
<q-btn
|
||||
label="LİSTEYE DÖN"
|
||||
flat
|
||||
icon="arrow_back"
|
||||
class="q-ml-sm"
|
||||
@click="goList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= BODY ================= -->
|
||||
<div class="q-pa-md">
|
||||
|
||||
<!-- 🔐 PASSWORD ACTIONS -->
|
||||
<q-card flat bordered class="q-mb-md">
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div>
|
||||
<div class="text-subtitle2 text-weight-bold">Parola İşlemleri</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
Kullanıcıya parola oluşturma / sıfırlama bağlantısı e-posta ile gönderilir.
|
||||
</div>
|
||||
|
||||
<div class="text-caption q-mt-xs">
|
||||
<span class="text-grey-7">E-posta:</span>
|
||||
<span class="text-weight-medium q-ml-xs">{{ form.email || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="lastPasswordMailSentAt" class="text-caption q-mt-xs text-grey-7">
|
||||
Son gönderim: {{ lastPasswordMailSentAt }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row items-center">
|
||||
<q-btn
|
||||
label="PAROLA MAİLİ GÖNDER"
|
||||
color="primary"
|
||||
icon="mail"
|
||||
:disable="!canSendPasswordMail"
|
||||
:loading="sendingPasswordMail"
|
||||
@click="confirmSendPasswordMail"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- USER FORM -->
|
||||
<q-card flat bordered>
|
||||
<q-card-section>
|
||||
<div class="row q-col-gutter-md">
|
||||
|
||||
<!-- EMAIL -->
|
||||
<div class="col-4">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">E-Posta</div>
|
||||
<q-input
|
||||
v-model="form.email"
|
||||
dense
|
||||
filled
|
||||
type="email"
|
||||
:rules="[emailRule]"
|
||||
lazy-rules
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- PHONE -->
|
||||
<div class="col-4">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Telefon</div>
|
||||
<q-input
|
||||
v-model="form.mobile"
|
||||
dense
|
||||
filled
|
||||
placeholder="+90XXXXXXXXXX"
|
||||
mask="+#############"
|
||||
fill-mask
|
||||
:rules="[phoneRule]"
|
||||
lazy-rules
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ADDRESS -->
|
||||
<div class="col-4">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Adres</div>
|
||||
<q-input
|
||||
v-model="form.address"
|
||||
type="textarea"
|
||||
dense
|
||||
filled
|
||||
autogrow
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ROLES -->
|
||||
<div class="col-6">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Roller</div>
|
||||
<q-select
|
||||
v-model="form.roles"
|
||||
:options="roleOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
map-options
|
||||
multiple
|
||||
use-input
|
||||
use-chips
|
||||
dense
|
||||
filled
|
||||
behavior="menu"
|
||||
>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps" clickable>
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="scope.selected"
|
||||
@update:model-value="scope.toggleOption(scope.opt)"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ scope.opt.label }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<!-- DEPARTMENT -->
|
||||
<div class="col-3">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Departman</div>
|
||||
<q-select
|
||||
v-model="form.departments"
|
||||
:options="departmentOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- PIYASALAR -->
|
||||
<div class="col-3">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Piyasalar</div>
|
||||
<q-select
|
||||
v-model="form.piyasalar"
|
||||
:options="piyasaOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
map-options
|
||||
multiple
|
||||
use-input
|
||||
use-chips
|
||||
dense
|
||||
filled
|
||||
behavior="menu"
|
||||
>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps" clickable>
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="scope.selected"
|
||||
@update:model-value="scope.toggleOption(scope.opt)"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ scope.opt.label }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<!-- NEBIM -->
|
||||
<div class="col-12">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">Nebim Kullanıcıları</div>
|
||||
<q-select
|
||||
v-model="form.nebim_users"
|
||||
:options="nebimUserOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserDetailStore } from 'src/stores/UserDetailStore'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const $q = useQuasar()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useUserDetailStore()
|
||||
|
||||
/* 🔒 REAKTİVİTE */
|
||||
const {
|
||||
form,
|
||||
loading,
|
||||
saving,
|
||||
roleOptions,
|
||||
departmentOptions,
|
||||
piyasaOptions,
|
||||
nebimUserOptions,
|
||||
sendingPasswordMail,
|
||||
lastPasswordMailSentAt
|
||||
} = storeToRefs(store)
|
||||
const codeRule = v => !!v || 'Kullanıcı kodu zorunludur'
|
||||
|
||||
/* ================= MODE ================= */
|
||||
const mode = computed(() => route.meta.mode || 'edit')
|
||||
const isNew = computed(() => mode.value === 'new')
|
||||
const isEdit = computed(() => mode.value === 'edit')
|
||||
const isView = computed(() => mode.value === 'view')
|
||||
|
||||
const userId = computed(() => (isEdit.value || isView.value) ? Number(route.params.id) : null)
|
||||
|
||||
const hasPassword = computed(() => store.hasPassword)
|
||||
|
||||
const pageTitle = computed(() => (isNew.value ? 'Yeni Kullanıcı' : 'Kullanıcı Düzenleme'))
|
||||
const saveLabel = computed(() => (isNew.value ? 'KAYDET' : 'GÜNCELLE'))
|
||||
|
||||
/* ================= VALIDATION ================= */
|
||||
const emailRule = v =>
|
||||
!v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Geçerli bir e-posta giriniz'
|
||||
|
||||
const phoneRule = v =>
|
||||
!v || /^\+\d{10,15}$/.test(v.replace(/_/g, '')) || 'Telefon +90XXXXXXXXXX formatında olmalı'
|
||||
|
||||
const canSendPasswordMail = computed(() => {
|
||||
if (isNew.value) return false // önce kullanıcı oluşmalı
|
||||
if (!userId.value) return false
|
||||
if (!form.value.is_active) return false // pasif kullanıcıya mail yok
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((form.value.email || '').trim())
|
||||
})
|
||||
|
||||
/* ================= LIFECYCLE ================= */
|
||||
watch(
|
||||
() => userId.value,
|
||||
async (id) => {
|
||||
await store.fetchLookups()
|
||||
|
||||
if (!id) {
|
||||
store.resetForm()
|
||||
return
|
||||
}
|
||||
|
||||
await store.fetchUser(id)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/* ================= ACTIONS ================= */
|
||||
async function onSave () {
|
||||
try {
|
||||
console.log('🟢 onSave() START', { mode: mode.value })
|
||||
|
||||
if (form.value.mobile) {
|
||||
form.value.mobile = form.value.mobile.replace(/_/g, '').trim()
|
||||
}
|
||||
|
||||
let id
|
||||
|
||||
if (isNew.value) {
|
||||
id = await store.createUser()
|
||||
|
||||
console.log('➡️ CREATE → EDIT MODE id=', id)
|
||||
|
||||
// 🔄 EDIT MODE’A GEÇ
|
||||
router.replace({
|
||||
name: 'user-edit',
|
||||
params: { id }
|
||||
})
|
||||
|
||||
} else {
|
||||
await store.saveUser(userId.value)
|
||||
router.push({ name: 'user-list' })
|
||||
}
|
||||
|
||||
$q.notify({ type: 'positive', message: 'İşlem başarılı' })
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ onSave ERROR', e)
|
||||
$q.notify({ type: 'negative', message: store.error || 'İşlem başarısız' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function goList () {
|
||||
router.push({ name: 'user-list' })
|
||||
}
|
||||
|
||||
|
||||
function confirmSendPasswordMail () {
|
||||
$q.dialog({
|
||||
title: 'Parola maili gönderilsin mi?',
|
||||
message: `${form.value.email} adresine parola oluşturma/sıfırlama bağlantısı gönderilecek.`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(async () => {
|
||||
await sendPasswordMail()
|
||||
})
|
||||
}
|
||||
|
||||
async function sendPasswordMail () {
|
||||
try {
|
||||
await store.sendPasswordMail(userId.value)
|
||||
$q.notify({ type: 'positive', message: 'Parola maili gönderildi' })
|
||||
} catch {
|
||||
$q.notify({ type: 'negative', message: store.error || 'Mail gönderilemedi' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user