This commit is contained in:
2026-02-11 17:46:22 +03:00
commit eacfacb13b
266 changed files with 51337 additions and 0 deletions

383
ui/src/pages/UserDetail.vue Normal file
View 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 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 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>