Files
bssapp/ui/src/pages/UserDetail.vue
2026-02-19 09:32:26 +03:00

497 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<q-page v-if="canAccessPage" 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 :rules="[codeRule]" lazy-rules />
</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
v-if="canSaveUser"
:label="saveLabel"
color="primary"
icon="save"
:loading="saving"
@click="onSave"
/>
<q-btn
v-if="canDeleteThisUser"
label="SIL"
color="negative"
icon="delete"
class="q-ml-sm"
:loading="deleting"
@click="confirmDeleteUser"
/>
<q-btn
v-if="canReadUser"
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
v-if="canUpdateUser"
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 #selected-item="scope">
<q-chip
removable
dense
class="q-mr-xs"
@remove="scope.removeAtIndex(scope.index)"
>
{{ scope.opt.label }}
</q-chip>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox
:model-value="scope.selected"
tabindex="-1"
/>
</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 #before-options>
<q-item clickable @click="selectAllPiyasalar">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable @click="clearPiyasalar">
<q-item-section>Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #selected-item="scope">
<q-chip
removable
dense
class="q-mr-xs"
@remove="scope.removeAtIndex(scope.index)"
>
{{ scope.opt.label }}
</q-chip>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox
:model-value="scope.selected"
tabindex="-1"
@update:model-value="() => scope.toggleOption(scope.opt)"
@click.stop
/>
</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>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu module erisim yetkiniz yok.
</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, canDelete } = usePermission()
const canReadUser = canRead('user')
const canWriteUser = canWrite('user')
const canUpdateUser = canUpdate('user')
const canDeleteUser = canDelete('user')
const $q = useQuasar()
const route = useRoute()
const router = useRouter()
const store = useUserDetailStore()
/* 🔒 REAKTİVİTE */
const {
form,
loading,
saving,
deleting,
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 canAccessPage = computed(() => {
if (isNew.value) return canWriteUser.value
if (isEdit.value) return canUpdateUser.value
return canReadUser.value
})
const canSaveUser = computed(() => isNew.value ? canWriteUser.value : canUpdateUser.value)
const userId = computed(() => (isEdit.value || isView.value) ? Number(route.params.id) : null)
const canDeleteThisUser = computed(() =>
(isEdit.value || isView.value) &&
!!userId.value &&
canDeleteUser.value
)
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())
})
function selectAllPiyasalar () {
form.value.piyasalar = (piyasaOptions.value || [])
.map((o) => o.value)
.filter(Boolean)
}
function clearPiyasalar () {
form.value.piyasalar = []
}
/* ================= LIFECYCLE ================= */
watch(
() => userId.value,
async (id) => {
if (!canAccessPage.value) return
await store.fetchLookups()
if (!id) {
store.resetForm()
return
}
await store.fetchUser(id)
},
{ immediate: true }
)
/* ================= ACTIONS ================= */
async function onSave () {
if (!canSaveUser.value) {
$q.notify({ type: 'negative', message: 'Kaydetme yetkiniz yok' })
return
}
if (!(form.value.code || '').trim()) {
$q.notify({ type: 'negative', message: 'Kullanıcı kodu zorunludur' })
return
}
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 MODEA 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 confirmDeleteUser () {
if (!canDeleteThisUser.value) {
$q.notify({ type: 'negative', message: 'Silme yetkiniz yok' })
return
}
const code = (form.value.code || '').trim()
const label = code || `ID ${userId.value}`
$q.dialog({
title: 'Kullanici silinsin mi?',
message: `${label} kaydi tablodan silinecek. Bu islem geri alinamaz.`,
cancel: true,
persistent: true
}).onOk(async () => {
await deleteUser()
})
}
async function deleteUser () {
try {
await store.deleteUser(userId.value)
$q.notify({ type: 'positive', message: 'Kullanici silindi' })
await router.replace({ name: 'user-list' })
window.location.reload()
} catch {
$q.notify({ type: 'negative', message: store.error || 'Kullanici silinemedi' })
}
}
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>