384 lines
11 KiB
Vue
384 lines
11 KiB
Vue
<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>
|