ilk
This commit is contained in:
341
ui/src/pages/ActivityLogs.vue
Normal file
341
ui/src/pages/ActivityLogs.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<q-page class="act-page with-bg">
|
||||
|
||||
<!-- =======================================================
|
||||
🔍 FILTER BAR
|
||||
======================================================= -->
|
||||
<div class="act-filter-bar">
|
||||
<div class="act-filter-row">
|
||||
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="store.filters.username"
|
||||
label="Kullanıcı"
|
||||
clearable
|
||||
class="act-filter-input"
|
||||
@keyup.enter="store.fetchLogs()"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
dense
|
||||
filled
|
||||
v-model="store.filters.actionCategory"
|
||||
:options="categoryOptions"
|
||||
label="Kategori"
|
||||
clearable
|
||||
emit-value
|
||||
map-options
|
||||
class="act-filter-input"
|
||||
@update:model-value="store.fetchLogs()"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
dense
|
||||
filled
|
||||
v-model="store.filters.actionType"
|
||||
:options="actionTypeOptions"
|
||||
label="Action"
|
||||
clearable
|
||||
emit-value
|
||||
map-options
|
||||
class="act-filter-input"
|
||||
@update:model-value="store.fetchLogs()"
|
||||
/>
|
||||
|
||||
|
||||
<q-select
|
||||
dense
|
||||
filled
|
||||
v-model="store.filters.success"
|
||||
:options="successOptions"
|
||||
label="Sonuç"
|
||||
clearable
|
||||
emit-value
|
||||
map-options
|
||||
class="act-filter-input"
|
||||
@update:model-value="store.fetchLogs()"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
type="date"
|
||||
v-model="store.filters.dateFrom"
|
||||
label="Başlangıç"
|
||||
class="act-filter-input"
|
||||
@update:model-value="store.fetchLogs()"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
type="date"
|
||||
v-model="store.filters.dateTo"
|
||||
label="Bitiş"
|
||||
class="act-filter-input"
|
||||
@update:model-value="store.fetchLogs()"
|
||||
/>
|
||||
|
||||
<div class="act-filter-actions">
|
||||
<q-btn color="primary" unelevated label="Ara" @click="store.fetchLogs()" />
|
||||
<q-btn flat label="Temizle" @click="store.resetFilters()" />
|
||||
|
||||
<!-- ✅ YENİ -->
|
||||
<q-btn
|
||||
outline
|
||||
color="secondary"
|
||||
label="Rol Değişimleri"
|
||||
@click="store.quickRoleChange()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
📊 LOG TABLE
|
||||
======================================================= -->
|
||||
<q-table
|
||||
class="act-table sticky-table"
|
||||
row-key="created_at"
|
||||
:rows="store.rows"
|
||||
:columns="columns"
|
||||
:loading="store.loading"
|
||||
binary-state-sort
|
||||
>
|
||||
|
||||
|
||||
<!-- ================= CHANGE DIFF MODAL ================= -->
|
||||
<q-dialog v-model="diffDialog">
|
||||
<q-card style="min-width:700px">
|
||||
|
||||
<q-card-section class="text-h6">
|
||||
Rol Değişiklik Detayı
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
|
||||
<div class="row q-col-gutter-md">
|
||||
|
||||
<div class="col-6">
|
||||
<div class="text-bold q-mb-sm">Önce</div>
|
||||
|
||||
<q-banner class="bg-grey-2 text-black">
|
||||
<pre>{{ selectedDiff.before }}</pre>
|
||||
</q-banner>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<div class="text-bold q-mb-sm">Sonra</div>
|
||||
|
||||
<q-banner class="bg-green-1 text-black">
|
||||
<pre>{{ selectedDiff.after }}</pre>
|
||||
</q-banner>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Kapat" v-close-popup />
|
||||
</q-card-actions>
|
||||
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Zaman -->
|
||||
<template #body-cell-created_at="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.row.created_at) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- Route -->
|
||||
<template #body-cell-action_target="props">
|
||||
<q-td :props="props" class="act-col-route">
|
||||
{{ props.row.action_target }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- HTTP -->
|
||||
<template #body-cell-http_status="props">
|
||||
<q-td :props="props">
|
||||
<q-badge
|
||||
v-if="props.row.http_status"
|
||||
:label="props.row.http_status"
|
||||
:class="props.row.http_status < 300 ? 'act-badge-ok' : 'act-badge-fail'"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- ✅ Diff -->
|
||||
<template #body-cell-diff="props">
|
||||
<q-td :props="props">
|
||||
<q-btn
|
||||
v-if="props.row.change_before || props.row.change_after"
|
||||
dense
|
||||
flat
|
||||
color="primary"
|
||||
icon="compare_arrows"
|
||||
@click="openDiff(props.row)"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- Sonuç -->
|
||||
<template #body-cell-is_success="props">
|
||||
<q-td :props="props">
|
||||
<q-badge
|
||||
:label="props.row.is_success ? 'OK' : 'FAIL'"
|
||||
:class="props.row.is_success ? 'act-badge-ok' : 'act-badge-fail'"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="full-width row flex-center q-pa-md text-grey-6">
|
||||
Log kaydı bulunamadı.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</q-table>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref,onMounted, watch } from 'vue'
|
||||
import { date } from 'quasar'
|
||||
import { useActivityLogStore } from 'src/stores/activityLogStore'
|
||||
import { useAuthStore } from 'stores/authStore.js'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const diffDialog = ref(false)
|
||||
|
||||
const selectedDiff = ref({
|
||||
before: '',
|
||||
after: ''
|
||||
})
|
||||
const store = useActivityLogStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const categoryOptions = [
|
||||
{ label: 'Auth', value: 'auth' },
|
||||
{ label: 'Navigation', value: 'nav' },
|
||||
{ label: 'Yetkilendirme (User)', value: 'user_permission' },
|
||||
{ label: 'Yetkilendirme (Role)', value: 'role_permission' },
|
||||
{ label: 'Genel Yetki', value: 'permission' }
|
||||
]
|
||||
const actionTypeOptions = [
|
||||
|
||||
// USER
|
||||
{
|
||||
label: 'User Permission Change',
|
||||
value: 'user_permission_change'
|
||||
},
|
||||
|
||||
// ROLE
|
||||
{
|
||||
label: 'Role Permission Change',
|
||||
value: 'permission_change'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Role + Dept Change',
|
||||
value: 'role_department_permission_change'
|
||||
},
|
||||
|
||||
// AUTH
|
||||
{
|
||||
label: 'Login',
|
||||
value: 'login'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Logout',
|
||||
value: 'logout'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const successOptions = [
|
||||
{ label: 'Başarılı', value: true },
|
||||
{ label: 'Hatalı', value: false }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ name: 'created_at', label: 'Zaman', field: 'created_at', sortable: true },
|
||||
{ name: 'username', label: 'İşlemi Yapan', field: 'username', sortable: true },
|
||||
{ name: 'target_username', label: 'Hedef Kullanıcı', field: 'target_username' },
|
||||
{ name: 'role_code', label: 'Rol', field: 'role_code' },
|
||||
{ name: 'action_category', label: 'Kategori', field: 'action_category' },
|
||||
{ name: 'action_type', label: 'Action', field: 'action_type' },
|
||||
{ name: 'action_target', label: 'Route', field: 'action_target' },
|
||||
{ name: 'http_status', label: 'HTTP', field: 'http_status' },
|
||||
{ name: 'duration_ms', label: 'Süre (ms)', field: 'duration_ms' },
|
||||
|
||||
// 🔥 field önemli değil ama name = diff şart
|
||||
{ name: 'diff', label: 'Değişiklik' },
|
||||
|
||||
{ name: 'is_success', label: 'Sonuç', field: 'is_success' }
|
||||
]
|
||||
|
||||
function formatDate(v) {
|
||||
if (!v) return '-'
|
||||
return date.formatDate(v, 'YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
🔐 TOKEN HAZIR OLDUĞUNDA LOG ÇEK
|
||||
======================================================= */
|
||||
function safeFetch() {
|
||||
if (auth.isAuthenticated && auth.token) {
|
||||
store.fetchLogs()
|
||||
}
|
||||
}
|
||||
function openDiff (row) {
|
||||
selectedDiff.value = {
|
||||
before: pretty(row.change_before),
|
||||
after: pretty(row.change_after)
|
||||
}
|
||||
|
||||
diffDialog.value = true
|
||||
}
|
||||
|
||||
function pretty (v) {
|
||||
if (!v) return '-'
|
||||
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(v), null, 2)
|
||||
} catch {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
safeFetch()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => auth.token,
|
||||
(token) => {
|
||||
if (token) {
|
||||
safeFetch()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
120
ui/src/pages/ChangePassword.vue
Normal file
120
ui/src/pages/ChangePassword.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
|
||||
<q-card style="width:420px; max-width:90vw">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-weight-bold">🔐 Şifre Değiştir</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
Mevcut şifrenizi girerek yeni şifre belirleyin
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="current"
|
||||
type="password"
|
||||
label="Mevcut Şifre"
|
||||
dense filled
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
label="Yeni Şifre"
|
||||
dense filled
|
||||
class="q-mt-sm"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="password2"
|
||||
type="password"
|
||||
label="Yeni Şifre (Tekrar)"
|
||||
dense filled
|
||||
class="q-mt-sm"
|
||||
/>
|
||||
|
||||
<q-banner
|
||||
v-if="error"
|
||||
class="bg-red-1 text-red q-mt-md"
|
||||
>
|
||||
{{ error }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="GÜNCELLE"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disable="!canSubmit"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { useAuthStore } from 'stores/authStore.js'
|
||||
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 auth = useAuthStore()
|
||||
|
||||
const current = ref('')
|
||||
const password = ref('')
|
||||
const password2 = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
current.value &&
|
||||
password.value.length >= 8 &&
|
||||
password.value === password2.value &&
|
||||
!loading.value
|
||||
)
|
||||
|
||||
/* =========================================================
|
||||
🔐 ŞİFRE DEĞİŞTİR
|
||||
========================================================= */
|
||||
async function submit () {
|
||||
error.value = null
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await api.post('/password/change', {
|
||||
current_password: current.value,
|
||||
new_password: password.value
|
||||
})
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Şifre güncellendi'
|
||||
})
|
||||
|
||||
current.value = ''
|
||||
password.value = ''
|
||||
password2.value = ''
|
||||
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err?.message ||
|
||||
'Şifre değiştirilemedi'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
9
ui/src/pages/Dashboard.vue
Normal file
9
ui/src/pages/Dashboard.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<p>DashBoard</p>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// buraya JS kodların gelecek
|
||||
</script>
|
||||
27
ui/src/pages/ErrorNotFound.vue
Normal file
27
ui/src/pages/ErrorNotFound.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
|
||||
<div>
|
||||
<div style="font-size: 30vh">
|
||||
404
|
||||
</div>
|
||||
|
||||
<div class="text-h2" style="opacity:.4">
|
||||
Oops. Nothing here...
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="q-mt-xl"
|
||||
color="white"
|
||||
text-color="blue"
|
||||
unelevated
|
||||
to="/"
|
||||
label="Go Home"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
120
ui/src/pages/FirstPasswordChange.vue
Normal file
120
ui/src/pages/FirstPasswordChange.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<q-card style="width:420px; max-width:90vw">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Şifre Yenileme Zorunlu</div>
|
||||
<div class="text-caption text-grey-7 q-mt-xs">
|
||||
Sistemi kullanabilmek için yeni bir şifre belirlemelisiniz.
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-gutter-md">
|
||||
<q-input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
label="Mevcut Şifre"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="newPassword"
|
||||
type="password"
|
||||
label="Yeni Şifre"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="newPassword2"
|
||||
type="password"
|
||||
label="Yeni Şifre (Tekrar)"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
|
||||
<q-banner
|
||||
v-if="error"
|
||||
class="bg-red-1 text-red q-mt-sm"
|
||||
rounded
|
||||
>
|
||||
{{ error }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="Kaydet"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import api from 'src/services/api'
|
||||
import { useAuthStore } from 'stores/authStore.js'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const newPassword2 = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
async function submit () {
|
||||
error.value = ''
|
||||
|
||||
if (!currentPassword.value || !newPassword.value || !newPassword2.value) {
|
||||
error.value = 'Tüm alanlar zorunludur'
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.value !== newPassword2.value) {
|
||||
error.value = 'Yeni şifreler eşleşmiyor'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 🔐 TOKEN interceptor ile otomatik
|
||||
const res = await api.post('/password/change', {
|
||||
current_password: currentPassword.value,
|
||||
new_password: newPassword.value
|
||||
})
|
||||
|
||||
// 🔄 Session güncelle
|
||||
auth.setSession(res.data)
|
||||
auth.forcePasswordChange = false
|
||||
localStorage.setItem('forcePasswordChange', '0')
|
||||
|
||||
router.replace('/app')
|
||||
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e?.data?.message ||
|
||||
e?.message ||
|
||||
'Şifre güncellenemedi'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
306
ui/src/pages/MainPage.vue
Normal file
306
ui/src/pages/MainPage.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center login-bg">
|
||||
|
||||
<q-card class="q-pa-lg shadow-4 login-card">
|
||||
<!-- HEADER -->
|
||||
<q-card-section class="text-center">
|
||||
<q-avatar size="80px" class="bg-white text-secondary shadow-2">
|
||||
<q-icon name="lock" size="40px" />
|
||||
</q-avatar>
|
||||
<div class="login-title q-mt-sm">Kullanıcı Girişi</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- FORM -->
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="username"
|
||||
label="Kullanıcı Adı"
|
||||
dense
|
||||
standout="bg-white"
|
||||
class="q-mb-md custom-input"
|
||||
autocomplete="username"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
label="Şifre"
|
||||
dense
|
||||
standout="bg-white"
|
||||
class="custom-input"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<div class="q-mt-md row items-center justify-between">
|
||||
<div>
|
||||
<q-checkbox
|
||||
v-model="rememberUser"
|
||||
label="Kullanıcıyı hatırla"
|
||||
color="secondary"
|
||||
dense
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="rememberPass"
|
||||
label="Parolayı kaydet"
|
||||
color="secondary"
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
color="primary"
|
||||
label="Şifremi Unuttum"
|
||||
@click="forgotOpen = true"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- ACTION -->
|
||||
<q-card-actions align="center">
|
||||
<q-btn
|
||||
label="Giriş Yap"
|
||||
color="primary"
|
||||
glossy
|
||||
unelevated
|
||||
icon="login"
|
||||
class="full-width"
|
||||
:loading="loading"
|
||||
@click="login"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
<!-- 🔐 FORGOT PASSWORD -->
|
||||
<q-dialog v-model="forgotOpen" persistent>
|
||||
<q-card style="width:420px; max-width:90vw">
|
||||
<q-card-section class="text-h6">
|
||||
Parola Sıfırlama
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div class="text-caption text-grey-7 q-mb-sm">
|
||||
Kullanıcı adınızı girin.
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
v-model="forgotUsername"
|
||||
label="Kullanıcı Adı"
|
||||
dense
|
||||
outlined
|
||||
:disable="forgotLoading"
|
||||
/>
|
||||
|
||||
<q-banner
|
||||
v-if="forgotMessage"
|
||||
class="q-mt-md"
|
||||
:class="forgotSuccess ? 'bg-green-1 text-green' : 'bg-red-1 text-red'"
|
||||
rounded
|
||||
>
|
||||
{{ forgotMessage }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Vazgeç" v-close-popup />
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Gönder"
|
||||
:loading="forgotLoading"
|
||||
@click="sendResetMail"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
import { useQuasar } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const $q = useQuasar()
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const rememberUser = ref(false)
|
||||
const rememberPass = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
/* 🔐 Forgot password */
|
||||
const forgotOpen = ref(false)
|
||||
const forgotUsername = ref('')
|
||||
const forgotLoading = ref(false)
|
||||
const forgotMessage = ref('')
|
||||
const forgotSuccess = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
if (localStorage.getItem('remember_user') === 'true') {
|
||||
username.value = localStorage.getItem('username') || ''
|
||||
rememberUser.value = true
|
||||
}
|
||||
if (localStorage.getItem('remember_pass') === 'true') {
|
||||
password.value = localStorage.getItem('password') || ''
|
||||
rememberPass.value = true
|
||||
}
|
||||
})
|
||||
|
||||
/* =========================================================
|
||||
🔐 LOGIN
|
||||
========================================================= */
|
||||
async function login () {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await auth.login(username.value, password.value)
|
||||
|
||||
// remember checks
|
||||
rememberUser.value
|
||||
? localStorage.setItem('username', username.value)
|
||||
: localStorage.removeItem('username')
|
||||
|
||||
rememberPass.value
|
||||
? localStorage.setItem('password', password.value)
|
||||
: localStorage.removeItem('password')
|
||||
|
||||
localStorage.setItem('remember_user', rememberUser.value ? 'true' : 'false')
|
||||
localStorage.setItem('remember_pass', rememberPass.value ? 'true' : 'false')
|
||||
|
||||
// 🔥 YÖNLENDİRME
|
||||
if (auth.mustChangePassword) {
|
||||
router.replace('/first-password-change')
|
||||
} else {
|
||||
router.replace('/app')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Login error:', err)
|
||||
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Kullanıcı adı veya şifre hatalı',
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
auth.clearSession()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
🔐 FORGOT PASSWORD
|
||||
========================================================= */
|
||||
async function sendResetMail () {
|
||||
if (!forgotUsername.value) return
|
||||
|
||||
forgotLoading.value = true
|
||||
forgotMessage.value = ''
|
||||
|
||||
try {
|
||||
await api.post('/password/forgot', {
|
||||
email: forgotUsername.value
|
||||
})
|
||||
|
||||
forgotSuccess.value = true
|
||||
forgotMessage.value =
|
||||
'Eğer hesabınız aktif ise parola sıfırlama bağlantısı e-posta adresinize gönderilmiştir.'
|
||||
} catch {
|
||||
// 👈 bilgi sızdırmamak için bilinçli olarak aynı mesaj
|
||||
forgotSuccess.value = true
|
||||
forgotMessage.value =
|
||||
'Eğer hesabınız aktif ise parola sıfırlama bağlantısı e-posta adresinize gönderilmiştir.'
|
||||
} finally {
|
||||
forgotLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.login-bg {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
.login-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: url("/images/Baggi-Fabrika-resmi.jpg") no-repeat center;
|
||||
background-size: cover;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.login-bg > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
border-radius: 16px;
|
||||
background: var(--q-secondary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--q-primary);
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
background: #fdfcfc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
/* ===============================
|
||||
LOGIN INPUT TEXT COLOR (GOLD)
|
||||
=============================== */
|
||||
|
||||
/* Input içindeki yazı */
|
||||
.login-card :deep(.q-field__native),
|
||||
.login-card :deep(.q-field__input) {
|
||||
color: var(--q-primary) !important; /* gold */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Placeholder rengi */
|
||||
.login-card :deep(.q-field__native::placeholder) {
|
||||
color: rgba(149, 113, 22, 0.6); /* gold soft */
|
||||
}
|
||||
|
||||
/* Label rengi */
|
||||
.login-card :deep(.q-field__label) {
|
||||
color: var(--q-primary);
|
||||
}
|
||||
|
||||
/* Focus olunca */
|
||||
.login-card :deep(.q-field--focused .q-field__native) {
|
||||
color: var(--q-primary);
|
||||
}
|
||||
|
||||
/* Autofill (Chrome sarı arkaplanı bastırmak için) */
|
||||
.login-card :deep(input:-webkit-autofill) {
|
||||
-webkit-text-fill-color: var(--q-primary) !important;
|
||||
transition: background-color 9999s ease-in-out 0s;
|
||||
}
|
||||
|
||||
</style>
|
||||
9
ui/src/pages/MainPanel.vue
Normal file
9
ui/src/pages/MainPanel.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<p>cari hesap tabloları</p>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// buraya JS kodların gelecek
|
||||
</script>
|
||||
107
ui/src/pages/MePassword.vue
Normal file
107
ui/src/pages/MePassword.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<q-card style="width:420px; max-width:90vw">
|
||||
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-weight-bold">🔐 Şifre Değiştir</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
Mevcut şifrenizi girerek yeni şifre belirleyin
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<q-input
|
||||
v-model="current"
|
||||
type="password"
|
||||
label="Mevcut Şifre"
|
||||
dense filled
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
label="Yeni Şifre"
|
||||
dense filled
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="password2"
|
||||
type="password"
|
||||
label="Yeni Şifre (Tekrar)"
|
||||
dense filled
|
||||
/>
|
||||
|
||||
<q-banner
|
||||
v-if="error"
|
||||
class="bg-red-1 text-red q-mt-sm"
|
||||
rounded
|
||||
>
|
||||
{{ error }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="GÜNCELLE"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disable="!canSubmit"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useMePasswordStore } from 'stores/mePasswordStore'
|
||||
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 store = useMePasswordStore()
|
||||
|
||||
const current = ref('')
|
||||
const password = ref('')
|
||||
const password2 = ref('')
|
||||
const error = ref(null)
|
||||
|
||||
const loading = computed(() => store.loading)
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
current.value &&
|
||||
password.value.length >= 8 &&
|
||||
password.value === password2.value &&
|
||||
!loading.value
|
||||
)
|
||||
|
||||
async function submit () {
|
||||
error.value = null
|
||||
|
||||
if (!canSubmit.value) return
|
||||
|
||||
try {
|
||||
await store.changePassword(current.value, password.value)
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Şifre başarıyla güncellendi',
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
current.value = password.value = password2.value = ''
|
||||
} catch {
|
||||
error.value = store.error
|
||||
}
|
||||
}
|
||||
</script>
|
||||
2987
ui/src/pages/OrderEntry.vue
Normal file
2987
ui/src/pages/OrderEntry.vue
Normal file
@@ -0,0 +1,2987 @@
|
||||
<template>
|
||||
<!-- ===========================================================
|
||||
🧾 ORDER ENTRY PAGE (BSSApp)
|
||||
v23 — Sticky-stack + Drawer uyumlu yapı
|
||||
============================================================ -->
|
||||
<q-page class="order-page">
|
||||
<!-- 🔄 SAYFA LOADERI -->
|
||||
<q-inner-loading :showing="loadingHeader || loadingCari || loadingModels" color="primary">
|
||||
<q-spinner size="50px" />
|
||||
</q-inner-loading>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 STICKY STACK (Filter + Save + Header)
|
||||
======================================================== -->
|
||||
<div class="sticky-stack">
|
||||
|
||||
<!-- 🔸 1. Satır: Filtre Bar -->
|
||||
<div class="filter-bar row q-col-gutter-md q-mb-sm">
|
||||
|
||||
<!-- 🧾 Cari Seçimi -->
|
||||
<div class="col-5">
|
||||
<q-select
|
||||
v-model="form.CurrAccCode"
|
||||
:options="filteredCariOptions"
|
||||
label="Cari Seçimi"
|
||||
filled
|
||||
use-input
|
||||
input-debounce="300"
|
||||
emit-value
|
||||
map-options
|
||||
option-value="Cari_Kod"
|
||||
:option-label="opt => `${opt.Cari_Kod} - ${opt.Cari_Ad}`"
|
||||
@filter="filterCari"
|
||||
@update:model-value="onCariChange"
|
||||
:loading="loadingCari"
|
||||
:disable="isEditMode || isClosedOrder || isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
clearable
|
||||
|
||||
>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.Cari_Ad }}</q-item-label>
|
||||
<q-item-label caption>{{ scope.opt.Cari_Kod }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<!-- 🔢 Sipariş No -->
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
v-model="form.OrderNumber"
|
||||
label="Sipariş No"
|
||||
filled
|
||||
dense
|
||||
:disable="isEditMode || isClosedOrder || isViewOnly"
|
||||
|
||||
:readonly="isViewOnly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 📅 Oluşturulma Tarihi -->
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
:model-value="formatDateInput(form.OrderDate)"
|
||||
label="Oluşturulma Tarihi"
|
||||
type="date"
|
||||
filled
|
||||
dense
|
||||
@update:model-value="v => form.OrderDate = v"
|
||||
:disable="isEditMode || isClosedOrder || isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 📅 Tahmini Termin Tarihi (AverageDueDate kilitlenmeyecek) -->
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
:model-value="formatDateInput(form.AverageDueDate)"
|
||||
label="Tahmini Termin Tarihi"
|
||||
type="date"
|
||||
filled
|
||||
dense
|
||||
@update:model-value="v => form.AverageDueDate = v"
|
||||
:readonly="isViewOnly"
|
||||
:disable="isViewOnly"
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 💰 TOPLAM TUTAR + KDV -->
|
||||
<div class="col-12 row q-col-gutter-sm q-mt-xs items-center">
|
||||
<!-- 💰 Toplam Tutar -->
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
:model-value="Number(orderStore.totalAmount || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 })"
|
||||
label="Toplam Tutar"
|
||||
readonly
|
||||
>
|
||||
<template #append>{{ form.pb }}</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<!-- 🔘 KDV Checkbox -->
|
||||
<div class="col-auto flex items-center">
|
||||
<q-checkbox
|
||||
v-model="form.includeVat"
|
||||
label="KDV Dahil"
|
||||
color="primary"
|
||||
@update:model-value="onVatToggle"
|
||||
:disable="isClosedRow||isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ⚙️ KDV ALANLARI: sadece tikliyken görünür -->
|
||||
<template v-if="form.includeVat">
|
||||
<!-- % oran sadece bilgi -->
|
||||
<div class="col-1">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
:model-value="form.vatRate"
|
||||
label="%"
|
||||
readonly
|
||||
>
|
||||
<template #append>%</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<!-- 🧮 KDV Tutarı (manuel düzenlenebilir) -->
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="form.vatAmountInput"
|
||||
label="KDV Tutarı"
|
||||
@update:model-value="onVatAmountChange"
|
||||
input-class="text-right"
|
||||
:disable="isClosedRow || isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
>
|
||||
<template #append>{{ form.pb }}</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<!-- 🧾 KDV Dahil Toplam -->
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
:model-value="Number(form.totalWithVat || 0).toLocaleString('tr-TR',{minimumFractionDigits:2})"
|
||||
label="KDV Dahil Toplam"
|
||||
readonly
|
||||
>
|
||||
<template #append>{{ form.pb }}</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<!-- 🔹 Cari Bilgi Barı -->
|
||||
<q-slide-transition>
|
||||
<div
|
||||
v-if="cariInfo"
|
||||
class="row q-col-gutter-md q-mt-xs cari-info-bar"
|
||||
>
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
:model-value="cariInfo.Musteri_Temsilcisi || '-'"
|
||||
label="Müşteri Temsilcisi"
|
||||
filled dense readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
:model-value="cariInfo.Musteri_Ana_Grubu || '-'"
|
||||
label="Ana Grup"
|
||||
filled dense readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
:model-value="cariInfo.Piyasa || '-'"
|
||||
label="Piyasa"
|
||||
filled dense readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
:model-value="cariInfo.Ulke || '-'"
|
||||
label="Ülke"
|
||||
filled dense readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
|
||||
<!-- 🔹 Save Toolbar -->
|
||||
<div class="save-toolbar">
|
||||
<div class="text-subtitle2 text-weight-bold">Sipariş Formu</div>
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="isViewOnly"
|
||||
label="🖨 SİPARİŞİ YAZDIR"
|
||||
color="primary"
|
||||
icon="print"
|
||||
class="q-ml-sm"
|
||||
@click="orderStore.downloadOrderPdf()"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
|
||||
color="primary"
|
||||
icon="save"
|
||||
class="q-ml-sm"
|
||||
:loading="orderStore.loading"
|
||||
@click="confirmAndSubmit"
|
||||
/>
|
||||
<q-btn
|
||||
label="YENİ SİPARİŞ"
|
||||
color="secondary"
|
||||
icon="add_circle"
|
||||
class="q-ml-sm"
|
||||
@click="resetEditor"
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 Grid Header -->
|
||||
<div class="order-grid-header">
|
||||
<div class="col-fixed model">MODEL</div>
|
||||
<div class="col-fixed renk">RENK</div>
|
||||
<div class="col-fixed ana">ÜRÜN ANA<br />GRUBU</div>
|
||||
<div class="col-fixed alt">ÜRÜN ALT<br />GRUBU</div>
|
||||
<div class="col-fixed aciklama-col">AÇIKLAMA</div>
|
||||
|
||||
<div class="beden-block">
|
||||
<div
|
||||
v-for="grp in (
|
||||
Object.keys(orderStore?.schemaMap || {}).length
|
||||
? Object.values(orderStore.schemaMap)
|
||||
: Object.values(storeSchemaByKey)
|
||||
)"
|
||||
:key="grp.key"
|
||||
class="grp-row"
|
||||
:class="{ 'hl-pan': grp.key === 'pan' && highlightPantolon }"
|
||||
>
|
||||
|
||||
<div class="grp-title">{{ grp.title }}</div>
|
||||
<div class="grp-body">
|
||||
<div
|
||||
v-for="v in (grp.values || [])"
|
||||
:key="'b-' + grp.key + '-' + v"
|
||||
class="grp-cell hdr"
|
||||
>
|
||||
{{ v }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="total-row">
|
||||
<div class="total-cell">ADET</div>
|
||||
<div class="total-cell">FİYAT</div>
|
||||
<div class="total-cell">PB</div>
|
||||
<div class="total-cell">TUTAR</div>
|
||||
<div class="total-cell">Tahmini Gönderim Tarihi</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- =======================================================
|
||||
🔹 GRID BODY (Final Stabil) + EDITOR aynı scroll’da
|
||||
======================================================== -->
|
||||
<div class="order-scroll-y"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll -->
|
||||
<div class="order-grid-body">
|
||||
<template v-for="grp in groupedRows" :key="grp.name">
|
||||
<div :class="['summary-group', grp.open ? 'open' : 'closed']">
|
||||
|
||||
<!-- 🟡 Sub-header -->
|
||||
<div class="order-sub-header" @click="toggleGroup(grp.name)">
|
||||
<div class="sub-left">{{ grp.name }}</div>
|
||||
|
||||
<div class="sub-center">
|
||||
<div
|
||||
v-for="v in (
|
||||
orderStore.schemaMap?.[grp.grpKey]?.values
|
||||
|| storeSchemaByKey?.[grp.grpKey]?.values
|
||||
|| []
|
||||
)"
|
||||
:key="'hdr-' + grp.grpKey + '-' + v"
|
||||
class="beden-cell"
|
||||
>
|
||||
{{ v }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-right">
|
||||
|
||||
|
||||
<div class="order-text-caption">
|
||||
Toplam {{ grp.name }} Adet: {{ grp.toplamAdet }}
|
||||
</div>
|
||||
<div class="order-text-caption">
|
||||
Toplam {{ grp.name }} Tutar:
|
||||
{{ Number(grp.toplamTutar || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
|
||||
{{ form.pb || aktifPB }}
|
||||
</div>
|
||||
<q-icon
|
||||
:name="grp.open ? 'expand_less' : 'expand_more'"
|
||||
size="20px"
|
||||
class="cursor-pointer text-grey-8 q-ml-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🧩 Grup satırları -->
|
||||
<template v-if="grp.open">
|
||||
<div
|
||||
v-for="row in grp.rows"
|
||||
:key="rowKey(row)"
|
||||
class="summary-row"
|
||||
:data-clientkey="row.clientKey"
|
||||
:class="{
|
||||
active: orderStore.editingKey === rowKey(row),
|
||||
'is-editing': orderStore.editingKey === rowKey(row),
|
||||
'row-closed': row.isClosed,
|
||||
'row-error': row._error
|
||||
}"
|
||||
@click="!row.isClosed && !isViewOnly && editRow(row)"
|
||||
>
|
||||
|
||||
<!-- 🔴 HATA İKONU (SADECE HATALI SATIRDA) -->
|
||||
<q-icon
|
||||
v-if="row._error"
|
||||
name="error"
|
||||
color="negative"
|
||||
size="18px"
|
||||
class="q-mr-sm row-error-icon"
|
||||
>
|
||||
<q-tooltip>
|
||||
{{ row._error.message }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<!-- Sol kolonlar -->
|
||||
<div class="cell model">{{ row.model }}</div>
|
||||
<div class="cell renk">
|
||||
{{ row.renk }}{{ row.renk2 ? '-' + row.renk2 : '' }}
|
||||
</div>
|
||||
<div class="cell ana">{{ row.urunAnaGrubu }}</div>
|
||||
<div class="cell alt">{{ row.urunAltGrubu }}</div>
|
||||
<div class="cell aciklama">{{ row.aciklama }}</div>
|
||||
|
||||
<!-- Beden kolonları -->
|
||||
<div class="grp-area">
|
||||
<div class="grp-row">
|
||||
<div
|
||||
v-for="v in (
|
||||
(orderStore.schemaMap?.[row.grpKey]?.values) ||
|
||||
(storeSchemaByKey[row.grpKey]?.values) ||
|
||||
(storeSchemaByKey.tak.values)
|
||||
)"
|
||||
|
||||
:key="'val-' + v"
|
||||
class="cell beden"
|
||||
>
|
||||
{{ resolveBedenValue(row.bedenMap, row.grpKey, v) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="i2 in (
|
||||
16 -
|
||||
(
|
||||
(orderStore.schemaMap?.[row.grpKey]?.values?.length) ||
|
||||
(storeSchemaByKey[row.grpKey]?.values?.length) ||
|
||||
(storeSchemaByKey.tak.values.length)
|
||||
)
|
||||
)"
|
||||
|
||||
:key="'empty-' + i2"
|
||||
class="cell beden ghost"
|
||||
></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sağ kolonlar -->
|
||||
<div class="cell adet">{{ row.adet }}</div>
|
||||
<div class="cell fiyat">{{ row.fiyat }}</div>
|
||||
<div class="cell pb">{{ row.pb }}</div>
|
||||
<div class="cell tutar">
|
||||
{{ Number(row.tutar || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
|
||||
</div>
|
||||
|
||||
<!-- 🗓 Termin Tarihi -->
|
||||
<div class="cell termin">
|
||||
<div class="termin-label text-center">
|
||||
{{ formatDate(row.terminTarihi) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- =======================================================
|
||||
🔹 SATIR DÜZENLEYİCİ FORM (EDITOR)
|
||||
======================================================== -->
|
||||
<div class="editor q-mt-lg q-pa-sm">
|
||||
|
||||
<!-- 🔸 1. Satır: Model ve Ürün Bilgileri -->
|
||||
<div class="row q-col-gutter-sm q-mb-sm">
|
||||
<div class="col-3">
|
||||
<!-- 🔹 Model Seçimi -->
|
||||
<q-select
|
||||
v-model="form.model"
|
||||
:options="filteredModelOptions"
|
||||
label="Model"
|
||||
filled dense
|
||||
use-input input-debounce="250"
|
||||
emit-value map-options
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
clearable behavior="menu"
|
||||
hint="Model kodu ile arayabilirsiniz"
|
||||
:loading="loadingModels"
|
||||
:disable=" isClosedRow||isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
@filter="filterModel"
|
||||
@update:model-value="(val) => useComboWatcher('model', onModelChange)(val)"
|
||||
/>
|
||||
|
||||
<!-- 🔹 1. Renk Seçimi -->
|
||||
<div class="q-mt-sm">
|
||||
<q-select
|
||||
ref="renkSelect"
|
||||
v-model="form.renk"
|
||||
:options="renkOptions"
|
||||
label="Renk"
|
||||
filled dense clearable
|
||||
emit-value map-options
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
:disable="isClosedRow||isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
@update:model-value="(val) => useComboWatcher('renk', onColorChange)(val)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 2. Renk Seçimi -->
|
||||
<div class="q-mt-sm">
|
||||
<q-select
|
||||
ref="renk2Select"
|
||||
v-model="form.renk2"
|
||||
:options="renkOptions2"
|
||||
label="2. Renk"
|
||||
filled dense clearable
|
||||
emit-value map-options
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
:disable="!renkOptions2.length || isEditing || isClosedRow"
|
||||
@update:model-value="(val) => useComboWatcher('renk2', onColor2Change)(val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ürün teknik alanları -->
|
||||
<div class="col-2">
|
||||
<q-input v-model="form.urunAnaGrubu" label="Ürün Ana Grubu" filled dense readonly />
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-input v-model="form.urunAltGrubu" label="Alt Grup" filled dense readonly />
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-input v-model="form.fit" label="Fit" filled dense readonly />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-input v-model="form.urunIcerik" label="İçerik" filled dense readonly />
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-input v-model="form.drop" label="Drop" filled dense readonly />
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-input v-model="form.askiliyan" label="ASKILI/YAN" filled dense readonly />
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-input v-model="form.kategori" label="Kategori" filled dense readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🔸 2. Satır: Seri Seçimi -->
|
||||
<div class="row q-col-gutter-sm q-mt-xs">
|
||||
<div class="col-3">
|
||||
<q-select
|
||||
ref="seriSelect"
|
||||
v-show="Array.isArray(activeSeriesOptions) && activeSeriesOptions.length > 0"
|
||||
v-model="selectedSeriSet"
|
||||
:options="activeSeriesOptions"
|
||||
label="Beden Seti Seç"
|
||||
filled dense
|
||||
emit-value map-options
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-2 q-mt-sm">
|
||||
<q-input
|
||||
v-if="selectedSeriSet"
|
||||
v-model.number="seriMultiplier"
|
||||
type="number"
|
||||
label="Çarpan"
|
||||
min="1"
|
||||
filled dense
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-2 q-mt-sm">
|
||||
<q-btn
|
||||
v-if="selectedSeriSet"
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="Seri Ekle"
|
||||
@click="applySeriSet"
|
||||
:disable="isClosedRow || isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 BEDEN GİRİŞ ALANI + STOK ETİKETİ GÖRÜNÜMÜ
|
||||
======================================================== -->
|
||||
<div class="row q-mt-sm q-col-gutter-xs beden-grid">
|
||||
<div
|
||||
v-for="(lbl, i) in form.bedenLabels || []"
|
||||
:key="'beden-'+i"
|
||||
class="col-auto beden-wrap"
|
||||
>
|
||||
<div class="beden-label">{{ lbl }}</div>
|
||||
|
||||
<q-input
|
||||
v-model.number="form.bedenler[i]"
|
||||
dense outlined type="number" min="0"
|
||||
style="width:60px"
|
||||
@focus="activeBeden = i"
|
||||
@blur="activeBeden = null"
|
||||
@update:model-value="updateTotals(form)"
|
||||
:class="{ 'beden-active': activeBeden === i }"
|
||||
:disable="isClosedRow||isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="getStockFor(lbl) !== null"
|
||||
class="stok-label text-caption text-center q-mt-xs"
|
||||
:class="stockColorClass(getStockFor(lbl))"
|
||||
>
|
||||
Stok: {{ getStockFor(lbl) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 🔹 Aktif beden için küçük stok etiketi -->
|
||||
<div
|
||||
v-if="form.model && activeBeden !== null && getStockFor(form.bedenLabels[activeBeden]) !== null"
|
||||
class="stok-label-sm"
|
||||
:class="stockColorClass(getStockFor(form.bedenLabels[activeBeden]))"
|
||||
>
|
||||
Stok: {{ getStockFor(form.bedenLabels[activeBeden]) }}
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 ADET / FİYAT / PB / TUTAR
|
||||
======================================================== -->
|
||||
<div class="row q-mt-sm q-col-gutter-sm">
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
v-model.number="form.adet"
|
||||
label="Adet"
|
||||
dense
|
||||
filled
|
||||
readonly
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
v-model.number="form.fiyat"
|
||||
label="Fiyat"
|
||||
dense
|
||||
filled
|
||||
type="number"
|
||||
min="0"
|
||||
@update:model-value="() => updateTotals(form)"
|
||||
:disable="isClosedRow||isViewOnly"
|
||||
:readonly="isViewOnly"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-select
|
||||
v-model="form.pb"
|
||||
:options="paraBirimOptions"
|
||||
label="PB"
|
||||
dense
|
||||
filled
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
v-model="form.tutar"
|
||||
label="Tutar"
|
||||
dense
|
||||
filled
|
||||
readonly
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 SATIR BAZINDA TAHMİNİ TERMİN TARİHİ
|
||||
======================================================== -->
|
||||
<div class="row q-mt-sm">
|
||||
<div class="col-4">
|
||||
<q-input
|
||||
v-model="form.terminTarihi"
|
||||
type="date"
|
||||
label="Tahmini Termin Tarihi"
|
||||
filled
|
||||
dense
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 AÇIKLAMA ALANI
|
||||
======================================================== -->
|
||||
<div class="row q-mt-sm">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="form.aciklama"
|
||||
label="Açıklama"
|
||||
type="textarea"
|
||||
filled
|
||||
dense
|
||||
autogrow
|
||||
maxlength="1500"
|
||||
counter
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- =======================================================
|
||||
🔹 BUTONLAR (Kaydet / Güncelle / Sil / Temizle)
|
||||
======================================================== -->
|
||||
<div class="row justify-between items-center q-mt-md">
|
||||
<div class="row q-gutter-sm">
|
||||
<q-btn
|
||||
:color="isEditing ? 'positive' : 'primary'"
|
||||
:label="isEditing ? 'Güncelle' : 'Kaydet'"
|
||||
@click="onSaveOrUpdateRow"
|
||||
:disable="isClosedRow || isViewOnly"
|
||||
|
||||
/>
|
||||
<q-btn
|
||||
v-if="isEditing"
|
||||
color="negative"
|
||||
flat
|
||||
label="Satırı Sil"
|
||||
@click="removeSelected"
|
||||
:disable="isClosedRow || isViewOnly"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
color="grey-8"
|
||||
label="Formu Temizle"
|
||||
@click="resetEditor"
|
||||
:disable="isClosedRow||isViewOnly"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 ALT BİLGİLENDİRME ALANI
|
||||
======================================================== -->
|
||||
<div class="q-mt-md text-caption text-grey-7 text-center">
|
||||
<q-icon name="info" size="16px" class="q-mr-xs" />
|
||||
Bu sayfada yapılan siparişler henüz gönderilmemiştir.
|
||||
<br />
|
||||
<span class="text-negative">"Tümünü Kaydet (Toplu Gönder)"</span>
|
||||
butonuna basarak işlemleri kaydedebilirsiniz.
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 SİPARİŞ GENEL AÇIKLAMASI
|
||||
======================================================== -->
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="form.Description"
|
||||
type="textarea"
|
||||
label="Sipariş Genel Açıklaması"
|
||||
filled
|
||||
dense
|
||||
autogrow
|
||||
maxlength="1500"
|
||||
counter
|
||||
placeholder="Siparişe genel açıklama giriniz (örn. teslimat, üretim notu, müşteri isteği...)"
|
||||
:disable="isClosedRow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- editor -->
|
||||
</div> <!-- ✅ order-scroll-y -->
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/* ===========================================================
|
||||
🧩 ORDER ENTRY (v22–Final) — Setup Başlangıcı
|
||||
=========================================================== */
|
||||
|
||||
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick, toRaw } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useRoute, useRouter, onBeforeRouteLeave} from 'vue-router'
|
||||
import { useOrderEntryStore,schemaByKey as storeSchemaByKey,detectBedenGroup} from 'src/stores/orderentryStore'
|
||||
import dayjs from 'dayjs'
|
||||
import api from 'src/services/api.js'
|
||||
import { useAuthStore } from 'src/stores/authStore'
|
||||
import { formatDateInput, formatDateDisplay } from 'src/utils/formatters'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
// script setup içinde
|
||||
const formatDate = formatDateDisplay
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 GLOBAL TANIMLAR VE ROUTE BİLGİLERİ
|
||||
=========================================================== */
|
||||
const $q = useQuasar()
|
||||
const orderStore = useOrderEntryStore()
|
||||
const orderentryStore = useOrderEntryStore()
|
||||
orderStore.initSchemaMap()
|
||||
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const isClosedOrder = computed(() => !!orderStore.hasClosedLines)
|
||||
|
||||
// 🔹 Param: Artık sadece :orderHeaderID kullanıyoruz
|
||||
const orderHeaderID = computed(() => route.params.orderHeaderID || null)
|
||||
console.log('🧩 Route parametresi alındı (orderHeaderID):', orderHeaderID.value)
|
||||
|
||||
|
||||
|
||||
const routeMode = computed(() => resolveMode())
|
||||
|
||||
|
||||
|
||||
// ✅ Pinia store: siparişler, localStorage, API çağrıları
|
||||
const auth = useAuthStore()
|
||||
const isViewOnly = computed(() => orderStore.mode === 'view')
|
||||
|
||||
|
||||
console.log('🧩 Route parametresi alındı (setup başında):', orderHeaderID.value)
|
||||
|
||||
// 🔹 Genel reaktif değişkenler
|
||||
const aktifPB = ref('USD') // Varsayılan para birimi (Cari seçimiyle değişebilir)
|
||||
// 🔹 Model detayları cache (product-detail API verilerini tutar)
|
||||
const productCache = reactive({})
|
||||
const confirmAndSubmit = async () => {
|
||||
if (orderStore.loading) return
|
||||
|
||||
// Grid boşsa
|
||||
if (!orderStore.summaryRows?.length) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Kaydedilecek satır yok'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// NEW veya EDIT ayrımı store.mode üzerinden
|
||||
await orderStore.submitAllReal(
|
||||
$q,
|
||||
router,
|
||||
form,
|
||||
summaryRows,
|
||||
productCache
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('❌ confirmAndSubmit hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🗓️ SİPARİŞ TARİHLERİ — Varsayılan Değerler
|
||||
Oluşturulma tarihi = bugünün tarihi
|
||||
Tahmini termin tarihi = bugünden + 5 hafta (35 gün)
|
||||
=========================================================== */
|
||||
const today = new Date()
|
||||
const Termindate = new Date(today)
|
||||
Termindate.setDate(Termindate.getDate() + 35) // +5 hafta
|
||||
const defaultOlusturmaTarihi = today.toISOString().substring(0, 10)
|
||||
const defaultTerminTarihi = Termindate.toISOString().substring(0, 10)
|
||||
|
||||
const isEditMode = computed(() => orderStore.mode === 'edit')
|
||||
|
||||
/* ===========================================================
|
||||
🔹 FORM NESNESİ — TEMEL ALANLAR
|
||||
=========================================================== */
|
||||
const form = reactive({
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 TEMEL ALANLAR
|
||||
// ----------------------------------------------------------
|
||||
OrderHeaderID: '', // string (GUID)
|
||||
OrderTypeCode: 1, // int8
|
||||
ProcessCode: 'WS', // string
|
||||
OrderNumber: '', // string
|
||||
OrderTime: dayjs().format('HH:mm:ss'),
|
||||
IsCancelOrder: false,
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 ADRES / REFERANS
|
||||
// ----------------------------------------------------------
|
||||
BillingPostalAddressID: '',
|
||||
GuarantorContactID: '',
|
||||
ApplicationCode: '',
|
||||
ApplicationID: '',
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 TARİH / AÇIKLAMA
|
||||
// ----------------------------------------------------------
|
||||
OrderDate: dayjs().format('YYYY-MM-DD'),
|
||||
AverageDueDate: dayjs().add(30, 'day').format('YYYY-MM-DD'),
|
||||
Description: '',
|
||||
InternalDescription: '',
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 CARİ BİLGİLERİ
|
||||
// ----------------------------------------------------------
|
||||
CurrAccTypeCode: 1,
|
||||
|
||||
CurrAccCode: '',
|
||||
CurrAccDescription: '',
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 PARA BİRİMİ
|
||||
// ----------------------------------------------------------
|
||||
DocCurrencyCode: 'USD',
|
||||
LocalCurrencyCode: 'TRY',
|
||||
ExchangeRate: 1,
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 DURUM ALANLARI
|
||||
// ----------------------------------------------------------
|
||||
IsCreditSale: true,
|
||||
IsCreditableConfirmed: false,
|
||||
IsSalesViaInternet: false,
|
||||
IsSuspended: false,
|
||||
IsCompleted: false,
|
||||
IsPrinted: false,
|
||||
IsLocked: false,
|
||||
IsClosed: false,
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 KULLANICI VE TARİH
|
||||
// ----------------------------------------------------------
|
||||
CreatedUserName: '',
|
||||
CreatedDate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
LastUpdatedUserName: '',
|
||||
LastUpdatedDate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
CreditableConfirmedUser: '',
|
||||
CreditableConfirmedDate: '',
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 SABİT / EK ALANLAR
|
||||
// ----------------------------------------------------------
|
||||
DocumentNumber: '',
|
||||
PaymentTerm: '',
|
||||
SubCurrAccID: '',
|
||||
ShipmentMethodCode: '',
|
||||
ContactID: '',
|
||||
ShippingPostalAddressID: '',
|
||||
GuarantorContactID2: '',
|
||||
RoundsmanCode: '',
|
||||
DeliveryCompanyCode: '',
|
||||
TaxTypeCode: '',
|
||||
WithHoldingTaxTypeCode: '',
|
||||
DOVCode: '',
|
||||
TaxExemptionCode: 0,
|
||||
CompanyCode: 1,
|
||||
OfficeCode: 101,
|
||||
StoreTypeCode: 5,
|
||||
StoreCode: 0,
|
||||
POSTerminalID: 0,
|
||||
WarehouseCode: '1-0-12',
|
||||
ToWarehouseCode: '',
|
||||
OrdererCompanyCode: 1,
|
||||
OrdererOfficeCode: 101,
|
||||
OrdererStoreCode: '',
|
||||
GLTypeCode: '',
|
||||
TDisRate1: 0,
|
||||
TDisRate2: 0,
|
||||
TDisRate3: 0,
|
||||
TDisRate4: 0,
|
||||
TDisRate5: 0,
|
||||
DiscountReasonCode: 0,
|
||||
SurplusOrderQtyToleranceRate: 0,
|
||||
ImportFileNumber: '',
|
||||
ExportFileNumber: '',
|
||||
IncotermCode1: '',
|
||||
IncotermCode2: '',
|
||||
LettersOfCreditNumber: '',
|
||||
PaymentMethodCode: '',
|
||||
IsIncludedVat: 0,
|
||||
UserLocked: 0,
|
||||
IsProposalBased: 0,
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 ÜRÜN / RENK / BEDEN
|
||||
// ----------------------------------------------------------
|
||||
model: '',
|
||||
renk: '',
|
||||
renk2: '',
|
||||
urunAnaGrubu: '',
|
||||
urunAltGrubu: '',
|
||||
fit: '',
|
||||
urunIcerik: '',
|
||||
drop: '',
|
||||
kategori: '',
|
||||
askiliyan: '',
|
||||
seri: '',
|
||||
bedenLabels: [],
|
||||
bedenler: [],
|
||||
adet: 0,
|
||||
fiyat: 0,
|
||||
pb: aktifPB.value,
|
||||
tutar: 0,
|
||||
aciklama: '',
|
||||
minFiyat: 0,
|
||||
kur: 1,
|
||||
minFiyatTRY: 0,
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 🔸 TARİHLER
|
||||
// ----------------------------------------------------------
|
||||
olusturmaTarihi: defaultOlusturmaTarihi,
|
||||
tahminiTerminTarihi: defaultTerminTarihi,
|
||||
terminTarihi: defaultTerminTarihi,
|
||||
includeVat: false, // 🔹 KDV dahil mi (q-toggle)
|
||||
vatRate: 10, // 🔹 yüzde (gösterge + manuel değiştirilebilir)
|
||||
subtotal: 0, // 🔹 KDV hariç tutar
|
||||
vatAmount: 0, // 🔹 hesaplanan KDV tutarı
|
||||
totalWithVat: 0 , // 🔹 toplam (KDV dahil)
|
||||
vatAmountInput: '', // 🟢 KDV manuel giriş buffer
|
||||
|
||||
})
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 REFS & COMPUTEDS (Grid + Edit Mode)
|
||||
=========================================================== */
|
||||
const summaryRows = computed(() => orderStore.summaryRows)
|
||||
// ✅ Tek kaynak: editingKey
|
||||
const isEditing = computed(() => !!orderStore.editingKey)
|
||||
const rowKey = (row) => row?.clientKey || row?.id || row?.OrderLineID
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const activeBeden = ref(null)
|
||||
const isClosedRow = computed(() => {
|
||||
const row = selectedRow.value
|
||||
return row?.isClosed === true
|
||||
})
|
||||
|
||||
|
||||
const selectedCari = ref(null)
|
||||
|
||||
/* ===========================================================
|
||||
🔹 EDITOR SEÇİMLERİ — Seri / Renk / Kategori setleri
|
||||
=========================================================== */
|
||||
const selectedSeriSet = ref(null)
|
||||
const seriMultiplier = ref(1)
|
||||
/** ----------------------------------------------------------
|
||||
* REQUIRED REFS (Mutlaka Tanımlı Olmalı)
|
||||
* ---------------------------------------------------------- */
|
||||
|
||||
/* ===========================================================
|
||||
🔹 CARI / MODEL / RENK YÜKLEYİCİ REFLERİ — DOĞRU SIRA
|
||||
=========================================================== */
|
||||
const loadingHeader = ref(true)
|
||||
const loadingCari = ref(true)
|
||||
const loadingModels = ref(true)
|
||||
/* ===========================================================
|
||||
🔹 CARİ INFO STATE
|
||||
=========================================================== */
|
||||
const cariInfo = ref(null)
|
||||
|
||||
// Cari listeleri
|
||||
const cariOptions = ref([])
|
||||
const filteredCariOptions = ref([])
|
||||
|
||||
|
||||
// Model listeleri
|
||||
const modelOptions = ref([])
|
||||
const filteredModelOptions = ref([])
|
||||
|
||||
|
||||
// RENK SELECTLER — 🔥 MUTLAKA BURADA OLMALI
|
||||
const renkSelect = ref(null) // <= BUNLAR EKSİKSE hata alırsın
|
||||
const renk2Select = ref(null)
|
||||
|
||||
const renkOptions = ref([])
|
||||
const renkOptions2 = ref([])
|
||||
|
||||
// Mode senkronu
|
||||
orderStore.mode = routeMode.value
|
||||
function resolveBedenValue(bedenMap, grpKey, v) {
|
||||
if (!bedenMap || !grpKey) return ''
|
||||
|
||||
const map = bedenMap[grpKey]
|
||||
if (!map) return ''
|
||||
|
||||
// 🔴 AKSBİR / boş beden KESİNLİKLE normalize edilmez
|
||||
if (v === ' ') {
|
||||
return map[' '] ?? ''
|
||||
}
|
||||
|
||||
// 🔹 Diğer bedenler normal akış
|
||||
return map[v] ?? ''
|
||||
}
|
||||
|
||||
|
||||
async function resetEditor(force = false) {
|
||||
console.log('🧹 resetEditor', { force, editingKey: orderStore.editingKey })
|
||||
|
||||
// 🔒 Edit varken reset yok
|
||||
if (!force && orderStore.editingKey) {
|
||||
console.log('⛔ resetEditor iptal (edit mode)')
|
||||
return
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 🔓 EDIT STATE RESET
|
||||
// ============================
|
||||
orderStore.editingKey = null
|
||||
orderStore.selected = null
|
||||
|
||||
// ============================
|
||||
// 🧼 FORM — TAM TEMİZ
|
||||
// ============================
|
||||
Object.assign(form, {
|
||||
model: '',
|
||||
renk: '',
|
||||
renk2: '',
|
||||
urunAnaGrubu: '',
|
||||
urunAltGrubu: '',
|
||||
kategori: '',
|
||||
aciklama: '',
|
||||
fit: '',
|
||||
urunIcerik: '',
|
||||
drop: '',
|
||||
askiliyan: '',
|
||||
|
||||
adet: 0,
|
||||
fiyat: 0,
|
||||
tutar: 0,
|
||||
|
||||
// ❌ BEDEN ŞEMASI TAMAMEN SIFIR
|
||||
grpKey: null,
|
||||
bedenLabels: [],
|
||||
bedenler: []
|
||||
})
|
||||
|
||||
// ============================
|
||||
// 🧹 UI STATE TEMİZLİĞİ
|
||||
// ============================
|
||||
selectedSeriSet.value = null
|
||||
seriMultiplier.value = 1
|
||||
|
||||
bedenStock.value = []
|
||||
stockMap.value = {}
|
||||
|
||||
renkOptions.value = []
|
||||
renkOptions2.value = []
|
||||
|
||||
await nextTick()
|
||||
|
||||
console.log('✅ resetEditor tamamlandı (BEDEN ŞEMASI YOK)')
|
||||
}
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔴 ROW ERROR — COMPONENT SCOPE (FIXED)
|
||||
- summaryRows computed yazımı yok
|
||||
- tek kaynak: store action
|
||||
=========================================================== */
|
||||
function applyRowError(err) {
|
||||
const key = err?.clientKey
|
||||
if (!key) return
|
||||
|
||||
// ✅ store üzerinden set et (tek kaynak)
|
||||
if (typeof orderStore.setRowErrorByClientKey === 'function') {
|
||||
orderStore.setRowErrorByClientKey(key, {
|
||||
code: err?.code,
|
||||
message: err?.message
|
||||
})
|
||||
} else {
|
||||
// fallback (action yoksa) — yine de computed'a yazmıyoruz, store array mutate ediyoruz
|
||||
const row = orderStore.summaryRows?.find(r => r?.clientKey === key)
|
||||
if (row) {
|
||||
row._error = { code: err?.code, message: err?.message }
|
||||
}
|
||||
}
|
||||
|
||||
scrollToRow(key)
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 applyTerminToRows (FIXED)
|
||||
- store.summaryRows reassign YOK
|
||||
- tek kaynak: store action
|
||||
=========================================================== */
|
||||
function applyTerminToRows(dateStr) {
|
||||
if (!dateStr) return
|
||||
|
||||
if (typeof orderStore.applyTerminToRowsIfEmpty === 'function') {
|
||||
orderStore.applyTerminToRowsIfEmpty(dateStr)
|
||||
return
|
||||
}
|
||||
|
||||
// fallback (action yoksa) — reassign yok, sadece mutate
|
||||
const rows = orderStore.summaryRows
|
||||
if (!Array.isArray(rows)) return
|
||||
for (const r of rows) {
|
||||
if (!r?.terminTarihi || r.terminTarihi === '') {
|
||||
r.terminTarihi = dateStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
✅ selectedRow (FIXED)
|
||||
- editingIndex yok
|
||||
- tek kaynak: orderStore.editingKey
|
||||
=========================================================== */
|
||||
const selectedRow = computed(() => {
|
||||
const key = orderStore.editingKey
|
||||
if (!key) return null
|
||||
|
||||
const rows = orderStore.summaryRows
|
||||
if (!Array.isArray(rows)) return null
|
||||
|
||||
// 🔑 store.getRowKey varsa onu kullan
|
||||
if (typeof orderStore.getRowKey === 'function') {
|
||||
return rows.find(r => orderStore.getRowKey(r) === key) || null
|
||||
}
|
||||
|
||||
// fallback: clientKey || OrderLineID
|
||||
return rows.find(r => (r?.clientKey || r?.OrderLineID) === key) || null
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🧩 GROUPED ROWS — FIXED (bedenValues bug fixed)
|
||||
- schemaMap tek kaynak
|
||||
- en geniş beden seti kazanır
|
||||
- aksbir özel kural korunur
|
||||
=========================================================== */
|
||||
/* ===========================================================
|
||||
🧩 GROUPED ROWS — FINAL & SAFE
|
||||
-----------------------------------------------------------
|
||||
✔ grpKey SADECE row.grpKey
|
||||
✔ schemaMap tek kaynak
|
||||
✔ detectBedenGroup YOK
|
||||
=========================================================== */
|
||||
const groupOpen = reactive({})
|
||||
|
||||
const groupedRows = computed(() => {
|
||||
const rows = Array.isArray(summaryRows.value) ? summaryRows.value : []
|
||||
const buckets = {}
|
||||
const order = []
|
||||
|
||||
const schemaMap =
|
||||
orderStore.schemaMap && typeof orderStore.schemaMap === 'object'
|
||||
? orderStore.schemaMap
|
||||
: storeSchemaByKey
|
||||
|
||||
for (const row of rows) {
|
||||
const ana = (row?.urunAnaGrubu || 'GENEL')
|
||||
.toUpperCase()
|
||||
.trim()
|
||||
|
||||
if (!buckets[ana]) {
|
||||
buckets[ana] = {
|
||||
name: ana,
|
||||
rows: [],
|
||||
toplamAdet: 0,
|
||||
toplamTutar: 0,
|
||||
open: groupOpen[ana] ?? true,
|
||||
|
||||
// 🔑 TEK KAYNAK
|
||||
grpKey: row.grpKey
|
||||
}
|
||||
order.push(ana)
|
||||
}
|
||||
|
||||
const bucket = buckets[ana]
|
||||
bucket.rows.push(row)
|
||||
bucket.toplamAdet += Number(row.adet || 0)
|
||||
bucket.toplamTutar += Number(row.tutar || 0)
|
||||
}
|
||||
|
||||
return order.map(name => {
|
||||
const grp = buckets[name]
|
||||
const schema = schemaMap?.[grp.grpKey]
|
||||
|
||||
return {
|
||||
...grp,
|
||||
bedenValues: schema?.values || []
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
✏️ GRID SATIR DÜZENLEME — editRow (Final v4 — IsClosed Safe)
|
||||
-----------------------------------------------------------
|
||||
- Kapalı satır (row.isClosed === true) → düzenlemeye izin yok
|
||||
- UI’da row.isClosed class ile gri görünür
|
||||
- Kullanıcı tıklasa bile edit mode açılmaz
|
||||
=========================================================== */
|
||||
function toDateOnly(v) {
|
||||
if (!v) return ''
|
||||
// '2025-10-27 00:00:00' → '2025-10-27'
|
||||
if (typeof v === 'string' && v.includes(' ')) {
|
||||
return v.split(' ')[0]
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 🧮 KDV Hesaplama Fonksiyonları
|
||||
|
||||
/* ===========================================================
|
||||
🧮 KDV HESAPLAMA — FINAL (X3)
|
||||
- Tek kaynak: orderStore.totalAmount
|
||||
- Tek hesap fonksiyonu: recalcVat
|
||||
=========================================================== */
|
||||
|
||||
const subtotal = computed(() => Number(orderStore.totalAmount || 0))
|
||||
/* ===========================================================
|
||||
🔹 KDV TOGGLE HANDLER (SAFE)
|
||||
=========================================================== */
|
||||
const onVatToggle = (val) => {
|
||||
form.includeVat = !!val
|
||||
recalcVat()
|
||||
}
|
||||
/* ===========================================================
|
||||
🧮 recalcVat — FINAL
|
||||
- Tek kaynak: orderStore.totalAmount
|
||||
- Manuel KDV girişini destekler
|
||||
=========================================================== */
|
||||
function recalcVat() {
|
||||
const baseTotal = Number(orderStore.totalAmount || 0)
|
||||
const rate = Number(form.vatRate || 0) / 100
|
||||
|
||||
// 🔹 KDV dahil değilse
|
||||
if (!form.includeVat) {
|
||||
form.subtotal = baseTotal
|
||||
form.vatAmount = 0
|
||||
form.vatAmountInput = ''
|
||||
form.totalWithVat = baseTotal
|
||||
return
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
🔹 MANUEL KDV VAR MI?
|
||||
--------------------------------------------------------- */
|
||||
let vatAmt = 0
|
||||
|
||||
if (form.vatAmountInput !== '' && form.vatAmountInput != null) {
|
||||
vatAmt = Number(
|
||||
String(form.vatAmountInput).replace(',', '.')
|
||||
)
|
||||
vatAmt = isNaN(vatAmt) ? 0 : vatAmt
|
||||
} else {
|
||||
vatAmt = Number((baseTotal * rate).toFixed(2))
|
||||
}
|
||||
|
||||
const totalWithVat = Number((baseTotal + vatAmt).toFixed(2))
|
||||
|
||||
form.subtotal = baseTotal
|
||||
form.vatAmount = vatAmt
|
||||
form.totalWithVat = totalWithVat
|
||||
}
|
||||
|
||||
|
||||
// Kullanıcı KDV tutarını manuel girdiyse true
|
||||
const vatManualMode = ref(false)
|
||||
|
||||
/* -----------------------------------------------------------
|
||||
Yardımcılar
|
||||
----------------------------------------------------------- */
|
||||
function toNumberTR(val) {
|
||||
const cleaned = String(val ?? '').replace(',', '.').trim()
|
||||
const n = parseFloat(cleaned)
|
||||
return isNaN(n) ? 0 : n
|
||||
}
|
||||
|
||||
function clampRate(val) {
|
||||
const n = Number(val)
|
||||
if (isNaN(n) || n < 0) return 0
|
||||
if (n > 100) return 100
|
||||
return n
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
✅ ROUTE FLOW — SINGLE SOURCE OF TRUTH (FINAL)
|
||||
=========================================================== */
|
||||
|
||||
/* -------------------- MODE HELPERS -------------------- */
|
||||
function isInvalidId(id) {
|
||||
return !id || ["new", "0", "null", "undefined"].includes(id)
|
||||
}
|
||||
|
||||
function isGuid(id) {
|
||||
return typeof id === "string" &&
|
||||
/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(id)
|
||||
}
|
||||
|
||||
function resolveMode() {
|
||||
const qm = String(route.query.mode || "").toLowerCase()
|
||||
const id = String(orderHeaderID.value || "")
|
||||
if (["edit", "view", "new"].includes(qm)) return qm
|
||||
if (!isInvalidId(id) && isGuid(id)) return "edit"
|
||||
return "new"
|
||||
}
|
||||
|
||||
/* -------------------- INTERNAL STATE -------------------- */
|
||||
const routeBusy = ref(false)
|
||||
const lastRouteSignature = ref("")
|
||||
let autosaveTimer = null
|
||||
let beforeUnloadHandler = null
|
||||
let resizeHandler = null
|
||||
/* ===========================================================
|
||||
🚫 BEFOREUNLOAD (Browser Close / Refresh)
|
||||
-----------------------------------------------------------
|
||||
✔ NEW → taslak KORUNUR, uyarı var
|
||||
✔ EDIT → snapshot KORUNUR, uyarı var
|
||||
✔ VIEW → ASLA uyarı yok
|
||||
✔ isControlledSubmit → ASLA uyarı yok
|
||||
✔ allowRouteLeaveOnce → ASLA uyarı yok
|
||||
❌ burada snapshot SİLİNMEZ (kritik)
|
||||
=========================================================== */
|
||||
|
||||
function installBeforeUnloadGuard() {
|
||||
clearBeforeUnload()
|
||||
|
||||
// 👁 VIEW MODE → hiç guard kurma
|
||||
if (orderStore.mode === 'view') return
|
||||
|
||||
beforeUnloadHandler = (e) => {
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🔒 Kontrollü submit (kaydet / gönder)
|
||||
→ browser close uyarısı YOK
|
||||
-------------------------------------------------------- */
|
||||
if (orderStore.isControlledSubmit) return
|
||||
|
||||
/* -------------------------------------------------------
|
||||
✅ Programatik yönlendirme sonrası 1 kere bypass
|
||||
(listeye dön, replace vs.)
|
||||
-------------------------------------------------------- */
|
||||
if (orderStore.allowRouteLeaveOnce) return
|
||||
|
||||
/* -------------------------------------------------------
|
||||
💾 Değişiklik yok → sessiz çık
|
||||
-------------------------------------------------------- */
|
||||
if (!orderStore.hasUnsavedChanges) return
|
||||
|
||||
/* -------------------------------------------------------
|
||||
⚠️ UYARI (NEW + EDIT için ortak)
|
||||
❗ Snapshot SİLME YOK
|
||||
❗ LocalStorage'a DOKUNMA YOK
|
||||
-------------------------------------------------------- */
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler)
|
||||
}
|
||||
|
||||
/* -------------------- CLEANUP -------------------- */
|
||||
function clearAutosave() {
|
||||
if (autosaveTimer) {
|
||||
clearInterval(autosaveTimer)
|
||||
autosaveTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function clearBeforeUnload() {
|
||||
if (beforeUnloadHandler) {
|
||||
window.removeEventListener("beforeunload", beforeUnloadHandler)
|
||||
beforeUnloadHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
function installGuards() {
|
||||
clearBeforeUnload()
|
||||
clearAutosave()
|
||||
|
||||
// 👁 View ise hiçbir guard yok
|
||||
if (orderStore.mode === 'view') return
|
||||
|
||||
/* ---------------- BEFOREUNLOAD ---------------- */
|
||||
installBeforeUnloadGuard()
|
||||
|
||||
/* ---------------- AUTOSAVE ---------------- */
|
||||
autosaveTimer = setInterval(() => {
|
||||
orderStore.persistLocalStorage?.()
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* -------------------- MAIN ROUTE ENGINE -------------------- */
|
||||
/* -------------------- MAIN ROUTE ENGINE -------------------- */
|
||||
|
||||
async function runRouteFlow() {
|
||||
const id = String(orderHeaderID.value || "")
|
||||
const mode = resolveMode()
|
||||
|
||||
/* =======================================================
|
||||
🔥 NEW MODE — signature BYPASS (KRİTİK)
|
||||
======================================================= */
|
||||
if (mode === 'new') {
|
||||
lastRouteSignature.value = ''
|
||||
}
|
||||
|
||||
const sig = `${mode}:${id}:${route.query.source || ""}`
|
||||
|
||||
if (routeBusy.value || lastRouteSignature.value === sig) return
|
||||
lastRouteSignature.value = sig
|
||||
|
||||
routeBusy.value = true
|
||||
loadingHeader.value = true
|
||||
|
||||
try {
|
||||
orderStore.mode = mode
|
||||
|
||||
// Ortak lookup’lar
|
||||
if (!cariOptions.value.length) await loadCariList($q)
|
||||
if (!modelOptions.value.length) await loadModels($q)
|
||||
|
||||
/* =======================================================
|
||||
🟢 NEW MODE — FINAL & SAFE
|
||||
-------------------------------------------------------
|
||||
✔ route param boş / "new" ise → aktif NEW header’a fix
|
||||
✔ draft varsa → hydrate
|
||||
✔ draft yoksa → startNewOrder (LOCAL UUID üretir)
|
||||
✔ URL her zaman GERÇEK OrderHeaderID taşır
|
||||
======================================================= */
|
||||
if (mode === "new") {
|
||||
const routeId = String(orderHeaderID.value || "")
|
||||
const activeId = orderStore.getActiveNewHeaderId?.()
|
||||
|
||||
/* 1️⃣ Route ID FIX (kritik) */
|
||||
if (!routeId || routeId === "new") {
|
||||
if (activeId) {
|
||||
orderStore.allowRouteLeaveOnce = true
|
||||
await router.replace({
|
||||
name: "order-entry",
|
||||
params: { orderHeaderID: activeId },
|
||||
query: {
|
||||
...route.query,
|
||||
mode: "new",
|
||||
source: route.query.source || "local"
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/* 2️⃣ EDIT snapshot temizle */
|
||||
orderStore.clearEditSnapshotIfExists?.()
|
||||
|
||||
/* 3️⃣ Draft hydrate → yoksa YENİ oluştur */
|
||||
const resumed = orderStore.hydrateFromLocalStorageIfExists?.()
|
||||
|
||||
if (!resumed) {
|
||||
const header = await orderStore.startNewOrder({ $q, form, productCache })
|
||||
const newId = header?.OrderHeaderID
|
||||
|
||||
if (newId && newId !== routeId) {
|
||||
orderStore.allowRouteLeaveOnce = true
|
||||
await router.replace({
|
||||
name: "order-entry",
|
||||
params: { orderHeaderID: newId },
|
||||
query: { mode: "new", source: "new" }
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/* 4️⃣ Form sync */
|
||||
if (orderStore.header) {
|
||||
Object.assign(form, orderStore.header)
|
||||
syncCurrencyFromHeader()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
🔵 EDIT / 👁 VIEW MODE
|
||||
======================================================= */
|
||||
if (isInvalidId(id)) {
|
||||
await router.replace({ name: "order-list" })
|
||||
return
|
||||
}
|
||||
|
||||
let ok = false
|
||||
try {
|
||||
ok = await orderStore.openExistingForEdit(id, {
|
||||
$q,
|
||||
form,
|
||||
productCache
|
||||
})
|
||||
} catch {}
|
||||
|
||||
if (!ok) {
|
||||
$q.notify({ type: "negative", message: "Sipariş açılamadı" })
|
||||
await router.replace({ name: "order-list" })
|
||||
return
|
||||
}
|
||||
|
||||
if (orderStore.header) {
|
||||
Object.assign(form, orderStore.header)
|
||||
syncCurrencyFromHeader()
|
||||
}
|
||||
|
||||
} finally {
|
||||
/* =======================================================
|
||||
🔒 GUARDS — TEK MERKEZ
|
||||
======================================================= */
|
||||
installGuards()
|
||||
loadingHeader.value = false
|
||||
routeBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* -------------------- ROUTE WATCH -------------------- */
|
||||
watch(
|
||||
() => [orderHeaderID.value, route.query.mode, route.query.source],
|
||||
runRouteFlow,
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/* -------------------- SIGNAL WATCHERS -------------------- */
|
||||
watch(() => orderStore.newOrderSignal, async (v) => {
|
||||
if (!v) return
|
||||
|
||||
// NEW header’ı üret (persistLocalStorage içinde draft + activeNewHeader yazılacak)
|
||||
const header = await orderStore.startNewOrder({ $q, form, productCache })
|
||||
|
||||
const id = header?.OrderHeaderID || orderStore.getActiveNewHeaderId?.()
|
||||
if (!id) return
|
||||
|
||||
await router.replace({
|
||||
name: "order-entry",
|
||||
params: { orderHeaderID: id },
|
||||
query: { mode: "new", source: "new" }
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
watch(() => orderStore.replaceRouteSignal, async (id) => {
|
||||
if (!id) return
|
||||
await router.replace({
|
||||
name: "order-entry",
|
||||
params: { orderHeaderID: id },
|
||||
query: { mode: "edit", source: "backend" }
|
||||
})
|
||||
})
|
||||
|
||||
/* -------------------- LIFECYCLE -------------------- */
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
/* ---------------- UI ---------------- */
|
||||
updateStickyVars()
|
||||
measureHeaderGap()
|
||||
|
||||
resizeHandler = () => updateStickyVars()
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
|
||||
/* ------------- HYDRATE DECISION ------------- */
|
||||
const mode = route.query.mode || 'new'
|
||||
const source = route.query.source || ''
|
||||
const id = orderHeaderID.value
|
||||
|
||||
console.log('🧩 hydrate decision', { mode, source, id })
|
||||
|
||||
if (mode === 'new' && source === 'draft' && id) {
|
||||
await orderentryStore.hydrateFromLocalStorage(id)
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'edit' && id) {
|
||||
await orderentryStore.hydrateFromLocalStorage(id)
|
||||
return
|
||||
}
|
||||
|
||||
await orderentryStore.startNewOrder({ $q })
|
||||
})
|
||||
|
||||
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeHandler) window.removeEventListener("resize", resizeHandler)
|
||||
clearAutosave()
|
||||
clearBeforeUnload()
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🚫 ROUTE LEAVE GUARD — FINAL (NEW DRAFT GUARANTEE)
|
||||
-----------------------------------------------------------
|
||||
✔ View mode → asla bloklama yok
|
||||
✔ isControlledSubmit → sessiz geç
|
||||
✔ allowRouteLeaveOnce → 1 kez bypass
|
||||
✔ hasUnsavedChanges=false → sessiz geç
|
||||
✔ EDIT mode → onayda edit snapshot temizlenir (+ optional reset)
|
||||
✔ NEW mode → onayda hiçbir şey silinmez
|
||||
✔ NEW mode → onayla çıkmadan önce DRAFT ZORLA PERSIST edilir
|
||||
=========================================================== */
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
|
||||
// 1) Kontrollü submit (kaydet / gönder akışı) → guard devre dışı
|
||||
if (orderStore.isControlledSubmit) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Programatik geçiş → 1 kere bypass
|
||||
if (orderStore.allowRouteLeaveOnce) {
|
||||
orderStore.allowRouteLeaveOnce = false
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 3) VIEW → serbest
|
||||
if (orderStore.mode === 'view') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 4) Değişiklik yok → serbest
|
||||
if (!orderStore.hasUnsavedChanges) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 5) Kullanıcı onayı
|
||||
$q.dialog({
|
||||
title: 'Sayfadan ayrılıyorsunuz',
|
||||
message:
|
||||
orderStore.mode === 'edit'
|
||||
? 'Değişiklikler kaybolacak. Devam edilsin mi?'
|
||||
: 'Taslak korunacak. Sayfadan çıkmak istiyor musunuz?',
|
||||
ok: { label: 'Evet', color: 'negative' },
|
||||
cancel: { label: 'Hayır' },
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => {
|
||||
|
||||
/* ===================================================
|
||||
✅ NEW MODE — DRAFT GARANTİSİ
|
||||
---------------------------------------------------
|
||||
Amaç: gateway’e dönünce taslak kartı %100 görünsün.
|
||||
Çözüm: çıkmadan hemen önce snapshot’ı tek kaynağa yaz.
|
||||
=================================================== */
|
||||
if (orderStore.mode === 'new') {
|
||||
try {
|
||||
// NEW taslak tek kaynağa yazılmalı: orderStore.getDraftKey
|
||||
// persistLocalStorage bununla uyumlu olmalı.
|
||||
orderStore.persistLocalStorage?.()
|
||||
} catch (e) {
|
||||
// persist başarısız olsa bile kullanıcı çıkmayı onayladıysa çıkışa engel olmayalım
|
||||
console.warn('⚠️ NEW draft persist edilemedi (route leave):', e)
|
||||
}
|
||||
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
/* ===================================================
|
||||
🔥 EDIT MODE — TEMİZLİK
|
||||
---------------------------------------------------
|
||||
✔ local edit snapshot silinir
|
||||
✔ (opsiyonel) state reset
|
||||
=================================================== */
|
||||
if (orderStore.mode === 'edit') {
|
||||
try {
|
||||
orderStore.clearEditSnapshotIfExists?.()
|
||||
} catch (e) {
|
||||
console.warn('⚠️ edit snapshot temizlenemedi:', e)
|
||||
}
|
||||
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// diğer modlar için default
|
||||
next()
|
||||
})
|
||||
.onCancel(() => next(false))
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 🔸 Cari Listesi
|
||||
// -----------------------------------------------------------
|
||||
// =======================================================
|
||||
// 📦 Cari Listesini Yükle (Sadece new modda çağrılır)
|
||||
// =======================================================
|
||||
|
||||
|
||||
async function loadCariList($q) {
|
||||
loadingCari.value = true
|
||||
try {
|
||||
|
||||
const res = await api.get('/customer-list')
|
||||
const data = res?.data
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
cariOptions.value = data
|
||||
} else if (Array.isArray(data?.data)) {
|
||||
cariOptions.value = data.data
|
||||
} else {
|
||||
cariOptions.value = []
|
||||
}
|
||||
|
||||
filteredCariOptions.value = [...cariOptions.value]
|
||||
console.log(`🧾 Cari listesi yüklendi: ${cariOptions.value.length} kayıt.`)
|
||||
} catch (err) {
|
||||
console.error('❌ Cari listesi alınamadı:', err)
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Cari listesi yüklenemedi ❌',
|
||||
position: 'top'
|
||||
})
|
||||
} finally {
|
||||
loadingCari.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔍 Cari Arama Filtresi (QSelect @filter)
|
||||
=========================================================== */
|
||||
function filterCari(val, update) {
|
||||
if (!val) {
|
||||
update(() => {
|
||||
filteredCariOptions.value = [...cariOptions.value]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const needle = val.toLowerCase()
|
||||
|
||||
update(() => {
|
||||
filteredCariOptions.value = cariOptions.value.filter(opt => {
|
||||
const kod = (opt.Cari_Kod || '').toLowerCase()
|
||||
const ad = (opt.Cari_Ad || '').toLowerCase()
|
||||
const unvan = (opt.Unvan || '').toLowerCase()
|
||||
return `${kod} ${ad} ${unvan}`.includes(needle)
|
||||
})
|
||||
})
|
||||
}
|
||||
const highlightPantolon = computed(() =>
|
||||
(summaryRows.value || []).some(r =>
|
||||
(r.urunAnaGrubu || '').toLowerCase().includes('pantolon')
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 🔸 Model Listesi
|
||||
// -----------------------------------------------------------
|
||||
async function loadModels($q) {
|
||||
loadingModels.value = true
|
||||
try {
|
||||
const res = await api.get('/products')
|
||||
const arr = res?.data || []
|
||||
|
||||
modelOptions.value = arr.map(x => ({
|
||||
label: x.ProductCode,
|
||||
value: x.ProductCode
|
||||
}))
|
||||
filteredModelOptions.value = modelOptions.value
|
||||
console.log('✅ Model listesi yüklendi:', modelOptions.value.length)
|
||||
} catch (err) {
|
||||
console.error('❌ Model listesi alınamadı:', err)
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Model listesi alınamadı ❌',
|
||||
position: 'top-right'
|
||||
})
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function syncCurrencyFromHeader() {
|
||||
const hdrPB = orderStore.header?.DocCurrencyCode || orderStore.header?.CurrencyCode
|
||||
if (!hdrPB) return
|
||||
|
||||
form.pb = hdrPB
|
||||
form.DocCurrencyCode = hdrPB
|
||||
|
||||
orderStore.setHeaderFields?.(
|
||||
{ DocCurrencyCode: hdrPB, PriceCurrencyCode: hdrPB },
|
||||
{ applyCurrencyToLines: true, immediatePersist: false }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 MODEL + PB Bazlı Minimum Fiyat
|
||||
=========================================================== */
|
||||
async function fetchMinPrice() {
|
||||
if (!form.model || !form.pb) return
|
||||
|
||||
try {
|
||||
const res = await api.get('/min-price', {
|
||||
params: { model: form.model, currency: form.pb }
|
||||
})
|
||||
const data = res.data
|
||||
|
||||
form.minFiyat = Number(data.price || 0)
|
||||
form.kur = Number(data.rateToTRY || 1)
|
||||
form.minFiyatTRY = Number(data.priceTRY || 0)
|
||||
|
||||
console.log(`💰 Min Fiyat: ${form.minFiyat} ${form.pb} (${form.minFiyatTRY} TRY)`)
|
||||
} catch (err) {
|
||||
console.error('❌ Min fiyat alınamadı:', err)
|
||||
form.minFiyat = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔽 SCROLL TO ROW — DOM SAFE
|
||||
=========================================================== */
|
||||
function scrollToRow(clientKey) {
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.querySelector(
|
||||
`[data-client-key="${clientKey}"]`
|
||||
)
|
||||
if (!el) return
|
||||
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
|
||||
el.classList.add('row-error-flash')
|
||||
setTimeout(() => {
|
||||
el.classList.remove('row-error-flash')
|
||||
}, 1500)
|
||||
})
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 SERİ SETİ UYGULAMA (FINAL — grpKey SAFE)
|
||||
=========================================================== */
|
||||
function applySeriSet() {
|
||||
if (!selectedSeriSet.value) return
|
||||
|
||||
/* 🔑 TEK KAYNAK */
|
||||
const grpKey = activeGrpKey.value
|
||||
if (!grpKey) {
|
||||
console.warn('⚠️ applySeriSet: grpKey bulunamadı')
|
||||
return
|
||||
}
|
||||
|
||||
const setKey =
|
||||
typeof selectedSeriSet.value === 'object'
|
||||
? selectedSeriSet.value.value
|
||||
: selectedSeriSet.value
|
||||
|
||||
const pattern = seriMatrix?.[grpKey]?.[setKey]
|
||||
if (!pattern) {
|
||||
console.warn(`⚠️ Seri seti bulunamadı → grpKey:${grpKey}, set:${setKey}`)
|
||||
return
|
||||
}
|
||||
|
||||
const mult = Number(seriMultiplier.value) || 1
|
||||
|
||||
/* =======================================================
|
||||
🔹 BEDENLER — LABEL BAZLI
|
||||
======================================================= */
|
||||
form.bedenler = form.bedenLabels.map((lbl, i) => {
|
||||
const base = Number(form.bedenler?.[i] || 0)
|
||||
const inc = Number(pattern[lbl] || 0) * mult
|
||||
return base + inc
|
||||
})
|
||||
|
||||
updateTotals(form)
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `Seri "${setKey}" uygulandı (${grpKey})`,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 TOPLAM HESAPLAMA
|
||||
=========================================================== */
|
||||
function updateTotals(f) {
|
||||
f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
|
||||
const fiyat = Number(f.fiyat) || 0
|
||||
f.tutar = Number((f.adet * fiyat).toFixed(2))
|
||||
|
||||
}
|
||||
|
||||
|
||||
function removeSelected() {
|
||||
const row = selectedRow.value
|
||||
if (!row) {
|
||||
$q.notify({ type: 'warning', message: 'Silmek için önce bir satır seçmelisiniz.' })
|
||||
return
|
||||
}
|
||||
|
||||
// 🔒 Kapalı satır koruması
|
||||
if (row.isClosed === true) {
|
||||
$q.notify({ type: 'warning', message: 'Kapalı satır silinemez.', position: 'top-right' })
|
||||
return
|
||||
}
|
||||
|
||||
$q.dialog({
|
||||
title: 'Satırı Sil',
|
||||
message: `<b>${row.model} / ${row.renk}</b> satırı silinsin mi?`,
|
||||
html: true,
|
||||
ok: { label: 'Sil', color: 'negative' },
|
||||
cancel: { label: 'Vazgeç', flat: true }
|
||||
}).onOk(() => {
|
||||
orderStore.removeRowInternal(row)
|
||||
|
||||
// ✅ edit state temizle
|
||||
orderStore.editingKey = null
|
||||
orderStore.selected = null
|
||||
|
||||
resetEditor()
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Satır silindi (DELETE ops oluşturuldu)',
|
||||
position: 'top-right'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// ✅ hydrateEditorFromRow — FINAL FIX (EDITOR BEDEN MAP)
|
||||
// ===========================================================
|
||||
async function hydrateEditorFromRow(row, opts = {}) {
|
||||
const {
|
||||
allowClosed = false,
|
||||
notify = true,
|
||||
message = 'Düzenleme moduna alındı',
|
||||
loadSizes = true,
|
||||
source = 'hydrate'
|
||||
} = opts
|
||||
|
||||
if (!row) return false
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🔒 KAPALI SATIR KONTROLÜ
|
||||
------------------------------------------------------- */
|
||||
if (!allowClosed && row.isClosed === true) {
|
||||
notify && $q.notify({
|
||||
type: 'warning',
|
||||
message: 'Bu satır kapalıdır ve düzenlenemez.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🔑 editingKey
|
||||
------------------------------------------------------- */
|
||||
const key =
|
||||
typeof orderStore.getRowKey === 'function'
|
||||
? orderStore.getRowKey(row)
|
||||
: (row.clientKey || row.OrderLineID)
|
||||
|
||||
if (!key) return false
|
||||
|
||||
orderStore.editingKey = key
|
||||
orderStore.selected = { ...row }
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🧩 FORM BASIC
|
||||
------------------------------------------------------- */
|
||||
Object.assign(form, {
|
||||
model: row.model,
|
||||
renk: row.renk,
|
||||
renk2: row.renk2,
|
||||
urunAnaGrubu: row.urunAnaGrubu,
|
||||
urunAltGrubu: row.urunAltGrubu,
|
||||
kategori: row.kategori,
|
||||
aciklama: row.aciklama,
|
||||
fiyat: Number(row.fiyat || 0),
|
||||
pb: row.pb || aktifPB.value || 'USD',
|
||||
terminTarihi: toDateOnly(row.terminTarihi || ''),
|
||||
grpKey: row.grpKey
|
||||
})
|
||||
|
||||
/* =======================================================
|
||||
🔑 BEDEN EDITOR — TEK DOĞRU KAYNAK (GARANTİLİ)
|
||||
-------------------------------------------------------
|
||||
✔ schemaMap hazır değilse init edilir
|
||||
✔ labels = schemaMap[grpKey].values
|
||||
✔ values = row.bedenMap[grpKey] || 0
|
||||
======================================================= */
|
||||
const grpKey = form.grpKey
|
||||
|
||||
// 🔒 GARANTİ: schemaMap + grpKey
|
||||
if (!orderStore.schemaMap || !orderStore.schemaMap[grpKey]) {
|
||||
orderStore.initSchemaMap()
|
||||
}
|
||||
|
||||
const schema = orderStore.schemaMap?.[grpKey]
|
||||
|
||||
if (schema?.values?.length) {
|
||||
const rowMap = row?.bedenMap?.[grpKey] || {}
|
||||
|
||||
form.bedenLabels = [...schema.values]
|
||||
form.bedenler = form.bedenLabels.map(lbl =>
|
||||
Number(rowMap[lbl] || 0)
|
||||
)
|
||||
} else {
|
||||
console.warn('⛔ schema bulunamadı:', grpKey)
|
||||
form.bedenLabels = []
|
||||
form.bedenler = []
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🧮 SATIR TOPLAM
|
||||
------------------------------------------------------- */
|
||||
updateTotals(form)
|
||||
|
||||
/* -------------------------------------------------------
|
||||
⚙️ STOK / BEDEN ENVANTERİ (LABEL DOKUNMAZ)
|
||||
------------------------------------------------------- */
|
||||
if (loadSizes && form.model) {
|
||||
await nextTick()
|
||||
|
||||
await orderStore.loadProductSizes(
|
||||
form,
|
||||
true,
|
||||
$q
|
||||
)
|
||||
|
||||
// backend’den gelen snapshot stokları varsa
|
||||
if (row.stokMap && typeof row.stokMap === 'object') {
|
||||
stockMap.value = { ...row.stokMap }
|
||||
}
|
||||
|
||||
await loadOrderInventory(true)
|
||||
}
|
||||
|
||||
notify && $q.notify({
|
||||
type: 'info',
|
||||
message: `${message} → ${row.model}`,
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
console.log('✅ hydrateEditorFromRow OK', {
|
||||
source,
|
||||
grpKey,
|
||||
labels: form.bedenLabels,
|
||||
values: form.bedenler
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 handleNewCombination (v6.3 — FINAL & STABLE)
|
||||
- Model varsa ÇALIŞIR (renk opsiyonel)
|
||||
- UI bedenleri → loadProductSizes
|
||||
- Gerçek stok → loadOrderInventory
|
||||
- Formu ASLA resetlemez
|
||||
=========================================================== */
|
||||
async function handleNewCombination() {
|
||||
if (!form.model) {
|
||||
console.warn('⚠️ handleNewCombination: model yok')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🆕 handleNewCombination', {
|
||||
model: form.model,
|
||||
renk: form.renk,
|
||||
renk2: form.renk2
|
||||
})
|
||||
|
||||
try {
|
||||
/* -------------------------------------------------------
|
||||
1️⃣ Reaktivite sakinleşsin
|
||||
------------------------------------------------------- */
|
||||
await nextTick()
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
await nextTick()
|
||||
|
||||
/* -------------------------------------------------------
|
||||
2️⃣ UI için beden / grpKey hazırlığı
|
||||
------------------------------------------------------- */
|
||||
await orderStore.loadProductSizes(
|
||||
form,
|
||||
true,
|
||||
$q,
|
||||
productCache
|
||||
)
|
||||
|
||||
/* -------------------------------------------------------
|
||||
3️⃣ GERÇEK STOK (MSSQL)
|
||||
- merge=true → editor bozulmaz
|
||||
------------------------------------------------------- */
|
||||
await loadOrderInventory(true)
|
||||
|
||||
/* -------------------------------------------------------
|
||||
4️⃣ Stok bilgilendirme (opsiyonel)
|
||||
------------------------------------------------------- */
|
||||
const stoklar = Object.values(stockMap.value || {})
|
||||
if (stoklar.length && stoklar.every(v => Number(v) === 0)) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Bu kombinasyon için stok bulunamadı (0)',
|
||||
position: 'top-right'
|
||||
})
|
||||
} else {
|
||||
console.log(`✅ Stok yüklendi (${stoklar.length} beden)`)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
5️⃣ Gridde aynı kombinasyon varsa → edit moduna al
|
||||
------------------------------------------------------- */
|
||||
await openExistingCombination()
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ handleNewCombination hata:', err)
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Stok bilgisi alınamadı ❌',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
✅ openExistingCombination (X3 FINAL — helper kullanır)
|
||||
- sadece eşleşen satırı bulur
|
||||
- hydrate işini helper yapar
|
||||
=========================================================== */
|
||||
async function openExistingCombination() {
|
||||
if (!form.model) return
|
||||
|
||||
const row = (orderStore.summaryRows || []).find(r =>
|
||||
r.model === form.model &&
|
||||
(r.renk || '') === (form.renk || '') &&
|
||||
(r.renk2 || '') === (form.renk2 || '') &&
|
||||
r.grpKey === form.grpKey
|
||||
|
||||
)
|
||||
|
||||
if (!row) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Kapalıysa uyar + çık
|
||||
if (row.isClosed === true) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Bu satır kapalıdır.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await hydrateEditorFromRow(row, {
|
||||
source: 'openExistingCombination',
|
||||
message: 'Düzenleme moduna alındı',
|
||||
notify: true,
|
||||
loadSizes: true
|
||||
})
|
||||
}
|
||||
|
||||
const editRow = async (row) => {
|
||||
try {
|
||||
await hydrateEditorFromRow(row, {
|
||||
source: 'editRow',
|
||||
message: 'Düzenleme moduna geçildi',
|
||||
notify: true,
|
||||
loadSizes: true
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('❌ editRow hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 STOK YARDIMCI FONKSİYONLARI
|
||||
=========================================================== */
|
||||
function getStockFor(lbl) {
|
||||
if (!lbl || !stockMap.value) return 0
|
||||
const val = stockMap.value[lbl]
|
||||
const num = Number(val)
|
||||
return isNaN(num) ? 0 : num
|
||||
}
|
||||
|
||||
function getStockForRow(row, beden) {
|
||||
if (!row || !beden) return 0
|
||||
|
||||
// 🔹 Eğer formda seçili model ve renk bu satıra aitse, bedenStock'tan getir
|
||||
if (row.model === form.model && row.renk === form.renk) {
|
||||
const f = bedenStock.value.find(x => x.beden === beden)
|
||||
if (f) return Number(f.stok) || 0
|
||||
}
|
||||
|
||||
// 🔹 Satırın kendi stokMap’inde varsa oradan getir
|
||||
if (row.stokMap && row.stokMap[beden] != null) {
|
||||
const num = Number(row.stokMap[beden])
|
||||
return isNaN(num) ? 0 : num
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function stockColorClass(qty) {
|
||||
const n = Number(qty)
|
||||
if (isNaN(n)) return ''
|
||||
if (n === 0) return 'stok-red'
|
||||
if (n > 0 && n <= 2) return 'stok-yellow'
|
||||
return 'stok-green'
|
||||
}
|
||||
const getKey =
|
||||
typeof orderStore.getRowKey === 'function'
|
||||
? orderStore.getRowKey
|
||||
: (r => r?.clientKey || r?.id || r?.OrderLineID)
|
||||
|
||||
const activeGrpKey = computed(() => {
|
||||
// 1️⃣ Edit edilen satır varsa
|
||||
if (orderStore.editingKey) {
|
||||
const row = (summaryRows.value || []).find(
|
||||
r => getKey(r) === orderStore.editingKey
|
||||
)
|
||||
if (row?.grpKey) return row.grpKey
|
||||
}
|
||||
|
||||
// 2️⃣ Editor formdan
|
||||
if (form.grpKey) return form.grpKey
|
||||
|
||||
// 3️⃣ Güvenli fallback
|
||||
return 'tak'
|
||||
})
|
||||
|
||||
|
||||
const editingRow = computed(() => {
|
||||
const key = orderStore.editingKey
|
||||
if (!key) return null
|
||||
const getKey = typeof orderStore.getRowKey === 'function'
|
||||
? orderStore.getRowKey
|
||||
: (r => r?.clientKey || r?.OrderLineID)
|
||||
return (summaryRows.value || []).find(r => getKey(r) === key) || null
|
||||
})
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Seri Matrix — Excel benzeri çarpan tabloları
|
||||
Her ürün tipi için (takım, gömlek, pantolon vs.)
|
||||
önceden tanımlanmış seri setlerini tutar.
|
||||
Örneğin “46–58 seri” seçilirse 46=1, 48=1, … şeklinde çarpanlar oluşur.
|
||||
=========================================================== */
|
||||
const seriMatrix = {
|
||||
tak: {
|
||||
'46-58 seri': { 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
|
||||
'46-58 ara çift': { 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
|
||||
'44-58 seri': { 44:1, 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
|
||||
'44-58 ara çift': { 44:1, 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
|
||||
'60-64 seri': { 60:1, 62:1, 64:1 },
|
||||
'66-70 seri': { 66:1, 68:1, 70:1 },
|
||||
'48-58 seri': { 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 }
|
||||
},
|
||||
gom: {
|
||||
'XS-XXL': { XS:1, S:1, M:1, L:1, XL:1, XXL:1 },
|
||||
'XS-XXL ara çift': { XS:1, S:1, M:2, L:2, XL:2, XXL:1 },
|
||||
'3XL-5XL': { '3XL':1, '4XL':1, '5XL':1 }
|
||||
},
|
||||
ayk: {
|
||||
'10\'lu seri': { 39:1, 40:2, 41:2, 42:2, 43:2, 44:1 },
|
||||
'39-44': { 39:1, 40:1, 41:1, 42:1, 43:1, 44:1 },
|
||||
'45-47': { 45:1, 46:1, 47:1 }
|
||||
},
|
||||
yas: {
|
||||
'2-14Y': { 2:1, 4:1, 6:1, 8:1, 10:1, 12:1, 14:1 }
|
||||
},
|
||||
pan: {
|
||||
'38-50 seri': { 38:1, 40:1, 42:1, 44:1, 46:1, 48:1, 50:1 },
|
||||
'38-50 ara çift': { 38:1, 40:1, 42:2, 44:2, 46:2, 48:1, 50:1 },
|
||||
'52-56 seri': { 52:1, 54:1, 56:1 },
|
||||
'58-62 seri': { 58:1, 60:1, 62:1 }
|
||||
}
|
||||
}
|
||||
const activeSeriesOptions = computed(() => {
|
||||
const grpKey = activeGrpKey.value
|
||||
const sets = seriMatrix[grpKey]
|
||||
if (!sets) return []
|
||||
|
||||
return Object.keys(sets).map(k => ({
|
||||
label: k,
|
||||
value: k
|
||||
}))
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Para Birimi ve Toplam Tutar Hesaplaması
|
||||
Sipariş toplamları ve para birimi seçimi burada yönetilir.
|
||||
=========================================================== */
|
||||
const paraBirimOptions = ['USD', 'EUR', 'TRY','GBP'] // Kullanıcıya sunulacak döviz seçenekleri
|
||||
|
||||
|
||||
|
||||
// 🔸 3. Ana veri yüklemeleri (cari + modeller)
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Model Arama (QSelect içinde)
|
||||
Kullanıcının yazdığı harflerle model kodlarını filtreler.
|
||||
=========================================================== */
|
||||
function filterModel(val, update) {
|
||||
if (val === '') {
|
||||
update(() => (filteredModelOptions.value = modelOptions.value))
|
||||
return
|
||||
}
|
||||
update(() => {
|
||||
const needle = val.toLowerCase()
|
||||
filteredModelOptions.value = modelOptions.value.filter(v =>
|
||||
(v.label || '').toLowerCase().includes(needle)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 MODEL SEÇİMİ (onModelChangeV2) — FINAL (grpKey FIXED)
|
||||
-----------------------------------------------------------
|
||||
✔ grpKey SADECE burada set edilir
|
||||
✔ grpKey, urunAnaGrubu/kategori’ye göre DETERMINISTIC
|
||||
✔ Editor / loadProductSizes tahmin yapmaz (form.grpKey kesin)
|
||||
✔ Pantolon gibi durumlarda 'tak' fallback engellenir
|
||||
=========================================================== */
|
||||
async function onModelChange(modelCode) {
|
||||
// 🧹 önceki renkleri tamamen sıfırla
|
||||
form.renk = ''
|
||||
form.renk2 = ''
|
||||
renkOptions.value = []
|
||||
renkOptions2.value = []
|
||||
|
||||
if (renkSelect.value?.reset) renkSelect.value.reset()
|
||||
if (renk2Select.value?.reset) renk2Select.value.reset()
|
||||
|
||||
if (!modelCode) {
|
||||
console.warn('⚠️ Model kodu boş, sorgu yapılmadı.')
|
||||
return
|
||||
}
|
||||
|
||||
// 🧩 Önceki değerleri yedekle (korunacak alanlar)
|
||||
const keep = {
|
||||
aciklama: form.aciklama,
|
||||
bedenler: Array.isArray(form.bedenler) ? [...form.bedenler] : [],
|
||||
bedenLabels: Array.isArray(form.bedenLabels) ? [...form.bedenLabels] : [],
|
||||
fiyat: form.fiyat,
|
||||
adet: form.adet,
|
||||
tutar: form.tutar
|
||||
}
|
||||
|
||||
try {
|
||||
/* -------------------------------------------------------
|
||||
🎨 1️⃣ Renk listesi
|
||||
------------------------------------------------------- */
|
||||
const resColors = await api.get('/product-colors', {
|
||||
params: { code: modelCode }
|
||||
})
|
||||
renkOptions.value = (resColors?.data || []).map(x => ({
|
||||
label: `${x.color_code || x.ColorCode} – ${x.color_description || x.ColorDesc || ''}`,
|
||||
value: x.color_code || x.ColorCode
|
||||
}))
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🧱 2️⃣ Ürün detayları
|
||||
------------------------------------------------------- */
|
||||
const resDetail = await api.get('/product-detail', {
|
||||
params: { code: modelCode }
|
||||
})
|
||||
const d = resDetail?.data || {}
|
||||
|
||||
// ✅ Cache
|
||||
if (modelCode && d) {
|
||||
orderStore.productCache[modelCode] = productCache[modelCode]
|
||||
productCache[modelCode] = {
|
||||
...d,
|
||||
ProductGroup: d.ProductGroup || d.UrunAnaGrubu || d.ProductAtt01Desc || '',
|
||||
ProductSubGroup: d.ProductSubGroup || d.UrunAltGrubu || d.ProductAtt02Desc || '',
|
||||
URUN_ANA_GRUBU: d.UrunAnaGrubu || d.ProductAtt01Desc || '',
|
||||
URUN_ALT_GRUBU: d.UrunAltGrubu || d.ProductAtt02Desc || ''
|
||||
}
|
||||
console.log('🗂️ Cache eklendi:', modelCode, Object.keys(productCache[modelCode]))
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🧩 Form temel alanları
|
||||
------------------------------------------------------- */
|
||||
Object.assign(form, {
|
||||
model: modelCode,
|
||||
urunAnaGrubu: d.UrunAnaGrubu || d.ProductGroup || d.ProductAtt01Desc || '',
|
||||
urunAltGrubu: d.UrunAltGrubu || d.ProductSubGroup || d.ProductAtt02Desc || '',
|
||||
fit: d.Fit1 || d.Fit || '',
|
||||
urunIcerik: d.UrunIcerik || d.Fabric || '',
|
||||
drop: d.Drop || '',
|
||||
kategori: d.Kategori || '',
|
||||
askiliyan: d.AskiliYan || '',
|
||||
aciklama: keep.aciklama,
|
||||
fiyat: keep.fiyat,
|
||||
adet: keep.adet,
|
||||
tutar: keep.tutar,
|
||||
})
|
||||
|
||||
/* =======================================================
|
||||
🔑 BEDEN GRUBU — TEK VE KESİN KARAR (FIXED)
|
||||
- detectBedenGroup içine "[]" verip 'tak' düşmesini engeller
|
||||
- Önce urunAnaGrubu/kategori üzerinden hard-match
|
||||
- Sonra detectBedenGroup (ürün bilgisiyle)
|
||||
- En sonda güvenli fallback: 'tak'
|
||||
======================================================= */
|
||||
const ana = String(form.urunAnaGrubu || '').toLowerCase().trim()
|
||||
const kat = String(form.kategori || '').toLowerCase().trim()
|
||||
|
||||
let bedenGrpKey = null
|
||||
|
||||
// ✅ Hard-match (senin ana gruplarına göre genişletebilirsin)
|
||||
if (ana.includes('pantolon') || kat.includes('pantolon')) {
|
||||
bedenGrpKey = 'pan'
|
||||
} else if (ana.includes('gömlek') || ana.includes('gomlek') || kat.includes('gömlek') || kat.includes('gomlek')) {
|
||||
bedenGrpKey = 'gom'
|
||||
} else if (ana.includes('ayakkabı') || ana.includes('ayakkabi') || kat.includes('ayakkabı') || kat.includes('ayakkabi')) {
|
||||
bedenGrpKey = 'ayk'
|
||||
} else if (ana.includes('yaş') || ana.includes('yas') || kat.includes('yaş') || kat.includes('yas')) {
|
||||
bedenGrpKey = 'yas'
|
||||
}
|
||||
|
||||
// ✅ Hard-match bulamadıysa mevcut helper ile belirle
|
||||
if (!bedenGrpKey) {
|
||||
try {
|
||||
// ⚠️ Boş array verme; ürün bilgisini kullanarak belirle
|
||||
bedenGrpKey = detectBedenGroup(null, form.urunAnaGrubu, form.kategori)
|
||||
} catch (e) {
|
||||
console.warn('⚠️ detectBedenGroup hata:', e)
|
||||
bedenGrpKey = null
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Son fallback
|
||||
if (!bedenGrpKey) bedenGrpKey = 'tak'
|
||||
|
||||
form.grpKey = bedenGrpKey
|
||||
console.log('🧭 Editor grpKey set edildi →', bedenGrpKey)
|
||||
// ✅ Editor bedenleri hemen aç (UI seed) — schemaMap tek kaynak
|
||||
const schema =
|
||||
orderStore.schemaMap?.[form.grpKey] ||
|
||||
storeSchemaByKey?.[form.grpKey]
|
||||
|
||||
if (Array.isArray(schema?.values) && schema.values.length) {
|
||||
// önceki adetleri label bazlı koru
|
||||
const prevMap = {}
|
||||
;(keep.bedenLabels || []).forEach((lbl, i) => {
|
||||
prevMap[lbl] = Number(keep.bedenler?.[i] || 0)
|
||||
})
|
||||
|
||||
form.bedenLabels = [...schema.values]
|
||||
form.bedenler = form.bedenLabels.map(lbl => Number(prevMap[lbl] || 0))
|
||||
} else {
|
||||
form.bedenLabels = []
|
||||
form.bedenler = []
|
||||
}
|
||||
|
||||
console.log('📦 Model detayları yüklendi:', form.urunAnaGrubu, form.fit)
|
||||
|
||||
/* -------------------------------------------------------
|
||||
💰 3️⃣ Min fiyat
|
||||
------------------------------------------------------- */
|
||||
await fetchMinPrice()
|
||||
|
||||
/* -------------------------------------------------------
|
||||
⚙️ 4️⃣ Renk yoksa direkt beden/stok
|
||||
------------------------------------------------------- */
|
||||
if (!renkOptions.value.length) {
|
||||
await orderStore.loadProductSizes(form, true, $q, productCache)
|
||||
await loadOrderInventory(true)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🧮 5️⃣ Gridde varsa → edit mod
|
||||
------------------------------------------------------- */
|
||||
await openExistingCombination()
|
||||
|
||||
$q.notify({
|
||||
type: 'info',
|
||||
message: `Model "${modelCode}" yüklendi ✅`,
|
||||
position: 'top-right'
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('❌ Model verileri alınamadı:', err)
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Model bilgileri alınamadı ❌',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 RENK SEÇİMİ (1. Renk Değişimi)
|
||||
=========================================================== */
|
||||
async function onColorChange(colorCode) {
|
||||
form.renk = colorCode || ''
|
||||
renkOptions2.value = []
|
||||
form.renk2 = ''
|
||||
|
||||
// 2. renk QSelect bileşenini sıfırla
|
||||
if (renk2Select.value) renk2Select.value.reset?.()
|
||||
|
||||
// ⚠️ Renk seçilmediyse işlemi iptal et
|
||||
if (!form.renk) {
|
||||
console.warn('⚠️ Renk seçilmedi, işlemler durduruldu.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
|
||||
// 🎨 2️⃣ İkinci renk listesini yükle
|
||||
const res = await api.get('/product-secondcolor', {
|
||||
params: { code: form.model, color: colorCode }
|
||||
})
|
||||
|
||||
const data = res?.data || []
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
renkOptions2.value = data.map(x => ({
|
||||
label: x.item_dim2_code,
|
||||
value: x.item_dim2_code
|
||||
}))
|
||||
console.log('🎨 2. renk listesi yüklendi:', renkOptions2.value.length)
|
||||
} else {
|
||||
// 2. renk yoksa doğrudan beden/stok yükle
|
||||
console.log('⚪ 2. renk yok, doğrudan beden/stok yükleniyor...')
|
||||
|
||||
}
|
||||
|
||||
await handleNewCombination()
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ 1. renk sonrası hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 2. RENK SEÇİMİ (onColor2Change)
|
||||
=========================================================== */
|
||||
async function onColor2Change(colorCode2) {
|
||||
if (typeof colorCode2 === 'object' && colorCode2?.value) {
|
||||
colorCode2 = colorCode2.value
|
||||
}
|
||||
form.renk2 = colorCode2 || ''
|
||||
|
||||
try {
|
||||
|
||||
await handleNewCombination()
|
||||
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ 2. renk sonrası hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Beden / Stok Yükleyici — Değişkenler
|
||||
=========================================================== */
|
||||
const bedenStock = ref([]) // Görsel tablo için stok listesi
|
||||
const stockMap = ref({}) // { "48": 12, "50": 7, ... } şeklinde key-value map
|
||||
const onSaveOrUpdateRow = async () => {
|
||||
await orderStore.saveOrUpdateRowUnified({
|
||||
form,
|
||||
|
||||
recalcVat: typeof recalcVat === 'function' ? recalcVat : null,
|
||||
resetEditor: typeof resetEditor === 'function' ? resetEditor : null,
|
||||
|
||||
// gerekiyorsa pass edebilirsin (store tarafında zaten optional)
|
||||
stockMap,
|
||||
$q
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 loadOrderInventory (GÜNCELLENMİŞ)
|
||||
MSSQL stok sorgusu — artık boş değerleri 0 yapmıyor.
|
||||
merge=true ise sadece dolu değerleri günceller.
|
||||
=========================================================== */
|
||||
async function loadOrderInventory(merge = false) {
|
||||
if (!form.model) {
|
||||
console.warn('⚠️ Stok yüklenemedi: model seçilmemiş.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const params = { code: form.model }
|
||||
if (form.renk?.trim()) params.color = form.renk.trim()
|
||||
if (form.renk2?.trim()) params.color2 = form.renk2.trim()
|
||||
|
||||
console.log('📦 MSSQL stok sorgusu gönderiliyor:', params)
|
||||
const res = await api.get('/order-inventory', { params })
|
||||
const data = res?.data || []
|
||||
|
||||
console.log(`📦 MSSQL stok verisi geldi: ${data.length}`)
|
||||
console.table(data)
|
||||
|
||||
// 1️⃣ Normalize (gelen büyük harfli)
|
||||
const invMap = {}
|
||||
for (const x of data) {
|
||||
const beden = String(x.Beden || '').trim() || ' '
|
||||
const stokDeger = Number(x.KullanilabilirAdet ?? 0)
|
||||
invMap[beden] = stokDeger
|
||||
}
|
||||
|
||||
// 2️⃣ Form bedenlerine göre map oluştur
|
||||
const newMap = {}
|
||||
for (const lbl of form.bedenLabels || []) {
|
||||
const key = lbl?.trim() === '' ? ' ' : lbl.trim()
|
||||
newMap[lbl] = invMap[key] ?? 0
|
||||
}
|
||||
|
||||
// 3️⃣ Merge veya replace
|
||||
if (merge && stockMap.value) {
|
||||
for (const lbl of Object.keys(newMap)) {
|
||||
stockMap.value[lbl] = newMap[lbl]
|
||||
}
|
||||
} else {
|
||||
stockMap.value = { ...newMap }
|
||||
}
|
||||
|
||||
// 4️⃣ Görsel listeyi güncelle
|
||||
bedenStock.value = Object.entries(stockMap.value).map(([beden, stok]) => ({
|
||||
beden,
|
||||
stok
|
||||
}))
|
||||
|
||||
console.log('✅ Stok haritası güncellendi:', stockMap.value)
|
||||
} catch (err) {
|
||||
console.error('❌ Order inventory yüklenemedi:', err)
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Stok verisi alınamadı ❌',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 🔹 Üst formdaki tahmini termin değişince:
|
||||
watch(
|
||||
() => form.AverageDueDate,
|
||||
(yeni) => {
|
||||
if (!yeni) return
|
||||
applyTerminToRows(yeni)
|
||||
}
|
||||
)
|
||||
|
||||
/* ===========================================================
|
||||
🔹 useComboWatcher (v6.3 — MUTATION AWARE & CLEAN)
|
||||
- Edit modda combo değişirse:
|
||||
DELETE → temiz edit state
|
||||
- Guard KURMAZ
|
||||
- Persist SADECE gerçek mutation varsa
|
||||
=========================================================== */
|
||||
function useComboWatcher(type, handler) {
|
||||
return async (val) => {
|
||||
const prevBusy = !!orderStore._uiBusy
|
||||
const prevPrevent = !!orderStore.preventPersist
|
||||
let mutated = false // 🔥 SADECE gerçekten değiştiyse true
|
||||
|
||||
try {
|
||||
const currentRow = selectedRow.value
|
||||
const isEditingNow = !!currentRow
|
||||
|
||||
/* =====================================================
|
||||
1️⃣ EDIT MODE → combo değiştiyse DELETE
|
||||
====================================================== */
|
||||
if (isEditingNow && currentRow) {
|
||||
const nextCombo = {
|
||||
model: type === 'model' ? val : form.model,
|
||||
renk : type === 'renk' ? val : form.renk,
|
||||
renk2: type === 'renk2' ? val : form.renk2
|
||||
}
|
||||
|
||||
const comboChanged =
|
||||
(currentRow.model || '') !== (nextCombo.model || '') ||
|
||||
(currentRow.renk || '') !== (nextCombo.renk || '') ||
|
||||
(currentRow.renk2 || '') !== (nextCombo.renk2 || '')
|
||||
|
||||
if (comboChanged) {
|
||||
console.warn('🟥 Combo değişti → DELETE')
|
||||
|
||||
mutated = true // 🔥 GERÇEK DEĞİŞİKLİK
|
||||
|
||||
orderStore._uiBusy = true
|
||||
orderStore.preventPersist = true
|
||||
|
||||
orderStore.removeRowInternal(currentRow)
|
||||
|
||||
// 🔑 Tek kaynak edit state
|
||||
orderStore.editingKey = null
|
||||
orderStore.selected = null
|
||||
|
||||
await nextTick()
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
2️⃣ ASIL HANDLER
|
||||
====================================================== */
|
||||
if (typeof handler === 'function') {
|
||||
await handler(val)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ useComboWatcher hata:', err)
|
||||
} finally {
|
||||
/* =====================================================
|
||||
3️⃣ STATE GERİ AL + ŞARTLI PERSIST
|
||||
====================================================== */
|
||||
orderStore._uiBusy = prevBusy
|
||||
orderStore.preventPersist = prevPrevent
|
||||
|
||||
// ✅ SADECE mutation olduysa snapshot al
|
||||
if (mutated) {
|
||||
orderStore.persistLocalStorage?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ======================================================
|
||||
// 👁🗨 GROUPED ROWS WATCHER
|
||||
// ======================================================
|
||||
watch(groupedRows, (val) => {
|
||||
if (!Array.isArray(val)) return
|
||||
console.log(
|
||||
'👀 groupedRows değişti:',
|
||||
val.map(g => ({
|
||||
name: g.name,
|
||||
count: g.rows?.length || 0
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// =============================================
|
||||
// ✅ onCariChange — %100 SAFE + CurrAccTypeCode Entegre
|
||||
// =============================================
|
||||
// =============================================
|
||||
// ✅ onCariChange — FINAL (pb scope FIX)
|
||||
// =============================================
|
||||
async function onCariChange(kod) {
|
||||
let pb = 'USD' // ✅ dış scope: try/catch/finally hepsinde erişilebilir
|
||||
|
||||
try {
|
||||
if (!kod) return
|
||||
|
||||
// 🔹 Cari kaydını bul
|
||||
const cari = cariOptions.value.find(c => c.Cari_Kod === kod)
|
||||
if (!cari) {
|
||||
console.warn('⚠️ Cari bulunamadı:', kod)
|
||||
return
|
||||
}
|
||||
|
||||
selectedCari.value = kod
|
||||
cariInfo.value = cari
|
||||
|
||||
// 🔹 Para birimi (fallback’li)
|
||||
pb =
|
||||
cari.Doviz_Cinsi ||
|
||||
cari.ParaBirimi ||
|
||||
cari.DocCurrencyCode ||
|
||||
'USD'
|
||||
|
||||
// 🔹 FORM sync (UI için)
|
||||
form.CurrAccTypeCode = cari.CurrAccTypeCode || 1
|
||||
form.CurrAccCode = kod
|
||||
form.DocCurrencyCode = pb
|
||||
form.pb = pb
|
||||
aktifPB.value = pb
|
||||
|
||||
/* =====================================================
|
||||
🔥 STORE HEADER SYNC
|
||||
===================================================== */
|
||||
orderStore.setHeaderFields(
|
||||
{
|
||||
CurrAccTypeCode: form.CurrAccTypeCode,
|
||||
CurrAccCode: kod,
|
||||
DocCurrencyCode: pb,
|
||||
PriceCurrencyCode: pb
|
||||
},
|
||||
{
|
||||
applyCurrencyToLines: true,
|
||||
immediatePersist: true
|
||||
}
|
||||
)
|
||||
|
||||
/* =====================================================
|
||||
💱 Kur (opsiyonel)
|
||||
===================================================== */
|
||||
if (orderStore.getTodayRate) {
|
||||
try {
|
||||
const rate = await orderStore.getTodayRate(pb, 'TRY')
|
||||
if (!isNaN(rate)) {
|
||||
orderStore.setHeaderFields({ ExchangeRate: Number(rate) })
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Kur alınamadı:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 🔁 Toplamları yenile
|
||||
recalcVat()
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `Cari değiştirildi → ${kod} (${pb})`,
|
||||
position: 'top-right'
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('❌ onCariChange hata:', err)
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Cari değiştirilemedi',
|
||||
position: 'top-right'
|
||||
})
|
||||
} finally {
|
||||
// 🔥 X3: para birimini satırlara yay (varsa)
|
||||
if (orderStore.applyCurrencyToLines) {
|
||||
orderStore.applyCurrencyToLines(pb)
|
||||
}
|
||||
// 💾 tek persist
|
||||
orderStore.persistLocalStorage?.()
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// 🔹 STICKY VARIABLE GÜNCELLEYİCİ (Eksik Olan Fonksiyon)
|
||||
// CSS değişkenlerini DOM üzerinden yeniden hesaplar
|
||||
// ===========================================================
|
||||
// 🔹 Sticky değişkenleri güncelle
|
||||
function updateStickyVars() {
|
||||
try {
|
||||
const root = document.documentElement
|
||||
const headerH = document.querySelector('.q-header')?.offsetHeight || 56
|
||||
const filterH = document.querySelector('.filter-bar')?.offsetHeight || 72
|
||||
const saveH = document.querySelector('.save-toolbar')?.offsetHeight || 52
|
||||
const totalSticky = headerH + filterH + saveH
|
||||
|
||||
root.style.setProperty('--header-h', `${headerH}px`)
|
||||
root.style.setProperty('--filter-h', `${filterH}px`)
|
||||
root.style.setProperty('--save-h', `${saveH}px`)
|
||||
root.style.setProperty('--sticky-total', `${totalSticky}px`)
|
||||
|
||||
console.log(`📐 Sticky vars → header:${headerH}, filter:${filterH}, save:${saveH}`)
|
||||
} catch (err) {
|
||||
console.warn('⚠️ updateStickyVars hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 Header ile grid arasındaki boşluğu ölç
|
||||
function measureHeaderGap() {
|
||||
try {
|
||||
const hdr = document.querySelector('.order-grid-header')
|
||||
if (!hdr) return
|
||||
const height = hdr.getBoundingClientRect().height || 0
|
||||
const gap = -height
|
||||
document.documentElement.style.setProperty('--header-body-gap', `${gap}px`)
|
||||
console.log('📏 Header boşluğu ölçüldü:', height, 'gap:', gap)
|
||||
} catch (err) {
|
||||
console.warn('⚠️ measureHeaderGap hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
272
ui/src/pages/OrderGateway.vue
Normal file
272
ui/src/pages/OrderGateway.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<q-page
|
||||
v-if="canReadOrder"
|
||||
class="order-gateway flex flex-center column"
|
||||
>
|
||||
|
||||
<div class="text-h5 text-primary q-mb-xl">
|
||||
🧾 Sipariş Modülü
|
||||
</div>
|
||||
|
||||
<!-- 🟡 TASLAK -->
|
||||
<div
|
||||
v-if="hasDraft && canUpdateOrder"
|
||||
class="draft-card q-pa-lg rounded-borders shadow-2 bg-white"
|
||||
>
|
||||
<div class="text-subtitle1 text-bold text-negative">
|
||||
📌 Devam Eden Taslak Bulundu
|
||||
</div>
|
||||
|
||||
<div class="q-mt-sm">
|
||||
<div v-if="draftNumber">
|
||||
<b>No:</b> {{ draftNumber }}
|
||||
</div>
|
||||
<div v-else class="text-grey-7">
|
||||
Numara alınamadı
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="q-mt-md"
|
||||
color="primary"
|
||||
icon="login"
|
||||
label="TASLAĞA DEVAM ET"
|
||||
:disable="!canUpdateOrder"
|
||||
@click="continueDraft"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 🔘 BUTTONS -->
|
||||
<div class="row q-gutter-lg q-mt-xl">
|
||||
|
||||
<!-- NEW ORDER -->
|
||||
<q-btn
|
||||
v-if="canWriteOrder"
|
||||
color="primary"
|
||||
icon="add_circle"
|
||||
label="YENİ SİPARİŞ OLUŞTUR"
|
||||
@click="confirmNewOrder"
|
||||
/>
|
||||
|
||||
<!-- ORDER LIST -->
|
||||
<q-btn
|
||||
v-if="canReadOrder"
|
||||
color="secondary"
|
||||
icon="folder_open"
|
||||
label="MEVCUT SİPARİŞİ AÇ"
|
||||
@click="goOrderList"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- YETKİ YOKSA -->
|
||||
<div
|
||||
v-if="!canReadOrder"
|
||||
class="text-negative text-subtitle1 q-mt-xl"
|
||||
>
|
||||
Bu modüle erişim yetkiniz yok.
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useOrderEntryStore } from 'src/stores/orderentryStore'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const router = useRouter()
|
||||
const $q = useQuasar()
|
||||
const store = useOrderEntryStore()
|
||||
const activeNewHeaderID = computed(() => {
|
||||
try {
|
||||
return localStorage.getItem(store.getLastTxnKey)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🔍 NEW DRAFT — TEK VE DOĞRU KAYNAK
|
||||
→ SADECE store.getDraftKey
|
||||
=========================================================== */
|
||||
const draftRaw = computed(() => {
|
||||
try {
|
||||
return localStorage.getItem(store.getDraftKey)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const hasDraft = computed(() => {
|
||||
if (!draftRaw.value) return false
|
||||
try {
|
||||
const snap = JSON.parse(draftRaw.value)
|
||||
return snap?.mode === 'new'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const draftNumber = computed(() => {
|
||||
if (!hasDraft.value) return null
|
||||
try {
|
||||
return JSON.parse(draftRaw.value)?.header?.OrderNumber || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
function continueDraft () {
|
||||
|
||||
if (!canUpdateOrder.value) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Taslak güncelleme yetkiniz yok'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 1) önce store meta
|
||||
let activeId = store.getActiveNewHeaderId?.()
|
||||
|
||||
// 2) fallback: draft payload içinden
|
||||
if (!activeId) {
|
||||
try {
|
||||
const raw = localStorage.getItem(store.getDraftKey)
|
||||
const snap = raw ? JSON.parse(raw) : null
|
||||
activeId = snap?.header?.OrderHeaderID || null
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!activeId) {
|
||||
$q.notify({ type: 'warning', message: 'Devam edecek taslak bulunamadı' })
|
||||
return
|
||||
}
|
||||
|
||||
// name resolve + path fallback
|
||||
const target = {
|
||||
name: 'order-entry',
|
||||
params: { orderHeaderID: String(activeId) },
|
||||
query: { mode: 'new', source: 'draft' }
|
||||
}
|
||||
|
||||
// DEBUG: resolve sonucu
|
||||
console.log('➡️ continueDraft resolve:', router.resolve(target))
|
||||
|
||||
router.push(target).catch(err => {
|
||||
console.warn('❌ continueDraft push failed, fallback to path:', err)
|
||||
router.push({
|
||||
path: `/app/order-entry/${encodeURIComponent(String(activeId))}`,
|
||||
query: { mode: 'new', source: 'draft' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
📂 Mevcut Sipariş Listesi
|
||||
=========================================================== */
|
||||
function goOrderList () {
|
||||
router.push({ name: 'order-list' })
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🧹 NEW Taslağı Temizle (SADECE NEW)
|
||||
=========================================================== */
|
||||
function clearNewDraft () {
|
||||
try {
|
||||
localStorage.removeItem(store.getDraftKey)
|
||||
if (store.getLastTxnKey) {
|
||||
localStorage.removeItem(store.getLastTxnKey)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🟢 Yeni Sipariş Onayı
|
||||
=========================================================== */
|
||||
function confirmNewOrder () {
|
||||
|
||||
if (!canWriteOrder.value) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Yeni sipariş yetkiniz yok'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasDraft.value) {
|
||||
goNewOrder()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
$q.dialog({
|
||||
title: 'Yeni Sipariş',
|
||||
message: 'Önceki NEW taslak silinecek. Onaylıyor musun?',
|
||||
ok: { label: 'Evet', color: 'negative' },
|
||||
cancel: { flat: true, label: 'Hayır' },
|
||||
persistent: true
|
||||
}).onOk(() => {
|
||||
clearNewDraft()
|
||||
goNewOrder()
|
||||
})
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🟦 Yeni Sipariş Başlat — TAM SIFIR
|
||||
=========================================================== */
|
||||
async function goNewOrder () {
|
||||
try {
|
||||
store.preventPersist = true
|
||||
store.resetForNewOrder()
|
||||
store.preventPersist = false
|
||||
|
||||
const header = await store.startNewOrder({ $q })
|
||||
const newId = header?.OrderHeaderID
|
||||
|
||||
if (!newId) {
|
||||
console.error('❌ startNewOrder OrderHeaderID üretmedi:', header)
|
||||
$q.notify({ type: 'negative', message: 'OrderHeaderID üretilemedi!' })
|
||||
return
|
||||
}
|
||||
|
||||
const target = {
|
||||
name: 'order-entry',
|
||||
params: { orderHeaderID: String(newId) },
|
||||
query: { mode: 'new', source: 'new' }
|
||||
}
|
||||
|
||||
// DEBUG: resolve sonucu (çok kritik)
|
||||
console.log('➡️ goNewOrder resolve:', router.resolve(target))
|
||||
|
||||
// ✅ mutlaka await + catch
|
||||
await router.push(target).catch(async (err) => {
|
||||
console.warn('❌ router.push failed, fallback to path:', err)
|
||||
await router.push({
|
||||
path: `/app/order-entry/${encodeURIComponent(String(newId))}`,
|
||||
query: { mode: 'new', source: 'new' }
|
||||
})
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ goNewOrder hata:', err)
|
||||
$q.notify({ type: 'negative', message: 'Yeni sipariş oluşturulamadı!' })
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
408
ui/src/pages/OrderList.vue
Normal file
408
ui/src/pages/OrderList.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<q-page class="ol-page">
|
||||
<!-- 🔍 Sticky Filter -->
|
||||
<div class="ol-filter-bar">
|
||||
|
||||
<!-- 🔹 TEK SATIR FLEX -->
|
||||
<div class="ol-filter-row">
|
||||
|
||||
<!-- 🔍 Arama -->
|
||||
<q-input
|
||||
class="ol-filter-input ol-search"
|
||||
dense
|
||||
filled
|
||||
v-model="store.filters.search"
|
||||
label="Arama (Sipariş No / Cari / Açıklama)"
|
||||
debounce="300"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- 🧾 Cari Kodu -->
|
||||
<q-input
|
||||
class="ol-filter-input"
|
||||
dense
|
||||
filled
|
||||
v-model="store.filters.CurrAccCode"
|
||||
label="Cari Kodu"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<!-- 📅 Sipariş Tarihi -->
|
||||
<q-input
|
||||
class="ol-filter-input"
|
||||
dense
|
||||
filled
|
||||
v-model="store.filters.OrderDate"
|
||||
label="Sipariş Tarihi"
|
||||
type="date"
|
||||
/>
|
||||
|
||||
<!-- 🔘 Butonlar -->
|
||||
<div class="ol-filter-actions">
|
||||
|
||||
<q-btn
|
||||
label="Temizle"
|
||||
icon="clear"
|
||||
color="grey-7"
|
||||
flat
|
||||
:disable="store.loading"
|
||||
@click="clearFilters"
|
||||
>
|
||||
<q-tooltip>
|
||||
Tüm filtreleri temizle
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
label="Yenile"
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
:loading="store.loading"
|
||||
@click="store.fetchOrders"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
label="Excel'e Aktar"
|
||||
icon="download"
|
||||
color="primary"
|
||||
outline
|
||||
:disable="store.loading || store.filteredOrders.length === 0"
|
||||
@click="exportExcel"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 💰 Toplam -->
|
||||
<div class="ol-filter-total">
|
||||
Toplam Görünen Sipariş Tutarı (USD):
|
||||
<strong>
|
||||
{{ store.totalVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
|
||||
USD
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 📋 ORDER LIST TABLE -->
|
||||
<q-table
|
||||
title="Mevcut Siparişler"
|
||||
class="ol-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
separator="cell"
|
||||
row-key="OrderHeaderID"
|
||||
:rows="store.filteredOrders"
|
||||
:columns="columns"
|
||||
:loading="store.loading"
|
||||
no-data-label="Sipariş bulunamadı"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
>
|
||||
|
||||
<!-- 📄 PDF + DURUM -->
|
||||
<template #body-cell-IsCreditableConfirmed="props">
|
||||
<q-td :props="props" class="text-center q-gutter-sm">
|
||||
|
||||
<q-btn
|
||||
icon="picture_as_pdf"
|
||||
color="red"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
@click="printPDF(props.row)"
|
||||
>
|
||||
<q-tooltip>Siparişi PDF olarak aç</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-icon
|
||||
:name="props.row.IsCreditableConfirmed ? 'check_circle' : 'cancel'"
|
||||
:color="props.row.IsCreditableConfirmed ? 'green' : 'red'"
|
||||
size="20px"
|
||||
>
|
||||
<q-tooltip>
|
||||
{{ props.row.IsCreditableConfirmed ? 'Onaylı' : 'Onaysız' }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- 📅 Tarih -->
|
||||
<template #body-cell-OrderDate="props">
|
||||
<q-td :props="props" class="text-center">
|
||||
{{ formatDate(props.row.OrderDate) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-CreditableConfirmedDate="props">
|
||||
<q-td :props="props" class="text-center">
|
||||
{{ formatDate(props.row.CreditableConfirmedDate) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- 🧾 Cari Adı — 2 Satır -->
|
||||
<template #body-cell-CurrAccDescription="props">
|
||||
<q-td :props="props" class="ol-col-cari ol-col-multiline">
|
||||
{{ props.value }}
|
||||
<q-tooltip v-if="props.value">
|
||||
{{ props.value }}
|
||||
</q-tooltip>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- 📝 Açıklama — 5 Satır -->
|
||||
<template #body-cell-Description="props">
|
||||
<q-td :props="props" class="ol-col-desc ol-col-multiline">
|
||||
{{ props.value }}
|
||||
<q-tooltip v-if="props.value">
|
||||
{{ props.value }}
|
||||
</q-tooltip>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- 🔗 Aç -->
|
||||
<template #body-cell-select="props">
|
||||
<q-td :props="props" class="text-center">
|
||||
<q-btn
|
||||
icon="open_in_new"
|
||||
color="primary"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
@click="selectOrder(props.row)"
|
||||
>
|
||||
<q-tooltip>Siparişi Aç</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
</q-table>
|
||||
|
||||
<!-- ❌ HATA -->
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
|
||||
❌ {{ store.error }}
|
||||
</q-banner>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useQuasar } from 'quasar'
|
||||
|
||||
import { useOrderListStore } from 'src/stores/OrdernewListStore'
|
||||
import { useAuthStore } from 'src/stores/authStore'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
/* =========================
|
||||
INIT
|
||||
========================= */
|
||||
|
||||
const router = useRouter()
|
||||
const $q = useQuasar()
|
||||
|
||||
// ⚠️ ÖNCE store tanımlanır
|
||||
const store = useOrderListStore()
|
||||
|
||||
/* =========================
|
||||
SEARCH DEBOUNCE
|
||||
========================= */
|
||||
|
||||
let searchTimer = null
|
||||
|
||||
watch(
|
||||
() => store.filters.search,
|
||||
() => {
|
||||
clearTimeout(searchTimer)
|
||||
|
||||
searchTimer = setTimeout(() => {
|
||||
store.fetchOrders()
|
||||
}, 400)
|
||||
}
|
||||
)
|
||||
|
||||
/* =========================
|
||||
HELPERS
|
||||
========================= */
|
||||
function exportExcel () {
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (!auth?.token) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Oturum bulunamadı',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
search: store.filters.search || '',
|
||||
CurrAccCode: store.filters.CurrAccCode || '',
|
||||
OrderDate: store.filters.OrderDate || ''
|
||||
})
|
||||
|
||||
const url = `http://localhost:8080/api/orders/export?${params.toString()}`
|
||||
|
||||
fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.token}`
|
||||
}
|
||||
})
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = 'siparis_listesi.xlsx'
|
||||
link.click()
|
||||
})
|
||||
}
|
||||
|
||||
function formatDate (s) {
|
||||
if (!s) return ''
|
||||
const [y, m, d] = s.split('-')
|
||||
return `${d}.${m}.${y}`
|
||||
}
|
||||
|
||||
/* =========================
|
||||
TABLE COLUMNS
|
||||
========================= */
|
||||
|
||||
const columns = [
|
||||
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false },
|
||||
|
||||
{ name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true },
|
||||
{ name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true },
|
||||
|
||||
{ name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true },
|
||||
|
||||
{
|
||||
name: 'CurrAccDescription',
|
||||
label: 'Cari Adı',
|
||||
field: 'CurrAccDescription',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
classes: 'ol-col-cari',
|
||||
headerClasses: 'ol-col-cari',
|
||||
style: 'max-width:200px'
|
||||
},
|
||||
|
||||
{ name: 'MusteriTemsilcisi', label: 'Temsilci', field: 'MusteriTemsilcisi', align: 'left', sortable: true },
|
||||
{ name: 'Piyasa', label: 'Piyasa', field: 'Piyasa', align: 'left', sortable: true },
|
||||
|
||||
{ name: 'CreditableConfirmedDate', label: 'Onay', field: 'CreditableConfirmedDate', align: 'center', sortable: true },
|
||||
{ name: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true },
|
||||
|
||||
{
|
||||
name: 'TotalAmount',
|
||||
label: 'Tutar',
|
||||
field: 'TotalAmount',
|
||||
align: 'right',
|
||||
sortable: true,
|
||||
format: (val, row) =>
|
||||
Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) +
|
||||
' ' + row.DocCurrencyCode
|
||||
},
|
||||
|
||||
{
|
||||
name: 'TotalAmountUSD',
|
||||
label: 'Tutar (USD)',
|
||||
field: 'TotalAmountUSD',
|
||||
align: 'right',
|
||||
sortable: true,
|
||||
format: val =>
|
||||
Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD'
|
||||
},
|
||||
|
||||
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
|
||||
|
||||
{
|
||||
name: 'Description',
|
||||
label: 'Açıklama',
|
||||
field: 'Description',
|
||||
align: 'left',
|
||||
sortable: false,
|
||||
classes: 'ol-col-desc',
|
||||
headerClasses: 'ol-col-desc',
|
||||
style: 'max-width:220px'
|
||||
},
|
||||
|
||||
{ name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false }
|
||||
]
|
||||
|
||||
/* =========================
|
||||
ACTIONS
|
||||
========================= */
|
||||
|
||||
function selectOrder (row) {
|
||||
if (!row?.OrderHeaderID) {
|
||||
$q.notify({ type: 'warning', message: 'OrderHeaderID bulunamadı' })
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
name: 'order-edit',
|
||||
params: { orderHeaderID: row.OrderHeaderID },
|
||||
query: { mode: 'edit' }
|
||||
})
|
||||
}
|
||||
|
||||
async function printPDF (row) {
|
||||
if (!row?.OrderHeaderID) return
|
||||
|
||||
const token = useAuthStore().token
|
||||
const url = `http://localhost:8080/api/order/pdf/${row.OrderHeaderID}`
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error()
|
||||
|
||||
const blob = await res.blob()
|
||||
window.open(URL.createObjectURL(blob), '_blank')
|
||||
} catch {
|
||||
$q.notify({ type: 'negative', message: 'PDF yüklenemedi' })
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters () {
|
||||
store.filters.search = ''
|
||||
store.filters.CurrAccCode = ''
|
||||
store.filters.OrderDate = ''
|
||||
|
||||
store.fetchOrders()
|
||||
|
||||
$q.notify({
|
||||
type: 'info',
|
||||
message: 'Filtreler temizlendi',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
/* =========================
|
||||
INIT LOAD
|
||||
========================= */
|
||||
|
||||
onMounted(() => {
|
||||
store.fetchOrders()
|
||||
})
|
||||
</script>
|
||||
|
||||
1
ui/src/pages/OrderPdf.vue
Normal file
1
ui/src/pages/OrderPdf.vue
Normal file
@@ -0,0 +1 @@
|
||||
<template></template>
|
||||
301
ui/src/pages/PermissionMatrix.vue
Normal file
301
ui/src/pages/PermissionMatrix.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<q-page padding>
|
||||
|
||||
<div class="text-h6 q-mb-md">
|
||||
Rol + Departman Yetkilendirme
|
||||
</div>
|
||||
|
||||
<!-- SELECTS -->
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
|
||||
<div class="col-4">
|
||||
<q-select
|
||||
v-model="roleId"
|
||||
:options="roles"
|
||||
label="Rol"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
@update:model-value="loadMatrix"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<q-select
|
||||
v-model="deptCode"
|
||||
:options="departments"
|
||||
label="Departman"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
@update:model-value="loadMatrix"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- TABLE -->
|
||||
<q-table
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
row-key="module"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
:loading="loading"
|
||||
>
|
||||
|
||||
<template v-slot:body-cell="props">
|
||||
|
||||
<q-td :props="props">
|
||||
|
||||
<!-- MODULE NAME -->
|
||||
<span v-if="props.col.name === 'module'">
|
||||
{{ props.row.label }}
|
||||
</span>
|
||||
|
||||
<!-- CHECKBOX -->
|
||||
<q-checkbox
|
||||
v-else
|
||||
v-model="props.row[props.col.name]"
|
||||
dense
|
||||
@update:model-value="dirty = true"
|
||||
/>
|
||||
|
||||
</q-td>
|
||||
|
||||
</template>
|
||||
|
||||
</q-table>
|
||||
|
||||
|
||||
<!-- SAVE -->
|
||||
<div class="q-mt-md">
|
||||
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Kaydet"
|
||||
:disable="!dirty"
|
||||
@click="save"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
|
||||
/* ================= STATE ================= */
|
||||
|
||||
const roles = ref([])
|
||||
const departments = ref([])
|
||||
|
||||
const roleId = ref(null)
|
||||
const deptCode = ref(null)
|
||||
|
||||
const rows = ref([])
|
||||
|
||||
const loading = ref(false)
|
||||
const dirty = ref(false)
|
||||
|
||||
|
||||
/* ================= ACTION MAP ================= */
|
||||
|
||||
const actions = [
|
||||
{ key: 'write', label: 'Ekleme' },
|
||||
{ key: 'read', label: 'Görüntüleme' },
|
||||
{ key: 'delete', label: 'Silme' },
|
||||
{ key: 'update', label: 'Güncelleme' },
|
||||
{ key: 'export', label: 'Çıktı' }
|
||||
]
|
||||
|
||||
|
||||
|
||||
/* ================= MODULES ================= */
|
||||
|
||||
const [r, d, m] = await Promise.all([
|
||||
api.get('/lookups/roles'),
|
||||
api.get('/lookups/departments'),
|
||||
api.get('/lookups/modules')
|
||||
])
|
||||
|
||||
modules.value = m.data || []
|
||||
|
||||
|
||||
/* ================= TABLE ================= */
|
||||
|
||||
const columns = [
|
||||
|
||||
{
|
||||
name: 'module',
|
||||
label: 'Modül',
|
||||
field: 'label',
|
||||
align: 'left'
|
||||
},
|
||||
|
||||
...actions.map(a => ({
|
||||
name: a.key,
|
||||
label: a.label,
|
||||
align: 'center'
|
||||
}))
|
||||
]
|
||||
|
||||
|
||||
/* ================= LOAD LOOKUPS ================= */
|
||||
|
||||
async function loadLookups () {
|
||||
|
||||
const [r, d] = await Promise.all([
|
||||
|
||||
api.get('/lookups/roles'),
|
||||
api.get('/lookups/departments')
|
||||
|
||||
])
|
||||
|
||||
roles.value = r.data
|
||||
departments.value = d.data
|
||||
}
|
||||
|
||||
|
||||
/* ================= INIT TABLE ================= */
|
||||
|
||||
function initMatrix () {
|
||||
|
||||
rows.value = modules.map(m => {
|
||||
|
||||
const row = {
|
||||
module: m.code,
|
||||
label: m.label
|
||||
}
|
||||
|
||||
actions.forEach(a => {
|
||||
row[a.key] = false
|
||||
})
|
||||
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* ================= LOAD ================= */
|
||||
|
||||
async function loadMatrix () {
|
||||
|
||||
if (!roleId.value || !deptCode.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
|
||||
initMatrix()
|
||||
|
||||
const res = await api.get(
|
||||
`/roles/${roleId.value}/departments/${deptCode.value}/permissions`
|
||||
)
|
||||
|
||||
list.forEach(p => {
|
||||
|
||||
const code = String(p.module_code || p.module)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
// 🔥 kritik
|
||||
|
||||
const row = rows.value.find(r => r.module === code)
|
||||
|
||||
if (row) {
|
||||
row[p.action] = p.allowed
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
dirty.value = false
|
||||
|
||||
} catch {
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Yetkiler yüklenemedi'
|
||||
})
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ================= SAVE ================= */
|
||||
|
||||
async function save () {
|
||||
|
||||
try {
|
||||
|
||||
loading.value = true
|
||||
|
||||
const payload = []
|
||||
|
||||
rows.value.forEach(r => {
|
||||
|
||||
actions.forEach(a => {
|
||||
|
||||
payload.push({
|
||||
module: r.module,
|
||||
action: a.key,
|
||||
allowed: r[a.key]
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
await api.post(
|
||||
`/roles/${roleId.value}/departments/${deptCode.value}/permissions`,
|
||||
payload
|
||||
)
|
||||
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Kaydedildi'
|
||||
})
|
||||
|
||||
dirty.value = false
|
||||
|
||||
} catch {
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Kayıt hatası'
|
||||
})
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ================= INIT ================= */
|
||||
|
||||
onMounted(() => {
|
||||
loadLookups()
|
||||
})
|
||||
|
||||
</script>
|
||||
224
ui/src/pages/ProductionWorker.vue
Normal file
224
ui/src/pages/ProductionWorker.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<q-page class="workorder-page with-bg">
|
||||
|
||||
<!-- ===============================
|
||||
🔹 ÜST: İŞ EMRİ BİLGİLERİ
|
||||
=============================== -->
|
||||
<q-card flat bordered class="q-mb-md">
|
||||
<q-card-section class="row q-col-gutter-md">
|
||||
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
label="İş Emri No"
|
||||
v-model="form.workOrderNo"
|
||||
dense
|
||||
filled
|
||||
:readonly="isViewMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<q-select
|
||||
label="Ürün Tipi"
|
||||
v-model="form.productType"
|
||||
:options="productTypes"
|
||||
dense
|
||||
filled
|
||||
:disable="isViewMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
label="Termin"
|
||||
type="date"
|
||||
v-model="form.deliveryDate"
|
||||
dense
|
||||
filled
|
||||
:readonly="isViewMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
label="Adet"
|
||||
type="number"
|
||||
v-model="form.quantity"
|
||||
dense
|
||||
filled
|
||||
:readonly="isViewMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- ===============================
|
||||
🔹 ORTA: GÖRSEL REFERANSLAR
|
||||
=============================== -->
|
||||
<div class="row q-col-gutter-md">
|
||||
|
||||
<!-- 🔸 SOL: ANA GÖRSEL -->
|
||||
<div class="col-4">
|
||||
<q-card bordered>
|
||||
<q-card-section class="text-weight-bold">
|
||||
Ana Görsel
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div v-if="mainImage">
|
||||
<img :src="mainImage.src" class="image-preview" />
|
||||
</div>
|
||||
|
||||
<div v-else class="text-grey text-caption">
|
||||
Ana görsel seçilmedi
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
v-if="isCreateMode"
|
||||
label="Ana Görsel Seç"
|
||||
icon="image"
|
||||
class="q-mt-sm"
|
||||
color="primary"
|
||||
@click="openImagePicker('MAIN')"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- 🔸 ORTA: DETAY GÖRSELLER -->
|
||||
<div class="col-5">
|
||||
<q-card bordered>
|
||||
<q-card-section class="text-weight-bold">
|
||||
Detay Görseller
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="row q-col-gutter-sm">
|
||||
<div
|
||||
v-for="(img, index) in detailImages"
|
||||
:key="index"
|
||||
class="col-6"
|
||||
>
|
||||
<q-card flat bordered>
|
||||
<img :src="img.src" class="image-thumb" />
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="img.note"
|
||||
type="textarea"
|
||||
dense
|
||||
label="Not"
|
||||
:readonly="isViewMode"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div v-if="isCreateMode" class="col-12">
|
||||
<q-btn
|
||||
label="Detay Görsel Ekle"
|
||||
icon="add"
|
||||
flat
|
||||
color="primary"
|
||||
@click="openImagePicker('DETAIL')"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- 🔸 SAĞ: TALİMAT / TELA -->
|
||||
<div class="col-3">
|
||||
<q-card bordered>
|
||||
<q-card-section class="text-weight-bold">
|
||||
Talimat / Tela
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div
|
||||
v-for="(img, index) in instructionImages"
|
||||
:key="index"
|
||||
class="q-mb-sm"
|
||||
>
|
||||
<img :src="img.src" class="image-thumb" />
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
v-if="isCreateMode"
|
||||
label="Talimat Görseli Ekle"
|
||||
icon="add"
|
||||
flat
|
||||
color="primary"
|
||||
@click="openImagePicker('INSTRUCTION')"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ===============================
|
||||
🔹 ALT: AKSİYONLAR
|
||||
=============================== -->
|
||||
<div class="row justify-end q-mt-md">
|
||||
<q-btn
|
||||
v-if="isCreateMode"
|
||||
label="Kaydet"
|
||||
color="positive"
|
||||
icon="save"
|
||||
@click="saveWorkOrder"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-if="isViewMode"
|
||||
label="PDF"
|
||||
color="primary"
|
||||
icon="picture_as_pdf"
|
||||
class="q-ml-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const isViewMode = computed(() => !!route.params.id)
|
||||
const isCreateMode = computed(() => !route.params.id)
|
||||
|
||||
const productTypes = ['CEKET', 'PANTOLON']
|
||||
|
||||
const form = ref({
|
||||
workOrderNo: '',
|
||||
productType: null,
|
||||
deliveryDate: '',
|
||||
quantity: null
|
||||
})
|
||||
|
||||
const mainImage = ref(null)
|
||||
const detailImages = ref([])
|
||||
const instructionImages = ref([])
|
||||
|
||||
function openImagePicker(type) {
|
||||
// Şimdilik stub
|
||||
console.log('Image picker:', type)
|
||||
}
|
||||
|
||||
function saveWorkOrder() {
|
||||
console.log('SAVE', {
|
||||
form: form.value,
|
||||
mainImage: mainImage.value,
|
||||
detailImages: detailImages.value,
|
||||
instructionImages: instructionImages.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
211
ui/src/pages/ProductionWorkerGateway.vue
Normal file
211
ui/src/pages/ProductionWorkerGateway.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
|
||||
<!-- =====================================================
|
||||
🔹 BAŞLIK + AKSİYONLAR
|
||||
====================================================== -->
|
||||
<div class="row items-center justify-between q-mb-md">
|
||||
<div class="text-h6 text-weight-bold">
|
||||
Üretim İş Emirleri
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="Yeni İş Emri"
|
||||
@click="goNew"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- =====================================================
|
||||
🔹 FİLTRE BAR (ileride genişletilir)
|
||||
====================================================== -->
|
||||
<q-card flat bordered class="q-mb-md">
|
||||
<q-card-section class="row q-col-gutter-md">
|
||||
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
v-model="filters.workOrderNo"
|
||||
dense
|
||||
filled
|
||||
label="İş Emri No"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<q-select
|
||||
v-model="filters.productType"
|
||||
:options="productTypes"
|
||||
dense
|
||||
filled
|
||||
label="Ürün Tipi"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<q-select
|
||||
v-model="filters.status"
|
||||
:options="statuses"
|
||||
dense
|
||||
filled
|
||||
label="Durum"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- =====================================================
|
||||
📋 İŞ EMRİ LİSTESİ
|
||||
====================================================== -->
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
row-key="id"
|
||||
:rows="filteredRows"
|
||||
:columns="columns"
|
||||
:pagination="{ rowsPerPage: 10 }"
|
||||
@row-click="goView"
|
||||
>
|
||||
<!-- 🔹 DURUM BADGE -->
|
||||
<template #body-cell-status="props">
|
||||
<q-td :props="props">
|
||||
<q-badge
|
||||
:color="statusColor(props.value)"
|
||||
outline
|
||||
>
|
||||
{{ props.value }}
|
||||
</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// =====================================================
|
||||
// 🔹 TABLO KOLONLARI
|
||||
// =====================================================
|
||||
const columns = [
|
||||
{
|
||||
name: 'workOrderNo',
|
||||
label: 'İş Emri No',
|
||||
field: 'workOrderNo',
|
||||
align: 'left',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'productType',
|
||||
label: 'Ürün',
|
||||
field: 'productType',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
label: 'Adet',
|
||||
field: 'quantity',
|
||||
align: 'right'
|
||||
},
|
||||
{
|
||||
name: 'deliveryDate',
|
||||
label: 'Termin',
|
||||
field: 'deliveryDate'
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Durum',
|
||||
field: 'status'
|
||||
}
|
||||
]
|
||||
|
||||
// =====================================================
|
||||
// 🔹 MOCK DATA (backend bağlanınca kaldırılacak)
|
||||
// =====================================================
|
||||
const rows = ref([
|
||||
{
|
||||
id: 101,
|
||||
workOrderNo: 'UIE-2026-001',
|
||||
productType: 'CEKET',
|
||||
quantity: 120,
|
||||
deliveryDate: '2026-02-15',
|
||||
status: 'Taslak'
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
workOrderNo: 'UIE-2026-002',
|
||||
productType: 'PANTOLON',
|
||||
quantity: 300,
|
||||
deliveryDate: '2026-02-20',
|
||||
status: 'Onaylandı'
|
||||
}
|
||||
])
|
||||
|
||||
// =====================================================
|
||||
// 🔹 FİLTRELER
|
||||
// =====================================================
|
||||
const productTypes = ['CEKET', 'PANTOLON']
|
||||
const statuses = ['Taslak', 'Onaylandı', 'Üretimde', 'Kapandı']
|
||||
|
||||
const filters = ref({
|
||||
workOrderNo: '',
|
||||
productType: null,
|
||||
status: null
|
||||
})
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter(r => {
|
||||
if (
|
||||
filters.value.workOrderNo &&
|
||||
!r.workOrderNo.includes(filters.value.workOrderNo)
|
||||
) return false
|
||||
|
||||
if (
|
||||
filters.value.productType &&
|
||||
r.productType !== filters.value.productType
|
||||
) return false
|
||||
|
||||
if (
|
||||
filters.value.status &&
|
||||
r.status !== filters.value.status
|
||||
) return false
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// =====================================================
|
||||
// 🔹 AKSİYONLAR
|
||||
// =====================================================
|
||||
function goNew () {
|
||||
router.push('/app/production-work-orders/new')
|
||||
}
|
||||
|
||||
function goView (evt, row) {
|
||||
router.push(`/app/production-work-orders/view/${row.id}`)
|
||||
}
|
||||
|
||||
function statusColor (status) {
|
||||
switch (status) {
|
||||
case 'Taslak': return 'grey'
|
||||
case 'Onaylandı': return 'blue'
|
||||
case 'Üretimde': return 'orange'
|
||||
case 'Kapandı': return 'green'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
264
ui/src/pages/ResetPassword.vue
Normal file
264
ui/src/pages/ResetPassword.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center bg-grey-2">
|
||||
|
||||
<!-- ⏳ VALIDATING -->
|
||||
<q-inner-loading v-if="validating" showing />
|
||||
|
||||
<!-- ✅ TOKEN OK → FORM -->
|
||||
<q-card
|
||||
v-else-if="tokenValid"
|
||||
class="q-pa-sm"
|
||||
style="width:420px; max-width:90vw"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-weight-bold">
|
||||
🔐 Parola Sıfırlama
|
||||
</div>
|
||||
<div class="text-caption text-grey-7 q-mt-xs">
|
||||
Yeni parolanızı belirleyin
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<!-- NEW PASSWORD -->
|
||||
<q-input
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="Yeni Parola"
|
||||
dense
|
||||
filled
|
||||
:rules="[passwordRule]"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon
|
||||
:name="showPassword ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- STRENGTH -->
|
||||
<div class="q-mt-xs">
|
||||
<q-linear-progress
|
||||
:value="passwordStrength.value"
|
||||
:color="passwordStrength.color"
|
||||
rounded
|
||||
size="6px"
|
||||
/>
|
||||
<div class="text-caption q-mt-xs" :class="passwordStrength.textColor">
|
||||
{{ passwordStrength.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONFIRM -->
|
||||
<q-input
|
||||
v-model="password2"
|
||||
:type="showPassword2 ? 'text' : 'password'"
|
||||
label="Parola Tekrar"
|
||||
dense
|
||||
filled
|
||||
class="q-mt-sm"
|
||||
:rules="[confirmRule]"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon
|
||||
:name="showPassword2 ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="showPassword2 = !showPassword2"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- ERROR -->
|
||||
<q-banner
|
||||
v-if="error"
|
||||
class="bg-red-1 text-red q-mt-md"
|
||||
rounded
|
||||
>
|
||||
{{ error }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="PAROLAYI GÜNCELLE"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disable="!canSubmit"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
<!-- ❌ TOKEN INVALID -->
|
||||
<q-card
|
||||
v-else
|
||||
class="q-pa-md text-center"
|
||||
style="width:420px; max-width:90vw"
|
||||
>
|
||||
<div class="text-h6 text-red">
|
||||
Bağlantı Geçersiz
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey-7 q-mt-sm">
|
||||
Parola sıfırlama bağlantısı süresi dolmuş veya daha önce kullanılmış olabilir.
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
label="GİRİŞ SAYFASINA DÖN"
|
||||
color="primary"
|
||||
class="q-mt-md"
|
||||
@click="router.push('/')"
|
||||
/>
|
||||
</q-card>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useQuasar } from 'quasar'
|
||||
|
||||
import api, { post } from 'src/services/api'
|
||||
import { useAuthStore } from 'stores/authStore.js'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* INIT */
|
||||
/* -------------------------------------------------- */
|
||||
const $q = useQuasar()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const token = ref('')
|
||||
const password = ref('')
|
||||
const password2 = ref('')
|
||||
const loading = ref(false)
|
||||
const validating = ref(true)
|
||||
const tokenValid = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const showPassword = ref(false)
|
||||
const showPassword2 = ref(false)
|
||||
|
||||
/* ---------------- VALIDATION ---------------- */
|
||||
const passwordRule = v =>
|
||||
(!!v && v.length >= 8) || 'En az 8 karakter olmalı'
|
||||
|
||||
const confirmRule = v =>
|
||||
v === password.value || 'Parolalar eşleşmiyor'
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
tokenValid.value &&
|
||||
password.value.length >= 8 &&
|
||||
password.value === password2.value &&
|
||||
!loading.value
|
||||
)
|
||||
|
||||
/* ---------------- PASSWORD STRENGTH ---------------- */
|
||||
const passwordStrength = computed(() => {
|
||||
const v = password.value || ''
|
||||
let score = 0
|
||||
|
||||
if (v.length >= 8) score++
|
||||
if (/[A-Z]/.test(v)) score++
|
||||
if (/[0-9]/.test(v)) score++
|
||||
if (/[^A-Za-z0-9]/.test(v)) score++
|
||||
|
||||
const map = [
|
||||
{ value: 0.1, label: 'Çok zayıf', color: 'red', textColor: 'text-red' },
|
||||
{ value: 0.25, label: 'Zayıf', color: 'orange', textColor: 'text-orange' },
|
||||
{ value: 0.5, label: 'Orta', color: 'amber', textColor: 'text-amber' },
|
||||
{ value: 0.75, label: 'İyi', color: 'blue', textColor: 'text-blue' },
|
||||
{ value: 1, label: 'Güçlü', color: 'green', textColor: 'text-green' }
|
||||
]
|
||||
|
||||
return map[Math.min(score, map.length - 1)]
|
||||
})
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* TOKEN VALIDATE */
|
||||
/* -------------------------------------------------- */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
token.value = decodeURIComponent(route.params.token || '')
|
||||
if (!token.value) throw new Error('empty-token')
|
||||
|
||||
// 🔥 MERKEZİ API
|
||||
await api.get(`/password/reset/validate/${token.value}`)
|
||||
tokenValid.value = true
|
||||
} catch {
|
||||
tokenValid.value = false
|
||||
} finally {
|
||||
validating.value = false
|
||||
}
|
||||
})
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* SUBMIT — RESET + AUTO LOGIN */
|
||||
/* -------------------------------------------------- */
|
||||
async function submit () {
|
||||
error.value = null
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await post('/password/reset', {
|
||||
token: token.value,
|
||||
password: password.value
|
||||
})
|
||||
|
||||
if (!res?.success || !res?.token) {
|
||||
throw new Error('reset-failed')
|
||||
}
|
||||
|
||||
/* 🔐 AUTH HYDRATE
|
||||
(login response ile birebir aynı) */
|
||||
auth.token = res.token
|
||||
auth.user = res.user
|
||||
auth.permissions = Array.isArray(res.permissions) ? res.permissions : []
|
||||
auth.role_id = Number(res.user?.role_id || null)
|
||||
auth.forcePasswordChange = false
|
||||
auth.lastLogin = new Date().toISOString()
|
||||
|
||||
// STORAGE
|
||||
localStorage.setItem('token', auth.token)
|
||||
localStorage.setItem('user', JSON.stringify(auth.user))
|
||||
localStorage.setItem('permissions', JSON.stringify(auth.permissions))
|
||||
localStorage.setItem('role_id', String(auth.role_id))
|
||||
localStorage.setItem('lastLogin', auth.lastLogin)
|
||||
localStorage.setItem('forcePasswordChange', '0')
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Parolanız güncellendi, giriş yapıldı',
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
router.replace('/app')
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err?.message ||
|
||||
'Parola politikaya uymuyor (büyük/küçük/rakam/özel karakter)'
|
||||
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.value,
|
||||
position: 'top-right'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
435
ui/src/pages/RoleDepartmentPermissionPage.vue
Normal file
435
ui/src/pages/RoleDepartmentPermissionPage.vue
Normal file
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div v-if="!lookupsLoaded" class="q-pa-xl flex flex-center">
|
||||
|
||||
<q-spinner
|
||||
color="primary"
|
||||
size="48px"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<q-page class="permissions-page">
|
||||
|
||||
<!-- ================= STICKY STACK ================= -->
|
||||
<div class="sticky-stack">
|
||||
|
||||
<!-- FILTER BAR -->
|
||||
<div
|
||||
v-if="lookupsLoaded"
|
||||
class="filter-bar row q-col-gutter-md"
|
||||
>
|
||||
|
||||
|
||||
<div class="col-4">
|
||||
<q-select
|
||||
v-model="roleId"
|
||||
:options="roles"
|
||||
option-value="id"
|
||||
option-label="title"
|
||||
emit-value
|
||||
map-options
|
||||
label="Rol"
|
||||
dense
|
||||
outlined
|
||||
@update:model-value="loadMatrix"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<q-select
|
||||
v-model="deptCode"
|
||||
:options="departments"
|
||||
option-value="id"
|
||||
option-label="title"
|
||||
emit-value
|
||||
map-options
|
||||
label="Departman"
|
||||
dense
|
||||
outlined
|
||||
@update:model-value="loadMatrix"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SAVE TOOLBAR -->
|
||||
<div class="save-toolbar">
|
||||
|
||||
<div class="label">
|
||||
Rol + Departman Yetkilendirme
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Kaydet"
|
||||
:disable="!dirty"
|
||||
@click="save"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ================= TABLE SCROLL ================= -->
|
||||
|
||||
<div
|
||||
v-if="lookupsLoaded"
|
||||
class="permissions-table-scroll"
|
||||
>
|
||||
|
||||
|
||||
<q-table
|
||||
class="permissions-table"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
row-key="module"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
:loading="loading"
|
||||
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
>
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<template v-slot:header-cell="props">
|
||||
|
||||
<q-th :props="props">
|
||||
|
||||
<!-- Module başlığı -->
|
||||
<span v-if="props.col.name === 'module'">
|
||||
{{ props.col.label }}
|
||||
</span>
|
||||
|
||||
<!-- Checkbox kolon başlığı -->
|
||||
<div v-else class="column items-center">
|
||||
|
||||
<span class="text-caption">
|
||||
{{ props.col.label }}
|
||||
</span>
|
||||
|
||||
<q-checkbox
|
||||
dense
|
||||
:model-value="isColumnChecked(props.col.name)"
|
||||
@update:model-value="toggleColumn(props.col.name, $event)"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</q-th>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<!-- ========== BODY ========== -->
|
||||
<template v-slot:body-cell="props">
|
||||
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="props.col.name === 'module'
|
||||
? 'permissions-sticky-col'
|
||||
: ''"
|
||||
>
|
||||
|
||||
<!-- Module adı -->
|
||||
<span v-if="props.col.name === 'module'">
|
||||
{{ props.row.label }}
|
||||
</span>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<q-checkbox
|
||||
v-else
|
||||
v-model="props.row[props.col.name]"
|
||||
dense
|
||||
@update:model-value="dirty = true"
|
||||
/>
|
||||
|
||||
</q-td>
|
||||
|
||||
</template>
|
||||
|
||||
</q-table>
|
||||
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
|
||||
/* ================= STATE ================= */
|
||||
|
||||
const roles = ref([])
|
||||
const departments = ref([])
|
||||
|
||||
const roleId = ref(null)
|
||||
const deptCode = ref(null)
|
||||
|
||||
const rows = ref([])
|
||||
|
||||
const loading = ref(false)
|
||||
const dirty = ref(false)
|
||||
const lookupsLoaded = ref(false)
|
||||
|
||||
|
||||
/* ================= ACTIONS ================= */
|
||||
|
||||
const actions = [
|
||||
{ key: 'write', label: 'Ekleme' },
|
||||
{ key: 'read', label: 'Görüntüleme' },
|
||||
{ key: 'delete', label: 'Silme' },
|
||||
{ key: 'update', label: 'Güncelleme' },
|
||||
{ key: 'export', label: 'Çıktı' }
|
||||
]
|
||||
|
||||
|
||||
/* ================= MODULES ================= */
|
||||
|
||||
const modules = ref([])
|
||||
|
||||
|
||||
/* ================= TABLE ================= */
|
||||
|
||||
const columns = [
|
||||
|
||||
{
|
||||
name: 'module',
|
||||
label: 'Modül',
|
||||
field: 'label',
|
||||
align: 'left'
|
||||
},
|
||||
|
||||
...actions.map(a => ({
|
||||
name: a.key,
|
||||
label: a.label,
|
||||
align: 'center'
|
||||
}))
|
||||
]
|
||||
|
||||
let matrixLoading = false
|
||||
|
||||
|
||||
/* ================= LOOKUPS ================= */
|
||||
|
||||
async function loadLookups () {
|
||||
|
||||
const [r, d, m] = await Promise.all([
|
||||
api.get('/lookups/roles-perm'),
|
||||
api.get('/lookups/departments-perm'),
|
||||
api.get('/lookups/modules')
|
||||
])
|
||||
|
||||
roles.value = r.data || []
|
||||
departments.value = d.data || []
|
||||
modules.value = m.data || []
|
||||
|
||||
lookupsLoaded.value = true
|
||||
}
|
||||
|
||||
|
||||
/* ================= INIT MATRIX ================= */
|
||||
|
||||
function initMatrix () {
|
||||
|
||||
rows.value = modules.value.map(m => {
|
||||
|
||||
const row = {
|
||||
module: String(m.value).toLowerCase().trim(),
|
||||
label: m.label
|
||||
}
|
||||
|
||||
actions.forEach(a => {
|
||||
row[a.key] = false
|
||||
})
|
||||
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* ================= LOAD ================= */
|
||||
|
||||
async function loadMatrix () {
|
||||
|
||||
if (!roleId.value || !deptCode.value) return
|
||||
if (matrixLoading) return
|
||||
|
||||
matrixLoading = true
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
|
||||
if (!modules.value.length) {
|
||||
await loadLookups()
|
||||
}
|
||||
|
||||
initMatrix()
|
||||
|
||||
const res = await api.get(
|
||||
`/roles/${roleId.value}/departments/${deptCode.value}/permissions`
|
||||
)
|
||||
|
||||
const list = Array.isArray(res.data) ? res.data : []
|
||||
|
||||
console.log('PERM LIST:', list.slice(0, 10))
|
||||
|
||||
|
||||
// ✅ BACKEND → UI ACTION MAP
|
||||
const actionMap = {
|
||||
insert: 'write',
|
||||
view: 'read',
|
||||
delete: 'delete',
|
||||
update: 'update',
|
||||
export: 'export'
|
||||
}
|
||||
|
||||
|
||||
list.forEach(p => {
|
||||
|
||||
const code = String(p.module_code || p.module)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
|
||||
const rawAction = String(p.action)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
|
||||
const mappedAction = actionMap[rawAction] || rawAction
|
||||
|
||||
|
||||
const row = rows.value.find(r => r.module === code)
|
||||
|
||||
if (row && row.hasOwnProperty(mappedAction)) {
|
||||
row[mappedAction] = Boolean(p.allowed)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
dirty.value = false
|
||||
|
||||
} catch (err) {
|
||||
|
||||
console.error('PERM LOAD ERROR:', err)
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Yetkiler yüklenemedi'
|
||||
})
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false
|
||||
matrixLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ================= SAVE ================= */
|
||||
|
||||
async function save () {
|
||||
|
||||
try {
|
||||
|
||||
loading.value = true
|
||||
|
||||
const payload = []
|
||||
|
||||
rows.value.forEach(r => {
|
||||
|
||||
actions.forEach(a => {
|
||||
|
||||
payload.push({
|
||||
module: r.module,
|
||||
action: a.key,
|
||||
allowed: r[a.key]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
await api.post(
|
||||
`/roles/${roleId.value}/departments/${deptCode.value}/permissions`,
|
||||
payload
|
||||
)
|
||||
|
||||
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Kaydedildi'
|
||||
})
|
||||
|
||||
dirty.value = false
|
||||
|
||||
} catch {
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Kayıt hatası'
|
||||
})
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ================= COLUMN ================= */
|
||||
|
||||
function isColumnChecked (key) {
|
||||
|
||||
if (!rows.value.length) return false
|
||||
|
||||
return rows.value.every(r => r[key] === true)
|
||||
}
|
||||
|
||||
|
||||
function toggleColumn (key, val) {
|
||||
|
||||
rows.value.forEach(r => {
|
||||
r[key] = val
|
||||
})
|
||||
|
||||
dirty.value = true
|
||||
}
|
||||
|
||||
|
||||
/* ================= INIT ================= */
|
||||
|
||||
onMounted(() => {
|
||||
loadLookups()
|
||||
})
|
||||
|
||||
watch(roleId, v => console.log('ROLE_ID >>>', v))
|
||||
watch(deptCode, v => console.log('DEPT >>>', v))
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
103
ui/src/pages/StatementHeaderReport.vue
Normal file
103
ui/src/pages/StatementHeaderReport.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<!-- src/pages/StatementHeaderReport.vue -->
|
||||
<template>
|
||||
<q-page class="q-pa-md page-col">
|
||||
<!-- Başlık ve PDF butonu -->
|
||||
<div class="row justify-between items-center q-mb-md">
|
||||
<div class="text-h6">📄 Cari Hesap Raporu</div>
|
||||
<q-btn
|
||||
color="red"
|
||||
icon="picture_as_pdf"
|
||||
label="PDF Yazdır"
|
||||
push
|
||||
glossy
|
||||
@click="handlestHeadDownload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-separator spaced />
|
||||
|
||||
<!-- Cari ve tarih seçim alanı -->
|
||||
<q-card flat bordered class="q-pa-md q-mt-md">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-sm-4">
|
||||
<q-input v-model="accountCode" label="Cari Kod" filled dense clearable />
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<q-input v-model="startDate" label="Başlangıç Tarihi" filled dense />
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<q-input v-model="endDate" label="Bitiş Tarihi" filled dense />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-12">
|
||||
<q-select
|
||||
v-model="selectedMonType"
|
||||
:options="monetaryTypeOptions"
|
||||
label="Parasal İşlem Tipi"
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useDownloadstHeadStore } from 'src/stores/downloadstHeadStore'
|
||||
import dayjs from 'dayjs'
|
||||
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 downloadstHeadStore = useDownloadstHeadStore()
|
||||
|
||||
// form değerleri
|
||||
const accountCode = ref('')
|
||||
const startDate = ref(dayjs().startOf('month').format('YYYY-MM-DD'))
|
||||
const endDate = ref(dayjs().format('YYYY-MM-DD'))
|
||||
|
||||
// parasal işlem tipleri
|
||||
const monetaryTypeOptions = [
|
||||
{ label: '1-2 hesap', value: ['1', '2'] },
|
||||
{ label: '1-3 hesap', value: ['1', '3'] }
|
||||
]
|
||||
const selectedMonType = ref(monetaryTypeOptions[0].value)
|
||||
|
||||
// indirme butonu
|
||||
async function handlestHeadDownload() {
|
||||
console.log("▶️ [DEBUG] handlestHeadDownload:", accountCode.value, startDate.value, endDate.value, selectedMonType.value)
|
||||
|
||||
if (!accountCode.value || !startDate.value || !endDate.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Cari ve tarih seçmeden PDF alınamaz!',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await downloadstHeadStore.handlestHeadDownload(
|
||||
accountCode.value,
|
||||
startDate.value,
|
||||
endDate.value,
|
||||
selectedMonType.value
|
||||
)
|
||||
|
||||
$q.notify({
|
||||
type: result.ok ? 'positive' : 'negative',
|
||||
message: result.message,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
499
ui/src/pages/StatementReport.vue
Normal file
499
ui/src/pages/StatementReport.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md page-col">
|
||||
|
||||
<!-- 🔹 Cari Kod / İsim (sabit) -->
|
||||
<div class="filter-sticky">
|
||||
<q-select
|
||||
v-model="selectedCari"
|
||||
:options="filteredOptions"
|
||||
label="Cari kod / isim"
|
||||
filled
|
||||
clearable
|
||||
use-input
|
||||
input-debounce="300"
|
||||
@filter="filterCari"
|
||||
emit-value
|
||||
map-options
|
||||
:loading="accountStore.loading"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
behavior="menu"
|
||||
:keep-selected="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 Filtre Alanı -->
|
||||
<div class="filter-collapsible">
|
||||
<div class="row items-center justify-between q-pa-sm bg-grey-2">
|
||||
<div class="text-subtitle1">Filtreler</div>
|
||||
<q-btn
|
||||
dense flat round
|
||||
:icon="filtersOpen ? 'expand_less' : 'expand_more'"
|
||||
@click="filtersOpen = !filtersOpen"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-slide-transition>
|
||||
<div v-show="filtersOpen" class="q-pa-md bg-grey-1">
|
||||
|
||||
<!-- Tarih Aralığı -->
|
||||
<div class="row q-col-gutter-sm q-mb-md">
|
||||
<div class="col-12 col-sm-6">
|
||||
<q-input
|
||||
v-model="dateFrom"
|
||||
label="Tarih aralığı - başlangıç"
|
||||
filled clearable readonly
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="dateFrom" mask="YYYY-MM-DD" locale="tr-TR"/>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6">
|
||||
<q-input
|
||||
v-model="dateTo"
|
||||
label="Tarih aralığı - bitiş"
|
||||
filled clearable readonly
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="dateTo" mask="YYYY-MM-DD" locale="tr-TR" />
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parasal İşlem Tipi -->
|
||||
<q-select
|
||||
v-model="selectedMonType"
|
||||
:options="monetaryTypeOptions"
|
||||
label="Parasal İşlem Tipi"
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
class="q-mb-md"
|
||||
/>
|
||||
|
||||
<!-- Filtre / Sıfırla Butonları -->
|
||||
<div class="row q-col-gutter-md items-center">
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="filter_alt"
|
||||
label="Filtrele"
|
||||
@click="onFilterClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey-8"
|
||||
icon="restart_alt"
|
||||
label="Sıfırla"
|
||||
@click="resetFilters"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 Tablo Alanı -->
|
||||
<div class="table-scroll">
|
||||
|
||||
<!-- Toggle butonları (sticky üst bar) -->
|
||||
<div class="sticky-bar row justify-between items-center q-pa-sm bg-grey-1">
|
||||
|
||||
<!-- Sol buton: CARİ BİLGİ DETAY göster/gizle -->
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
icon="view_column"
|
||||
:label="showLeftCols ? 'CARİ BİLGİ DETAY Gizle' : 'CARİ BİLGİ DETAY Sütunu Göster'"
|
||||
@click="toggleLeftCols"
|
||||
/>
|
||||
|
||||
<!-- Sağ taraftaki buton grubu -->
|
||||
<div class="row items-center q-gutter-sm">
|
||||
|
||||
<!-- Tüm detayları aç/kapat -->
|
||||
<q-btn
|
||||
flat
|
||||
color="secondary"
|
||||
icon="list"
|
||||
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
|
||||
@click="toggleAllDetails"
|
||||
/>
|
||||
|
||||
<!-- ✅ PDF Yazdır Dropdown -->
|
||||
<q-btn-dropdown
|
||||
flat
|
||||
color="red"
|
||||
icon="picture_as_pdf"
|
||||
label="Yazdır"
|
||||
>
|
||||
<q-list style="min-width: 200px">
|
||||
<!-- 1. Seçenek -->
|
||||
<q-item clickable v-close-popup @click="handleDownload" >
|
||||
<q-item-section class="text-primary">
|
||||
Detaylı Cari Ekstre Yazdır
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- 2. Seçenek -->
|
||||
<q-item clickable v-close-popup @click="CurrheadDownload">
|
||||
<q-item-section class="text-secondary">
|
||||
Cari Hesap Ekstresi Yazdır
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
|
||||
</div> <!-- sağdaki row kapandı -->
|
||||
</div> <!-- sticky-bar kapandı -->
|
||||
|
||||
<!-- Ana Tablo -->
|
||||
<q-table
|
||||
class="sticky-table"
|
||||
title="Hareketler"
|
||||
:rows="statementheaderStore.groupedRows"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
:row-key="row => row.OrderHeaderID + '_' + row.OrderNumber"
|
||||
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
:rows-per-page-options="[0]"
|
||||
:loading="statementheaderStore.loading"
|
||||
:table-style="{ tableLayout: 'auto', minWidth: '1600px' }"
|
||||
>
|
||||
<template #body="props">
|
||||
|
||||
<!-- Grup başlığı satırı -->
|
||||
<q-tr
|
||||
v-if="props.row._type === 'group'"
|
||||
class="group-row bg-grey-3 text-weight-bold"
|
||||
>
|
||||
<q-td colspan="100%" class="q-pa-sm">
|
||||
<div class="row items-center justify-between">
|
||||
<div class="row items-center">
|
||||
<q-btn
|
||||
dense flat round
|
||||
:icon="statementheaderStore.groupOpen[props.row.para_birimi] ? 'expand_less' : 'expand_more'"
|
||||
class="q-mr-sm"
|
||||
@click="statementheaderStore.toggleGroup(props.row.para_birimi)"
|
||||
/>
|
||||
<span>Para Birimi: {{ props.row.para_birimi }}</span>
|
||||
</div>
|
||||
<div class="row items-center q-gutter-md text-right">
|
||||
<div>Bakiye: {{ formatAmount(props.row.sonBakiye) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<!-- Normal data satırı -->
|
||||
<q-tr
|
||||
v-else-if="props.row._type === 'data'"
|
||||
:props="props"
|
||||
class="main-row"
|
||||
>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
@click="col.name === 'belge_no' ? toggleRowDetails(props.row) : null"
|
||||
:class="[
|
||||
'cursor-pointer',
|
||||
col.name === 'aciklama' ? 'resizable-cell' : '',
|
||||
col.name === 'belge_no' ? 'text-primary text-bold' : ''
|
||||
]"
|
||||
>
|
||||
<span v-if="['borc','alacak','bakiye'].includes(col.name)">
|
||||
{{ formatAmount(props.row[col.field]) }}
|
||||
</span>
|
||||
|
||||
<div v-else-if="col.name === 'aciklama'" class="resizable-cell-content">
|
||||
{{ props.row[col.field] ?? '' }}
|
||||
</div>
|
||||
|
||||
<span v-else>
|
||||
{{ props.row[col.field] ?? '' }}
|
||||
</span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<!-- Detay tablosu -->
|
||||
<q-tr
|
||||
v-if="props.row._type === 'data' && expandedRows[props.row.belge_no]"
|
||||
class="sub-row"
|
||||
>
|
||||
<q-td colspan="100%">
|
||||
<q-table
|
||||
:rows="detailStore.getDetailsByBelge(props.row.belge_no)"
|
||||
:columns="detailColumns(props.row.belge_no)"
|
||||
row-key="Urun_Kodu"
|
||||
flat
|
||||
dense
|
||||
bordered
|
||||
hide-bottom
|
||||
no-data-label="Detay bulunamadı"
|
||||
class="custom-subtable"
|
||||
:loading="detailStore.loading"
|
||||
:table-style="{ minWidth: '1200px' }"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useAccountStore } from 'src/stores/accountStore'
|
||||
import { useStatementheaderStore } from 'src/stores/statementheaderStore'
|
||||
import { useStatementdetailStore } from 'src/stores/statementdetailStore'
|
||||
import { useDownloadstpdfStore } from 'src/stores/downloadstpdfStore'
|
||||
import dayjs from 'dayjs'
|
||||
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 accountStore = useAccountStore()
|
||||
const statementheaderStore = useStatementheaderStore()
|
||||
const detailStore = useStatementdetailStore()
|
||||
const downloadstpdfStore = useDownloadstpdfStore()
|
||||
|
||||
/* Cari seçimi */
|
||||
const selectedCari = ref(null)
|
||||
const filteredOptions = ref([])
|
||||
|
||||
function filterCari(val, update) {
|
||||
if (val === '') {
|
||||
update(() => { filteredOptions.value = accountStore.accountOptions })
|
||||
return
|
||||
}
|
||||
const needle = val.toLowerCase()
|
||||
update(() => {
|
||||
filteredOptions.value = accountStore.accountOptions.filter(o =>
|
||||
o.label.toLowerCase().includes(needle) || o.value.toLowerCase().includes(needle)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await accountStore.fetchAccounts()
|
||||
filteredOptions.value = accountStore.accountOptions
|
||||
|
||||
// ✅ Backend erişimi için global fonksiyon
|
||||
window.toggleAllDetails = toggleAllDetails
|
||||
})
|
||||
|
||||
/* Tarih aralığı */
|
||||
const dateFrom = ref(dayjs().startOf('year').format('YYYY-MM-DD'))
|
||||
const dateTo = ref(dayjs().format('YYYY-MM-DD'))
|
||||
|
||||
/* Parasal İşlem Tipi */
|
||||
const monetaryTypeOptions = [
|
||||
{ label: '1-2 hesap', value: ['1', '2'] },
|
||||
{ label: '1-3 r hesap', value: ['1', '3'] }
|
||||
]
|
||||
const selectedMonType = ref(monetaryTypeOptions[0].value)
|
||||
|
||||
/* Expand kontrolü */
|
||||
const expandedRows = ref({})
|
||||
const allDetailsOpen = ref(false)
|
||||
|
||||
/* Kolonları dinamik üretelim */
|
||||
function buildColumns(data) {
|
||||
if (!data || data.length === 0) return []
|
||||
return Object.keys(data[0]).map(key => ({
|
||||
name: key,
|
||||
label: key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
||||
field: key,
|
||||
align: typeof data[0][key] === 'number' ? 'right' : 'left',
|
||||
sortable: true
|
||||
}))
|
||||
}
|
||||
|
||||
const columns = computed(() => buildColumns(statementheaderStore.headers))
|
||||
|
||||
function detailColumns(belgeNo) {
|
||||
const details = detailStore.getDetailsByBelge(belgeNo)
|
||||
return buildColumns(details)
|
||||
}
|
||||
|
||||
/* Filtrele */
|
||||
async function onFilterClick() {
|
||||
if (!selectedCari.value || !dateFrom.value || !dateTo.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Lütfen cari ve tarih aralığını seçiniz.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await statementheaderStore.loadStatements({
|
||||
startdate: dateFrom.value,
|
||||
enddate: dateTo.value,
|
||||
accountcode: selectedCari.value,
|
||||
langcode: 'TR',
|
||||
parislemler: selectedMonType.value
|
||||
})
|
||||
|
||||
await detailStore.loadDetails({
|
||||
accountCode: selectedCari.value,
|
||||
startDate: dateFrom.value,
|
||||
endDate: dateTo.value
|
||||
})
|
||||
}
|
||||
|
||||
/* Grup satırları için özel rowKey */
|
||||
const rowKeyFn = (row) =>
|
||||
row._type === 'group' ? `grp-${row.para_birimi}` : row.belge_no
|
||||
|
||||
/* Detay açma sadece expand kontrolü */
|
||||
function toggleRowDetails(row) {
|
||||
if (row._type === 'group') return
|
||||
expandedRows.value[row.belge_no] = !expandedRows.value[row.belge_no]
|
||||
}
|
||||
|
||||
/* 🔹 Tüm detayları aç/kapat */
|
||||
function toggleAllDetails() {
|
||||
allDetailsOpen.value = !allDetailsOpen.value
|
||||
if (allDetailsOpen.value) {
|
||||
for (const row of statementheaderStore.headers) {
|
||||
if (row.belge_no) {
|
||||
expandedRows.value[row.belge_no] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
expandedRows.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
function resetFilters() {
|
||||
selectedCari.value = null
|
||||
dateFrom.value = ''
|
||||
dateTo.value = ''
|
||||
selectedMonType.value = monetaryTypeOptions[0].value
|
||||
statementheaderStore.headers = []
|
||||
detailStore.reset()
|
||||
}
|
||||
|
||||
/* Format */
|
||||
function formatAmount(n) {
|
||||
if (n == null || isNaN(n)) return '0,00'
|
||||
return new Intl.NumberFormat('tr-TR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(n)
|
||||
}
|
||||
|
||||
const filtersOpen = ref(true)
|
||||
|
||||
/* 🔹 Kolon gizle/göster */
|
||||
const visibleColumns = ref([])
|
||||
const showLeftCols = ref(true)
|
||||
|
||||
watch(columns, (cols) => {
|
||||
if (cols.length > 0 && visibleColumns.value.length === 0) {
|
||||
visibleColumns.value = cols.map(c => c.name)
|
||||
}
|
||||
})
|
||||
|
||||
function toggleLeftCols() {
|
||||
if (showLeftCols.value) {
|
||||
visibleColumns.value = columns.value.map((c, i) =>
|
||||
i < 3 ? null : c.name
|
||||
).filter(Boolean)
|
||||
} else {
|
||||
visibleColumns.value = columns.value.map(c => c.name)
|
||||
}
|
||||
showLeftCols.value = !showLeftCols.value
|
||||
}
|
||||
|
||||
/* 🔹 PDF İndirme Butonuna bağla */
|
||||
async function handleDownload() {
|
||||
console.log("▶️ [DEBUG] handleDownload:", selectedCari.value, dateFrom.value, dateTo.value)
|
||||
|
||||
if (!selectedCari.value || !dateFrom.value || !dateTo.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Cari ve tarih aralığını seçmeden PDF alınamaz!',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Seçilen parasal işlem tipini gönder
|
||||
const result = await downloadstpdfStore.downloadPDF(
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // <-- eklendi (['1','2'] veya ['1','3'])
|
||||
)
|
||||
|
||||
console.log("📤 [DEBUG] Store’dan gelen result:", result)
|
||||
|
||||
$q.notify({
|
||||
type: result.ok ? 'positive' : 'negative',
|
||||
message: result.message,
|
||||
position: 'top-right'
|
||||
})
|
||||
}/* 🔹 Cari Hesap Ekstresi (2. seçenek) */
|
||||
import { useDownloadstHeadStore } from 'src/stores/downloadstHeadStore'
|
||||
|
||||
const downloadstHeadStore = useDownloadstHeadStore()
|
||||
|
||||
async function CurrheadDownload() {
|
||||
console.log("▶️ [DEBUG] CurrheadDownload:", selectedCari.value, dateFrom.value, dateTo.value)
|
||||
|
||||
if (!selectedCari.value || !dateFrom.value || !dateTo.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Cari ve tarih aralığını seçmeden PDF alınamaz!',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Yeni store fonksiyonu doğru şekilde çağrılıyor
|
||||
const result = await downloadstHeadStore.handlestHeadDownload(
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // parasal işlem tipi (parislemler)
|
||||
)
|
||||
|
||||
console.log("📤 [DEBUG] CurrheadDownloadresult:", result)
|
||||
|
||||
$q.notify({
|
||||
type: result.ok ? 'positive' : 'negative',
|
||||
message: result.message,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
63
ui/src/pages/TestMail.vue
Normal file
63
ui/src/pages/TestMail.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
|
||||
<q-card flat bordered class="q-pa-md" style="max-width: 500px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">SMTP Test Mail</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="to"
|
||||
label="Gönderilecek mail"
|
||||
filled
|
||||
dense
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Test Mail Gönder"
|
||||
:loading="store.loading"
|
||||
@click="send"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useMailTestStore } from 'src/stores/mailTestStore'
|
||||
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 store = useMailTestStore()
|
||||
|
||||
const to = ref('mehmet.kececi@baggi.com.tr')
|
||||
|
||||
async function send () {
|
||||
try {
|
||||
await store.sendTestMail(to.value)
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Test mail gönderildi'
|
||||
})
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: err?.message || 'Mail gönderilemedi'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
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>
|
||||
108
ui/src/pages/UserGateway.vue
Normal file
108
ui/src/pages/UserGateway.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<q-page class="user-gateway-page flex flex-center">
|
||||
|
||||
<div class="gateway-container">
|
||||
|
||||
<div class="gateway-header">
|
||||
<div class="text-h5">Kullanıcı Yönetim Merkezi</div>
|
||||
<div class="text-subtitle2 text-grey-7">
|
||||
Kullanıcı oluşturma ve yetkilendirme işlemleri
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gateway-actions row q-col-gutter-lg q-mt-lg">
|
||||
|
||||
<!-- ➕ YENİ KULLANICI -->
|
||||
<q-card
|
||||
class="gateway-card cursor-pointer"
|
||||
flat
|
||||
bordered
|
||||
@click="goCreate"
|
||||
>
|
||||
<q-card-section class="text-center">
|
||||
<q-icon name="person_add" size="48px" color="primary" />
|
||||
<div class="text-h6 q-mt-sm">Yeni Kullanıcı</div>
|
||||
<div class="text-caption text-grey-7 q-mt-xs">
|
||||
Sisteme yeni kullanıcı ekle
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- 👥 MEVCUT KULLANICILAR -->
|
||||
<q-card
|
||||
class="gateway-card cursor-pointer"
|
||||
flat
|
||||
bordered
|
||||
@click="goList"
|
||||
>
|
||||
<q-card-section class="text-center">
|
||||
<q-icon name="groups" size="48px" color="primary" />
|
||||
<div class="text-h6 q-mt-sm">Mevcut Kullanıcılar</div>
|
||||
<div class="text-caption text-grey-7 q-mt-xs">
|
||||
Kullanıcıları görüntüle ve düzenle
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goCreate () {
|
||||
router.push({
|
||||
path: '/app/users/new',
|
||||
query: { mode: 'new' }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function goList () {
|
||||
router.push({ name: 'user-list' })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.user-gateway-page {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.gateway-container {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.gateway-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gateway-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gateway-card {
|
||||
width: 280px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.gateway-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
272
ui/src/pages/UserList.vue
Normal file
272
ui/src/pages/UserList.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<q-page class="ol-page with-bg">
|
||||
|
||||
<!-- 🔍 Sticky Filter -->
|
||||
<div class="ol-filter-bar">
|
||||
<div class="ol-filter-row">
|
||||
|
||||
<q-input
|
||||
class="ol-filter-input ol-search"
|
||||
dense
|
||||
filled
|
||||
clearable
|
||||
v-model="store.filters.search"
|
||||
label="Arama (Kullanıcı / Rol / Piyasa)"
|
||||
debounce="300"
|
||||
@update:model-value="store.fetchUsers"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-toggle
|
||||
v-model="store.filters.onlyActive"
|
||||
label="Sadece Aktifler"
|
||||
/>
|
||||
|
||||
<div class="ol-filter-actions">
|
||||
<q-btn
|
||||
label="Yenile"
|
||||
icon="refresh"
|
||||
color="primary"
|
||||
:loading="store.loading"
|
||||
@click="store.fetchUsers"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
label="Yeni Kullanıcı"
|
||||
icon="person_add"
|
||||
color="primary"
|
||||
outline
|
||||
@click="goCreate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 📋 USER LIST TABLE -->
|
||||
<q-table
|
||||
title="Mevcut Kullanıcılar"
|
||||
class="ol-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
separator="cell"
|
||||
row-key="id"
|
||||
:rows="store.filteredUsers"
|
||||
:columns="columns"
|
||||
:loading="store.loading"
|
||||
no-data-label="Kullanıcı bulunamadı"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
>
|
||||
|
||||
|
||||
<!-- 🔗 OPEN -->
|
||||
<template #body-cell-open="props">
|
||||
<q-td class="text-center">
|
||||
<q-btn
|
||||
icon="open_in_new"
|
||||
color="primary"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
@click="openDetail(props.row.id)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- ✅ DURUM -->
|
||||
<template #body-cell-is_active="props">
|
||||
<q-td class="text-center">
|
||||
<q-icon
|
||||
:name="props.row.is_active ? 'check_circle' : 'cancel'"
|
||||
:color="props.row.is_active ? 'green' : 'red'"
|
||||
size="18px"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- 👤 ROLLER -->
|
||||
<template #body-cell-role_names="props">
|
||||
<q-td>
|
||||
<q-chip
|
||||
v-for="r in splitNames(props.row.role_names)"
|
||||
:key="r"
|
||||
dense
|
||||
color="primary"
|
||||
text-color="white"
|
||||
class="q-mr-xs"
|
||||
>
|
||||
{{ r }}
|
||||
</q-chip>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- 🏢 DEPARTMAN -->
|
||||
<template #body-cell-department_names="props">
|
||||
<q-td>
|
||||
<q-chip
|
||||
v-for="d in splitNames(props.row.department_names)"
|
||||
:key="d"
|
||||
dense
|
||||
color="grey-7"
|
||||
text-color="white"
|
||||
class="q-mr-xs"
|
||||
>
|
||||
{{ d }}
|
||||
</q-chip>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- 🌍 PİYASALAR -->
|
||||
<template #body-cell-piyasa_names="props">
|
||||
<q-td class="ol-col-piyasa">
|
||||
<div class="piyasa-wrap">
|
||||
<q-chip
|
||||
v-for="p in splitPiyasalar(props.row.piyasa_names)"
|
||||
:key="p"
|
||||
dense
|
||||
outline
|
||||
color="indigo"
|
||||
class="piyasa-chip"
|
||||
:title="p"
|
||||
>
|
||||
{{ p }}
|
||||
</q-chip>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
|
||||
</q-table>
|
||||
|
||||
<!-- ❌ HATA -->
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
|
||||
❌ {{ store.error }}
|
||||
</q-banner>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserListStore } from 'src/stores/UserListStore'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const router = useRouter()
|
||||
const store = useUserListStore()
|
||||
|
||||
/* ==========================================================
|
||||
📌 QTable Columns
|
||||
========================================================== */
|
||||
const columns = [
|
||||
{ name: 'open', label: '', align: 'center' },
|
||||
|
||||
{
|
||||
name: 'id',
|
||||
label: 'No',
|
||||
field: row => row.id,
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{
|
||||
name: 'code',
|
||||
label: 'Kullanıcı',
|
||||
field: row => row.code || '',
|
||||
sortable: true,
|
||||
sort: (a, b) => a.localeCompare(b, 'tr', { sensitivity: 'base' })
|
||||
},
|
||||
|
||||
{
|
||||
name: 'nebim_username',
|
||||
label: 'Nebim',
|
||||
field: row => row.nebim_username || '',
|
||||
sortable: true,
|
||||
sort: (a, b) => a.localeCompare(b, 'tr')
|
||||
},
|
||||
|
||||
{
|
||||
name: 'user_group_code',
|
||||
label: 'Grup',
|
||||
field: row => row.user_group_code || '',
|
||||
sortable: true,
|
||||
sort: (a, b) => a.localeCompare(b, 'tr')
|
||||
},
|
||||
|
||||
{
|
||||
name: 'is_active',
|
||||
label: 'Durum',
|
||||
field: row => row.is_active,
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
sort: (a, b) => Number(b) - Number(a)
|
||||
},
|
||||
|
||||
{
|
||||
name: 'role_names',
|
||||
label: 'Roller',
|
||||
field: row => row.role_names || '',
|
||||
sortable: true,
|
||||
sort: (a, b) => a.localeCompare(b, 'tr')
|
||||
},
|
||||
|
||||
{
|
||||
name: 'department_names',
|
||||
label: 'Departmanlar',
|
||||
field: row => row.department_names || '',
|
||||
sortable: true,
|
||||
sort: (a, b) => a.localeCompare(b, 'tr')
|
||||
},
|
||||
|
||||
{
|
||||
name: 'piyasa_names',
|
||||
label: 'Piyasalar',
|
||||
field: row => row.piyasa_names || '',
|
||||
sortable: true,
|
||||
sort: (a, b) => a.localeCompare(b, 'tr')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
/* ==========================================================
|
||||
HELPERS
|
||||
========================================================== */
|
||||
function splitNames(val) {
|
||||
if (!val) return []
|
||||
return val.split(',').map(v => v.trim())
|
||||
}
|
||||
|
||||
function openDetail(id) {
|
||||
router.push({
|
||||
path: `/app/users/edit/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
function goCreate() {
|
||||
router.push({ name: 'user-new' })
|
||||
}
|
||||
|
||||
function splitPiyasalar (val) {
|
||||
if (!val) return []
|
||||
return val
|
||||
.split(',')
|
||||
.map(v => v.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 24) // ✅ 6 satır × 4 kolon
|
||||
}
|
||||
|
||||
|
||||
onMounted(store.fetchUsers)
|
||||
</script>
|
||||
357
ui/src/pages/UserPermissionPage.vue
Normal file
357
ui/src/pages/UserPermissionPage.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
|
||||
<div v-if="!lookupsLoaded" class="q-pa-xl flex flex-center">
|
||||
<q-spinner color="primary" size="48px" />
|
||||
</div>
|
||||
|
||||
<q-page class="permissions-page">
|
||||
|
||||
<div class="sticky-stack">
|
||||
|
||||
<!-- USER SELECT -->
|
||||
<div v-if="lookupsLoaded" class="filter-bar row q-col-gutter-md">
|
||||
|
||||
<div class="col-4">
|
||||
<q-select
|
||||
v-model="userId"
|
||||
:options="users"
|
||||
option-value="id"
|
||||
option-label="title"
|
||||
emit-value
|
||||
map-options
|
||||
label="Kullanıcı"
|
||||
dense
|
||||
outlined
|
||||
@update:model-value="loadMatrix"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SAVE -->
|
||||
<div class="save-toolbar">
|
||||
|
||||
<div class="label">
|
||||
Kullanıcı Override Yetkileri
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Kaydet"
|
||||
:disable="!dirty"
|
||||
@click="save"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
|
||||
<div v-if="lookupsLoaded" class="permissions-table-scroll">
|
||||
|
||||
<q-table
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
row-key="module"
|
||||
dense
|
||||
bordered
|
||||
flat
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
>
|
||||
|
||||
<!-- HEADER -->
|
||||
<template v-slot:header-cell="props">
|
||||
|
||||
<q-th :props="props">
|
||||
|
||||
<span v-if="props.col.name === 'module'">
|
||||
{{ props.col.label }}
|
||||
</span>
|
||||
|
||||
<div v-else class="column items-center">
|
||||
|
||||
<span class="text-caption">
|
||||
{{ props.col.label }}
|
||||
</span>
|
||||
|
||||
<q-checkbox
|
||||
dense
|
||||
:model-value="isColumnChecked(props.col.name)"
|
||||
@update:model-value="toggleColumn(props.col.name, $event)"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</q-th>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- BODY -->
|
||||
<template v-slot:body-cell="props">
|
||||
|
||||
<q-td :props="props">
|
||||
|
||||
<span v-if="props.col.name === 'module'">
|
||||
{{ props.row.label }}
|
||||
</span>
|
||||
|
||||
<q-checkbox
|
||||
v-else
|
||||
v-model="props.row[props.col.name]"
|
||||
dense
|
||||
@update:model-value="dirty = true"
|
||||
/>
|
||||
|
||||
</q-td>
|
||||
|
||||
</template>
|
||||
|
||||
</q-table>
|
||||
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
|
||||
/* ================= STATE ================= */
|
||||
|
||||
const users = ref([])
|
||||
const userId = ref(null)
|
||||
|
||||
const modules = ref([])
|
||||
const rows = ref([])
|
||||
|
||||
const loading = ref(false)
|
||||
const dirty = ref(false)
|
||||
const lookupsLoaded = ref(false)
|
||||
|
||||
|
||||
/* ================= ACTIONS ================= */
|
||||
|
||||
const actions = [
|
||||
{ key: 'write', label: 'Ekleme' },
|
||||
{ key: 'read', label: 'Görüntüleme' },
|
||||
{ key: 'delete', label: 'Silme' },
|
||||
{ key: 'update', label: 'Güncelleme' },
|
||||
{ key: 'export', label: 'Çıktı' }
|
||||
]
|
||||
|
||||
|
||||
/* ================= TABLE ================= */
|
||||
|
||||
const columns = [
|
||||
|
||||
{
|
||||
name: 'module',
|
||||
label: 'Modül',
|
||||
field: 'label',
|
||||
align: 'left'
|
||||
},
|
||||
|
||||
...actions.map(a => ({
|
||||
name: a.key,
|
||||
label: a.label,
|
||||
align: 'center'
|
||||
}))
|
||||
]
|
||||
|
||||
|
||||
/* ================= LOOKUPS ================= */
|
||||
|
||||
async function loadLookups () {
|
||||
|
||||
const [u, m] = await Promise.all([
|
||||
api.get('/lookups/users-perm'),
|
||||
api.get('/lookups/modules')
|
||||
])
|
||||
|
||||
users.value = u.data || []
|
||||
modules.value = m.data || []
|
||||
|
||||
lookupsLoaded.value = true
|
||||
}
|
||||
|
||||
|
||||
/* ================= INIT MATRIX ================= */
|
||||
|
||||
function initMatrix () {
|
||||
|
||||
rows.value = modules.value.map(m => {
|
||||
|
||||
const row = {
|
||||
module: String(m.value).toLowerCase().trim(),
|
||||
label: m.label
|
||||
}
|
||||
|
||||
actions.forEach(a => {
|
||||
row[a.key] = false
|
||||
})
|
||||
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* ================= LOAD ================= */
|
||||
|
||||
async function loadMatrix () {
|
||||
|
||||
if (!userId.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
|
||||
initMatrix()
|
||||
|
||||
const res = await api.get(
|
||||
`/users/${userId.value}/permissions`
|
||||
)
|
||||
|
||||
const list = Array.isArray(res.data) ? res.data : []
|
||||
|
||||
|
||||
// ✅ BACKEND → UI MAP
|
||||
const actionMap = {
|
||||
insert: 'write',
|
||||
view: 'read',
|
||||
delete: 'delete',
|
||||
update: 'update',
|
||||
export: 'export'
|
||||
}
|
||||
|
||||
|
||||
list.forEach(p => {
|
||||
|
||||
const code = String(p.module_code || p.module)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
|
||||
const rawAction = String(p.action)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
|
||||
const mappedAction = actionMap[rawAction] || rawAction
|
||||
|
||||
|
||||
const row = rows.value.find(r => r.module === code)
|
||||
|
||||
if (row && row.hasOwnProperty(mappedAction)) {
|
||||
row[mappedAction] = Boolean(p.allowed)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
dirty.value = false
|
||||
|
||||
} catch (e) {
|
||||
|
||||
console.error('PERM LOAD ERROR:', e)
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Yükleme hatası'
|
||||
})
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ================= SAVE ================= */
|
||||
|
||||
async function save () {
|
||||
|
||||
try {
|
||||
|
||||
loading.value = true
|
||||
|
||||
const payload = []
|
||||
|
||||
rows.value.forEach(r => {
|
||||
|
||||
actions.forEach(a => {
|
||||
|
||||
payload.push({
|
||||
module: r.module,
|
||||
action: a.key,
|
||||
allowed: r[a.key]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
await api.post(
|
||||
`/users/${userId.value}/permissions`,
|
||||
payload
|
||||
)
|
||||
|
||||
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Kaydedildi'
|
||||
})
|
||||
|
||||
dirty.value = false
|
||||
|
||||
} catch {
|
||||
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Kayıt hatası'
|
||||
})
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ================= COLUMN ================= */
|
||||
|
||||
function isColumnChecked (key) {
|
||||
|
||||
if (!rows.value.length) return false
|
||||
|
||||
return rows.value.every(r => r[key] === true)
|
||||
}
|
||||
|
||||
|
||||
function toggleColumn (key, val) {
|
||||
|
||||
rows.value.forEach(r => {
|
||||
r[key] = val
|
||||
})
|
||||
|
||||
dirty.value = true
|
||||
}
|
||||
|
||||
|
||||
/* ================= INIT ================= */
|
||||
|
||||
onMounted(() => {
|
||||
loadLookups()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
172
ui/src/pages/UserSync.vue
Normal file
172
ui/src/pages/UserSync.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md user-sync-page">
|
||||
|
||||
<div class="row items-center justify-between q-mb-md">
|
||||
<div class="text-h6 text-primary">👤 Kullanıcı Yönetimi</div>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="sync"
|
||||
label="Sync Now"
|
||||
:loading="store.loading"
|
||||
@click="store.syncNow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<!-- 🔹 PostgreSQL Kullanıcıları -->
|
||||
<div class="col-6">
|
||||
<q-card flat bordered>
|
||||
<q-card-section class="bg-primary text-white text-subtitle1">
|
||||
PostgreSQL Kullanıcıları
|
||||
</q-card-section>
|
||||
|
||||
<q-table
|
||||
:rows="store.pgUsers"
|
||||
:columns="pgColumns"
|
||||
row-key="id"
|
||||
flat
|
||||
dense
|
||||
bordered
|
||||
separator="cell"
|
||||
>
|
||||
<template v-slot:body-cell-sync_status="props">
|
||||
<q-td :props="props">
|
||||
<q-chip
|
||||
:color="statusColor(props.row.sync_status)"
|
||||
text-color="white"
|
||||
dense
|
||||
>
|
||||
{{ props.row.sync_status }}
|
||||
</q-chip>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-actions="props">
|
||||
<q-td :props="props">
|
||||
<q-btn
|
||||
dense flat icon="link"
|
||||
color="primary"
|
||||
size="sm"
|
||||
@click="openMapDialog(props.row)"
|
||||
:disable="store.loading"
|
||||
/>
|
||||
<q-btn
|
||||
dense flat icon="link_off"
|
||||
color="negative"
|
||||
size="sm"
|
||||
@click="store.unmap(props.row.id)"
|
||||
:disable="!props.row.mssql_username"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- 🔸 MSSQL Kullanıcıları -->
|
||||
<div class="col-6">
|
||||
<q-card flat bordered>
|
||||
<q-card-section class="bg-secondary text-white text-subtitle1">
|
||||
MSSQL Kullanıcıları
|
||||
</q-card-section>
|
||||
|
||||
<q-table
|
||||
:rows="store.msUsers"
|
||||
:columns="msColumns"
|
||||
row-key="username"
|
||||
flat
|
||||
dense
|
||||
bordered
|
||||
separator="cell"
|
||||
>
|
||||
<template v-slot:body-cell-is_blocked="props">
|
||||
<q-td :props="props">
|
||||
<q-chip
|
||||
:color="props.row.is_blocked ? 'negative' : 'positive'"
|
||||
text-color="white"
|
||||
dense
|
||||
>
|
||||
{{ props.row.is_blocked ? 'Engelli' : 'Aktif' }}
|
||||
</q-chip>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserSyncStore } from 'src/stores/userSyncStore'
|
||||
import { Dialog } from 'quasar'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
const store = useUserSyncStore()
|
||||
|
||||
const pgColumns = [
|
||||
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
|
||||
{ name: 'code', label: 'Kodu', field: 'code', align: 'left' },
|
||||
{ name: 'full_name', label: 'Ad Soyad', field: 'full_name', align: 'left' },
|
||||
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' },
|
||||
{ name: 'mssql_username', label: 'MSSQL Kullanıcı', field: 'mssql_username', align: 'left' },
|
||||
{ name: 'sync_status', label: 'Durum', field: 'sync_status', align: 'center' },
|
||||
{ name: 'actions', label: 'İşlemler', field: 'actions', align: 'center' }
|
||||
]
|
||||
|
||||
const msColumns = [
|
||||
{ name: 'username', label: 'Kullanıcı Adı', field: 'username', align: 'left' },
|
||||
{ name: 'first_name', label: 'Ad', field: 'first_name', align: 'left' },
|
||||
{ name: 'last_name', label: 'Soyad', field: 'last_name', align: 'left' },
|
||||
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' },
|
||||
{ name: 'is_blocked', label: 'Durum', field: 'is_blocked', align: 'center' }
|
||||
]
|
||||
|
||||
function statusColor(status) {
|
||||
switch (status) {
|
||||
case 'synced': return 'positive'
|
||||
case 'manual': return 'primary'
|
||||
case 'blocked': return 'warning'
|
||||
case 'orphan': return 'negative'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
function openMapDialog(pgUser) {
|
||||
Dialog.create({
|
||||
title: 'Kullanıcı Eşleme',
|
||||
message: 'Bu PostgreSQL kullanıcısını hangi MSSQL kullanıcısına bağlamak istiyorsunuz?',
|
||||
options: {
|
||||
type: 'radio',
|
||||
model: '',
|
||||
items: store.msUsers.map(u => ({ label: `${u.username} (${u.email})`, value: u.username }))
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(val => {
|
||||
store.map(pgUser.id, val)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.loadDummy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-sync-page {
|
||||
background: #fafafa;
|
||||
}
|
||||
.q-card-section {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
521
ui/src/pages/statementofaccount.vue
Normal file
521
ui/src/pages/statementofaccount.vue
Normal file
@@ -0,0 +1,521 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md page-col">
|
||||
|
||||
<!-- 🔹 Cari Kod / İsim (sabit) -->
|
||||
<div class="filter-sticky">
|
||||
<q-select
|
||||
v-model="selectedCari"
|
||||
:options="filteredOptions"
|
||||
label="Cari kod / isim"
|
||||
filled
|
||||
clearable
|
||||
use-input
|
||||
input-debounce="300"
|
||||
@filter="filterCari"
|
||||
emit-value
|
||||
map-options
|
||||
:loading="accountStore.loading"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
behavior="menu"
|
||||
:keep-selected="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 Filtre Alanı -->
|
||||
<div class="filter-collapsible">
|
||||
<div class="row items-center justify-between q-pa-sm bg-grey-2">
|
||||
<div class="text-subtitle1">Filtreler</div>
|
||||
<q-btn
|
||||
dense flat round
|
||||
:icon="filtersOpen ? 'expand_less' : 'expand_more'"
|
||||
@click="filtersOpen = !filtersOpen"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-slide-transition>
|
||||
<div v-show="filtersOpen" class="q-pa-md bg-grey-1">
|
||||
|
||||
<!-- Tarih Aralığı -->
|
||||
<div class="row q-col-gutter-sm q-mb-md">
|
||||
<div class="col-12 col-sm-6">
|
||||
<q-input
|
||||
v-model="dateFrom"
|
||||
label="Tarih aralığı - başlangıç"
|
||||
filled clearable readonly
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="dateFrom" mask="YYYY-MM-DD" locale="tr-TR"/>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6">
|
||||
<q-input
|
||||
v-model="dateTo"
|
||||
label="Tarih aralığı - bitiş"
|
||||
filled clearable readonly
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="dateTo" mask="YYYY-MM-DD" locale="tr-TR" />
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parasal İşlem Tipi -->
|
||||
<q-select
|
||||
v-model="selectedMonType"
|
||||
:options="monetaryTypeOptions"
|
||||
label="Parasal İşlem Tipi"
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
class="q-mb-md"
|
||||
/>
|
||||
|
||||
<!-- Filtre / Sıfırla Butonları -->
|
||||
<div class="row q-col-gutter-md items-center">
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="filter_alt"
|
||||
label="Filtrele"
|
||||
@click="onFilterClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey-8"
|
||||
icon="restart_alt"
|
||||
label="Sıfırla"
|
||||
@click="resetFilters"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 Tablo Alanı -->
|
||||
<div class="table-scroll">
|
||||
|
||||
<!-- Toggle butonları (sticky üst bar) -->
|
||||
<div class="sticky-bar row justify-between items-center q-pa-sm bg-grey-1">
|
||||
|
||||
<!-- Sol buton: CARİ BİLGİ DETAY göster/gizle -->
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
icon="view_column"
|
||||
:label="showLeftCols ? 'CARİ BİLGİ DETAY Gizle' : 'CARİ BİLGİ DETAY Sütunu Göster'"
|
||||
@click="toggleLeftCols"
|
||||
/>
|
||||
|
||||
<!-- Sağ taraftaki buton grubu -->
|
||||
<div class="row items-center q-gutter-sm">
|
||||
|
||||
<!-- Tüm detayları aç/kapat -->
|
||||
<q-btn
|
||||
flat
|
||||
color="secondary"
|
||||
icon="list"
|
||||
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
|
||||
@click="toggleAllDetails"
|
||||
/>
|
||||
|
||||
<!-- ✅ PDF Yazdır Dropdown -->
|
||||
<q-btn-dropdown
|
||||
flat
|
||||
color="red"
|
||||
icon="picture_as_pdf"
|
||||
label="Yazdır"
|
||||
>
|
||||
<q-list style="min-width: 200px">
|
||||
<!-- 1. Seçenek -->
|
||||
<q-item clickable v-close-popup @click="handleDownload" >
|
||||
<q-item-section class="text-primary">
|
||||
Detaylı Cari Ekstre Yazdır
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- 2. Seçenek -->
|
||||
<q-item clickable v-close-popup @click="CurrheadDownload">
|
||||
<q-item-section class="text-secondary">
|
||||
Cari Hesap Ekstresi Yazdır
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
|
||||
</div> <!-- sağdaki row kapandı -->
|
||||
</div> <!-- sticky-bar kapandı -->
|
||||
|
||||
<!-- Ana Tablo -->
|
||||
<q-table
|
||||
class="sticky-table"
|
||||
title="Hareketler"
|
||||
:rows="statementheaderStore.groupedRows"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
:row-key="row => row.OrderHeaderID + '_' + row.OrderNumber"
|
||||
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
:rows-per-page-options="[0]"
|
||||
:loading="statementheaderStore.loading"
|
||||
:table-style="{ tableLayout: 'auto', minWidth: '1600px' }"
|
||||
>
|
||||
<template #body="props">
|
||||
|
||||
<!-- Grup başlığı satırı -->
|
||||
<q-tr
|
||||
v-if="props.row._type === 'group'"
|
||||
class="group-row bg-grey-3 text-weight-bold"
|
||||
>
|
||||
<q-td colspan="100%" class="q-pa-sm">
|
||||
<div class="row items-center justify-between">
|
||||
<div class="row items-center">
|
||||
<q-btn
|
||||
dense flat round
|
||||
:icon="statementheaderStore.groupOpen[props.row.para_birimi] ? 'expand_less' : 'expand_more'"
|
||||
class="q-mr-sm"
|
||||
@click="statementheaderStore.toggleGroup(props.row.para_birimi)"
|
||||
/>
|
||||
<span>Para Birimi: {{ props.row.para_birimi }}</span>
|
||||
</div>
|
||||
<div class="row items-center q-gutter-md text-right">
|
||||
<div>Bakiye: {{ formatAmount(props.row.sonBakiye) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<!-- Normal data satırı -->
|
||||
<q-tr
|
||||
v-else-if="props.row._type === 'data'"
|
||||
:props="props"
|
||||
class="main-row"
|
||||
>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
@click="col.name === 'belge_no' ? toggleRowDetails(props.row) : null"
|
||||
:class="[
|
||||
'cursor-pointer',
|
||||
col.name === 'aciklama' ? 'resizable-cell' : '',
|
||||
col.name === 'belge_no' ? 'text-primary text-bold' : ''
|
||||
]"
|
||||
>
|
||||
<span v-if="['borc','alacak','bakiye'].includes(col.name)">
|
||||
{{ formatAmount(props.row[col.field]) }}
|
||||
</span>
|
||||
|
||||
<div v-else-if="col.name === 'aciklama'" class="resizable-cell-content">
|
||||
{{ props.row[col.field] ?? '' }}
|
||||
</div>
|
||||
|
||||
<span v-else>
|
||||
{{ props.row[col.field] ?? '' }}
|
||||
</span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<!-- Detay tablosu -->
|
||||
<q-tr
|
||||
v-if="props.row._type === 'data' && expandedRows[props.row.belge_no]"
|
||||
class="sub-row"
|
||||
>
|
||||
<q-td colspan="100%">
|
||||
<q-table
|
||||
:rows="detailStore.getDetailsByBelge(props.row.belge_no)"
|
||||
:columns="detailColumns(props.row.belge_no)"
|
||||
row-key="Urun_Kodu"
|
||||
flat
|
||||
dense
|
||||
bordered
|
||||
hide-bottom
|
||||
no-data-label="Detay bulunamadı"
|
||||
class="custom-subtable"
|
||||
:loading="detailStore.loading"
|
||||
:table-style="{ minWidth: '1200px' }"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useAccountStore } from 'src/stores/accountStore'
|
||||
import { useStatementheaderStore } from 'src/stores/statementheaderStore'
|
||||
import { useStatementdetailStore } from 'src/stores/statementdetailStore'
|
||||
import { useDownloadstpdfStore } from 'src/stores/downloadstpdfStore'
|
||||
import dayjs from 'dayjs'
|
||||
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 accountStore = useAccountStore()
|
||||
const statementheaderStore = useStatementheaderStore()
|
||||
const detailStore = useStatementdetailStore()
|
||||
const downloadstpdfStore = useDownloadstpdfStore()
|
||||
|
||||
/* Cari seçimi */
|
||||
const selectedCari = ref(null)
|
||||
const filteredOptions = ref([])
|
||||
|
||||
function filterCari (val, update) {
|
||||
const needle = normalizeText(val)
|
||||
|
||||
update(() => {
|
||||
if (!needle) {
|
||||
filteredOptions.value = accountStore.accountOptions
|
||||
return
|
||||
}
|
||||
|
||||
filteredOptions.value =
|
||||
accountStore.accountOptions.filter(o => {
|
||||
|
||||
const label = normalizeText(o.label)
|
||||
const value = normalizeText(o.value)
|
||||
|
||||
return (
|
||||
label.includes(needle) ||
|
||||
value.includes(needle)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await accountStore.fetchAccounts()
|
||||
console.log("ACCOUNTS LEN:", accountStore.accounts?.length)
|
||||
console.log("OPTIONS LEN:", accountStore.accountOptions?.length)
|
||||
console.log("FIRST 5:", accountStore.accountOptions?.slice(0,5))
|
||||
filteredOptions.value = accountStore.accountOptions
|
||||
|
||||
|
||||
// ✅ Backend erişimi için global fonksiyon
|
||||
window.toggleAllDetails = toggleAllDetails
|
||||
})
|
||||
|
||||
/* Tarih aralığı */
|
||||
const dateFrom = ref(dayjs().startOf('year').format('YYYY-MM-DD'))
|
||||
const dateTo = ref(dayjs().format('YYYY-MM-DD'))
|
||||
|
||||
/* Parasal İşlem Tipi */
|
||||
const monetaryTypeOptions = [
|
||||
{ label: '1-2 hesap', value: ['1', '2'] },
|
||||
{ label: '1-3 r hesap', value: ['1', '3'] }
|
||||
]
|
||||
const selectedMonType = ref(monetaryTypeOptions[0].value)
|
||||
|
||||
/* Expand kontrolü */
|
||||
const expandedRows = ref({})
|
||||
const allDetailsOpen = ref(false)
|
||||
|
||||
/* Kolonları dinamik üretelim */
|
||||
function buildColumns(data) {
|
||||
if (!data || data.length === 0) return []
|
||||
return Object.keys(data[0]).map(key => ({
|
||||
name: key,
|
||||
label: key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
||||
field: key,
|
||||
align: typeof data[0][key] === 'number' ? 'right' : 'left',
|
||||
sortable: true
|
||||
}))
|
||||
}
|
||||
|
||||
const columns = computed(() => buildColumns(statementheaderStore.headers))
|
||||
|
||||
function detailColumns(belgeNo) {
|
||||
const details = detailStore.getDetailsByBelge(belgeNo)
|
||||
return buildColumns(details)
|
||||
}
|
||||
|
||||
/* Filtrele */
|
||||
async function onFilterClick() {
|
||||
if (!selectedCari.value || !dateFrom.value || !dateTo.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Lütfen cari ve tarih aralığını seçiniz.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await statementheaderStore.loadStatements({
|
||||
startdate: dateFrom.value,
|
||||
enddate: dateTo.value,
|
||||
accountcode: selectedCari.value,
|
||||
langcode: 'TR',
|
||||
parislemler: selectedMonType.value
|
||||
})
|
||||
|
||||
await detailStore.loadDetails({
|
||||
accountCode: selectedCari.value,
|
||||
startDate: dateFrom.value,
|
||||
endDate: dateTo.value
|
||||
})
|
||||
}
|
||||
|
||||
/* Grup satırları için özel rowKey */
|
||||
const rowKeyFn = (row) =>
|
||||
row._type === 'group' ? `grp-${row.para_birimi}` : row.belge_no
|
||||
|
||||
/* Detay açma sadece expand kontrolü */
|
||||
function toggleRowDetails(row) {
|
||||
if (row._type === 'group') return
|
||||
expandedRows.value[row.belge_no] = !expandedRows.value[row.belge_no]
|
||||
}
|
||||
|
||||
/* 🔹 Tüm detayları aç/kapat */
|
||||
function toggleAllDetails() {
|
||||
allDetailsOpen.value = !allDetailsOpen.value
|
||||
if (allDetailsOpen.value) {
|
||||
for (const row of statementheaderStore.headers) {
|
||||
if (row.belge_no) {
|
||||
expandedRows.value[row.belge_no] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
expandedRows.value = {}
|
||||
}
|
||||
}
|
||||
function normalizeText (str) {
|
||||
return (str || '')
|
||||
.toString()
|
||||
.toLocaleLowerCase('tr-TR') // 🔥 Türkçe uyumlu
|
||||
.normalize('NFD') // aksan temizleme
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
}
|
||||
/* Reset */
|
||||
function resetFilters() {
|
||||
selectedCari.value = null
|
||||
dateFrom.value = ''
|
||||
dateTo.value = ''
|
||||
selectedMonType.value = monetaryTypeOptions[0].value
|
||||
statementheaderStore.headers = []
|
||||
detailStore.reset()
|
||||
}
|
||||
|
||||
/* Format */
|
||||
function formatAmount(n) {
|
||||
if (n == null || isNaN(n)) return '0,00'
|
||||
return new Intl.NumberFormat('tr-TR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(n)
|
||||
}
|
||||
|
||||
const filtersOpen = ref(true)
|
||||
|
||||
/* 🔹 Kolon gizle/göster */
|
||||
const visibleColumns = ref([])
|
||||
const showLeftCols = ref(true)
|
||||
|
||||
watch(columns, (cols) => {
|
||||
if (cols.length > 0 && visibleColumns.value.length === 0) {
|
||||
visibleColumns.value = cols.map(c => c.name)
|
||||
}
|
||||
})
|
||||
|
||||
function toggleLeftCols() {
|
||||
if (showLeftCols.value) {
|
||||
visibleColumns.value = columns.value.map((c, i) =>
|
||||
i < 3 ? null : c.name
|
||||
).filter(Boolean)
|
||||
} else {
|
||||
visibleColumns.value = columns.value.map(c => c.name)
|
||||
}
|
||||
showLeftCols.value = !showLeftCols.value
|
||||
}
|
||||
|
||||
/* 🔹 PDF İndirme Butonuna bağla */
|
||||
async function handleDownload() {
|
||||
console.log("▶️ [DEBUG] handleDownload:", selectedCari.value, dateFrom.value, dateTo.value)
|
||||
|
||||
if (!selectedCari.value || !dateFrom.value || !dateTo.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Cari ve tarih aralığını seçmeden PDF alınamaz!',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Seçilen parasal işlem tipini gönder
|
||||
const result = await downloadstpdfStore.downloadPDF(
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // <-- eklendi (['1','2'] veya ['1','3'])
|
||||
)
|
||||
|
||||
console.log("📤 [DEBUG] Store’dan gelen result:", result)
|
||||
|
||||
$q.notify({
|
||||
type: result.ok ? 'positive' : 'negative',
|
||||
message: result.message,
|
||||
position: 'top-right'
|
||||
})
|
||||
}/* 🔹 Cari Hesap Ekstresi (2. seçenek) */
|
||||
import { useDownloadstHeadStore } from 'src/stores/downloadstHeadStore'
|
||||
|
||||
const downloadstHeadStore = useDownloadstHeadStore()
|
||||
|
||||
async function CurrheadDownload() {
|
||||
console.log("▶️ [DEBUG] CurrheadDownload:", selectedCari.value, dateFrom.value, dateTo.value)
|
||||
|
||||
if (!selectedCari.value || !dateFrom.value || !dateTo.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Cari ve tarih aralığını seçmeden PDF alınamaz!',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Yeni store fonksiyonu doğru şekilde çağrılıyor
|
||||
const result = await downloadstHeadStore.handlestHeadDownload(
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // parasal işlem tipi (parislemler)
|
||||
)
|
||||
|
||||
console.log("📤 [DEBUG] CurrheadDownloadresult:", result)
|
||||
|
||||
$q.notify({
|
||||
type: result.ok ? 'positive' : 'negative',
|
||||
message: result.message,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user