ilk
This commit is contained in:
7
ui/src/App.vue
Normal file
7
ui/src/App.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
15
ui/src/assets/quasar-logo-vertical.svg
Normal file
15
ui/src/assets/quasar-logo-vertical.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
|
||||
<path
|
||||
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
|
||||
<path fill="#050A14"
|
||||
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
|
||||
<path fill="#050A14"
|
||||
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
|
||||
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
0
ui/src/boot/.gitkeep
Normal file
0
ui/src/boot/.gitkeep
Normal file
21
ui/src/boot/axios.js
Normal file
21
ui/src/boot/axios.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { boot } from 'quasar/wrappers'
|
||||
import axios from 'axios'
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: 'http://localhost:8080/api',
|
||||
timeout: 180000,
|
||||
withCredentials: true // refresh cookie kullanıyorsan kalsın
|
||||
})
|
||||
|
||||
export default boot(() => {
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token') // ✅ senin authStore key’in
|
||||
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
})
|
||||
14
ui/src/boot/dayjs.js
Normal file
14
ui/src/boot/dayjs.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// src/boot/dayjs.js
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime.js'
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat.js'
|
||||
import 'dayjs/locale/tr.js'
|
||||
|
||||
// 🔹 Plugin’leri aktif et
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(localizedFormat)
|
||||
dayjs.locale('tr')
|
||||
|
||||
export default dayjs
|
||||
44
ui/src/components/EssentialLink.vue
Normal file
44
ui/src/components/EssentialLink.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<q-item
|
||||
clickable
|
||||
tag="a"
|
||||
target="_blank"
|
||||
:href="props.link"
|
||||
>
|
||||
<q-item-section
|
||||
v-if="props.icon"
|
||||
avatar
|
||||
>
|
||||
<q-icon :name="props.icon" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ props.title }}</q-item-label>
|
||||
<q-item-label caption>{{ props.caption }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
caption: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
link: {
|
||||
type: String,
|
||||
default: '#'
|
||||
},
|
||||
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
20
ui/src/composables/usePermission.js
Normal file
20
ui/src/composables/usePermission.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { computed } from 'vue'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
|
||||
export function usePermission () {
|
||||
|
||||
const perm = usePermissionStore()
|
||||
|
||||
return {
|
||||
|
||||
canRead: (m) => computed(() => perm.hasPermission(m, 'read')),
|
||||
canWrite: (m) => computed(() => perm.hasPermission(m, 'write')),
|
||||
canUpdate:(m) => computed(() => perm.hasPermission(m, 'update')),
|
||||
canDelete:(m) => computed(() => perm.hasPermission(m, 'delete')),
|
||||
canExport:(m) => computed(() => perm.hasPermission(m, 'export')),
|
||||
|
||||
canApi: (p) => computed(() => perm.hasApiPermission(p)),
|
||||
|
||||
hasModule: (m) => computed(() => perm.hasModule(m))
|
||||
}
|
||||
}
|
||||
1532
ui/src/css/app.css
Normal file
1532
ui/src/css/app.css
Normal file
@@ -0,0 +1,1532 @@
|
||||
/* ===========================================================
|
||||
GLOBAL CUSTOM CSS
|
||||
=========================================================== */
|
||||
.with-bg {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
.with-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: url('/images/Baggi-tekstilas-logolu.jpg') no-repeat center top;
|
||||
background-size: 400px auto;
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.with-bg > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.q-page {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.with-bg::before {
|
||||
background-size: 260px auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ÜST BLOKLAR (SABİT) ===== */
|
||||
.filter-sticky {
|
||||
position: sticky;
|
||||
top: 56px; /* q-header yüksekliği */
|
||||
z-index: 300;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.filter-collapsible {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ===== TABLO SCROLL ===== */
|
||||
.table-scroll {
|
||||
margin-top: 0; /* 🔹 Boşluğu kaldır */
|
||||
height: calc(100vh - 56px); /* 🔹 Header yüksekliği kadar kısalt */
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sticky-table .q-table__middle {
|
||||
overflow: visible !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.sticky-table .q-table__top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 220;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.sticky-table thead th {
|
||||
position: sticky;
|
||||
top: 40px;
|
||||
z-index: 210;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 🔹 Toggle bar */
|
||||
.sticky-bar {
|
||||
position: sticky;
|
||||
top: 0; /* tablo scroll başladığında en üstte kalsın */
|
||||
z-index: 230;
|
||||
background: #fff;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* ===== KOLON DARALTMA + WRAP ===== */
|
||||
.sticky-table thead th {
|
||||
resize: horizontal;
|
||||
overflow: auto;
|
||||
min-width: 80px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.sticky-table td {
|
||||
min-width: 80px;
|
||||
max-width: 400px;
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
line-height: 1.2rem;
|
||||
padding: 4px 8px !important;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ===== GÖRSEL ===== */
|
||||
.baggi-ppct {
|
||||
display: block;
|
||||
margin: 30px auto 0;
|
||||
max-width: 400px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.col-desc {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 0.75rem !important;
|
||||
line-height: 1.1rem;
|
||||
width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
min-width: 180px !important;
|
||||
}
|
||||
|
||||
/* ===== TABLO GÖRÜNÜM ===== */
|
||||
.custom-table { font-size: 0.8rem; }
|
||||
.custom-table th { background: #fff; font-weight: 800; color: #222; }
|
||||
.custom-table td { font-weight: 600; color: #333; }
|
||||
|
||||
.custom-subtable { font-size: 0.72rem; background: #fafafa; }
|
||||
.custom-subtable th { background: #f9f9f9; font-weight: 500; color: #555; }
|
||||
.custom-subtable td { font-weight: 400; color: #666; }
|
||||
|
||||
/* dar sütunlar için */
|
||||
.col-narrow {
|
||||
font-size: 0.72rem;
|
||||
padding: 2px 6px !important;
|
||||
max-width: 90px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ===== GRUP SATIRI ===== */
|
||||
.group-row {
|
||||
background: #f1f1f1 !important;
|
||||
font-weight: 700 !important;
|
||||
color: #222;
|
||||
border-top: 2px solid #ccc;
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
||||
/* ===== BALANCE CARD ===== */
|
||||
.balance-card {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.q-table td[data-col="belge_no"],
|
||||
.q-table td[data-col="Belge_No"],
|
||||
.q-table td[data-col="BELGE_NO"] {
|
||||
color: var(--q-primary) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
PERMISSIONS PAGE (FINAL)
|
||||
=========================================================== */
|
||||
|
||||
/* Toolbar */
|
||||
.permissions-toolbar {
|
||||
position: sticky;
|
||||
top: 42px; /* q-header yüksekliği */
|
||||
z-index: 300;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Table scroll alanı */
|
||||
.permissions-table-scroll {
|
||||
height: calc(100vh - 112px); /* header (56) + toolbar (56) */
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Tablo gövdesi */
|
||||
.permissions-table .q-table__middle {
|
||||
overflow: auto !important;
|
||||
max-height: none !important;
|
||||
padding-top: 0px; /* 🔑 Başlık yüksekliği kadar boşluk bırak */
|
||||
}
|
||||
|
||||
/* Sticky başlıklar – toolbar’ın altında */
|
||||
.permissions-table thead th {
|
||||
position: sticky;
|
||||
top:10px; /* toolbar altında hizalanır */
|
||||
z-index: 210;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Hücreler */
|
||||
.permissions-table td {
|
||||
min-width: 80px;
|
||||
max-width: 400px;
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
line-height: 1.2rem;
|
||||
padding: 4px 8px !important;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* İlk kolon (role) sabit */
|
||||
.permissions-table .permissions-sticky-col {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 205;
|
||||
background: #fff;
|
||||
box-shadow: 2px 0 4px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
1️⃣ ROOT & GLOBAL RESET
|
||||
=========================================================== */
|
||||
:root {
|
||||
--header-h: 0px;
|
||||
--filter-h: 72px;
|
||||
--save-h: 60px;
|
||||
--grid-header-h: 172px;
|
||||
--sub-header-h: 34px;
|
||||
--drawer-w: 240px;
|
||||
|
||||
/* Grid kolon genişlikleri */
|
||||
--col-model: 90px;
|
||||
--col-renk: 80px;
|
||||
--col-ana: 100px;
|
||||
--col-alt: 100px;
|
||||
--col-aciklama: 140px;
|
||||
--col-adet: 70px;
|
||||
--col-fiyat: 70px;
|
||||
--col-pb: 70px;
|
||||
--col-tutar: 70px;
|
||||
--col-termin: 142px; /* 🔹 termin tarihi kolon genişliği */
|
||||
|
||||
/* Beden blok ölçüleri */
|
||||
--grp-title-w: 90px;
|
||||
--grp-title-gap: 4px;
|
||||
--beden-w: 44px;
|
||||
--beden-h: 28px;
|
||||
--beden-count: 16;
|
||||
|
||||
/* Tema renkleri */
|
||||
--baggi-gold: #c9a227;
|
||||
--baggi-gold-pale: #fff9e6;
|
||||
--baggi-gold-light: #fff7d2;
|
||||
--baggi-cream: #fffef9;
|
||||
--baggi-gray-border: #bbb;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
background: #fff;
|
||||
color: #222;
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
#q-app, .q-page-container { margin: 0; padding: 0; }
|
||||
.q-layout__page { top: 0 !important; }
|
||||
/* ===========================================================
|
||||
2️⃣ PAGE STRUCTURE & SCROLL
|
||||
=========================================================== */
|
||||
.order-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--header-h));
|
||||
overflow-y: auto;
|
||||
overflow-x: visible;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.body--drawer-left-open .q-page-container {
|
||||
margin-left: var(--drawer-w);
|
||||
width: calc(100% - var(--drawer-w));
|
||||
}
|
||||
.body--drawer-left-closed .q-page-container {
|
||||
margin-left: 0; width: 100%;
|
||||
}
|
||||
|
||||
/* 🔸 Yatay scroll sadece grid alanında */
|
||||
.order-scroll-x {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background: #fff;
|
||||
}
|
||||
/* 🔸 Scrollbar stili */
|
||||
|
||||
.order-page::-webkit-scrollbar-thumb,
|
||||
.order-scroll-x::-webkit-scrollbar {
|
||||
height: 8px; width: 8px;
|
||||
}
|
||||
.order-scroll-x::-webkit-scrollbar-thumb {
|
||||
background: #c0a75e;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.order-scroll-x::-webkit-scrollbar-track {
|
||||
background: #f9f5e6;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
3️⃣ STICKY STACK (HEADER + TOOLBARS)
|
||||
=========================================================== */
|
||||
.q-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.sticky-stack {
|
||||
position: sticky;
|
||||
top: var(--header-h);
|
||||
margin-top: 0 !important;
|
||||
z-index: 950;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* 🔹 Filtre bar */
|
||||
.filter-bar {
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 12px 24px;
|
||||
margin-top:0 !important;
|
||||
}
|
||||
|
||||
/* 🔹 Save toolbar */
|
||||
.save-toolbar {
|
||||
background: var(--baggi-gold-pale);
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 940;
|
||||
}
|
||||
.save-toolbar .label { font-weight: 700; color: #6a5314; }
|
||||
.save-toolbar .value { font-weight: 700; color: #000; }
|
||||
.save-toolbar .q-btn {
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
text-transform: none;
|
||||
}
|
||||
/* ===========================================================
|
||||
4️⃣ GRID HEADER (ANA BAŞLIK BLOKU)
|
||||
=========================================================== */
|
||||
.order-grid-header {
|
||||
position: sticky;
|
||||
top: calc(var(--header-h) + var(--filter-h) + var(--save-h));
|
||||
z-index: 700;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns:
|
||||
var(--col-model)
|
||||
var(--col-renk)
|
||||
var(--col-ana)
|
||||
var(--col-alt)
|
||||
var(--col-aciklama)
|
||||
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count)))
|
||||
var(--col-adet)
|
||||
var(--col-fiyat)
|
||||
var(--col-pb)
|
||||
var(--col-tutar)
|
||||
var(--col-termin);
|
||||
background: var(--baggi-cream);
|
||||
border-bottom: 2px solid var(--baggi-gray-border);
|
||||
box-shadow: 0 2px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* Sabit kolonlar */
|
||||
.order-grid-header .col-fixed {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
writing-mode: vertical-lr;
|
||||
transform: rotate(180deg);
|
||||
background: var(--baggi-gold-light);
|
||||
border: 1px solid #aaa;
|
||||
font-weight: 700;
|
||||
font-size: 12.5px;
|
||||
height: var(--grid-header-h);
|
||||
}
|
||||
|
||||
.order-grid-header .aciklama-col {
|
||||
background: #fff9c4;
|
||||
border-right: 2px solid #a6a6a6;
|
||||
}
|
||||
/* ===========================================================
|
||||
5️⃣ BEDEN BLOKLARI & SAĞ TOPLAM
|
||||
=========================================================== */
|
||||
.order-grid-header .beden-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: var(--grid-header-h);
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.order-grid-header .grp-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--beden-h);
|
||||
}
|
||||
|
||||
.order-grid-header .grp-title {
|
||||
width: var(--grp-title-w);
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.order-grid-header .grp-body {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: var(--beden-w);
|
||||
}
|
||||
.order-grid-header .grp-cell.hdr {
|
||||
width: var(--beden-w);
|
||||
height: var(--beden-h);
|
||||
border: 1px solid #bbb;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.order-grid-header .total-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
background: #fff59d;
|
||||
}
|
||||
.order-grid-header .total-cell {
|
||||
width: var(--col-adet);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
writing-mode: vertical-lr;
|
||||
transform: rotate(180deg);
|
||||
border-right: 1px solid #bbb;
|
||||
background: var(--baggi-gold-pale);
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* ===========================================================
|
||||
6️⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ
|
||||
=========================================================== */
|
||||
.order-sub-header {
|
||||
padding-right: 0 !important; /* 🔹 Ekstra sağ boşluğu kaldır */
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.order-sub-header {
|
||||
position: sticky;
|
||||
top: calc(
|
||||
var(--header-h)
|
||||
+ var(--filter-h)
|
||||
+ var(--save-h)
|
||||
+ var(--grid-header-h)
|
||||
);
|
||||
z-index: 650;
|
||||
|
||||
/* 🔹 Header ile birebir grid düzeni */
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns:
|
||||
var(--col-model)
|
||||
var(--col-renk)
|
||||
var(--col-ana)
|
||||
var(--col-alt)
|
||||
var(--col-aciklama)
|
||||
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
|
||||
var(--col-adet)
|
||||
var(--col-fiyat)
|
||||
var(--col-pb)
|
||||
var(--col-tutar)
|
||||
var(--col-termin);
|
||||
|
||||
align-items: center;
|
||||
justify-items: stretch;
|
||||
height: var(--sub-header-h);
|
||||
min-height: var(--sub-header-h);
|
||||
|
||||
/* 🔹 Görsel */
|
||||
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
|
||||
border-top: 1px solid #d6c06a;
|
||||
border-bottom: 1px solid #d6c06a;
|
||||
|
||||
/* 🔹 Hatalı hizalamaları engelle */
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding-right: 0 !important; /* ✅ sağ taşmayı önler */
|
||||
}
|
||||
|
||||
/* 🔹 Genişlik eşitleme */
|
||||
:root {
|
||||
--col-termin: 142px; /* ✅ q-input genişliğiyle birebir */
|
||||
}
|
||||
|
||||
/* 🔹 Sub-header hover efekti */
|
||||
.order-sub-header:hover {
|
||||
background: linear-gradient(90deg, #fff9cf 0%, #fff3b0 70%, #ffe88f 100%);
|
||||
}
|
||||
|
||||
|
||||
/* 🔹 Sol taraf (MODEL–AÇIKLAMA alanı) */
|
||||
.order-sub-header .sub-left {
|
||||
grid-column: 1 / span 5;
|
||||
font-weight: 800;
|
||||
padding-left: 6px;
|
||||
color: #2b1f05;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 🔹 Orta beden bloğu (header’la aynı yapı) */
|
||||
.order-sub-header .sub-center {
|
||||
grid-column: 6 / 7;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: var(--beden-w);
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
width: calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)));
|
||||
padding-left: var(--grp-title-w);
|
||||
margin-left: var(--grp-title-gap);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.order-sub-header .beden-cell {
|
||||
width: var(--beden-w);
|
||||
height: 100%;
|
||||
border: 1px solid #d8c16b;
|
||||
border-right: none;
|
||||
background: #fffdf3;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.order-sub-header .beden-cell:last-child {
|
||||
border-right: 1px solid #d8c16b;
|
||||
}
|
||||
|
||||
/* 🔹 Sağ taraf (adet–fiyat–pb–tutar–termin toplamları) */
|
||||
.order-sub-header .sub-right {
|
||||
grid-column: 7 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
padding-right: 0px;
|
||||
font-weight: 900;
|
||||
color: #3b2f09;
|
||||
line-height: 1.3;
|
||||
text-transform: uppercase;
|
||||
font-size: 13.5px;
|
||||
transform: translateX(-60px);
|
||||
}
|
||||
|
||||
.order-sub-header:hover {
|
||||
background: linear-gradient(90deg,#fff9cf 0%,#fff3b0 70%,#ffe88f 100%);
|
||||
}
|
||||
|
||||
:root {
|
||||
--sub-header-h: 60px;
|
||||
}
|
||||
|
||||
|
||||
/* Taşmayı engelle (ihtiyaten) */
|
||||
.order-sub-header {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
7️⃣ GRID BODY & SATIRLAR — TAM HİZALANMIŞ
|
||||
=========================================================== */
|
||||
.order-grid-body {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
margin-top: 0 !important;
|
||||
padding-top: var(--sub-header-h);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--col-model)
|
||||
var(--col-renk)
|
||||
var(--col-ana)
|
||||
var(--col-alt)
|
||||
var(--col-aciklama)
|
||||
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
|
||||
var(--col-adet)
|
||||
var(--col-fiyat)
|
||||
var(--col-pb)
|
||||
var(--col-tutar)
|
||||
var(--col-termin);}
|
||||
|
||||
.summary-row:hover {
|
||||
background: #fffce0;
|
||||
}
|
||||
.summary-row.is-editing {
|
||||
background: #fff3cd;
|
||||
outline: 2px solid #caa83f;
|
||||
}
|
||||
|
||||
.summary-row .cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--beden-h);
|
||||
padding: 4px 6px;
|
||||
font-size: 13px;
|
||||
color: #222;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.summary-row.row-closed {
|
||||
opacity: 0.55;
|
||||
background: #f5f5f5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.summary-row.row-closed:hover {
|
||||
background: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
.summary-row:nth-child(odd) { background: #fffef9; }
|
||||
|
||||
/* 🔹 Beden blok hizalaması */
|
||||
.summary-row .grp-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
transform: translateX(calc(var(--grp-title-w) - var(--beden-w)));
|
||||
}
|
||||
.summary-row .grp-row {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: var(--beden-w);
|
||||
}
|
||||
.summary-row .grp-row .cell.beden {
|
||||
width: var(--beden-w);
|
||||
height: var(--beden-h);
|
||||
border: 1px solid #ddd;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cell.beden.ghost {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
/* 🔹 Sağ kolonlar */
|
||||
.summary-row .cell.adet,
|
||||
.summary-row .cell.fiyat,
|
||||
.summary-row .cell.pb,
|
||||
.summary-row .cell.tutar,
|
||||
.summary-row .cell.termin {
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
border-left: none !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.summary-row .cell.tutar {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.summary-row .cell.termin {
|
||||
background: #fffef9;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: var(--col-termin);
|
||||
}
|
||||
.summary-row .cell.termin .q-input {
|
||||
width: 100%;
|
||||
max-width: 142px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.summary-row .cell.termin input {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
9️⃣ ORDER EDITOR (ALT FORM)
|
||||
=========================================================== */
|
||||
.editor {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
background: #fffef9;
|
||||
border-top: 1px solid #ddd;
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
}
|
||||
.editor::before {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 4px;
|
||||
background: linear-gradient(to right,#c9a227,#e5d28b,#fff7d2);
|
||||
margin-bottom: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.editor .q-btn:hover { background: #d2b04d; }
|
||||
.editor .q-input,
|
||||
.editor .q-select { margin-bottom: 8px; font-size: 14px; }
|
||||
.cell.termin .termin-label {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #222;
|
||||
background: #fffef9;
|
||||
border-left: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔟 RESPONSIVE + MİNÖR DÜZEN
|
||||
=========================================================== */
|
||||
@media (max-width: 1024px) {
|
||||
:root { --beden-w: 40px; --col-aciklama: 120px; }
|
||||
.order-grid-header .col-fixed { font-size: 11px; }
|
||||
.order-sub-header { font-size: 12.5px; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--beden-w: 36px;
|
||||
--col-model: 70px;
|
||||
--col-renk: 60px;
|
||||
--col-aciklama: 100px;
|
||||
}
|
||||
.order-page { font-size: 13px; }
|
||||
.order-grid-header .total-cell { font-size: 10.5px; }
|
||||
}
|
||||
|
||||
.summary-row .cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.summary-row .grp-area,
|
||||
.summary-row .grp-row,
|
||||
.summary-row .grp-row .cell.beden {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.summary-row .cell.aciklama {
|
||||
grid-column: 5 / 6 !important; /* sadece 5. kolon */
|
||||
position: relative !important;
|
||||
width: calc(var(--col-aciklama) + 92px) !important; /* 🔹 74px genişletme */
|
||||
margin-right: -92px !important; /* 🔹 bedenle tam hizalanır */
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 6px 12px !important;
|
||||
font-size: 13px !important;
|
||||
text-align: left !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
justify-content: flex-start !important;
|
||||
min-height: 36px !important;
|
||||
background: #fff !important;
|
||||
box-sizing: border-box !important;
|
||||
border-right: 1px solid #ccc !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
/* 🧩 Grid çizgi kontrastı güçlendirme */
|
||||
.summary-row .cell,
|
||||
.order-grid-header .col-fixed,
|
||||
.summary-row .grp-row .cell.beden {
|
||||
border-color: #bbb !important; /* 🔹 daha belirgin çizgi */
|
||||
}
|
||||
|
||||
.summary-row .cell:not(:last-child) {
|
||||
border-right: 1px solid #bdbdbd !important;
|
||||
}
|
||||
/* ===========================================================
|
||||
🧱 ALT GRID ÇİZGİLERİ – TÜM SATIRLAR İÇİN
|
||||
=========================================================== */
|
||||
.summary-row {
|
||||
border-bottom: 1px solid #ccc; /* 🔹 satır alt çizgisi */
|
||||
}
|
||||
|
||||
.summary-row:last-child {
|
||||
border-bottom: 2px solid #b7a33a; /* 🔹 son satırda Baggi gold tonu */
|
||||
}
|
||||
|
||||
/* 🔹 Hücrelerin alt çizgisi (beden dahil) */
|
||||
.summary-row .cell,
|
||||
.summary-row .grp-row .cell.beden {
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */
|
||||
.summary-row:hover .cell,
|
||||
.summary-row:hover .grp-row .cell.beden {
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
}
|
||||
.summary-row:hover {
|
||||
background: #fffce0;
|
||||
}
|
||||
|
||||
.summary-row.is-editing {
|
||||
background: #fff3cd;
|
||||
outline: 2px solid #caa83f;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.editor .q-btn:hover {
|
||||
background: #d2b04d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */
|
||||
.summary-row:hover .cell,
|
||||
.summary-row:hover .grp-row .cell.beden {
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🎨 STOK RENKLERİ (LOW–MID–HIGH)
|
||||
=========================================================== */
|
||||
.stok-red {
|
||||
color: #e53935; /* 🔴 Kırmızı */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stok-yellow {
|
||||
color: #f9a825; /* 🟡 Sarı */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stok-green {
|
||||
color: #43a047; /* 🟢 Yeşil */
|
||||
font-weight: 600;
|
||||
}
|
||||
.q-banner.rounded-borders {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.order-gateway {
|
||||
background: linear-gradient(145deg, #fff 0%, #fafafa 100%);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.order-btn {
|
||||
font-size: 1.2rem;
|
||||
padding: 20px 40px;
|
||||
border-radius: 12px;
|
||||
min-width: 280px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.order-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
/* ===========================================================
|
||||
🧭 DRAWER AÇIKKEN GRID HİZALAMA FIX
|
||||
=========================================================== */
|
||||
|
||||
/* Drawer açıkken içerik kaymaması */
|
||||
.body--drawer-left-open .order-page {
|
||||
width: calc(100vw - var(--drawer-w)); /* viewport'tan drawer genişliği kadar düş */
|
||||
overflow-x: visible; /* dış overflow’u kes */
|
||||
}
|
||||
|
||||
/* Scroll konteyner sadece grid içinde çalışsın */
|
||||
.order-scroll-x {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Scrollbar ve sağ boşluğu dengeler */
|
||||
.order-grid-header,
|
||||
.order-sub-header,
|
||||
.order-grid-body {
|
||||
min-width: fit-content;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* ===========================================================
|
||||
🧱 DRAWER AÇIKKEN TAM HİZALAMA FIX (v2)
|
||||
=========================================================== */
|
||||
|
||||
/* Drawer açıkken tüm üst bloklar sağdan taşmasın */
|
||||
.body--drawer-left-open .filter-bar,
|
||||
.body--drawer-left-open .save-toolbar,
|
||||
.body--drawer-left-open .order-grid-header,
|
||||
.body--drawer-left-open .order-sub-header,
|
||||
.body--drawer-left-open .order-grid-body {
|
||||
width: calc(100vw - var(--drawer-w)); /* drawer genişliği kadar daralt */
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Drawer kapalıyken tam genişlik */
|
||||
.body--drawer-left-closed .filter-bar,
|
||||
.body--drawer-left-closed .save-toolbar,
|
||||
.body--drawer-left-closed .order-grid-header,
|
||||
.body--drawer-left-closed .order-sub-header,
|
||||
.body--drawer-left-closed .order-grid-body {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Order grid sağ sınırı altın kenarlıkla bitir (optik kapanış) */
|
||||
.order-grid-header,
|
||||
.order-sub-header,
|
||||
.order-grid-body {
|
||||
border-right: 2px solid var(--baggi-gold);
|
||||
}
|
||||
/* ===========================================================
|
||||
🎯 SAĞ ALT BOŞLUK FİNAL FIX
|
||||
=========================================================== */
|
||||
|
||||
/* Drawer açıkken tüm grid konteynerleri sağdan tam sıfırla */
|
||||
.body--drawer-left-open .order-page,
|
||||
.body--drawer-left-open .filter-bar,
|
||||
.body--drawer-left-open .save-toolbar,
|
||||
.body--drawer-left-open .order-grid-header,
|
||||
.body--drawer-left-open .order-sub-header,
|
||||
.body--drawer-left-open .order-grid-body {
|
||||
width: calc(100vw - var(--drawer-w) - 8px); /* 🔹 scrollbar toleransı */
|
||||
padding-right: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
overflow-x: visible !important;
|
||||
}
|
||||
|
||||
/* Son altın kenarlık hizasını koru */
|
||||
.order-grid-body {
|
||||
border-right: 2px solid var(--baggi-gold);
|
||||
}
|
||||
/* ===========================================================
|
||||
🎯 GRID SAĞ HİZALAMA (FILTER + SAVE + HEADER)
|
||||
=========================================================== */
|
||||
|
||||
/* Ana scroll container referansı */
|
||||
.order-scroll-x {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; /* hizalama sola */
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Filter ve Save barlar grid genişliğini takip etsin */
|
||||
.filter-bar,
|
||||
.save-toolbar,
|
||||
.order-grid-header,
|
||||
.order-sub-header {
|
||||
width: fit-content; /* içeriğe göre genişlik */
|
||||
min-width: 100%; /* minimum ekran kadar */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Grid body’nin genişliği kadar sağ hizalama */
|
||||
.order-grid-body {
|
||||
width: fit-content;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* 🔒 Kapalı satır */
|
||||
.summary-row.row-closed {
|
||||
background: #e6e6e6 !important;
|
||||
opacity: 0.65;
|
||||
pointer-events: none; /* tüm inputları disable eder */
|
||||
}
|
||||
|
||||
/* Hover iptal */
|
||||
.summary-row.row-closed:hover {
|
||||
background: #e6e6e6 !important;
|
||||
}
|
||||
|
||||
/* Edit efekti de kapansın */
|
||||
.summary-row.row-closed.is-editing {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Sağ kenarda taşma veya padding olmasın */
|
||||
.filter-bar,
|
||||
.save-toolbar,
|
||||
.order-grid-header,
|
||||
.order-sub-header,
|
||||
.order-grid-body {
|
||||
margin-right: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
border-right: none !important; /* altın çizgi istemiyorsan kaldırılır */
|
||||
}
|
||||
.summary-row.row-error {
|
||||
background: rgba(193, 0, 21, 0.08);
|
||||
}
|
||||
|
||||
.row-error-icon {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
|
||||
/* Drawer açık/kapalı fark etmeden */
|
||||
.body--drawer-left-open .order-scroll-x,
|
||||
.body--drawer-left-closed .order-scroll-x {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
ORDER LIST (ol-) — Sticky Stack
|
||||
=============================== */
|
||||
|
||||
:root {
|
||||
/* Quasar header yüksekliği */
|
||||
--ol-header-h: 56px;
|
||||
/* Filter bar yüksekliği (px) — inputlar tek satırsa 56 idealdir */
|
||||
--ol-filter-h: 96px;
|
||||
}
|
||||
|
||||
/* q-page tek scroller: header altından başlar */
|
||||
.ol-page {
|
||||
height: calc(100vh - var(--ol-header-h));
|
||||
overflow: auto; /* 🔑 tek scroll container */
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Filter bar: q-header’ın altında sticky */
|
||||
.ol-filter-bar {
|
||||
position: sticky;
|
||||
top: 0; /* 🔑 .ol-page scroller’ında en üst */
|
||||
z-index: 600;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 16px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||
min-height: var(--ol-filter-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* QTable: sticky thead, zebra aktif ve çakışma yok */
|
||||
.ol-table .q-table__middle {
|
||||
overflow: visible !important; /* sticky thead için güvenli */
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
/* thead sabitleme: filter bar’ın ALTINA oturur */
|
||||
.ol-table thead th {
|
||||
position: sticky;
|
||||
top: var(--ol-filter-h); /* 🔑 filter yüksekliği kadar boşluk */
|
||||
z-index: 500;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Zebra */
|
||||
.ol-table .q-table__body .q-tr:nth-child(odd) {
|
||||
background-color: #f7f7f7 !important;
|
||||
}
|
||||
.ol-table .q-table__body .q-tr:nth-child(even) {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.ol-table .q-table__body .q-tr:hover {
|
||||
background-color: #fff7d1 !important;
|
||||
transition: background-color .15s ease;
|
||||
}
|
||||
|
||||
/* Hücreler */
|
||||
.ol-table .q-td {
|
||||
font-size: .9rem;
|
||||
line-height: 1.3;
|
||||
padding: 6px 8px !important;
|
||||
}
|
||||
|
||||
/* Güvenli z-index hiyerarşisi */
|
||||
.q-header { z-index: 1000 !important; } /* header en üstte */
|
||||
.q-drawer { z-index: 950 !important; } /* drawer header’ın altında */
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
:root { --ol-filter-h: 64px; } /* input kırılıyorsa biraz artır */
|
||||
.ol-filter-bar { padding: 8px 12px; }
|
||||
}
|
||||
/* ===========================================================
|
||||
🟡 ORDERLIST ZEBRA FIX (v3)
|
||||
=========================================================== */
|
||||
|
||||
/* Her iki tr katmanını da hedefliyoruz (Quasar q-tr + native tr) */
|
||||
.ol-table tbody tr:nth-child(odd),
|
||||
.ol-table .q-table__body .q-tr:nth-child(odd) {
|
||||
background-color: #faf8ef !important; /* açık krem tonu */
|
||||
}
|
||||
|
||||
.ol-table tbody tr:nth-child(even),
|
||||
.ol-table .q-table__body .q-tr:nth-child(even) {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Hover tonu: hafif Baggi gold dokunuşu */
|
||||
.ol-table tbody tr:hover,
|
||||
.ol-table .q-table__body .q-tr:hover {
|
||||
background-color: #fff4cc !important;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
/* =========================================================
|
||||
📌 OrderList — Toplam USD Banner
|
||||
========================================================= */
|
||||
.ol-qbanner {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.ol-qbanner-amount {
|
||||
color: #1976d2; /* Quasar primary */
|
||||
margin-left: 6px;
|
||||
}
|
||||
/* =========================================================
|
||||
📌 ORDER LIST (ol-) — MULTILINE COLUMN FIX
|
||||
========================================================= */
|
||||
|
||||
/* Ortak çok satırlı davranış */
|
||||
.ol-col-multiline {
|
||||
white-space: normal !important;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
/* 🧾 Cari Adı — 2 SATIR */
|
||||
.ol-col-cari {
|
||||
max-width: 200px;
|
||||
min-width: 150px;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
font-weight: 600;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
/* 📝 Açıklama — 5 SATIR */
|
||||
.ol-col-desc {
|
||||
max-width: 220px;
|
||||
min-width: 160px;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
font-size: 0.82rem;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/* Hücre padding iyileştirme */
|
||||
.ol-table .q-td.ol-col-cari,
|
||||
.ol-table .q-td.ol-col-desc {
|
||||
padding-top: 6px !important;
|
||||
padding-bottom: 6px !important;
|
||||
}
|
||||
|
||||
/* Header hizalama */
|
||||
.ol-table th.ol-col-cari,
|
||||
.ol-table th.ol-col-desc {
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* =========================================================
|
||||
ORDER LIST — FILTER BAR FLEX FIX (FINAL)
|
||||
========================================================= */
|
||||
|
||||
.ol-filter-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 600;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* 🔑 TEK SATIR */
|
||||
.ol-filter-row {
|
||||
display: flex;
|
||||
align-items: flex-end; /* 🔑 tüm input & butonlar aynı çizgi */
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap; /* ❌ alt satıra düşme */
|
||||
}
|
||||
|
||||
/* Inputlar */
|
||||
.ol-filter-input {
|
||||
min-width: 180px;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
/* Arama biraz geniş */
|
||||
.ol-search {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
/* Butonlar */
|
||||
.ol-filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Toplam banner */
|
||||
.ol-filter-total {
|
||||
margin-left: auto; /* 🔑 sağa yasla */
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Mobile fallback */
|
||||
@media (max-width: 1200px) {
|
||||
.ol-filter-row {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.ol-filter-total {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.order-gateway {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.draft-card {
|
||||
width: 320px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
/* ===========================================================
|
||||
🕵️ ACTIVITY LOGS (act-) — GLOBAL
|
||||
=========================================================== */
|
||||
|
||||
/* Sayfa konteyneri */
|
||||
.act-page {
|
||||
height: calc(100vh - 56px); /* q-header */
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Sticky filter bar */
|
||||
.act-filter-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 620;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 16px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Filtre satırı */
|
||||
.act-filter-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
/* Input genişlikleri */
|
||||
.act-filter-input {
|
||||
min-width: 160px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.act-filter-wide {
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
/* Aksiyon butonları */
|
||||
.act-filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Tablo */
|
||||
.act-table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Sticky thead */
|
||||
.act-table thead th {
|
||||
position: sticky;
|
||||
top: 56px; /* filter bar yüksekliği */
|
||||
z-index: 500;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Zebra */
|
||||
.act-table tbody tr:nth-child(odd) {
|
||||
background: #faf8ef;
|
||||
}
|
||||
.act-table tbody tr:nth-child(even) {
|
||||
background: #fff;
|
||||
}
|
||||
.act-table tbody tr:hover {
|
||||
background: #fff4cc;
|
||||
}
|
||||
|
||||
/* Hücreler */
|
||||
.act-table .q-td {
|
||||
padding: 6px 8px !important;
|
||||
line-height: 1.25;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Başarılı / hatalı satır */
|
||||
.act-row-success {
|
||||
background: rgba(67, 160, 71, 0.06);
|
||||
}
|
||||
.act-row-fail {
|
||||
background: rgba(211, 47, 47, 0.06);
|
||||
}
|
||||
|
||||
/* Badge renkleri */
|
||||
.act-badge-ok {
|
||||
background: #43a047;
|
||||
}
|
||||
.act-badge-fail {
|
||||
background: #e53935;
|
||||
}
|
||||
|
||||
/* Dar kolon */
|
||||
.act-col-narrow {
|
||||
max-width: 90px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Route kolonu */
|
||||
.act-col-route {
|
||||
max-width: 260px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* IP / UA */
|
||||
.act-col-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #555;
|
||||
}
|
||||
/* ================================
|
||||
UserList — Piyasa Wrap (NO CUT)
|
||||
================================ */
|
||||
|
||||
/* Hücre yukarıdan başlasın */
|
||||
.ol-col-piyasa {
|
||||
vertical-align: top;
|
||||
padding-top: 6px !important;
|
||||
padding-bottom: 6px !important;
|
||||
}
|
||||
|
||||
/* Chip konteyner */
|
||||
.piyasa-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* 🔑 alt satıra insin */
|
||||
align-content: flex-start;
|
||||
|
||||
row-gap: 4px;
|
||||
column-gap: 6px;
|
||||
|
||||
/* ❌ KESİNLİKLE YOK */
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Chip */
|
||||
.piyasa-chip {
|
||||
flex: 0 0 calc(25% - 6px); /* 🔑 satırda 4 adet */
|
||||
max-width: calc(25% - 6px);
|
||||
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.user-detail-page {
|
||||
background: #fafafa;
|
||||
}
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.image-thumb {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.workorder-page {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
/* ===============================
|
||||
PERMISSIONS — BAGGI THEME
|
||||
=============================== */
|
||||
|
||||
.permissions-page {
|
||||
background: #fff;
|
||||
height: calc(100vh - 56px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Scroll alanı */
|
||||
.permissions-table-scroll {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Tablo */
|
||||
.permissions-table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Sticky header */
|
||||
.permissions-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 300;
|
||||
background: var(--baggi-cream);
|
||||
font-weight: 800;
|
||||
color: #222;
|
||||
box-shadow: 0 2px 3px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Hücreler */
|
||||
.permissions-table td {
|
||||
font-weight: 600;
|
||||
padding: 6px 8px !important;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Zebra */
|
||||
.permissions-table tbody tr:nth-child(odd) {
|
||||
background: #fffef7;
|
||||
}
|
||||
.permissions-table tbody tr:nth-child(even) {
|
||||
background: #ffffff;
|
||||
}
|
||||
.permissions-table tbody tr:hover {
|
||||
background: #fff4cc;
|
||||
}
|
||||
|
||||
/* Sol kolon sabit */
|
||||
.permissions-sticky-col {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 250;
|
||||
background: #fff;
|
||||
box-shadow: 2px 0 4px rgba(0,0,0,0.04);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Header checkbox */
|
||||
.permissions-table .q-th .column {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
8
ui/src/css/quasar.variables.sass
Normal file
8
ui/src/css/quasar.variables.sass
Normal file
@@ -0,0 +1,8 @@
|
||||
$primary : #957116e8 // baggi altın
|
||||
$secondary : #dac197 // baggi ikinci ton
|
||||
$accent : #ff9800 // turuncu
|
||||
$dark : #5a4c4c // siyah-gri
|
||||
$positive : #21ba45 // yeşil
|
||||
$negative : #c10015 // kırmızı
|
||||
$info : #31ccec // açık mavi
|
||||
$warning : #f2c037 // sarı
|
||||
7
ui/src/layouts/EmptyLayout.vue
Normal file
7
ui/src/layouts/EmptyLayout.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
302
ui/src/layouts/MainLayout.vue
Normal file
302
ui/src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<q-layout view="hHh Lpr fFf">
|
||||
|
||||
<!-- HEADER -->
|
||||
<q-header elevated class="bg-primary text-white">
|
||||
<q-toolbar>
|
||||
|
||||
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
|
||||
|
||||
<q-toolbar-title>
|
||||
<q-avatar class="bg-secondary q-mr-sm">
|
||||
<img src="/images/Baggi-tekstilas-logolu.jpg" />
|
||||
</q-avatar>
|
||||
Baggi Software System
|
||||
</q-toolbar-title>
|
||||
|
||||
<q-btn flat dense round icon="logout" @click="confirmLogout" />
|
||||
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<!-- DRAWER -->
|
||||
<q-drawer
|
||||
v-if="perm.loaded"
|
||||
v-model="leftDrawerOpen"
|
||||
show-if-above
|
||||
bordered
|
||||
class="bg-secondary text-white"
|
||||
>
|
||||
|
||||
<q-scroll-area style="height:100%">
|
||||
|
||||
<q-list padding>
|
||||
|
||||
<!-- DYNAMIC MENU -->
|
||||
<template
|
||||
v-for="(item, i) in filteredMenu"
|
||||
:key="i"
|
||||
>
|
||||
|
||||
<!-- GROUP -->
|
||||
<q-expansion-item
|
||||
v-if="item.children"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
expand-separator
|
||||
>
|
||||
|
||||
<q-item
|
||||
v-for="(c, j) in item.children"
|
||||
:key="j"
|
||||
clickable
|
||||
:to="c.to"
|
||||
>
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-icon name="chevron_right" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
{{ c.label }}
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
|
||||
</q-expansion-item>
|
||||
|
||||
|
||||
<!-- SINGLE -->
|
||||
<q-item
|
||||
v-else
|
||||
clickable
|
||||
:to="item.to"
|
||||
>
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="item.icon" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
{{ item.label }}
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
|
||||
<q-separator spaced />
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<!-- PASSWORD -->
|
||||
<q-item clickable to="/app/change-password">
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-icon name="vpn_key" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
Şifre Değiştir
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
|
||||
</q-list>
|
||||
|
||||
</q-scroll-area>
|
||||
|
||||
</q-drawer>
|
||||
|
||||
|
||||
<!-- CONTENT -->
|
||||
<q-page-container class="with-bg">
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
|
||||
|
||||
<!-- FOOTER -->
|
||||
<q-footer class="bg-grey-8 text-white">
|
||||
<q-toolbar class="bg-secondary">
|
||||
|
||||
<q-toolbar-title>
|
||||
Baggi Software System
|
||||
</q-toolbar-title>
|
||||
|
||||
</q-toolbar>
|
||||
</q-footer>
|
||||
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Dialog } from 'quasar'
|
||||
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead, canWrite, canUpdate } = usePermission()
|
||||
|
||||
const canReadOrder = canRead('order')
|
||||
const canWriteOrder = canWrite('order')
|
||||
const canUpdateOrder = canUpdate('order')
|
||||
|
||||
|
||||
/* ================= STORES ================= */
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const perm = usePermissionStore()
|
||||
|
||||
|
||||
/* ================= UI ================= */
|
||||
|
||||
const leftDrawerOpen = ref(true)
|
||||
|
||||
function toggleLeftDrawer () {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||
}
|
||||
|
||||
function confirmLogout () {
|
||||
Dialog.create({
|
||||
title: 'Çıkış Yap',
|
||||
message: 'Oturumunuzu kapatmak istediğinize emin misiniz?',
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => {
|
||||
auth.clearSession()
|
||||
perm.clear()
|
||||
router.push('/login')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* ================= LOAD PERMISSIONS ================= */
|
||||
|
||||
onMounted(async () => {
|
||||
if (!perm.loaded) {
|
||||
await perm.fetchPermissions()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
/* ================= MENU CONFIG ================= */
|
||||
|
||||
const menuItems = [
|
||||
|
||||
{
|
||||
label: 'Ana Panel',
|
||||
icon: 'dashboard',
|
||||
to: '/app',
|
||||
permission: 'system:view'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Finans',
|
||||
icon: 'account_balance',
|
||||
|
||||
children: [
|
||||
{
|
||||
label: 'Cari Ekstre',
|
||||
to: '/app/statementofaccount',
|
||||
permission: 'finance:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Sipariş',
|
||||
icon: 'shopping_cart',
|
||||
|
||||
children: [
|
||||
{
|
||||
label: 'Siparişler',
|
||||
to: '/app/order-gateway',
|
||||
permission: 'order:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Sistem',
|
||||
icon: 'settings',
|
||||
|
||||
children: [
|
||||
|
||||
{
|
||||
label: 'Rol + Departman Yetkileri',
|
||||
to: '/app/role-dept-permissions',
|
||||
permission: 'user:update'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Kullanıcı Yetkileri',
|
||||
to: '/app/user-permissions',
|
||||
permission: 'user:update'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Loglar',
|
||||
to: '/app/activity-logs',
|
||||
permission: 'user:view'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Test Mail',
|
||||
to: '/app/test-mail',
|
||||
permission: 'user:insert'
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Kullanıcı Yönetimi',
|
||||
icon: 'people',
|
||||
|
||||
children: [
|
||||
{
|
||||
label: 'Kullanıcılar',
|
||||
to: '/app/users',
|
||||
permission: 'user:view'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
|
||||
/* ================= FILTERED MENU ================= */
|
||||
|
||||
const filteredMenu = computed(() => {
|
||||
|
||||
if (!perm.loaded) return []
|
||||
|
||||
return menuItems
|
||||
.map(item => {
|
||||
|
||||
if (item.children) {
|
||||
|
||||
const children = item.children.filter(c =>
|
||||
perm.hasApiPermission(c.permission)
|
||||
)
|
||||
|
||||
if (!children.length) return null
|
||||
|
||||
return {
|
||||
...item,
|
||||
children
|
||||
}
|
||||
}
|
||||
|
||||
if (!perm.hasApiPermission(item.permission)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
.filter(Boolean)
|
||||
})
|
||||
</script>
|
||||
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>
|
||||
96
ui/src/router/index.js
Normal file
96
ui/src/router/index.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { route } from 'quasar/wrappers'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import routes from 'src/router/routes.js'
|
||||
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
|
||||
|
||||
export default route(function () {
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
/* ============================================================
|
||||
🔐 GLOBAL GUARD
|
||||
============================================================ */
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
|
||||
const auth = useAuthStore()
|
||||
const perm = usePermissionStore()
|
||||
|
||||
|
||||
/* ================= PUBLIC ================= */
|
||||
|
||||
if (to.meta?.public === true) {
|
||||
return next()
|
||||
}
|
||||
|
||||
|
||||
/* ================= LOGIN ================= */
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
return next('/login')
|
||||
}
|
||||
|
||||
|
||||
/* ================= PASSWORD ================= */
|
||||
|
||||
if (
|
||||
auth.mustChangePassword &&
|
||||
to.path !== '/first-password-change'
|
||||
) {
|
||||
return next('/first-password-change')
|
||||
}
|
||||
|
||||
|
||||
/* ================= ADMIN ================= */
|
||||
|
||||
if (auth.isAdmin) {
|
||||
return next()
|
||||
}
|
||||
|
||||
|
||||
/* ================= LOAD PERMS ================= */
|
||||
|
||||
if (!perm.loaded) {
|
||||
try {
|
||||
await perm.fetchPermissions()
|
||||
} catch (e) {
|
||||
console.error('Permission load failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ================= CHECK ================= */
|
||||
|
||||
const required = to.meta?.permission
|
||||
|
||||
if (!required) {
|
||||
return next()
|
||||
}
|
||||
|
||||
|
||||
const allowed = perm.hasApiPermission(required)
|
||||
|
||||
if (!allowed) {
|
||||
|
||||
console.warn('⛔ ACCESS DENIED:', {
|
||||
path: to.fullPath,
|
||||
permission: required
|
||||
})
|
||||
|
||||
return next('/unauthorized')
|
||||
}
|
||||
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
return router
|
||||
})
|
||||
10
ui/src/router/meta.d.js
Normal file
10
ui/src/router/meta.d.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// src/router/meta.js
|
||||
|
||||
/**
|
||||
* Route meta fields reference
|
||||
*
|
||||
* @typedef {Object} RouteMeta
|
||||
* @property {boolean} [public] - Auth gerekmez
|
||||
* @property {string} [permission] - Backend route permission (/api/...)
|
||||
*/
|
||||
export {}
|
||||
260
ui/src/router/routes.js
Normal file
260
ui/src/router/routes.js
Normal file
@@ -0,0 +1,260 @@
|
||||
// src/router/routes.js
|
||||
|
||||
const routes = [
|
||||
|
||||
/* ==========================================================
|
||||
🌍 ROOT
|
||||
========================================================== */
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/login'
|
||||
},
|
||||
|
||||
|
||||
/* ==========================================================
|
||||
🔐 PUBLIC
|
||||
========================================================== */
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('layouts/EmptyLayout.vue'),
|
||||
meta: { public: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'login',
|
||||
component: () => import('pages/MainPage.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/first-password-change',
|
||||
component: () => import('layouts/EmptyLayout.vue'),
|
||||
meta: { public: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'first-password-change',
|
||||
component: () => import('pages/FirstPasswordChange.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/password-reset/:token',
|
||||
component: () => import('layouts/EmptyLayout.vue'),
|
||||
meta: { public: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'password-reset',
|
||||
component: () => import('pages/ResetPassword.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/* ==========================================================
|
||||
🏠 MAIN APP
|
||||
========================================================== */
|
||||
{
|
||||
path: '/app',
|
||||
component: () => import('layouts/MainLayout.vue'),
|
||||
|
||||
children: [
|
||||
|
||||
/* ================= DASHBOARD ================= */
|
||||
|
||||
{
|
||||
path: '',
|
||||
name: 'dashboard',
|
||||
component: () => import('pages/Dashboard.vue'),
|
||||
meta: { permission: 'system:read' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= PERMISSIONS ================= */
|
||||
|
||||
{
|
||||
path: 'permissions',
|
||||
name: 'permissions',
|
||||
component: () => import('pages/PermissionMatrix.vue'),
|
||||
meta: { permission: 'system:read' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'role-dept-permissions',
|
||||
name: 'role-dept-permissions',
|
||||
component: () => import('pages/RoleDepartmentPermissionPage.vue'),
|
||||
meta: { permission: 'user:update' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'user-permissions',
|
||||
name: 'user-permissions',
|
||||
component: () => import('pages/UserPermissionPage.vue'),
|
||||
meta: { permission: 'user:update' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= FINANCE ================= */
|
||||
|
||||
{
|
||||
path: 'statementofaccount',
|
||||
name: 'statementofaccount',
|
||||
component: () => import('pages/statementofaccount.vue'),
|
||||
meta: { permission: 'finance:view' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'statementreport',
|
||||
name: 'statementreport',
|
||||
component: () => import('pages/StatementReport.vue'),
|
||||
meta: { permission: 'finance:view' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'statementheaderreport',
|
||||
name: 'statementheaderreport',
|
||||
component: () => import('pages/StatementHeaderReport.vue'),
|
||||
meta: { permission: 'finance:view' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= USERS ================= */
|
||||
|
||||
{
|
||||
path: 'users',
|
||||
name: 'user-gateway',
|
||||
component: () => import('pages/UserGateway.vue'),
|
||||
meta: { permission: 'user:view' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'users/list',
|
||||
name: 'user-list',
|
||||
component: () => import('pages/UserList.vue'),
|
||||
meta: { permission: 'user:view' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'users/new',
|
||||
name: 'user-new',
|
||||
component: () => import('pages/UserDetail.vue'),
|
||||
meta: {
|
||||
mode: 'new',
|
||||
permission: 'user:insert'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'users/edit/:id',
|
||||
name: 'user-edit',
|
||||
component: () => import('pages/UserDetail.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
mode: 'edit',
|
||||
permission: 'user:update'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'users/view/:id',
|
||||
name: 'user-view',
|
||||
component: () => import('pages/UserDetail.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
mode: 'view',
|
||||
permission: 'user:view'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/* ================= LOGS ================= */
|
||||
|
||||
{
|
||||
path: 'activity-logs',
|
||||
name: 'activity-logs',
|
||||
component: () => import('pages/ActivityLogs.vue'),
|
||||
meta: { permission: 'user:view' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= TEST MAIL ================= */
|
||||
|
||||
{
|
||||
path: 'test-mail',
|
||||
name: 'test-mail',
|
||||
component: () => import('pages/TestMail.vue'),
|
||||
meta: { permission: 'user:insert' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= ORDERS ================= */
|
||||
|
||||
{
|
||||
path: 'order-gateway',
|
||||
name: 'order-gateway',
|
||||
component: () => import('pages/OrderGateway.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order-entry/:orderHeaderID',
|
||||
name: 'order-entry',
|
||||
component: () => import('pages/OrderEntry.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
mode: 'new',
|
||||
permission: 'order:insert'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order-edit/:orderHeaderID',
|
||||
name: 'order-edit',
|
||||
component: () => import('pages/OrderEntry.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
mode: 'edit',
|
||||
permission: 'order:update'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order-list',
|
||||
name: 'order-list',
|
||||
component: () => import('pages/OrderList.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order-pdf/:id',
|
||||
name: 'order-pdf',
|
||||
component: () => import('pages/OrderPdf.vue'),
|
||||
props: true,
|
||||
meta: { permission: 'order:export' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= PASSWORD ================= */
|
||||
|
||||
{
|
||||
path: 'change-password',
|
||||
name: 'change-password',
|
||||
component: () => import('pages/ChangePassword.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/* ==========================================================
|
||||
❌ 404
|
||||
========================================================== */
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
component: () => import('pages/ErrorNotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
export default routes
|
||||
67
ui/src/services/api.js
Normal file
67
ui/src/services/api.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// src/services/api.js
|
||||
import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8080/api',
|
||||
timeout: 180000,
|
||||
paramsSerializer: params =>
|
||||
qs.stringify(params, { arrayFormat: 'repeat' })
|
||||
})
|
||||
|
||||
// REQUEST
|
||||
api.interceptors.request.use((config) => {
|
||||
const auth = useAuthStore()
|
||||
const url = config.url || ''
|
||||
|
||||
const isPublic =
|
||||
url.startsWith('/auth/login') ||
|
||||
url.startsWith('/auth/refresh') ||
|
||||
url.startsWith('/password/forgot') ||
|
||||
url.startsWith('/password/reset')
|
||||
|
||||
|
||||
if (!isPublic && auth?.token) {
|
||||
config.headers ||= {}
|
||||
config.headers.Authorization = `Bearer ${auth.token}`
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
// RESPONSE
|
||||
let isLoggingOut = false
|
||||
api.interceptors.response.use(
|
||||
r => r,
|
||||
async (error) => {
|
||||
if (error?.response?.status === 401 && !isLoggingOut) {
|
||||
isLoggingOut = true
|
||||
try {
|
||||
useAuthStore().clearSession()
|
||||
} finally {
|
||||
isLoggingOut = false
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// HELPERS
|
||||
export const get = (u, p = {}, c = {}) =>
|
||||
api.get(u, { params: p, ...c }).then(r => r.data)
|
||||
|
||||
export const post = (u, b = {}, c = {}) =>
|
||||
api.post(u, b, c).then(r => r.data)
|
||||
|
||||
export const put = (u, b = {}, c = {}) =>
|
||||
api.put(u, b, c).then(r => r.data)
|
||||
|
||||
export const del = (u, p = {}, c = {}) =>
|
||||
api.delete(u, { params: p, ...c }).then(r => r.data)
|
||||
|
||||
export const download = (u, p = {}, c = {}) =>
|
||||
api.get(u, { params: p, responseType: 'blob', ...c })
|
||||
.then(r => r.data)
|
||||
|
||||
export default api
|
||||
62
ui/src/services/orderService.js
Normal file
62
ui/src/services/orderService.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// src/services/orderService.js
|
||||
import { get, post, put } from './api'
|
||||
|
||||
/**
|
||||
* 🔹 Tek bir siparişi ID’ye göre getirir.
|
||||
* @param {string} id - OrderHeaderID (GUID)
|
||||
*/
|
||||
export async function getOrderById(id) {
|
||||
try {
|
||||
const data = await get(`/order/get/${id}`)
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('❌ getOrderById hatası:', err.message)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔹 Yeni sipariş oluşturur (insert).
|
||||
* Backend: POST /api/order/create
|
||||
* @param {Object} header - Sipariş başlığı (OrderHeader tablosu)
|
||||
* @param {Array} lines - Satırlar (OrderLine tablosu)
|
||||
*/
|
||||
export async function createOrder(header, lines) {
|
||||
const payload = {
|
||||
header,
|
||||
lines,
|
||||
username: header?.CreatedUserName || 'system',
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await post('/order/create', payload)
|
||||
console.log('✅ Sipariş oluşturuldu:', data)
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('❌ createOrder hatası:', err.message)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔹 Mevcut siparişi günceller (update).
|
||||
* Backend: PUT /api/order/update
|
||||
* @param {Object} header - Sipariş başlığı (OrderHeader tablosu)
|
||||
* @param {Array} lines - Satırlar (OrderLine tablosu)
|
||||
*/
|
||||
export async function updateOrder(header, lines) {
|
||||
const payload = {
|
||||
header,
|
||||
lines,
|
||||
username: header?.LastUpdatedUserName || 'system',
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await put('/order/update', payload)
|
||||
console.log('✅ Sipariş güncellendi:', data)
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('❌ updateOrder hatası:', err.message)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
167
ui/src/stores/OrdernewListStore.js
Normal file
167
ui/src/stores/OrdernewListStore.js
Normal file
@@ -0,0 +1,167 @@
|
||||
// src/stores/OrdernewListStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
let lastRequestId = 0
|
||||
|
||||
export const useOrderListStore = defineStore('orderlist', {
|
||||
state: () => ({
|
||||
orders: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
filters: {
|
||||
search: '',
|
||||
CurrAccCode: '',
|
||||
OrderDate: ''
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
filteredOrders (state) {
|
||||
let result = state.orders
|
||||
|
||||
if (state.filters.CurrAccCode) {
|
||||
result = result.filter(o => o.CurrAccCode === state.filters.CurrAccCode)
|
||||
}
|
||||
|
||||
if (state.filters.OrderDate) {
|
||||
result = result.filter(o =>
|
||||
o.OrderDate?.startsWith(state.filters.OrderDate)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
totalVisibleUSD (state) {
|
||||
return state.filteredOrders.reduce(
|
||||
(sum, o) => sum + Number(o.TotalAmountUSD || 0),
|
||||
0
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchOrders () {
|
||||
|
||||
// ==============================
|
||||
// 📌 REQUEST ID
|
||||
// ==============================
|
||||
const rid = ++lastRequestId
|
||||
|
||||
// ==============================
|
||||
// 📌 SEARCH SNAPSHOT
|
||||
// ==============================
|
||||
const raw = this.filters.search ?? ''
|
||||
const trimmed = String(raw).trim()
|
||||
|
||||
// ==============================
|
||||
// 📌 REQUEST LOG
|
||||
// ==============================
|
||||
console.groupCollapsed(
|
||||
`%c[orders] FETCH rid=${rid}`,
|
||||
'color:#1976d2;font-weight:bold'
|
||||
)
|
||||
|
||||
console.log('raw =', JSON.stringify(raw), 'len=', String(raw).length)
|
||||
console.log('trimmed =', JSON.stringify(trimmed), 'len=', trimmed.length)
|
||||
console.log('filters =', JSON.parse(JSON.stringify(this.filters)))
|
||||
console.log('lastRID =', lastRequestId)
|
||||
|
||||
console.groupEnd()
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
|
||||
// ==============================
|
||||
// 📌 PARAMS
|
||||
// ==============================
|
||||
const params = {}
|
||||
if (trimmed) params.search = trimmed
|
||||
|
||||
// ==============================
|
||||
// 📌 API CALL
|
||||
// ==============================
|
||||
const res = await api.get('/orders/list', { params })
|
||||
|
||||
// ==============================
|
||||
// 📌 STALE CHECK
|
||||
// ==============================
|
||||
if (rid !== lastRequestId) {
|
||||
console.warn(
|
||||
`[orders] IGNORE stale response rid=${rid} last=${lastRequestId}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 📌 DATA
|
||||
// ==============================
|
||||
const data = res?.data
|
||||
this.orders = Array.isArray(data) ? data : []
|
||||
|
||||
// ==============================
|
||||
// 📌 RESPONSE LOG
|
||||
// ==============================
|
||||
console.groupCollapsed(
|
||||
`%c[orders] RESPONSE rid=${rid} count=${this.orders.length}`,
|
||||
'color:#2e7d32;font-weight:bold'
|
||||
)
|
||||
|
||||
console.log('status =', res?.status)
|
||||
|
||||
console.log(
|
||||
'sample =',
|
||||
this.orders.slice(0, 5).map(o => ({
|
||||
id: o.OrderHeaderID,
|
||||
no: o.OrderNumber,
|
||||
code: o.CurrAccCode,
|
||||
name: o.CurrAccDescription
|
||||
}))
|
||||
)
|
||||
|
||||
// ==============================
|
||||
// 📌 DUPLICATE CHECK
|
||||
// ==============================
|
||||
const ids = this.orders.map(o => String(o.OrderHeaderID))
|
||||
const dup = ids.filter((v, i) => ids.indexOf(v) !== i)
|
||||
|
||||
if (dup.length) {
|
||||
console.warn(
|
||||
'DUPLICATE OrderHeaderID sample =',
|
||||
dup.slice(0, 10)
|
||||
)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
|
||||
} catch (err) {
|
||||
|
||||
if (rid !== lastRequestId) return
|
||||
|
||||
console.error(
|
||||
'[orders] FETCH FAILED',
|
||||
err?.response?.status,
|
||||
err?.response?.data || err
|
||||
)
|
||||
|
||||
this.orders = []
|
||||
|
||||
this.error =
|
||||
err?.response?.data ||
|
||||
err?.message ||
|
||||
'Sipariş listesi alınamadı'
|
||||
|
||||
} finally {
|
||||
|
||||
if (rid === lastRequestId) {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
238
ui/src/stores/UserDetailStore.js
Normal file
238
ui/src/stores/UserDetailStore.js
Normal file
@@ -0,0 +1,238 @@
|
||||
// src/stores/userDetailStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api, { get, post, put } from 'src/services/api'
|
||||
|
||||
export const useUserDetailStore = defineStore('userDetail', {
|
||||
state: () => ({
|
||||
sendingPasswordMail: false,
|
||||
lastPasswordMailSentAt: null,
|
||||
hasPassword: false,
|
||||
|
||||
/* ================= FLAGS ================= */
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
|
||||
/* ================= FORM ================= */
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
full_name: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
is_active: true,
|
||||
address: '',
|
||||
roles: [],
|
||||
departments: [],
|
||||
piyasalar: [],
|
||||
nebim_users: []
|
||||
},
|
||||
|
||||
/* ================= LOOKUPS ================= */
|
||||
roleOptions: [],
|
||||
departmentOptions: [],
|
||||
piyasaOptions: [],
|
||||
nebimUserOptions: []
|
||||
}),
|
||||
|
||||
actions: {
|
||||
/* =====================================================
|
||||
🔄 RESET (NEW MODE)
|
||||
===================================================== */
|
||||
resetForm () {
|
||||
this.form = {
|
||||
id: null,
|
||||
code: '',
|
||||
full_name: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
is_active: true,
|
||||
address: '',
|
||||
roles: [],
|
||||
departments: [],
|
||||
piyasalar: [],
|
||||
nebim_users: []
|
||||
}
|
||||
this.error = null
|
||||
this.hasPassword = false
|
||||
this.lastPasswordMailSentAt = null
|
||||
},
|
||||
|
||||
/* =====================================================
|
||||
🔐 ADMIN RESET PASSWORD
|
||||
===================================================== */
|
||||
async adminResetPassword (id, payload) {
|
||||
// token otomatik (interceptor)
|
||||
await post(`/users/${id}/admin-reset-password`, payload)
|
||||
this.hasPassword = true
|
||||
},
|
||||
|
||||
/* =====================================================
|
||||
✉️ SEND PASSWORD MAIL
|
||||
===================================================== */
|
||||
async sendPasswordMail (id) {
|
||||
this.sendingPasswordMail = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
await post(`/users/${id}/send-password-mail`, {})
|
||||
|
||||
// UI takip (DB’siz): sadece “son gönderim” gösterir
|
||||
this.lastPasswordMailSentAt = new Date().toLocaleString('tr-TR')
|
||||
} catch (e) {
|
||||
this.error = 'Parola maili gönderilemedi'
|
||||
throw e
|
||||
} finally {
|
||||
this.sendingPasswordMail = false
|
||||
}
|
||||
},
|
||||
|
||||
/* =====================================================
|
||||
📦 PAYLOAD BUILDER (BACKEND SÖZLEŞMESİYLE UYUMLU)
|
||||
===================================================== */
|
||||
buildPayload () {
|
||||
return {
|
||||
code: this.form.code,
|
||||
full_name: this.form.full_name,
|
||||
email: this.form.email,
|
||||
mobile: this.form.mobile,
|
||||
is_active: this.form.is_active,
|
||||
address: this.form.address,
|
||||
|
||||
roles: this.form.roles,
|
||||
|
||||
// ✅ TEK DEPARTMAN (string → backend array)
|
||||
departments: this.form.departments
|
||||
? [{ code: this.form.departments }]
|
||||
: [],
|
||||
|
||||
piyasalar: (this.form.piyasalar || []).map(code => ({ code })),
|
||||
|
||||
nebim_users: (this.form.nebim_users || []).map(username => {
|
||||
const opt = (this.nebimUserOptions || []).find(x => x.value === username)
|
||||
return {
|
||||
username,
|
||||
user_group_code: opt?.user_group_code || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/* =====================================================
|
||||
📥 GET USER (EDIT MODE)
|
||||
===================================================== */
|
||||
async fetchUser (id) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const data = await get(`/users/${id}`)
|
||||
|
||||
this.form.id = data.id
|
||||
this.form.code = data.code || ''
|
||||
this.form.full_name = data.full_name || ''
|
||||
this.form.email = data.email || ''
|
||||
this.form.mobile = data.mobile || ''
|
||||
this.form.is_active = !!data.is_active
|
||||
this.form.address = data.address || ''
|
||||
|
||||
this.form.roles = data.roles || []
|
||||
this.form.departments = (data.departments || []).map(x => x.code)
|
||||
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
|
||||
this.form.nebim_users = (data.nebim_users || []).map(x => x.username)
|
||||
|
||||
this.hasPassword = !!data.has_password
|
||||
} catch (e) {
|
||||
this.error = 'Kullanıcı bilgileri alınamadı'
|
||||
throw e
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/* =====================================================
|
||||
✍️ UPDATE USER (PUT)
|
||||
===================================================== */
|
||||
async saveUser (id) {
|
||||
this.saving = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('🟦 saveUser() START', id)
|
||||
|
||||
const payload = this.buildPayload()
|
||||
console.log('📤 PUT payload', payload)
|
||||
|
||||
await put(`/users/${id}`, payload)
|
||||
|
||||
console.log('✅ PUT OK → REFETCH USER')
|
||||
await this.fetchUser(id)
|
||||
|
||||
console.log('🔄 USER REFRESHED', {
|
||||
hasPassword: this.hasPassword,
|
||||
roles: this.form.roles,
|
||||
departments: this.form.departments
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('❌ saveUser FAILED', e)
|
||||
this.error = 'Kullanıcı güncellenemedi'
|
||||
throw e
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
/* =====================================================
|
||||
➕ CREATE USER (POST)
|
||||
===================================================== */
|
||||
async createUser () {
|
||||
this.saving = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('🟢 createUser() START')
|
||||
|
||||
const payload = this.buildPayload()
|
||||
console.log('📤 POST payload', payload)
|
||||
|
||||
const data = await post('/users', payload)
|
||||
|
||||
console.log('✅ CREATE OK response', data)
|
||||
|
||||
const newId = data?.id
|
||||
if (!newId) {
|
||||
throw new Error('CREATE response id yok')
|
||||
}
|
||||
|
||||
console.log('🔁 FETCH NEW USER id=', newId)
|
||||
await this.fetchUser(newId)
|
||||
|
||||
return newId
|
||||
} catch (e) {
|
||||
console.error('❌ createUser FAILED', e)
|
||||
this.error = 'Kullanıcı oluşturulamadı'
|
||||
throw e
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
/* =====================================================
|
||||
📚 LOOKUPS (NEW + EDIT ORTAK)
|
||||
===================================================== */
|
||||
async fetchLookups () {
|
||||
// token otomatik
|
||||
const [roles, depts, piyasalar, nebims] = await Promise.all([
|
||||
api.get('/lookups/roles'),
|
||||
api.get('/lookups/departments'),
|
||||
api.get('/lookups/piyasalar'),
|
||||
api.get('/lookups/nebim-users')
|
||||
])
|
||||
|
||||
this.roleOptions = roles?.data || roles || []
|
||||
this.departmentOptions = depts?.data || depts || []
|
||||
this.piyasaOptions = piyasalar?.data || piyasalar || []
|
||||
this.nebimUserOptions = nebims?.data || nebims || []
|
||||
}
|
||||
}
|
||||
})
|
||||
72
ui/src/stores/UserListStore.js
Normal file
72
ui/src/stores/UserListStore.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// src/stores/userListStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
export const useUserListStore = defineStore('userlist', {
|
||||
state: () => ({
|
||||
users: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
filters: {
|
||||
search: '',
|
||||
onlyActive: false
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
filteredUsers(state) {
|
||||
let result = state.users
|
||||
const term = state.filters.search?.toLowerCase() || ''
|
||||
|
||||
if (term) {
|
||||
result = result.filter(u =>
|
||||
u.code?.toLowerCase().includes(term) ||
|
||||
u.nebim_username?.toLowerCase().includes(term) ||
|
||||
u.role_names?.toLowerCase().includes(term) ||
|
||||
u.department_names?.toLowerCase().includes(term) ||
|
||||
u.piyasa_names?.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
|
||||
if (state.filters.onlyActive) {
|
||||
result = result.filter(u => u.is_active)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchUsers() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const params = {}
|
||||
|
||||
if (this.filters.search) {
|
||||
params.search = this.filters.search
|
||||
}
|
||||
|
||||
const { data } = await api.get(
|
||||
'/users/list',
|
||||
{ params }
|
||||
)
|
||||
|
||||
this.users = Array.isArray(data) ? data : []
|
||||
|
||||
console.log('✅ User listesi alındı:', this.users.length)
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ User listesi alınamadı:', err)
|
||||
this.users = []
|
||||
this.error =
|
||||
err?.message ||
|
||||
'Kullanıcı listesi alınamadı'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
43
ui/src/stores/accountStore.js
Normal file
43
ui/src/stores/accountStore.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// src/stores/accountStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
export const useAccountStore = defineStore('account', {
|
||||
state: () => ({
|
||||
accountOptions: [],
|
||||
loading: false,
|
||||
error: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchAccounts () {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
// 🔐 Token interceptor ile otomatik eklenir
|
||||
const { data } = await api.get('/accounts')
|
||||
|
||||
this.accountOptions = (Array.isArray(data) ? data : []).map(acc => ({
|
||||
label: `${acc.display_code || ''} ${acc.account_name || ''}`.trim(),
|
||||
value: acc.account_code
|
||||
}))
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Error fetching accounts:', err)
|
||||
|
||||
if (err?.response?.status === 401) {
|
||||
this.error = 'Cari hesapları görüntüleme yetkiniz yok.'
|
||||
} else {
|
||||
this.error =
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
'Cari hesaplar yüklenemedi'
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
102
ui/src/stores/activityLogStore.js
Normal file
102
ui/src/stores/activityLogStore.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { get } from 'src/services/api'
|
||||
|
||||
export const useActivityLogStore = defineStore('activityLogStore', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
|
||||
rows: [],
|
||||
total: 0,
|
||||
|
||||
pagination: {
|
||||
page: 1,
|
||||
rowsPerPage: 0, // ✅ SINIRSIZ
|
||||
sortBy: 'created_at',
|
||||
descending: true
|
||||
}
|
||||
,
|
||||
|
||||
filters: {
|
||||
username: '',
|
||||
actionCategory: null,
|
||||
actionType: '',
|
||||
success: null,
|
||||
dateFrom: '',
|
||||
dateTo: ''
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchLogs () {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {}
|
||||
|
||||
if (this.pagination.rowsPerPage > 0) {
|
||||
params.page = this.pagination.page
|
||||
params.limit = this.pagination.rowsPerPage
|
||||
}
|
||||
|
||||
|
||||
if (this.filters.username)
|
||||
params.username = this.filters.username
|
||||
|
||||
if (this.filters.actionCategory)
|
||||
params.action_category = this.filters.actionCategory
|
||||
|
||||
if (this.filters.actionType)
|
||||
params.action_type = this.filters.actionType
|
||||
|
||||
if (this.filters.success !== null)
|
||||
params.success = this.filters.success
|
||||
|
||||
if (this.filters.dateFrom)
|
||||
params.date_from = this.filters.dateFrom
|
||||
|
||||
if (this.filters.dateTo)
|
||||
params.date_to = this.filters.dateTo
|
||||
|
||||
const data = await get('/activity-logs', params)
|
||||
|
||||
this.rows = data.items || []
|
||||
this.total = data.total || 0
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
quickRoleChange () {
|
||||
|
||||
this.filters.actionCategory = 'role_permission'
|
||||
this.filters.actionType = 'role_department_permission_change'
|
||||
|
||||
this.pagination.page = 1
|
||||
|
||||
this.fetchLogs()
|
||||
}
|
||||
,
|
||||
|
||||
onTableRequest (props) {
|
||||
const { page, rowsPerPage, sortBy, descending } = props.pagination
|
||||
|
||||
this.pagination.page = page
|
||||
this.pagination.rowsPerPage = rowsPerPage
|
||||
this.pagination.sortBy = sortBy
|
||||
this.pagination.descending = descending
|
||||
|
||||
this.fetchLogs()
|
||||
}
|
||||
,
|
||||
resetFilters () {
|
||||
this.filters = {
|
||||
username: '',
|
||||
actionCategory: null,
|
||||
actionType: '',
|
||||
success: null,
|
||||
dateFrom: '',
|
||||
dateTo: ''
|
||||
}
|
||||
this.pagination.page = 1
|
||||
this.fetchLogs()
|
||||
}
|
||||
}
|
||||
})
|
||||
117
ui/src/stores/authStore.js
Normal file
117
ui/src/stores/authStore.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// src/stores/authStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => {
|
||||
let user = null
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem('user')
|
||||
if (raw && raw !== 'undefined' && raw !== 'null') {
|
||||
user = JSON.parse(raw)
|
||||
}
|
||||
} catch {
|
||||
console.warn('⚠️ Invalid user in localStorage, cleared')
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
return {
|
||||
token: localStorage.getItem('token'),
|
||||
user,
|
||||
forcePasswordChange: localStorage.getItem('forcePasswordChange') === '1'
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
isAuthenticated: s => !!s.token,
|
||||
mustChangePassword: s => !!s.forcePasswordChange,
|
||||
|
||||
// 🔥 TEK ADMIN KURALI
|
||||
isAdmin: s =>
|
||||
String(s.user?.role_code || '').toLowerCase() === 'admin'
|
||||
},
|
||||
|
||||
actions: {
|
||||
/* =========================================================
|
||||
🔐 SESSION
|
||||
========================================================= */
|
||||
setSession ({ token, user }) {
|
||||
this.token = token
|
||||
this.user = user ?? null
|
||||
this.forcePasswordChange = !!user?.force_password_change
|
||||
|
||||
localStorage.setItem('token', token)
|
||||
|
||||
if (user) {
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
} else {
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
'forcePasswordChange',
|
||||
this.forcePasswordChange ? '1' : '0'
|
||||
)
|
||||
},
|
||||
|
||||
clearSession () {
|
||||
this.token = null
|
||||
this.user = null
|
||||
this.forcePasswordChange = false
|
||||
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('forcePasswordChange')
|
||||
|
||||
usePermissionStore().clear()
|
||||
},
|
||||
|
||||
/* =========================================================
|
||||
🔐 LOGIN
|
||||
========================================================= */
|
||||
async login (username, password) {
|
||||
const res = await api.post('/auth/login', { username, password })
|
||||
|
||||
const token =
|
||||
res?.token ||
|
||||
res?.data?.token ||
|
||||
res?.access_token ||
|
||||
res?.data?.access_token
|
||||
|
||||
const user =
|
||||
res?.user ||
|
||||
res?.data?.user
|
||||
|
||||
// ✅ JWT doğrulama
|
||||
const tokenStr = typeof token === 'string' ? token.trim() : ''
|
||||
const looksLikeJwt = tokenStr.split('.').length === 3
|
||||
|
||||
if (!tokenStr || !looksLikeJwt) {
|
||||
console.error('❌ LOGIN RESPONSE (unexpected):', res)
|
||||
throw new Error('Invalid login token')
|
||||
}
|
||||
|
||||
this.setSession({ token: tokenStr, user })
|
||||
|
||||
// 🔥 PERMISSIONS
|
||||
const perm = usePermissionStore()
|
||||
await perm.fetchPermissions()
|
||||
|
||||
|
||||
|
||||
|
||||
// 🧪 DEBUG (istersen sonra kaldır)
|
||||
console.log('🔐 AUTH DEBUG', {
|
||||
isAdmin: this.isAdmin,
|
||||
users: perm.hasPermission('/api/users/list'),
|
||||
orders: perm.hasPermission('/api/orders/list'),
|
||||
logs: perm.hasPermission('/api/activity-logs'),
|
||||
permissions: perm.hasPermission('/api/permissions/matrix')
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
3023
ui/src/stores/deneme
Normal file
3023
ui/src/stores/deneme
Normal file
@@ -0,0 +1,3023 @@
|
||||
<template>
|
||||
<!-- ===========================================================
|
||||
🧾 ORDER ENTRY PAGE (BSSApp)
|
||||
v22 — Model + Renk + 2. Renk + Beden/Stok Otomatik Eşleme
|
||||
============================================================ -->
|
||||
<q-page class="q-pa-md order-page">
|
||||
<div class="order-scroll-x">
|
||||
<div class="order-width">
|
||||
<div class="row items-center q-mb-xs">
|
||||
<q-chip
|
||||
:color="isEditMode ? 'blue-7' : 'green-7'"
|
||||
text-color="white"
|
||||
icon="edit"
|
||||
v-if="isEditMode"
|
||||
>
|
||||
Düzenleme Modu
|
||||
</q-chip>
|
||||
<q-chip
|
||||
text-color="white"
|
||||
icon="add_circle"
|
||||
v-else
|
||||
>
|
||||
Yeni Sipariş
|
||||
</q-chip>
|
||||
</div>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 FILTER BAR — OrderHeader Bilgileri
|
||||
SQL eşleşmeli alanlar
|
||||
======================================================= -->
|
||||
<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"
|
||||
behavior="menu"
|
||||
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
|
||||
:readonly="isEditMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 📅 Oluşturulma Tarihi -->
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
v-model="form.OrderDate"
|
||||
label="Oluşturulma Tarihi"
|
||||
type="date"
|
||||
filled
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 📅 Tahmini Termin Tarihi -->
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
v-model="form.AverageDueDate"
|
||||
label="Tahmini Termin Tarihi"
|
||||
type="date"
|
||||
filled
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 💰 TOPLAM TUTAR + KDV -->
|
||||
<div class="col-12 row items-center q-gutter-sm q-mt-sm">
|
||||
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model.number="toplamTutar"
|
||||
label="Toplam Tutar"
|
||||
readonly
|
||||
:suffix="form.pb"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-checkbox
|
||||
v-model="includeKDV"
|
||||
label="KDV Dahil"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model.number="manualKDV"
|
||||
label="KDV"
|
||||
readonly
|
||||
suffix="%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
:model-value="toplamKDVli.toFixed(2)"
|
||||
label="KDV Dahil Toplam"
|
||||
readonly
|
||||
:suffix="form.pb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🔸 Cari Bilgi Kutuları -->
|
||||
<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>
|
||||
<!-- =======================================================
|
||||
🔹 KAYDET / GÜNCELLE BUTONU
|
||||
======================================================= -->
|
||||
<q-btn
|
||||
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
|
||||
color="primary"
|
||||
icon="save"
|
||||
class="q-ml-sm"
|
||||
:loading="orderStore.loading"
|
||||
@click="submitAll"
|
||||
/>
|
||||
|
||||
<!-- =======================================================
|
||||
🔹 YENİ SİPARİŞ / FORM SIFIRLA BUTONU
|
||||
======================================================= -->
|
||||
<q-btn
|
||||
label="YENİ SİPARİŞ"
|
||||
color="secondary"
|
||||
icon="add_circle"
|
||||
class="q-ml-sm"
|
||||
@click="resetForm"
|
||||
/>
|
||||
|
||||
</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 schema" :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
|
||||
======================================================== -->
|
||||
<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 (grp.bedenValues || [])"
|
||||
:key="'hdr-' + v"
|
||||
class="beden-cell"
|
||||
>
|
||||
{{ v }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-right">
|
||||
<div class="text-caption">
|
||||
Toplam {{ grp.name }} Adet: {{ grp.toplamAdet }}
|
||||
</div>
|
||||
<div class="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 }, i) in grp.rows"
|
||||
:key="i"
|
||||
class="summary-row"
|
||||
:class="{
|
||||
active: editingIndex === summaryRows.findIndex(r => r === row),
|
||||
'is-editing': editingIndex === summaryRows.findIndex(r => r === row)
|
||||
}"
|
||||
@click="editRow(row, i)"
|
||||
>
|
||||
<!-- 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 (schemaByKey[row.grpKey]?.values || [])" :key="'val-' + v" class="cell beden">
|
||||
{{ row.bedenMap?.[row.grpKey]?.[v] ?? '' }}
|
||||
</div>
|
||||
<div v-for="i in (16 - (schemaByKey[row.grpKey]?.values?.length || 0))" :key="'empty-' + i" 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>
|
||||
<!-- ESKİ (inline düzenlenebilir) -->
|
||||
<!-- 🔹 Termin Tarihi — sadece gösterge -->
|
||||
<!-- 🔹 Termin Tarihi — sadece gösterge -->
|
||||
<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="isEditing"
|
||||
@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="isEditing"
|
||||
@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"
|
||||
@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
|
||||
v-if="activeSeriesOptions && activeSeriesOptions.length"
|
||||
v-model="selectedSeriSet"
|
||||
:options="activeSeriesOptions"
|
||||
label="Beden Seti Seç"
|
||||
filled dense
|
||||
emit-value map-options
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
/>
|
||||
</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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-2 q-mt-sm">
|
||||
<q-btn
|
||||
v-if="selectedSeriSet"
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="Seri Ekle"
|
||||
@click="applySeriSet"
|
||||
/>
|
||||
</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 }"
|
||||
/>
|
||||
|
||||
<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 />
|
||||
</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)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-select v-model="form.pb" :options="paraBirimOptions" label="PB" dense filled />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<q-input v-model="form.tutar" label="Tutar" dense filled readonly />
|
||||
</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
|
||||
/>
|
||||
</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
|
||||
/>
|
||||
</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="editingIndex === -1 ? 'primary' : 'positive'"
|
||||
:label="editingIndex === -1 ? 'Kaydet' : 'Güncelle'"
|
||||
@click="saveOrUpdate"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="editingIndex !== -1"
|
||||
color="negative" flat label="Satırı Sil"
|
||||
@click="removeSelected"
|
||||
/>
|
||||
<q-btn
|
||||
flat color="grey-8"
|
||||
label="Formu Temizle"
|
||||
@click="resetForm"
|
||||
/>
|
||||
</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...)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- editor -->
|
||||
</div> <!-- order-width -->
|
||||
</div> <!-- order-scroll-x -->
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/* ===========================================================
|
||||
🧩 ORDER ENTRY (v22-final)
|
||||
Tüm fonksiyonları kapsayan gelişmiş setup bloğu.
|
||||
Bu dosya backend ile sıkı entegredir, axios ve Pinia store
|
||||
ile veri alışverişi yapar.
|
||||
=========================================================== */
|
||||
|
||||
// Vue çekirdek importları
|
||||
import { ref, reactive, computed, onMounted, watch, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { api } from 'boot/axios'
|
||||
import { useRoute, useRouter } from 'vue-router' // ✅ Buradan olmalı
|
||||
import { useQuasar } from 'quasar'
|
||||
import axios from 'axios'
|
||||
import { useOrderentryStore } from 'src/stores/orderentryStore'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const $q = useQuasar()
|
||||
const orderId =
|
||||
route.params.id ||
|
||||
route.query.id ||
|
||||
route.query.orderId ||
|
||||
null
|
||||
|
||||
console.log('🧩 Route parametresi alındı (setup başında):', orderId)
|
||||
const isEditMode = ref(false)
|
||||
// ===========================================================
|
||||
// 🔹 Mode & Transaction State
|
||||
// Yeni sipariş mi, düzenleme mi kontrolü için
|
||||
// ===========================================================
|
||||
const mode = ref('new') // 'new' | 'edit'
|
||||
const txId = ref('')
|
||||
const headerId = ref('') // Quasar bileşenleri ve $q.notify kullanımı için
|
||||
const orderStore = useOrderentryStore() // Pinia store: siparişler, localStorage, API çağrıları
|
||||
|
||||
// 🔹 Genel reaktif değişkenler
|
||||
const aktifPB = ref('USD') // Varsayılan para birimi (Cari seçimiyle değişebilir)
|
||||
const siparisNo = ref('SP-2025-001') // Geçici sipariş numarası örneği
|
||||
const allSeriesSets = ref([]) // Seri listeleri (seriMatrix'ten dinamik doldurulur)
|
||||
/* ===========================================================
|
||||
🗓️ 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(today.getDate() + 35) // 5 hafta sonrası
|
||||
|
||||
const defaultOlusturmaTarihi = today.toISOString().substring(0, 10)
|
||||
const defaultTerminTarihi = terminDate.toISOString().substring(0, 10)
|
||||
// 💰 KDV Hesaplama Alanları
|
||||
const includeKDV = ref(false) // KDV kutusu işaretli mi?
|
||||
const manualKDV = ref(0) // Kullanıcının elle girdiği KDV tutarı
|
||||
const kdvOrani = 0.10 // Varsayılan %10 oran
|
||||
|
||||
/* ===========================================================
|
||||
🔹 FORM TANIMI (reactive form)
|
||||
Tüm giriş alanları ve hesaplanan değerler tek reactive obje içinde.
|
||||
=========================================================== */
|
||||
|
||||
const form = reactive({
|
||||
/* =========================================================
|
||||
🔹 TEMEL ALANLAR
|
||||
========================================================= */
|
||||
OrderHeaderID: '', // string (GUID)
|
||||
OrderTypeCode: 1, // int8
|
||||
ProcessCode: 'WS', // string
|
||||
OrderNumber: '', // string
|
||||
OrderTime: dayjs().format('HH:mm:ss'), // saat formatı (örn. "14:35:22")
|
||||
IsCancelOrder: false,
|
||||
|
||||
/* =========================================================
|
||||
🔹 ADRES / REFERANS ALANLARI
|
||||
========================================================= */
|
||||
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: 0,
|
||||
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: '', // backend dolduracak (auth user)
|
||||
CreatedDate: dayjs().toISOString(),
|
||||
LastUpdatedUserName: '',
|
||||
LastUpdatedDate: dayjs().toISOString(),
|
||||
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,
|
||||
model: '', // Ürün kodu
|
||||
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,
|
||||
|
||||
// 🗓️ Tarihler
|
||||
olusturmaTarihi: defaultOlusturmaTarihi,
|
||||
tahminiTerminTarihi: defaultTerminTarihi,
|
||||
terminTarihi: defaultTerminTarihi
|
||||
})
|
||||
const orderLine = reactive({
|
||||
// 🔹 Temel Bilgiler
|
||||
OrderLineID: '', // GUID
|
||||
OrderHeaderID: '', // foreign key
|
||||
SortOrder: 1,
|
||||
ItemTypeCode: 0,
|
||||
ItemCode: '',
|
||||
ColorCode: '',
|
||||
ItemDim1Code: '',
|
||||
ItemDim2Code: '',
|
||||
ItemDim3Code: '',
|
||||
|
||||
// 🔹 Miktarlar
|
||||
Qty1: 0,
|
||||
Qty2: 0,
|
||||
CancelQty1: 0,
|
||||
CancelQty2: 0,
|
||||
|
||||
// 🔹 Tarihler
|
||||
CancelDate: null, // null ya da ISO string
|
||||
ClosedDate: null,
|
||||
DeliveryDate: null,
|
||||
PlannedDateOfLading: '',
|
||||
|
||||
// 🔹 Durum & Neden
|
||||
OrderCancelReasonCode: '',
|
||||
IsClosed: false,
|
||||
|
||||
// 🔹 Satış Planı
|
||||
SalespersonCode: '',
|
||||
PaymentPlanCode: '',
|
||||
PurchasePlanCode: '',
|
||||
|
||||
// 🔹 Ürün Bilgisi
|
||||
LineDescription: '',
|
||||
UsedBarcode: '',
|
||||
CostCenterCode: '',
|
||||
VatCode: '',
|
||||
VatRate: 0,
|
||||
PCTCode: '',
|
||||
PCTRate: 0,
|
||||
|
||||
// 🔹 Satır Bazlı İndirimler
|
||||
LDisRate1: 0,
|
||||
LDisRate2: 0,
|
||||
LDisRate3: 0,
|
||||
LDisRate4: 0,
|
||||
LDisRate5: 0,
|
||||
|
||||
// 🔹 Para / Fiyat Bilgileri
|
||||
DocCurrencyCode: 'USD',
|
||||
PriceCurrencyCode: 'USD',
|
||||
PriceExchangeRate: 1,
|
||||
Price: 0,
|
||||
|
||||
// 🔹 Referans Bilgiler
|
||||
PriceListLineID: '',
|
||||
BaseProcessCode: '',
|
||||
BaseOrderNumber: '',
|
||||
BaseCustomerTypeCode: 0,
|
||||
BaseCustomerCode: '',
|
||||
BaseSubCurrAccID: '',
|
||||
BaseStoreCode: '',
|
||||
SupportRequestHeaderID: '',
|
||||
|
||||
// 🔹 Takip / Sistem Bilgileri
|
||||
OrderLineSumID: 0,
|
||||
OrderLineBOMID: 0,
|
||||
|
||||
// 🔹 Kullanıcı / Tarih
|
||||
CreatedUserName: '',
|
||||
CreatedDate: dayjs().toISOString(),
|
||||
LastUpdatedUserName: '',
|
||||
LastUpdatedDate: dayjs().toISOString(),
|
||||
|
||||
// 🔹 Vergi & Ek Kodlar
|
||||
SurplusOrderQtyToleranceRate: 0,
|
||||
PurchaseRequisitionLineID: '',
|
||||
WithHoldingTaxTypeCode: '',
|
||||
DOVCode: '',
|
||||
OrderLineLinkedProductID: '',
|
||||
|
||||
// 🔹 Frontend’e özel ek alanlar (UI binding için)
|
||||
selectedModel: '', // model (ürün kodu)
|
||||
selectedColor: '', // renk
|
||||
selectedColor2: '', // ikinci renk
|
||||
bedenLabels: [], // beden başlıkları
|
||||
bedenValues: {}, // {38:2, 40:1, 42:0} gibi
|
||||
unitPrice: 0,
|
||||
totalAmount: 0,
|
||||
note: '',
|
||||
})
|
||||
const editingIndex = ref(-1) // aktif düzenlenen satırın index’i
|
||||
const summaryRows = ref([]) // tüm satırların listesi (grid kaynağı)
|
||||
// 🔹 Düzenleme durumunu hesaplayan computed
|
||||
const isEditing = computed(() => editingIndex.value >= 0)
|
||||
/* ===========================================================
|
||||
🔹 1. ve 2. Renk Select Referansları
|
||||
QSelect bileşenlerine erişmek için template ref’leri tutulur.
|
||||
=========================================================== */
|
||||
const renkSelect = ref(null)
|
||||
const renk2Select = ref(null)
|
||||
|
||||
const renkOptions = ref([]) // 1. renk seçenekleri
|
||||
const renkOptions2 = ref([]) // 2. renk seçenekleri
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Kombinasyon Anahtarı Fonksiyonları
|
||||
Aynı model + renk + 2. renk kombinasyonunun gridde olup olmadığını bulmak için
|
||||
bu yardımcı fonksiyonlar kullanılır.
|
||||
=========================================================== */
|
||||
const getComboKey = (o) => [o.model || '', o.renk || '', o.renk2 || ''].join('||')
|
||||
|
||||
// 99999 veya boş renkleri toleranslı eşleştirme
|
||||
const isSameCombo = (row, form) => {
|
||||
const sameModel = row.model === form.model
|
||||
const renkOk =
|
||||
(row.renk || '') === (form.renk || '') ||
|
||||
(row.renk || '') === '99999' ||
|
||||
(form.renk || '') === '99999'
|
||||
const renk2Ok =
|
||||
(row.renk2 || '') === (form.renk2 || '') ||
|
||||
(row.renk2 || '') === '99999' ||
|
||||
(form.renk2 || '') === '99999'
|
||||
return sameModel && renkOk && renk2Ok
|
||||
}
|
||||
|
||||
// Grid içinde aynı kombinasyonun index’ini bulur
|
||||
const findExistingIndexByForm = () =>
|
||||
summaryRows.value.findIndex(r => isSameCombo(r, form))
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Ürün Ana Grubu Bazında Gruplanmış Satırlar
|
||||
groupedRows computed fonksiyonu, satırları urunAnaGrubu’na göre gruplar.
|
||||
Her grup alt toplamları, açık/kapalı durumu ve beden setlerini içerir.
|
||||
=========================================================== */
|
||||
const groupOpen = reactive({}) // {"TAKIM ELBİSE": true, "GÖMLEK": false, ...}
|
||||
|
||||
const groupedRows = computed(() => {
|
||||
const buckets = {}
|
||||
const order = []
|
||||
|
||||
// Null veya hatalı satırları filtrele
|
||||
const safeRows = (summaryRows.value || []).filter(r => r && r.urunAnaGrubu)
|
||||
|
||||
for (const [idx, row] of safeRows.entries()) {
|
||||
const anaGrup = row.urunAnaGrubu.trim().toUpperCase()
|
||||
if (!anaGrup) continue
|
||||
|
||||
// 🔹 Beden grubu anahtarını tespit et (schemaByKey üzerinden)
|
||||
const bedenList = Object.keys(row.bedenMap?.[row.grpKey] || {})
|
||||
const grpKey = detectBedenGroup(bedenList, row.urunAnaGrubu, row.kategori)
|
||||
const grpSchema = schemaByKey.value[grpKey]
|
||||
const bedenValues = grpSchema ? grpSchema.values : []
|
||||
|
||||
// 🔹 Eğer grup ilk kez görülüyorsa, yeni obje oluştur
|
||||
if (!buckets[anaGrup]) {
|
||||
buckets[anaGrup] = {
|
||||
name: anaGrup,
|
||||
key: grpKey,
|
||||
rows: [],
|
||||
toplamAdet: 0,
|
||||
toplamTutar: 0,
|
||||
bedenValues
|
||||
}
|
||||
order.push(anaGrup)
|
||||
// ilk kez eklendiğinde default açık bırak
|
||||
if (groupOpen[anaGrup] === undefined) groupOpen[anaGrup] = true
|
||||
}
|
||||
|
||||
// 🔹 Satırları grup altına ekle
|
||||
const adet = Number(row.adet || 0)
|
||||
const tutar = Number(row.tutar || 0)
|
||||
buckets[anaGrup].rows.push({ row, idx })
|
||||
buckets[anaGrup].toplamAdet += adet
|
||||
buckets[anaGrup].toplamTutar += tutar
|
||||
|
||||
// 🔹 Daha geniş beden seti varsa grup seviyesinde güncelle
|
||||
if (buckets[anaGrup].bedenValues.length < bedenValues.length)
|
||||
buckets[anaGrup].bedenValues = bedenValues
|
||||
}
|
||||
|
||||
// 🔹 Sonuç sıralı dizi olarak döner (UI grid için)
|
||||
return order.map(name => ({
|
||||
...buckets[name],
|
||||
open: groupOpen[name]
|
||||
}))
|
||||
})
|
||||
/* ===========================================================
|
||||
🔹 Grup Aç/Kapa Fonksiyonu
|
||||
groupedRows içindeki grupOpen reactive objesini günceller.
|
||||
Kullanıcı bir ürün grubunu kapattığında alt satırlar gizlenir.
|
||||
=========================================================== */
|
||||
function toggleGroup(groupName) {
|
||||
if (!groupName) return
|
||||
groupOpen[groupName] = !groupOpen[groupName]
|
||||
console.log(`📂 Grup "${groupName}" artık ${groupOpen[groupName] ? 'açık' : 'kapalı'}`)
|
||||
}
|
||||
// ===========================================================
|
||||
// 🔹 Grup Açık/Kapalı Durumunu summaryRows’a göre otomatik güncelle
|
||||
// Eski sipariş çağrıldığında tüm gruplar açık gelsin
|
||||
// ===========================================================
|
||||
watch(summaryRows, rows => {
|
||||
if (!Array.isArray(rows)) return
|
||||
rows.forEach(r => {
|
||||
if (r.urunAnaGrubu && groupOpen[r.urunAnaGrubu] === undefined) {
|
||||
groupOpen[r.urunAnaGrubu] = true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// 🔹 Sipariş genel açıklaması (ör. “Yaz sezonu toplu sipariş”)
|
||||
const siparisGenelAciklama = ref('')
|
||||
const DRAFT_KEY = computed(() =>
|
||||
mode.value === 'edit'
|
||||
? `bssapp:order:draft:edit:${headerId.value}`
|
||||
: `bssapp:order:draft:new:${txId.value || 'noguid'}`
|
||||
)
|
||||
|
||||
// ===========================================================
|
||||
// ✅ AXIOS INSTANCE TANIMI
|
||||
// Tüm backend çağrıları bu instance üzerinden geçer.
|
||||
// Token otomatik eklenir, 401 durumunda login sayfasına yönlendirir.
|
||||
// ===========================================================
|
||||
const API_BASE = 'http://localhost:8080'
|
||||
|
||||
|
||||
// İstek öncesi interceptor — token ekleme
|
||||
api.interceptors.request.use(cfg => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) cfg.headers.Authorization = `Bearer ${token}`
|
||||
return cfg
|
||||
})
|
||||
|
||||
// Yanıt interceptor — 401 durumunda login'e yönlendir
|
||||
api.interceptors.response.use(
|
||||
r => r,
|
||||
err => {
|
||||
if (err?.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
if (typeof window !== 'undefined') window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
/* ===========================================================
|
||||
🔹 detectBedenGroup — Beden Grubunu Otomatik Tespit Et
|
||||
Bu fonksiyon, ürünün "ana grup" ve "kategori" bilgilerine
|
||||
göre hangi beden setine (takım, gömlek, pantolon, ayakkabı vs.)
|
||||
ait olduğunu belirler. Bu bilgi grid yapısını belirler.
|
||||
=========================================================== */
|
||||
function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
|
||||
// Beden listesi normalize edilir (trim, uppercase, boşsa default ' ')
|
||||
const list = Array.isArray(bedenList) && bedenList.length > 0
|
||||
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
|
||||
: [' ']
|
||||
|
||||
// Ana grup temizleme: parantez içlerini ve özel karakterleri kaldırır
|
||||
const ana = (urunAnaGrubu || '')
|
||||
.toUpperCase()
|
||||
.trim()
|
||||
.replace(/\(.*?\)/g, '') // (Regular Fit) gibi notları siler
|
||||
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '') // özel karakterleri temizler
|
||||
.replace(/\s+/g, ' ') // fazla boşlukları tek boşluk yapar
|
||||
|
||||
// Kategori de temizlenir
|
||||
const kat = (urunKategori || '').toUpperCase().trim()
|
||||
|
||||
// ✅ Aksesuar gruplarının listesi
|
||||
const aksesuarGruplari = [
|
||||
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER',
|
||||
'CORAP', 'ÇORAP', 'FULAR', 'MENDIL', 'MENDİL',
|
||||
'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI', 'KOL DÜĞMESİ'
|
||||
]
|
||||
|
||||
// ✅ Giyim grupları
|
||||
const giyimGruplari = ['GÖMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT', 'TİŞÖRT']
|
||||
|
||||
// ✅ Harfli beden tespiti: XS, S, M, L, XL, XXL gibi
|
||||
const harfliBedenler = ['XS', 'S', 'M', 'L', 'XL', 'XXL', '3XL', '4XL']
|
||||
if (list.some(b => harfliBedenler.includes(b))) {
|
||||
return 'gom' // gömlek / tişört tarzı gruplar
|
||||
}
|
||||
|
||||
// ⚙️ Eğer aksesuar kelimesi geçiyor ama giyim grubu değilse
|
||||
if (
|
||||
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
|
||||
!giyimGruplari.some(g => ana.includes(g))
|
||||
) {
|
||||
return 'aksbir'
|
||||
}
|
||||
|
||||
// 🔢 Pantolon + Garson (yaş grubu) özel kuralları
|
||||
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
|
||||
if (kat.includes('GARSON')) return 'yas'
|
||||
|
||||
// 🔢 Tamamen sayısal bedenler (örneğin 39–44)
|
||||
const allNumeric = list.every(v => /^\d+$/.test(v))
|
||||
if (allNumeric) {
|
||||
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
|
||||
const diffs = nums.slice(1).map((v, i) => v - nums[i])
|
||||
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
|
||||
}
|
||||
|
||||
// 🧩 Eğer hiçbiri değilse:
|
||||
// Harf içeriyorsa 'gom', değilse 'tak' (takım elbise)
|
||||
const sample = list[0]
|
||||
if (/[A-Z]/.test(sample)) return 'gom'
|
||||
return 'tak'
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 🔹 Aktif ürün grubuna göre uygun seri setlerini dinamik hesaplar
|
||||
const activeSeriesOptions = computed(() => {
|
||||
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
|
||||
const sets = seriMatrix[grpKey] || {}
|
||||
return Object.keys(sets).map(k => ({ label: k, value: k, isActive: true }))
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Para Birimi ve Toplam Tutar Hesaplaması
|
||||
Sipariş toplamları ve para birimi seçimi burada yönetilir.
|
||||
=========================================================== */
|
||||
const paraBirimOptions = ['USD', 'EUR', 'TRY'] // Kullanıcıya sunulacak döviz seçenekleri
|
||||
|
||||
// Grid altındaki “Toplam Tutar” alanı dinamik hesaplanır.
|
||||
const toplamTutar = computed(() => {
|
||||
const sum = orderStore.totalAmount ? Number(orderStore.totalAmount) : 0
|
||||
return isNaN(sum) ? 0 : sum
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Cari Bilgileri
|
||||
Cari (müşteri) listesi, arama filtresi ve seçim sonrası
|
||||
para birimi kontrolü burada yapılır.
|
||||
=========================================================== */
|
||||
const selectedCari = ref(null) // Kullanıcının seçtiği cari kodu
|
||||
const cariOptions = ref([]) // Tüm cari listesi (backend'den gelir)
|
||||
const filteredCariOptions = ref([]) // Arama filtrelenmiş hali
|
||||
const loadingCari = ref(false) // Yükleniyor göstergesi
|
||||
const cariInfo = ref(null) // Seçilen carinin tüm bilgisi
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Cari Listesini Yükleme Fonksiyonu
|
||||
Uygulama açıldığında veya cari seçimi değiştiğinde çağrılır.
|
||||
Backend'den /api/customer-list endpoint'ini çağırır.
|
||||
=========================================================== */
|
||||
async function loadCariList() {
|
||||
loadingCari.value = true
|
||||
try {
|
||||
ensureAuthOrRedirect() // Token kontrolü, gerekirse login yönlendirmesi
|
||||
const res = await api.get('/customer-list')
|
||||
const data = res?.data
|
||||
|
||||
// Gelen data farklı formatlarda olabilir, esnek parse yapılır
|
||||
if (!data) {
|
||||
cariOptions.value = []
|
||||
} else if (Array.isArray(data)) {
|
||||
cariOptions.value = data
|
||||
} else if (Array.isArray(data?.data)) {
|
||||
cariOptions.value = data.data
|
||||
} else {
|
||||
cariOptions.value = []
|
||||
}
|
||||
|
||||
// Filtre listesi de aynı anda güncellenir
|
||||
filteredCariOptions.value = cariOptions.value
|
||||
console.log('🧾 Cari listesi yüklendi:', cariOptions.value.length)
|
||||
} catch (err) {
|
||||
console.error('❌ Cari listesi alınamadı:', err)
|
||||
$q.notify({ type: 'negative', message: 'Cari listesi yüklenemedi ❌' })
|
||||
} finally {
|
||||
loadingCari.value = false
|
||||
}
|
||||
}
|
||||
// ===========================================================
|
||||
// 🔹 Local Draft Yönetimi
|
||||
// ===========================================================
|
||||
function saveDraft() {
|
||||
const draft = {
|
||||
header: {
|
||||
OrderDate: form.olusturmaTarihi,
|
||||
AverageDueDate: form.tahminiTerminTarihi,
|
||||
CurrAccCode: selectedCari.value,
|
||||
DocCurrencyCode: form.pb,
|
||||
Description: siparisGenelAciklama.value
|
||||
},
|
||||
lines: summaryRows.value
|
||||
}
|
||||
localStorage.setItem(DRAFT_KEY.value, JSON.stringify(draft))
|
||||
}
|
||||
|
||||
function loadDraft() {
|
||||
const raw = localStorage.getItem(DRAFT_KEY.value)
|
||||
if (!raw) return false
|
||||
try {
|
||||
const { header, lines } = JSON.parse(raw)
|
||||
if (header) {
|
||||
form.olusturmaTarihi = header.OrderDate || form.olusturmaTarihi
|
||||
form.tahminiTerminTarihi = header.AverageDueDate || form.tahminiTerminTarihi
|
||||
form.pb = header.DocCurrencyCode || form.pb
|
||||
selectedCari.value = header.CurrAccCode || ''
|
||||
siparisGenelAciklama.value = header.Description || ''
|
||||
}
|
||||
if (Array.isArray(lines)) summaryRows.value = [...lines]
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function clearDraft() {
|
||||
localStorage.removeItem(DRAFT_KEY.value)
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 onMounted — İlk Yüklemeler
|
||||
Uygulama ilk açıldığında auth kontrolü, store restore,
|
||||
cari ve model listesi yükleme işlemleri yapılır.
|
||||
=========================================================== */
|
||||
/* ===========================================================
|
||||
🔹 onMounted — İlk Yüklemeler
|
||||
Uygulama ilk açıldığında auth kontrolü, store restore,
|
||||
cari ve model listesi yükleme işlemleri yapılır.
|
||||
=========================================================== */
|
||||
onMounted(async () => {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
if (orderId) {
|
||||
console.log('✏️ Düzenleme modu başlatılıyor:', orderId)
|
||||
mode.value = 'edit'
|
||||
isEditMode.value = true
|
||||
headerId.value = orderId
|
||||
await loadOrderById(orderId)
|
||||
} else {
|
||||
console.log('🆕 Yeni sipariş modu başlatılıyor')
|
||||
mode.value = 'new'
|
||||
isEditMode.value = false
|
||||
form.OrderHeaderID = crypto.randomUUID()
|
||||
txId.value = form.OrderHeaderID
|
||||
resetForm()
|
||||
}
|
||||
|
||||
await Promise.all([loadCariList(), loadModels()])
|
||||
console.log('✅ OrderEntry ekranı hazır — mode:', mode.value)
|
||||
})
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Cari Filtreleme (Arama Kutusu)
|
||||
QSelect bileşeninde “use-input” aktif olduğunda çalışır.
|
||||
Kullanıcının yazdığı değeri cari listesinde arar (kod + ad bazlı).
|
||||
=========================================================== */
|
||||
function filterCari(val, update) {
|
||||
if (val === '') {
|
||||
// Boş arama → tüm cari listesi geri yüklenir
|
||||
update(() => (filteredCariOptions.value = cariOptions.value))
|
||||
return
|
||||
}
|
||||
|
||||
// Küçük harfe çevirip hem ad hem kod üzerinden arama yap
|
||||
update(() => {
|
||||
const needle = val.toLowerCase()
|
||||
filteredCariOptions.value = cariOptions.value.filter(v =>
|
||||
(v.Cari_Ad || '').toLowerCase().includes(needle) ||
|
||||
(v.Cari_Kod || '').toLowerCase().includes(needle)
|
||||
)
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
// ♻️ Daha önce kaydedilmiş siparişler varsa geri yükle
|
||||
orderStore.loadFromStorage()
|
||||
|
||||
// 💾 Order değişikliklerini izleyip her değişiklikte kaydet
|
||||
orderStore.watchOrders()
|
||||
|
||||
// 💾 LocalStorage geri yüklendikten sonra grid senkronizasyonu
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
if (orderStore.orders && orderStore.orders.length > 0) {
|
||||
summaryRows.value = [...orderStore.orders]
|
||||
console.log('💾 Grid satırları LocalStorage’dan yüklendi:', summaryRows.value.length)
|
||||
} else {
|
||||
console.log('ℹ️ LocalStorage boş, grid başlatılmadı.')
|
||||
}
|
||||
})
|
||||
|
||||
// 🔄 Store değişiklikleri anlık olarak grid’e yansıt
|
||||
watch(
|
||||
() => orderStore.orders,
|
||||
newOrders => {
|
||||
summaryRows.value = [...newOrders]
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
console.log(
|
||||
'♻️ LocalStorage geri yükleme tamamlandı. Aktif transaction:',
|
||||
orderStore.activeTransactionId || '—'
|
||||
)
|
||||
})
|
||||
|
||||
// 🧹 Sayfa kapanmadan önce tekrar yedekle (fail-safe)
|
||||
onBeforeUnmount(() => {
|
||||
orderStore.saveToStorage()
|
||||
console.log('💾 Sayfa kapatılırken veriler son kez kaydedildi.')
|
||||
})
|
||||
|
||||
// 🔄 Reaktif izleme: orderStore.orders değiştiğinde kalıcı yaz
|
||||
watch(
|
||||
() => orderStore.orders,
|
||||
() => orderStore.saveToStorage(),
|
||||
{ deep: true }
|
||||
)
|
||||
/* ===========================================================
|
||||
🔹 onMounted: Header Gap Güncelleme
|
||||
order-grid-header yüksekliğini ölçüp CSS değişkeni olarak kaydeder.
|
||||
Böylece sticky header ile grid gövdesi arasında tam hizalama sağlanır.
|
||||
=========================================================== */
|
||||
onMounted(() => {
|
||||
nextTick(() => { // DOM tamamen yüklensin
|
||||
const hdr = document.querySelector('.order-grid-header')
|
||||
if (!hdr) {
|
||||
console.warn('⚠️ .order-grid-header bulunamadı, ölçüm atlandı.')
|
||||
return
|
||||
}
|
||||
|
||||
const updateHeaderGap = () => {
|
||||
if (!hdr || !hdr.parentNode) return // ⚠️ güvenlik kontrolü eklendi
|
||||
const rect = hdr.getBoundingClientRect()
|
||||
const height = rect.height || 0
|
||||
const fineAdjust = -height
|
||||
document.documentElement.style.setProperty('--header-body-gap', `${fineAdjust}px`)
|
||||
console.log('📏 Header yüksekliği ölçüldü:', height, 'gap:', fineAdjust)
|
||||
}
|
||||
|
||||
updateHeaderGap()
|
||||
|
||||
const resizeObs = new ResizeObserver(() => {
|
||||
if (hdr?.parentNode) updateHeaderGap()
|
||||
})
|
||||
resizeObs.observe(hdr)
|
||||
|
||||
const onResize = () => {
|
||||
if (hdr?.parentNode) updateHeaderGap()
|
||||
}
|
||||
window.addEventListener('resize', onResize)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObs.disconnect()
|
||||
window.removeEventListener('resize', onResize)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Cari Seçimi (onCariChange)
|
||||
Kullanıcı cari seçtiğinde hem cari bilgisi yüklenir,
|
||||
hem de ilgili para birimi (PB) otomatik olarak set edilir.
|
||||
=========================================================== */
|
||||
async function onCariChange(kod) {
|
||||
// 1️⃣ Cari bilgiyi lokal listeden bul
|
||||
cariInfo.value = cariOptions.value.find(c => c.Cari_Kod === kod) || null
|
||||
|
||||
// 2️⃣ Varsayılan PB USD (fallback)
|
||||
let pb = cariInfo.value?.Doviz_Cinsi || 'USD'
|
||||
|
||||
try {
|
||||
// 3️⃣ Eğer local veride Doviz_Cinsi yoksa backend'den çağır
|
||||
if (!cariInfo.value?.Doviz_Cinsi && kod) {
|
||||
const res = await api.get('/customer-detail', { params: { code: kod } })
|
||||
const data = res?.data || {}
|
||||
|
||||
// Backend farklı property isimleri döndürebileceği için esnek kontrol
|
||||
if (data.Doviz_Cinsi || data.ParaBirimi || data.currency) {
|
||||
pb = data.Doviz_Cinsi || data.ParaBirimi || data.currency
|
||||
console.log(`💱 Cari (${kod}) para birimi backend'den alındı: ${pb}`)
|
||||
} else {
|
||||
console.log(`💵 Cari (${kod}) için PB bulunamadı, USD olarak atanıyor.`)
|
||||
pb = 'USD'
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Cari detay alınamadı, USD olarak devam ediliyor.', err)
|
||||
pb = 'USD'
|
||||
}
|
||||
|
||||
// 4️⃣ Global ve form seviyesinde PB'yi güncelle
|
||||
aktifPB.value = pb
|
||||
form.pb = pb
|
||||
|
||||
// 5️⃣ Eğer model seçiliyse, PB değiştiği için min fiyatı yeniden çek
|
||||
if (form.model) {
|
||||
try {
|
||||
await fetchMinPrice()
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Min fiyat yenilenemedi:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 6️⃣ Bilgi logu
|
||||
console.log(`💱 Aktif PB setlendi: ${pb}`)
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Model Listesi (Ürün Seçimi)
|
||||
Backend'den /api/products endpoint’inden tüm ürün kodları çekilir.
|
||||
=========================================================== */
|
||||
const modelOptions = ref([]) // Model seçenekleri
|
||||
const filteredModelOptions = ref([]) // Arama ile filtrelenmiş hali
|
||||
const loadingModels = ref(false) // Spinner için flag
|
||||
|
||||
async function loadModels() {
|
||||
loadingModels.value = true
|
||||
try {
|
||||
ensureAuthOrRedirect()
|
||||
const res = await api.get('/products')
|
||||
const arr = res?.data || []
|
||||
|
||||
// Backend'den ProductCode alanı çekilir
|
||||
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ı ❌' })
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 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)
|
||||
Yeni model seçildiğinde renkler, ürün bilgileri, min fiyat,
|
||||
stok ve beden grubu eksiksiz yenilenir; açıklama ve adet korunur.
|
||||
=========================================================== */
|
||||
/* ===========================================================
|
||||
🔹 MODEL SEÇİMİ (onModelChange)
|
||||
Yeni model seçildiğinde renkler, ürün bilgileri, min fiyat,
|
||||
stok ve beden grubu eksiksiz yenilenir; açıklama ve adet korunur.
|
||||
=========================================================== */
|
||||
async function onModelChange(modelCode) {
|
||||
if (!modelCode) {
|
||||
console.warn('⚠️ Model kodu boş, sorgu yapılmadı.')
|
||||
return
|
||||
}
|
||||
|
||||
// 🧩 Önceki değerleri yedekle (korunacak alanlar)
|
||||
const keep = {
|
||||
aciklama: form.aciklama,
|
||||
bedenler: [...form.bedenler],
|
||||
bedenLabels: [...form.bedenLabels],
|
||||
fiyat: form.fiyat,
|
||||
adet: form.adet,
|
||||
tutar: form.tutar
|
||||
}
|
||||
|
||||
try {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
// 🎨 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 || {}
|
||||
|
||||
Object.assign(form, {
|
||||
model: modelCode,
|
||||
urunAnaGrubu: d.UrunAnaGrubu || d.ProductGroup || '',
|
||||
urunAltGrubu: d.UrunAltGrubu || d.ProductSubGroup || '',
|
||||
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,
|
||||
bedenLabels: keep.bedenLabels,
|
||||
bedenler: keep.bedenler
|
||||
})
|
||||
|
||||
console.log('📦 Model detayları yüklendi:', d.UrunAnaGrubu, d.Fit1)
|
||||
|
||||
// 💰 3️⃣ Min fiyatı yükle
|
||||
await fetchMinPrice()
|
||||
|
||||
// ⚙️ 4️⃣ Renk bulunmazsa doğrudan bedenleri yükle
|
||||
if (!renkOptions.value.length) {
|
||||
await loadProductSizes(true)
|
||||
}
|
||||
|
||||
// 🧮 5️⃣ Gridde mevcut kombinasyon varsa düzenleme moduna al
|
||||
await openExistingCombination()
|
||||
|
||||
// ✅ 6️⃣ Yeni model sonrası otomatik stok/beden yükle (forceRefresh)
|
||||
await handleNewCombination()
|
||||
|
||||
$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)
|
||||
Kullanıcı model seçtikten sonra 1. rengi seçtiğinde:
|
||||
- 2. renk seçenekleri sıfırlanır
|
||||
- 2. renk listesi backend'den yüklenir
|
||||
- Eğer 2. renk tanımı yoksa doğrudan bedenler yüklenir
|
||||
=========================================================== */
|
||||
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 {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
// 🎨 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 loadProductSizes(true)
|
||||
}
|
||||
|
||||
await handleNewCombination()
|
||||
} catch (err) {
|
||||
console.error('❌ 1. renk sonrası hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 2. RENK SEÇİMİ (onColor2Change)
|
||||
Kullanıcı 2. renk seçtiğinde beden/stok sorgusu yeniden yapılır.
|
||||
Ayrıca kombinasyon gridde varsa form otomatik doldurulur.
|
||||
=========================================================== */
|
||||
async function onColor2Change(colorCode2) {
|
||||
if (typeof colorCode2 === 'object' && colorCode2?.value) {
|
||||
colorCode2 = colorCode2.value
|
||||
}
|
||||
|
||||
form.renk2 = colorCode2 || ''
|
||||
|
||||
try {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
// 2. renk seçildikten sonra stok/beden yükle
|
||||
await loadProductSizes(true)
|
||||
|
||||
// Aynı kombinasyon varsa düzenleme moduna al
|
||||
await openExistingCombination()
|
||||
|
||||
await handleNewCombination()
|
||||
} catch (err) {
|
||||
console.error('❌ 2. renk sonrası hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 MODEL + PB Bazlı Minimum Fiyat
|
||||
Backend’de her model + para birimi kombinasyonu için
|
||||
minimum satış fiyatı tutulur. Kullanıcı fiyat girdiğinde
|
||||
bu alt limitin altına inmemesi sağlanır.
|
||||
=========================================================== */
|
||||
async function fetchMinPrice() {
|
||||
if (!form.model || !form.pb) {
|
||||
console.warn('⚠️ Fiyat sorgusu için model veya PB eksik.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Store’daki fetchMinPrice fonksiyonu backend’den veri çeker
|
||||
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
|
||||
if (priceData) {
|
||||
// Döviz bazlı fiyatlar + TL karşılığı (rateToTRY) setlenir
|
||||
form.minFiyat = Number(priceData.price || 0)
|
||||
form.kur = Number(priceData.rateToTRY || 1)
|
||||
form.minFiyatTRY = Number(priceData.priceTRY || 0)
|
||||
console.log(
|
||||
`💰 Fiyatlar yüklendi: ${form.minFiyat} ${form.pb} (${form.minFiyatTRY.toFixed(2)} TRY)`
|
||||
)
|
||||
} else {
|
||||
// Backend boş döndüyse default sıfırla
|
||||
form.minFiyat = 0
|
||||
form.kur = 1
|
||||
form.minFiyatTRY = 0
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Min fiyat alınamadı:', err)
|
||||
form.minFiyat = 0
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Beden / Stok Yükleyici (loadProductSizes)
|
||||
Bu fonksiyon ERP’den renk+model bazlı beden ve stok bilgisini çeker.
|
||||
Ayrıca MSSQL stoklarıyla merge eder ve cache’ler.
|
||||
=========================================================== */
|
||||
const sizeCache = ref({}) // Tekrar sorguları engellemek için cache
|
||||
const bedenStock = ref([]) // Görsel tablo için stok listesi
|
||||
const stockMap = ref({}) // { "48": 12, "50": 7, ... } şeklinde key-value map
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Beden / Stok Yükleme Fonksiyonu
|
||||
forceRefresh = true → cache'i yok say, API'den güncel çek
|
||||
=========================================================== */
|
||||
async function loadProductSizes(forceRefresh = false) {
|
||||
if (!form.model) {
|
||||
console.warn('⚠️ Beden yüklenemedi: model seçilmemiş.')
|
||||
return
|
||||
}
|
||||
|
||||
const colorKey = (form.renk && form.renk.trim() !== '') ? form.renk.trim() : 'nocolor'
|
||||
const color2Key = (form.renk2 && form.renk2.trim() !== '') ? form.renk2.trim() : 'no2color'
|
||||
const key = `${form.model}_${colorKey}_${color2Key}`
|
||||
|
||||
console.log('🧩 loadProductSizes → key:', key, '| forceRefresh:', forceRefresh)
|
||||
|
||||
// 💾 Cache’den veri varsa ve forceRefresh=false ise cache kullan
|
||||
if (!forceRefresh && sizeCache.value[key]) {
|
||||
console.log(`💾 Cache’den yüklendi: ${key}`)
|
||||
const cached = sizeCache.value[key]
|
||||
|
||||
// ✅ Mevcut adetleri koru
|
||||
const previousMap = {}
|
||||
form.bedenLabels?.forEach((lbl, i) => {
|
||||
previousMap[lbl] = Number(form.bedenler?.[i] || 0)
|
||||
})
|
||||
|
||||
form.bedenLabels = cached.labels
|
||||
form.bedenler = form.bedenLabels.map(lbl => Number(previousMap[lbl] || 0))
|
||||
bedenStock.value = cached.stockArray
|
||||
stockMap.value = { ...cached.stockMap }
|
||||
|
||||
// ⚡ Cache yüklenmiş olsa bile MSSQL stoklarını güncelle (edit mode için)
|
||||
console.log('🔄 Cache sonrası MSSQL stokları tazeleniyor...')
|
||||
await loadOrderInventory(true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
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('📦 Beden/stok sorgusu gönderiliyor:', params)
|
||||
const res = await api.get('/product-colorsize', { params })
|
||||
const data = res?.data || []
|
||||
|
||||
console.log(`📦 Gelen beden/stok kayıt sayısı: ${data.length}`)
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
console.warn('⚪ Bu kombinasyon için tanımlı beden bulunamadı.')
|
||||
form.bedenLabels = []
|
||||
form.bedenler = []
|
||||
bedenStock.value = []
|
||||
stockMap.value = {}
|
||||
return
|
||||
}
|
||||
|
||||
// 🔹 Gelen beden kodlarını normalize et
|
||||
const bedenList = Array.from(
|
||||
new Set(
|
||||
data.map(x => x.item_dim1_code?.trim() || x.ItemDim1Code?.trim()).filter(Boolean)
|
||||
)
|
||||
)
|
||||
|
||||
// 🔹 Önceki adetleri koru
|
||||
const previousMap = {}
|
||||
form.bedenLabels?.forEach((lbl, i) => {
|
||||
previousMap[lbl] = Number(form.bedenler?.[i] || 0)
|
||||
})
|
||||
|
||||
// 🔹 Aktif beden grubu belirle
|
||||
const grpKey = detectBedenGroup(bedenList, form.urunAnaGrubu, form.kategori)
|
||||
const grp = schemaByKey.value[grpKey] || schemaByKey.value.tak
|
||||
form.bedenLabels = grp.values
|
||||
form.bedenler = form.bedenLabels.map(lbl => Number(previousMap[lbl] || 0))
|
||||
|
||||
console.log(`✅ Aktif beden grubu: ${grp.title} (${grpKey})`)
|
||||
|
||||
// 🔹 ERP stoklarını işle
|
||||
const stockArray = data.map(x => ({
|
||||
beden: x.item_dim1_code?.trim() || x.ItemDim1Code?.trim(),
|
||||
stok: Number(
|
||||
x.kullanilabilir_envanter ||
|
||||
x.Kullanilabilir_Envanter ||
|
||||
x.stock ||
|
||||
0
|
||||
)
|
||||
}))
|
||||
const stockMapLocal = {}
|
||||
for (const s of stockArray) stockMapLocal[s.beden] = s.stok
|
||||
stockMap.value = { ...stockMapLocal }
|
||||
bedenStock.value = [...stockArray]
|
||||
|
||||
console.log(`🧮 İlk stok verisi işlendi (${stockArray.length} beden)`)
|
||||
|
||||
// 🔹 MSSQL stoklarını merge et
|
||||
await loadOrderInventory(true)
|
||||
|
||||
// 💾 Cache güncelle
|
||||
sizeCache.value[key] = {
|
||||
labels: [...form.bedenLabels],
|
||||
stockArray: [...bedenStock.value],
|
||||
stockMap: { ...stockMap.value }
|
||||
}
|
||||
|
||||
console.log(`✅ Cache güncellendi: ${key}`)
|
||||
} catch (err) {
|
||||
console.error('❌ Beden/stok verisi yüklenirken hata oluştu:', err)
|
||||
$q.notify({ type: 'negative', message: 'Beden/stok verisi alınamadı ❌' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 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 {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
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)
|
||||
|
||||
// 1️⃣ Normalize et
|
||||
const invMap = {}
|
||||
for (const x of data) {
|
||||
let beden = (x.Beden || x.beden || '').trim()
|
||||
if (beden === '') beden = ' '
|
||||
const stokDeger =
|
||||
x.KullanilabilirEnvanter ??
|
||||
x.kullanilabilir_envanter ??
|
||||
x.Kullanilabilir_Envanter ??
|
||||
null
|
||||
if (stokDeger != null) invMap[beden] = Number(stokDeger)
|
||||
}
|
||||
|
||||
// 2️⃣ Form bedenlerine göre map oluştur
|
||||
const newMap = {}
|
||||
for (const lbl of form.bedenLabels || []) {
|
||||
const key = lbl?.trim() === '' ? ' ' : lbl.trim()
|
||||
if (invMap[key] != null) newMap[lbl] = invMap[key]
|
||||
}
|
||||
|
||||
// 3️⃣ Merge veya replace
|
||||
if (merge && stockMap.value) {
|
||||
for (const lbl of Object.keys(newMap)) {
|
||||
// sadece yeni stok bilgisi varsa güncelle
|
||||
if (newMap[lbl] != null && !isNaN(newMap[lbl])) {
|
||||
stockMap.value[lbl] = newMap[lbl]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stockMap.value = { ...newMap }
|
||||
}
|
||||
|
||||
// 4️⃣ bedenStock listesini senkronize et
|
||||
bedenStock.value = Object.keys(stockMap.value).map(k => ({
|
||||
beden: k,
|
||||
stok: stockMap.value[k]
|
||||
}))
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
}
|
||||
function formatDate(v) {
|
||||
if (!v) return '—'
|
||||
try {
|
||||
// ISO tarih ise parçala
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
||||
const [y, m, d] = v.split('-')
|
||||
return `${d}.${m}.${y}`
|
||||
}
|
||||
// diğer olası formatlar
|
||||
const date = new Date(v)
|
||||
if (isNaN(date.getTime())) return '—'
|
||||
const dd = String(date.getDate()).padStart(2, '0')
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const yyyy = date.getFullYear()
|
||||
return `${dd}.${mm}.${yyyy}`
|
||||
} catch {
|
||||
return '—'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 applyTerminToRows — Tahmini Termin Tarihini Grid Satırlarına Aktar
|
||||
Formdaki tahmini termin tarihi değiştiğinde,
|
||||
griddeki tüm satırlara aynı tarih işlenir (boş olanlara veya yeni eklenenlere)
|
||||
=========================================================== */
|
||||
function applyTerminToRows(dateStr) {
|
||||
if (!dateStr || !Array.isArray(summaryRows.value)) return
|
||||
summaryRows.value = summaryRows.value.map(r => {
|
||||
if (!r.terminTarihi || r.terminTarihi === '') {
|
||||
return { ...r, terminTarihi: dateStr }
|
||||
}
|
||||
return r
|
||||
})
|
||||
}
|
||||
|
||||
// 🔹 Watcher: Formdaki tahmini termin tarihi değiştiğinde tüm satırlara uygula
|
||||
// ÜST formdaki tahmini termin değişince:
|
||||
watch(
|
||||
() => form.tahminiTerminTarihi,
|
||||
(yeni, eski) => {
|
||||
// 1) Önce editördeki tarih alanını güncelle
|
||||
// Eğer kullanıcı editörde manuel farklı bir tarih tutmuyorsa, senkronla.
|
||||
if (!form.terminTarihi || form.terminTarihi === eski) {
|
||||
form.terminTarihi = yeni || ''
|
||||
}
|
||||
|
||||
// 2) İstersen griddeki BOŞ termin alanlarına da uygula
|
||||
// (dolu satırlara dokunmaz)
|
||||
applyTerminToRows(yeni)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
/*=========================================================== */
|
||||
const selectedSeriSet = ref(null) // Seçili seri seti (ör: “46-58 seri”)
|
||||
const seriMultiplier = ref(1) // Çarpan (ör: 2 → her bedene 2 ekler)
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Seri Uygulama (Tüm bedenlere aynı değeri atar)
|
||||
Kullanıcı “ALL1” veya “ALL2” gibi genel seri modunu seçerse
|
||||
tüm bedenlere aynı miktar yazılır.
|
||||
=========================================================== */
|
||||
function applySeri(f) {
|
||||
f.bedenler = (f.bedenLabels || []).map(() =>
|
||||
f.seri === 'ALL2' ? 2 : f.seri === 'ALL1' ? 1 : 0
|
||||
)
|
||||
updateTotals(f)
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Toplam Güncelleme Fonksiyonu
|
||||
Beden girişlerinde adet veya fiyat değiştiğinde toplam tutarı hesaplar.
|
||||
=========================================================== */
|
||||
function updateTotals(f) {
|
||||
f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
|
||||
const fiyat = Number(f.fiyat) || 0
|
||||
f.tutar = (f.adet * fiyat).toFixed(2)
|
||||
}
|
||||
|
||||
// dışarıya alınmalı:
|
||||
watch([includeKDV, toplamTutar], ([aktif, toplam]) => {
|
||||
manualKDV.value = aktif ? Number(toplam || 0) * kdvOrani : 0
|
||||
})
|
||||
|
||||
const toplamKDVli = computed(() => {
|
||||
return Number(toplamTutar.value || 0) + (includeKDV.value ? Number(manualKDV.value || 0) : 0)
|
||||
})
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Seri Seti Uygulama (applySeriSet)
|
||||
Kullanıcı bir seri seti seçtiğinde, o setin beden-adet oranlarını
|
||||
ilgili gruptan alır ve formdaki bedenlere ekler.
|
||||
=========================================================== */
|
||||
function applySeriSet() {
|
||||
if (!selectedSeriSet.value) return
|
||||
|
||||
// 🔹 Aktif beden grubu anahtarını tespit et
|
||||
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
|
||||
|
||||
// 🔹 Seçilen seri setinin anahtarını al (object veya string olabilir)
|
||||
const setKey =
|
||||
typeof selectedSeriSet.value === 'object'
|
||||
? (selectedSeriSet.value.value || selectedSeriSet.value.label)
|
||||
: selectedSeriSet.value
|
||||
|
||||
// 🔹 Seri pattern’ini bul
|
||||
const pattern = seriMatrix[grpKey]?.[setKey] || {}
|
||||
const mult = Number(seriMultiplier.value) || 1
|
||||
|
||||
// 🔸 EKLEYEREK uygula — mevcut değerlere çarpanlı ekleme yapar
|
||||
form.bedenler = form.bedenLabels.map((lbl, idx) => {
|
||||
const current = Number(form.bedenler[idx] || 0)
|
||||
const inc = Number(pattern[lbl] || 0) * mult
|
||||
return current + inc
|
||||
})
|
||||
|
||||
// 🔹 Toplam adet ve tutarı güncelle
|
||||
updateTotals(form)
|
||||
|
||||
// 🔔 Kullanıcıya bilgi bildirimi
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `Seri seti "${setKey}" başarıyla eklendi ✅`
|
||||
})
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Editing ve Satır İşlemleri
|
||||
summaryRows, ekranda gridde görülen satırların tam listesidir.
|
||||
editingIndex aktif olarak düzenlenen satırı tutar.
|
||||
=========================================================== */
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Form → Grid Satırı Dönüştürücü (v2)
|
||||
Kullanıcının formda yaptığı girişleri griddeki summaryRows
|
||||
formatına dönüştürür. Ayrıca formdaki tahmini termin tarihini
|
||||
grid satırına "terminTarihi" olarak işler.
|
||||
=========================================================== */
|
||||
function toSummaryRowFromForm() {
|
||||
// 🔸 Beden grubu anahtarını tespit et
|
||||
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
|
||||
|
||||
// 🔸 Beden map oluştur
|
||||
const bedenMap = {}
|
||||
for (const lbl of form.bedenLabels) {
|
||||
const index = form.bedenLabels.indexOf(lbl)
|
||||
bedenMap[lbl] = Number(form.bedenler[index] || 0)
|
||||
}
|
||||
|
||||
// 🔸 Grid satır objesi
|
||||
const row = {
|
||||
id: Date.now(), // geçici ID
|
||||
model: form.model,
|
||||
renk: form.renk,
|
||||
renk2: form.renk2,
|
||||
urunAnaGrubu: form.urunAnaGrubu,
|
||||
urunAltGrubu: form.urunAltGrubu,
|
||||
aciklama: form.aciklama,
|
||||
fiyat: Number(form.fiyat || 0),
|
||||
pb: form.pb || aktifPB.value || 'USD',
|
||||
adet: Number(form.adet || 0),
|
||||
tutar: Number(form.tutar || 0),
|
||||
|
||||
// 🔹 Grup yapısı
|
||||
grpKey,
|
||||
bedenMap: { [grpKey]: { ...bedenMap } },
|
||||
|
||||
// 🔹 Yeni: Tahmini termin tarihi formdan alınır
|
||||
terminTarihi: form.terminTarihi || form.tahminiTerminTarihi || ''
|
||||
|
||||
}
|
||||
|
||||
console.log('📄 Grid satırı oluşturuldu:', row)
|
||||
return row
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 updateRow (Grid satırını güncelle veya ekle)
|
||||
- Edit mode aktifse mevcut satırı günceller
|
||||
- Yeni kayıt ise summaryRows’a ekler
|
||||
- Adet, fiyat, tutar, aciklama, pb, bedenMap alanlarını senkronize eder
|
||||
=========================================================== */
|
||||
async function updateRow() {
|
||||
try {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
// 1️⃣ Temel doğrulama
|
||||
if (!form.model) {
|
||||
$q.notify({ type: 'warning', message: 'Lütfen model seçin ⚠️' })
|
||||
return
|
||||
}
|
||||
|
||||
// 2️⃣ Toplamları güncelle
|
||||
updateTotals(form)
|
||||
|
||||
// 3️⃣ Formu summaryRow formatına çevir
|
||||
const row = toSummaryRowFromForm()
|
||||
|
||||
// 4️⃣ Edit mode aktifse → mevcut satırı güncelle
|
||||
if (editingIndex.value !== -1 && summaryRows.value[editingIndex.value]) {
|
||||
const idx = editingIndex.value
|
||||
const existing = summaryRows.value[idx]
|
||||
|
||||
console.log(`✏️ updateRow çalıştı → ID: ${existing.id || '(geçici)'}`)
|
||||
|
||||
// 🔹 Mevcut satır verilerini koruyarak güncelle
|
||||
summaryRows.value[idx] = {
|
||||
...existing,
|
||||
...row, // form’daki yeni değerleri uygula
|
||||
id: existing.id || row.id, // ID aynı kalmalı
|
||||
grpKey: existing.grpKey || row.grpKey,
|
||||
bedenMap: row.bedenMap, // beden adetleri
|
||||
fiyat: form.fiyat,
|
||||
pb: form.pb,
|
||||
tutar: form.tutar,
|
||||
aciklama: form.aciklama,
|
||||
adet: form.adet
|
||||
}
|
||||
|
||||
// 🧮 Grup ve toplamlar yeniden hesaplanır (recomputed)
|
||||
updateTotals(form)
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Satır başarıyla güncellendi ✅',
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
console.log('✅ Satır güncellendi:', summaryRows.value[idx])
|
||||
}
|
||||
else {
|
||||
// 5️⃣ Yeni satır ekleme
|
||||
summaryRows.value.push(row)
|
||||
console.log('🆕 Yeni satır eklendi:', row)
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Yeni kombinasyon eklendi 🧩',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
// 6️⃣ Formu temizle (sadece yeni ekleme sonrası)
|
||||
if (editingIndex.value === -1) {
|
||||
resetForm(true)
|
||||
} else {
|
||||
// Edit sonrası formu bırak ama editing’i kapat
|
||||
editingIndex.value = -1
|
||||
orderStore.selected = null
|
||||
}
|
||||
|
||||
console.log('🧾 Toplam satır sayısı:', summaryRows.value.length)
|
||||
} catch (err) {
|
||||
console.error('❌ Satır güncellenirken hata oluştu:', err)
|
||||
$q.notify({ type: 'negative', message: 'Satır güncellenemedi ❌' })
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 openExistingCombination
|
||||
Formda seçilen model + renk + renk2 kombinasyonu zaten gridde varsa,
|
||||
o satırı düzenleme moduna alır ve formu otomatik doldurur.
|
||||
Yoksa formu temizleyip yeni girişe hazırlar.
|
||||
=========================================================== */
|
||||
/* ===========================================================
|
||||
🔹 openExistingCombination (Zero-Stock Safe v3)
|
||||
Edit modunda stokların 0 görünme hatası çözülmüş versiyon.
|
||||
Reactive flush + parametre doğrulaması içerir.
|
||||
=========================================================== */
|
||||
async function openExistingCombination() {
|
||||
if (!form.model) return
|
||||
|
||||
// 🔍 1️⃣ Aynı kombinasyon gridde var mı?
|
||||
const idx = findExistingIndexByForm()
|
||||
if (idx === -1) {
|
||||
// 🆕 Yeni kombinasyon
|
||||
editingIndex.value = -1
|
||||
orderStore.selected = null
|
||||
|
||||
Object.assign(form, {
|
||||
adet: 0,
|
||||
fiyat: 0,
|
||||
tutar: 0,
|
||||
pb: aktifPB.value || 'USD'
|
||||
})
|
||||
console.log('🆕 Yeni kombinasyon → Form temizlendi')
|
||||
return
|
||||
}
|
||||
|
||||
// 🔹 2️⃣ Mevcut satırı bul
|
||||
const r = summaryRows.value[idx]
|
||||
editingIndex.value = idx
|
||||
orderStore.selected = { ...r }
|
||||
|
||||
// 🔒 Edit modda aktif model ve renk seçeneklerini sabitle
|
||||
renkOptions.value = [{ label: r.renk, value: r.renk }]
|
||||
renkOptions2.value = r.renk2
|
||||
? [{ label: r.renk2, value: r.renk2 }]
|
||||
: []
|
||||
|
||||
// 🔹 3️⃣ Formu doldur
|
||||
Object.assign(form, {
|
||||
model: r.model,
|
||||
renk: r.renk,
|
||||
renk2: r.renk2,
|
||||
urunAnaGrubu: r.urunAnaGrubu,
|
||||
urunAltGrubu: r.urunAltGrubu,
|
||||
aciklama: r.aciklama,
|
||||
fiyat: Number(r.fiyat || 0),
|
||||
pb: r.pb || aktifPB.value || 'USD',
|
||||
adet: Number(r.adet || 0),
|
||||
tutar: Number(r.tutar || 0)
|
||||
})
|
||||
|
||||
// ✅ 3B️⃣ GRIDDEKİ TERMİN → EDİTÖR
|
||||
form.terminTarihi = r.terminTarihi || form.terminTarihi || form.tahminiTerminTarihi || ''
|
||||
|
||||
// 🔹 4️⃣ Beden grubunu geri yükle
|
||||
const key = r.grpKey || activeGroupKeyForRow(r)
|
||||
const grp = schemaByKey.value[key]
|
||||
form.bedenLabels = grp?.values || []
|
||||
|
||||
const savedMap = r.bedenMap?.[key] || {}
|
||||
form.bedenler = form.bedenLabels.map(lbl => Number(savedMap[lbl] || 0))
|
||||
|
||||
// ✅ 5️⃣ Stokları yeniden getir (forceRefresh = true)
|
||||
try {
|
||||
console.log('🔄 Stoklar tazeleniyor (edit mode)...')
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 250))
|
||||
await nextTick()
|
||||
|
||||
console.log('🧩 loadProductSizes çağrısı öncesi:', {
|
||||
model: form.model,
|
||||
renk: form.renk,
|
||||
renk2: form.renk2
|
||||
})
|
||||
|
||||
if (!form.model || !form.renk) {
|
||||
console.warn('⚠️ Model veya renk eksik, stok çağrısı atlanıyor.', form)
|
||||
} else {
|
||||
await loadProductSizes(true)
|
||||
const stoklar = Object.values(stockMap.value || {})
|
||||
console.log(`📦 Güncel stoklar: ${stoklar.join(', ')}`)
|
||||
if (stoklar.length && stoklar.every(v => Number(v) === 0)) {
|
||||
console.warn('⚠️ Backend 0 stok döndürdü → form:', form)
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Bu kombinasyon için stok bulunamadı (0).',
|
||||
position: 'top-right'
|
||||
})
|
||||
} else {
|
||||
console.log('✅ Stoklar başarıyla yüklendi.')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('❌ Stok bilgisi yenilenemedi:', err)
|
||||
}
|
||||
|
||||
// 💱 Para birimi doğrulaması
|
||||
if (!form.pb || form.pb === '') {
|
||||
form.pb = aktifPB.value || 'USD'
|
||||
console.log('💱 Para birimi formda eksikti, otomatik setlendi:', form.pb)
|
||||
}
|
||||
|
||||
// 💰 Minimum fiyat kontrolü
|
||||
try {
|
||||
if (form.model && form.pb) {
|
||||
console.log('💰 Min fiyat kontrolü başlatılıyor...')
|
||||
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
|
||||
const minFiyat = Number(priceData?.price || 0)
|
||||
if (minFiyat > 0 && form.fiyat < minFiyat) {
|
||||
form.fiyat = minFiyat
|
||||
form.tutar = (form.adet || 0) * (form.fiyat || 0)
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: `Fiyat min. seviyeye güncellendi (${minFiyat.toLocaleString('tr-TR')} ${form.pb})`,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Fiyat bilgisi yenilenemedi:', e)
|
||||
}
|
||||
|
||||
// 🔢 Toplam güncelle
|
||||
updateTotals(form)
|
||||
|
||||
// 💬 Kullanıcıya bilgi ver
|
||||
$q.notify({
|
||||
type: 'info',
|
||||
message: 'Mevcut kombinasyon düzenleme moduna alındı ✏️',
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
console.log(
|
||||
`✏️ Editör aktif → model=${r.model}, renk=${r.renk || '-'}, renk2=${r.renk2 || '-'}, pb=${form.pb}, termin=${form.terminTarihi}`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Yeni Kombinasyon Seçimi (Model / Renk / 2. Renk)
|
||||
Kullanıcı yeni model veya renk seçtiğinde stokların 0 görünmesini önler.
|
||||
Vue flush tamamlandıktan sonra loadProductSizes(true) çağrılır.
|
||||
=========================================================== */
|
||||
async function handleNewCombination() {
|
||||
if (!form.model) {
|
||||
console.warn('⚠️ Model seçilmeden stok yüklenemez.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🆕 Yeni kombinasyon seçildi:', {
|
||||
model: form.model,
|
||||
renk: form.renk,
|
||||
renk2: form.renk2
|
||||
})
|
||||
|
||||
try {
|
||||
// 🧠 Reaktivite flush bitene kadar bekle
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 250))
|
||||
await nextTick()
|
||||
|
||||
// ⚙️ Gereksiz çağrıları önle
|
||||
if (!form.model || !form.renk) {
|
||||
console.warn('⚠️ Model veya renk eksik, loadProductSizes atlandı.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🧩 loadProductSizes çağrılıyor (handleNewCombination)...')
|
||||
await loadProductSizes(true)
|
||||
|
||||
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('✅ Stoklar başarıyla yüklendi (yeni kombinasyon).')
|
||||
}
|
||||
|
||||
// 🔹 Gridde varsa düzenleme moduna al
|
||||
await openExistingCombination()
|
||||
} catch (err) {
|
||||
console.error('❌ handleNewCombination hatası:', err)
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Stok bilgisi alınamadı ❌',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
}
|
||||
/* ===========================================================
|
||||
🔹 useComboWatcher — Model / Renk / Renk2 değişimlerini izler
|
||||
Tek bir yardımcı fonksiyonla handleNewCombination çağrısını yönetir.
|
||||
Parametre: hangi alan değişti (model, renk, renk2)
|
||||
=========================================================== */
|
||||
/* ===========================================================
|
||||
🔹 useComboWatcher v3 (Yeni Satır Güvenli)
|
||||
Model / Renk / Renk2 değişimlerinde reaktif flush sonrası
|
||||
otomatik stok yükleme + mevcut satır koruma.
|
||||
=========================================================== */
|
||||
function useComboWatcher(type, handler) {
|
||||
return async (val) => {
|
||||
try {
|
||||
console.log(`🎯 useComboWatcher tetiklendi → ${type}:`, val)
|
||||
const isNewRow = editingIndex.value === -1
|
||||
const prevModel = form.model
|
||||
|
||||
// 1️⃣ İlgili handler'ı (ör. onModelChange) çalıştır
|
||||
if (typeof handler === 'function') {
|
||||
await handler(val)
|
||||
}
|
||||
|
||||
// 2️⃣ Vue flush tamamlanmasını bekle
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
await nextTick()
|
||||
|
||||
// 3️⃣ Türüne göre zinciri yürüt
|
||||
if (type === 'model') {
|
||||
if (isNewRow || form.model !== prevModel) {
|
||||
// 🗓️ Yeni satır açılıyorsa, editörde termin boşsa default terminle doldur
|
||||
if (!form.terminTarihi || form.terminTarihi === '') {
|
||||
form.terminTarihi = form.tahminiTerminTarihi || ''
|
||||
}
|
||||
console.log('🆕 Yeni satır veya model değişimi → stok yükleniyor...')
|
||||
await handleNewCombination()
|
||||
}
|
||||
} else if (type === 'renk' || type === 'renk2') {
|
||||
// 🗓️ Editörde termin boşsa default terminle doldur
|
||||
if (!form.terminTarihi || form.terminTarihi === '') {
|
||||
form.terminTarihi = form.tahminiTerminTarihi || ''
|
||||
}
|
||||
await handleNewCombination()
|
||||
}
|
||||
|
||||
console.log(`✅ useComboWatcher(${type}) tamamlandı.`)
|
||||
} catch (err) {
|
||||
console.error(`❌ useComboWatcher(${type}) hatası:`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Satır Düzenleme (Manuel Edit – stok güncelleme dahil)
|
||||
Grid’deki bir satırı tıklayınca formu o satırla doldurur.
|
||||
Termin: GRID → EDİTÖR senkronu eklendi.
|
||||
=========================================================== */
|
||||
const editRow = async (row, localIndex) => {
|
||||
const globalIndex = summaryRows.value.findIndex(r =>
|
||||
r.model === row.model &&
|
||||
r.renk === row.renk &&
|
||||
r.renk2 === row.renk2 &&
|
||||
r.aciklama === row.aciklama
|
||||
)
|
||||
|
||||
if (globalIndex === -1) {
|
||||
console.warn('⚠️ Editlenecek satır bulunamadı.')
|
||||
return
|
||||
}
|
||||
|
||||
editingIndex.value = globalIndex
|
||||
orderStore.selected = { ...row }
|
||||
|
||||
// 🔒 Edit modda seçili renkleri kilitle (seçenek olarak satırdaki değerleri koy)
|
||||
renkOptions.value = row.renk ? [{ label: row.renk, value: row.renk }] : []
|
||||
renkOptions2.value = row.renk2 ? [{ label: row.renk2, value: row.renk2 }] : []
|
||||
|
||||
// 🔄 Formu satır bilgileriyle doldur
|
||||
Object.assign(form, {
|
||||
model: row.model,
|
||||
renk: row.renk,
|
||||
renk2: row.renk2,
|
||||
urunAnaGrubu: row.urunAnaGrubu,
|
||||
urunAltGrubu: row.urunAltGrubu,
|
||||
aciklama: row.aciklama,
|
||||
fiyat: Number(row.fiyat || 0),
|
||||
pb: row.pb,
|
||||
adet: Number(row.adet || 0),
|
||||
tutar: Number(row.tutar || 0)
|
||||
})
|
||||
|
||||
// 🗓️ GRID → EDİTÖR termin senkronu
|
||||
form.terminTarihi = row.terminTarihi || form.terminTarihi || form.tahminiTerminTarihi || ''
|
||||
|
||||
// 🔹 Beden şemasını geri yükle
|
||||
const key = row.grpKey || activeGroupKeyForRow(row)
|
||||
const grp = schemaByKey.value[key]
|
||||
form.bedenLabels = grp?.values || []
|
||||
const savedMap = row.bedenMap?.[key] || {}
|
||||
form.bedenler = form.bedenLabels.map(lbl => Number(savedMap[lbl] || 0))
|
||||
|
||||
// 🔢 Toplamları yeniden hesapla
|
||||
updateTotals(form)
|
||||
form.tutar = (form.adet || 0) * (form.fiyat || 0)
|
||||
|
||||
// 🧩 Stokları forceRefresh ile yükle
|
||||
if (form.model && form.renk) {
|
||||
try {
|
||||
await nextTick()
|
||||
await loadProductSizes(true) // 💾 forceRefresh
|
||||
console.log('📦 Edit mode stoklar yenilendi:', stockMap.value)
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Edit modda stok yenileme başarısız:', err)
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Stok verisi yenilenemedi ⚠️',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('⚪ Model veya renk eksik, stok çağrısı atlandı.')
|
||||
}
|
||||
|
||||
console.log(`✏️ Edit mode aktif → index: ${globalIndex}, model: ${row.model}, termin: ${form.terminTarihi}`)
|
||||
}
|
||||
|
||||
// Bu fonksiyonu parent bileşenlerden çağırabilmek için export et
|
||||
defineExpose({ editRow })
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Kaydet / Güncelle (saveOrUpdate)
|
||||
- Aynı kombinasyon varsa güncelleme moduna geçer,
|
||||
yoksa yeni satır ekler.
|
||||
- Kaydetmeden hemen önce stokları forceRefresh ile tazeler.
|
||||
- Stok ve Min Fiyat validasyonları içerir.
|
||||
=========================================================== */
|
||||
async function saveOrUpdate() {
|
||||
// 0️⃣ Kombinasyon kontrolü (99999 toleranslı)
|
||||
const existingIndex = summaryRows.value.findIndex(r =>
|
||||
r.model === form.model &&
|
||||
((r.renk || '') === (form.renk || '') || (r.renk || '') === '99999' || (form.renk || '') === '99999') &&
|
||||
((r.renk2 || '') === (form.renk2 || '') || (r.renk2 || '') === '99999' || (form.renk2 || '') === '99999')
|
||||
)
|
||||
|
||||
if (existingIndex !== -1 && editingIndex.value === -1) {
|
||||
console.log(`⚙️ Kombinasyon zaten var → index ${existingIndex} güncellenecek`)
|
||||
editingIndex.value = existingIndex
|
||||
}
|
||||
|
||||
// 1️⃣ Model zorunlu alan
|
||||
if (!form.model) {
|
||||
$q.notify({ type: 'warning', message: '⚠️ Model seçimi gerekli!' })
|
||||
return
|
||||
}
|
||||
|
||||
// 2️⃣ Para birimi doğrulaması (cari yoksa USD)
|
||||
if (!form.pb || form.pb === '') {
|
||||
form.pb = aktifPB.value || 'USD'
|
||||
console.log('💱 Para birimi otomatik setlendi:', form.pb)
|
||||
}
|
||||
|
||||
// 3️⃣ Kaydetmeden önce stok verisini tazele (forceRefresh)
|
||||
try {
|
||||
await loadProductSizes(true)
|
||||
console.log('📦 Stok bilgisi kaydetmeden önce yenilendi (forceRefresh).')
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Stok bilgisi kaydetmeden önce yenilenemedi:', err)
|
||||
}
|
||||
|
||||
// 4️⃣ Stok kontrolü (beden bazında)
|
||||
let stokOK = true
|
||||
const overLimit = []
|
||||
for (let i = 0; i < form.bedenLabels.length; i++) {
|
||||
const lbl = form.bedenLabels[i]
|
||||
const stok = Number(stockMap.value?.[lbl] || 0)
|
||||
const girilen = Number(form.bedenler[i] || 0)
|
||||
if (stok > 0 && girilen > stok) {
|
||||
overLimit.push({ beden: lbl, stok, girilen })
|
||||
}
|
||||
}
|
||||
|
||||
if (overLimit.length > 0) {
|
||||
const msgLines = overLimit
|
||||
.map(x => `🟡 ${x.beden}: ${x.girilen} (Stok: ${x.stok})`)
|
||||
.join('<br>')
|
||||
|
||||
stokOK = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Stok Uyarısı',
|
||||
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:<br><br>${msgLines}`,
|
||||
html: true,
|
||||
ok: { label: 'Devam Et', color: 'primary' },
|
||||
cancel: { label: 'İptal', color: 'negative' }
|
||||
})
|
||||
.onOk(() => resolve(true))
|
||||
.onCancel(() => resolve(false))
|
||||
})
|
||||
}
|
||||
if (!stokOK) return
|
||||
|
||||
// 5️⃣ Min fiyat kontrolü (stok OK sonrası)
|
||||
let fiyatOK = true
|
||||
try {
|
||||
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
|
||||
const minFiyat = Number(priceData?.price || 0)
|
||||
if (priceData && form.fiyat < minFiyat) {
|
||||
fiyatOK = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Fiyat Uyarısı',
|
||||
message: `
|
||||
<b>Min. Fiyat:</b> ${minFiyat.toLocaleString('tr-TR')} ${form.pb}<br>
|
||||
<b>Girdiğin:</b> ${form.fiyat.toLocaleString('tr-TR')} ${form.pb}`,
|
||||
html: true,
|
||||
ok: { label: 'Devam Et', color: 'primary' },
|
||||
cancel: { label: 'İptal', color: 'negative' }
|
||||
})
|
||||
.onOk(() => resolve(true))
|
||||
.onCancel(() => resolve(false))
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Fiyat kontrolü yapılamadı:', err)
|
||||
}
|
||||
if (!fiyatOK) return
|
||||
// 6️⃣ Satır objesi oluştur (form → grid row)
|
||||
const row = toSummaryRowFromForm()
|
||||
if (!row || !row.model) return
|
||||
|
||||
// 7️⃣ Tutarlılık kontrolleri
|
||||
row.adet = Number(row.adet || form.adet || 0)
|
||||
row.fiyat = Number(row.fiyat || form.fiyat || 0)
|
||||
row.tutar = Number(row.tutar || (row.adet * row.fiyat) || 0)
|
||||
row.pb = form.pb || aktifPB.value || 'USD'
|
||||
|
||||
// 8️⃣ Güncelle veya yeni satır ekle
|
||||
if (editingIndex.value !== -1) {
|
||||
const currentRow = summaryRows.value[editingIndex.value]
|
||||
if (currentRow?.id) {
|
||||
// 🔹 ID varsa doğrudan güncelle
|
||||
row.id = currentRow.id
|
||||
summaryRows.value.splice(editingIndex.value, 1, row)
|
||||
orderStore.updateRow(currentRow.id, { ...row })
|
||||
} else {
|
||||
// 🔹 ID yoksa index bazlı güncelle
|
||||
summaryRows.value.splice(editingIndex.value, 1, row)
|
||||
if (orderStore.orders[editingIndex.value])
|
||||
orderStore.orders[editingIndex.value] = { ...row }
|
||||
}
|
||||
|
||||
orderStore.saveToStorage?.()
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Mevcut kombinasyon güncellendi ✏️',
|
||||
position: 'top-right'
|
||||
})
|
||||
} else {
|
||||
// 🆕 Yeni satır ekleme akışı
|
||||
orderStore.addRow({ ...row })
|
||||
summaryRows.value = [...orderStore.orders]
|
||||
orderStore.saveToStorage?.()
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Yeni kombinasyon eklendi ✅',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
// 9️⃣ Düzenleme durumunu sıfırla
|
||||
editingIndex.value = -1
|
||||
orderStore.selected = null
|
||||
resetForm()
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Form Sıfırlama (Tam Temizleme)
|
||||
Tüm form alanlarını, renk seçeneklerini, stok map’lerini
|
||||
ve select bileşenlerini sıfırlar.
|
||||
=========================================================== */
|
||||
/* ===========================================================
|
||||
🔹 resetForm (Güvenli Sıfırlama)
|
||||
Yeni sipariş başlatılırken formu temizler.
|
||||
Edit modunda ise aktif satırı korur.
|
||||
=========================================================== */
|
||||
/* ===========================================================
|
||||
🔹 Reset Form (Yeni Sipariş Başlatma)
|
||||
=========================================================== */
|
||||
function resetForm() {
|
||||
form.olusturmaTarihi = dayjs().format('YYYY-MM-DD')
|
||||
form.tahminiTerminTarihi = dayjs().add(30, 'day').format('YYYY-MM-DD')
|
||||
form.pb = 'USD'
|
||||
aktifPB.value = 'USD'
|
||||
selectedCari.value = ''
|
||||
siparisGenelAciklama.value = ''
|
||||
summaryRows.value = []
|
||||
orderStore.newOrderTemplate()
|
||||
isEditMode.value = false
|
||||
console.log('🧹 Form sıfırlandı.')
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Satır Silme (removeSelected)
|
||||
Kullanıcı bir satırı düzenleme modundayken silmek isterse
|
||||
güvenlik diyaloğu açılır. Onay verilirse hem gridden hem
|
||||
store’dan kaldırılır.
|
||||
=========================================================== */
|
||||
function removeSelected() {
|
||||
if (editingIndex.value === -1) {
|
||||
$q.notify({
|
||||
type: 'info',
|
||||
message: 'Silinecek satır seçili değil ⚠️'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const idx = editingIndex.value
|
||||
const selectedRow = summaryRows.value[idx]
|
||||
if (!selectedRow) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Geçersiz satır, silinemedi ⚠️'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 🔹 Onay penceresi
|
||||
$q.dialog({
|
||||
title: 'Satırı Sil',
|
||||
message: `
|
||||
Bu satırı silmek istediğinizden emin misiniz?<br><br>
|
||||
<b>Model:</b> ${selectedRow.model || '-'}<br>
|
||||
<b>Renk:</b> ${selectedRow.renk || '-'}<br>
|
||||
<b>PB:</b> ${selectedRow.pb || '-'}<br>
|
||||
<b>Tutar:</b> ${Number(selectedRow.tutar || 0).toLocaleString(
|
||||
'tr-TR',
|
||||
{ minimumFractionDigits: 2 }
|
||||
)}`,
|
||||
html: true,
|
||||
ok: { label: 'Evet, Sil', color: 'negative' },
|
||||
cancel: { label: 'Vazgeç', flat: true }
|
||||
}).onOk(() => {
|
||||
// 🔹 Grid'den kaldır
|
||||
summaryRows.value.splice(idx, 1)
|
||||
|
||||
// 🔹 Store'dan kaldır (id varsa id’ye göre, yoksa index’e göre)
|
||||
if (selectedRow.id != null) {
|
||||
orderStore.removeRow(selectedRow.id)
|
||||
} else {
|
||||
orderStore.orders.splice(idx, 1)
|
||||
orderStore.saveToStorage?.()
|
||||
}
|
||||
|
||||
// 🔹 Formu sıfırla
|
||||
editingIndex.value = -1
|
||||
orderStore.selected = null
|
||||
resetForm()
|
||||
|
||||
// 🔹 Kullanıcıya bilgi
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Satır silindi ✅',
|
||||
position: 'top-right'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Grid Şeması (Beden Haritası)
|
||||
Uygulamada hangi ürün grubunda hangi beden seti kullanılacak
|
||||
burada tanımlanır. Key → kod, title → görünür ad, values → bedenler.
|
||||
=========================================================== */
|
||||
const schema = ref([
|
||||
{
|
||||
key: 'ayk',
|
||||
title: 'AYAKKABI',
|
||||
values: ['39','40','41','42','43','44','45']
|
||||
},
|
||||
{
|
||||
key: 'yas',
|
||||
title: 'YAŞ',
|
||||
values: ['2','4','6','8','10','12','14']
|
||||
},
|
||||
{
|
||||
key: 'pan',
|
||||
title: 'PANTOLON',
|
||||
values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68']
|
||||
},
|
||||
{
|
||||
key: 'gom',
|
||||
title: 'GÖMLEK',
|
||||
values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL']
|
||||
},
|
||||
{
|
||||
key: 'tak',
|
||||
title: 'TAKIM ELBİSE',
|
||||
values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74']
|
||||
},
|
||||
{
|
||||
key: 'aksbir',
|
||||
title: 'AKSESUAR',
|
||||
values: [' ', '44', 'STD', '110CM', '115CM', '120CM', '125CM', '130CM', '135CM']
|
||||
}
|
||||
])
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Aktif Grup Anahtarı Tespiti
|
||||
Ürün grubunun adından (ör. “TAKIM”, “PANTOLON”) hareketle
|
||||
uygun schema.key değerini döndürür. Default: 'tak'
|
||||
=========================================================== */
|
||||
function activeGroupKeyForRow(row) {
|
||||
const g = row.urunAnaGrubu?.toUpperCase() || ''
|
||||
if (g.includes('TAKIM')) return 'tak'
|
||||
if (g.includes('PANTOLON')) return 'pan'
|
||||
if (g.includes('GÖMLEK')) return 'gom'
|
||||
if (g.includes('AYAKKABI')) return 'ayk'
|
||||
if (g.includes('YAŞ')) return 'yas'
|
||||
return 'tak' // default fallback
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 schemaByKey (Computed)
|
||||
Reaktif olarak schema listesinden key → object map üretir.
|
||||
Böylece örn. schemaByKey.value['pan'] diyerek doğrudan erişim sağlanır.
|
||||
=========================================================== */
|
||||
const schemaByKey = computed(() => {
|
||||
const m = {}
|
||||
schema.value.forEach(grp => (m[grp.key] = grp))
|
||||
return m
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🔹 highlightPantolon (Computed)
|
||||
Eğer form’un aktif ürünü PANTOLON ise,
|
||||
grid veya formda özel vurgu yapılabilir.
|
||||
=========================================================== */
|
||||
const highlightPantolon = computed(() =>
|
||||
form.urunAnaGrubu?.toUpperCase()?.includes('PANTOLON')
|
||||
)
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Sayfa Kapanırken Verileri Kaydet
|
||||
onBeforeUnmount: sayfa kapatıldığında sipariş verileri
|
||||
LocalStorage’a otomatik yazılır.
|
||||
=========================================================== */
|
||||
onBeforeUnmount(() => {
|
||||
orderStore.saveToStorage()
|
||||
console.log('💾 Sayfa kapatılırken veriler kaydedildi.')
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Tümünü Kaydet (Toplu Gönder) — Store Entegrasyonlu Versiyon
|
||||
=========================================================== */
|
||||
async function submitAll() {
|
||||
try {
|
||||
ensureAuthOrRedirect()
|
||||
|
||||
const headerPayload = {
|
||||
OrderDate: form.olusturmaTarihi,
|
||||
AverageDueDate: form.tahminiTerminTarihi,
|
||||
CurrAccCode: selectedCari.value,
|
||||
DocCurrencyCode: form.pb,
|
||||
Description: siparisGenelAciklama.value
|
||||
}
|
||||
const linesPayload = summaryRows.value
|
||||
|
||||
let res
|
||||
if (mode.value === 'new') {
|
||||
res = await api.post('/api/orders', { header: headerPayload, lines: linesPayload })
|
||||
$q.notify({ type: 'positive', message: 'Yeni sipariş oluşturuldu ✅' })
|
||||
mode.value = 'edit'
|
||||
headerId.value = res?.data?.OrderHeaderID || headerId.value
|
||||
form.OrderHeaderID = headerId.value
|
||||
} else {
|
||||
res = await api.put(`/api/orders/${form.OrderHeaderID}`, { header: headerPayload, lines: linesPayload })
|
||||
$q.notify({ type: 'positive', message: 'Sipariş güncellendi ✅' })
|
||||
}
|
||||
|
||||
saveDraft()
|
||||
} catch (err) {
|
||||
console.error('❌ submitAll hata:', err)
|
||||
$q.notify({ type: 'negative', message: 'Kaydedilemedi ❌' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===========================================================
|
||||
// 🔹 Sipariş Yükleme Fonksiyonu (Güvenli Sürüm)
|
||||
// ===========================================================
|
||||
async function loadOrderById(orderId) {
|
||||
console.log('📦 loadOrderById çağrıldı →', orderId)
|
||||
|
||||
const hasLoading = !!$q.loading
|
||||
const hasNotify = !!$q.notify
|
||||
|
||||
try {
|
||||
// 🔹 Loading başlat
|
||||
if (hasLoading) {
|
||||
$q.loading.show({
|
||||
message: 'Sipariş yükleniyor...',
|
||||
spinnerSize: 50,
|
||||
spinnerColor: 'gold',
|
||||
backgroundColor: 'rgba(255,255,255,0.6)',
|
||||
})
|
||||
} else {
|
||||
console.warn('⚠️ $q.loading tanımlı değil (plugin aktif mi?).')
|
||||
}
|
||||
|
||||
// 🔹 API çağrısı
|
||||
const { data } = await api.get(`/order/get/${orderId}`)
|
||||
console.log('📡 API yanıtı:', data)
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
if (hasNotify) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Sipariş verisi bulunamadı.',
|
||||
position: 'top',
|
||||
})
|
||||
}
|
||||
console.error('❌ Boş yanıt geldi.')
|
||||
return
|
||||
}
|
||||
|
||||
// 🔹 Header → form’a aktar
|
||||
if (data.header) {
|
||||
Object.assign(form, data.header)
|
||||
console.log('✅ Header yüklendi:', form.OrderNumber || '(numara yok)')
|
||||
}
|
||||
|
||||
// 🔹 Detay satırlarını frontend modeline çevir
|
||||
if (Array.isArray(data.lines)) {
|
||||
summaryRows.value = data.lines.map(l => {
|
||||
const grpKey = detectBedenGroup([l.ItemDim1Code], l.ProductGroup, '')
|
||||
return {
|
||||
model: l.ItemCode || '',
|
||||
renk: l.ColorCode || '',
|
||||
renk2: l.ItemDim2Code || '',
|
||||
urunAnaGrubu: l.ProductGroup || l.UrunAnaGrubu || '',
|
||||
urunAltGrubu: l.ProductSubGroup || l.UrunAltGrubu || '',
|
||||
aciklama: l.LineDescription || l.Aciklama || '',
|
||||
fiyat: Number(l.Price || 0),
|
||||
pb: l.PriceCurrencyCode || l.DocCurrencyCode || 'USD',
|
||||
adet: Number(l.Qty1 || 0),
|
||||
tutar: Number((l.Qty1 || 0) * (l.Price || 0)),
|
||||
grpKey,
|
||||
bedenMap: {
|
||||
[grpKey]: { [l.ItemDim1Code]: Number(l.Qty1 || 0) },
|
||||
},
|
||||
terminTarihi: l.DeliveryDate
|
||||
? dayjs(l.DeliveryDate).format('YYYY-MM-DD')
|
||||
: form.tahminiTerminTarihi,
|
||||
}
|
||||
})
|
||||
console.log('📋 Grid satırları hazır:', summaryRows.value.length)
|
||||
} else {
|
||||
summaryRows.value = []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ loadOrderById hata:', err)
|
||||
if (hasNotify) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Sipariş yüklenirken hata oluştu.',
|
||||
position: 'top',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
// 🔹 Loading gizle
|
||||
if (hasLoading) {
|
||||
try {
|
||||
$q.loading.hide()
|
||||
} catch (hideErr) {
|
||||
console.warn('⚠️ Loading kapatılamadı:', hideErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Stok Bilgisi Görüntüleme Fonksiyonları
|
||||
Her beden veya satır için stok bilgisini UI’da göstermek için kullanılır.
|
||||
=========================================================== */
|
||||
function getStockFor(lbl) {
|
||||
if (!lbl || !stockMap.value) return 0
|
||||
const val = stockMap.value[lbl]
|
||||
const num = Number(val)
|
||||
return isNaN(num) ? 0 : num // 🎯 string "433" bile olsa 433 olarak döner
|
||||
}
|
||||
|
||||
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 // 🎯 string → number dönüşümü eklendi
|
||||
}
|
||||
|
||||
// 🔹 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
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Watchers — Model & Renk Değişimlerinde Otomatik Temizlik
|
||||
Kullanıcı model veya renk değiştirdiğinde önceki form verileri
|
||||
sıfırlanır, gereksiz stok veya beden bilgileri temizlenir.
|
||||
=========================================================== */
|
||||
watch(
|
||||
() => form.model,
|
||||
async (newVal, oldVal) => {
|
||||
// MODEL TEMİZLENDİYSE: her şey sıfırlanır
|
||||
if (!newVal) {
|
||||
console.log('🧹 Model kaldırıldı, beden seti sıfırlanıyor...')
|
||||
resetForm()
|
||||
selectedSeriSet.value = null
|
||||
seriMultiplier.value = 1
|
||||
return
|
||||
}
|
||||
|
||||
// MODEL DEĞİŞTİYSE: form yeniden başlatılır ve yeni model yüklenir
|
||||
if (oldVal && newVal !== oldVal) {
|
||||
console.log('🌀 Model değişti, form temizleniyor...')
|
||||
resetForm()
|
||||
await nextTick()
|
||||
form.model = newVal
|
||||
onModelChange(newVal)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => form.renk,
|
||||
(newVal, oldVal) => {
|
||||
// Renk değiştiğinde ikinci renk ve ilgili stok bilgileri temizlenir
|
||||
if (oldVal && newVal && newVal !== oldVal) {
|
||||
console.log('🎨 Renk değişti, alt veriler sıfırlanıyor...')
|
||||
form.renk2 = ''
|
||||
renkOptions2.value = []
|
||||
if (renk2Select.value) renk2Select.value.reset()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Beden + Stok Etiketli Görünüm (Frontend Haritalama)
|
||||
Grid veya tablo üzerinde bedenlerin yanında stok göstermek için
|
||||
kullanılacak computed alan.
|
||||
=========================================================== */
|
||||
const bedenWithStock = computed(() => {
|
||||
if (!form.bedenLabels.length) return []
|
||||
return form.bedenLabels.map(lbl => ({
|
||||
label: lbl,
|
||||
stok: stockMap.value[lbl] ?? null,
|
||||
value: form.bedenler[form.bedenLabels.indexOf(lbl)]
|
||||
}))
|
||||
})
|
||||
|
||||
/* ===========================================================
|
||||
🔹 PB (Para Birimi) değişiminde min fiyat yenileme
|
||||
Eğer kullanıcı para birimini değiştirirse, backend’den yeniden
|
||||
min fiyat sorgusu yapılır (örneğin USD → EUR dönüşüm).
|
||||
=========================================================== */
|
||||
watch(
|
||||
() => form.pb,
|
||||
async (newVal, oldVal) => {
|
||||
if (newVal && newVal !== oldVal) {
|
||||
console.log('💱 PB değişti:', newVal)
|
||||
await fetchMinPrice()
|
||||
}
|
||||
}
|
||||
)
|
||||
/* ===========================================================
|
||||
🔹 Aktif Beden Alanı ve Renkli Stok Etiketleri
|
||||
Stok miktarına göre hücre arka planı veya yazı rengini belirler.
|
||||
Kullanıcı stok durumunu görsel olarak hemen fark eder.
|
||||
=========================================================== */
|
||||
const activeBeden = ref(null)
|
||||
|
||||
function stockColorClass(qty) {
|
||||
const n = Number(qty)
|
||||
if (isNaN(n)) return ''
|
||||
if (n === 0) return 'stok-red' // 🔴 Stok yok
|
||||
if (n > 0 && n <= 2) return 'stok-yellow' // 🟡 Kritik stok (1–2)
|
||||
return 'stok-green' // 🟢 Yeterli stok (3+)
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Yardımcı Fonksiyonlar — Auth ve Token Kontrolü
|
||||
Kullanıcının token’ı yoksa login sayfasına yönlendirir.
|
||||
Tüm backend çağrıları öncesi güvenlik katmanı sağlar.
|
||||
=========================================================== */
|
||||
function getToken() {
|
||||
return localStorage.getItem('token')
|
||||
}
|
||||
|
||||
function ensureAuthOrRedirect() {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
if (typeof window !== 'undefined') window.location.href = '/login'
|
||||
throw new Error('🚫 Yetkilendirme gerekli.')
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 Son Bilgilendirme ve Debug Log
|
||||
Geliştirme aşamasında konsolda bileşenin başarıyla
|
||||
yüklendiği bilgisini verir.
|
||||
=========================================================== */
|
||||
console.log('🧩 OrderEntry (v22-final) bileşeni başarıyla yüklendi.')
|
||||
</script>
|
||||
1128
ui/src/stores/deneme2
Normal file
1128
ui/src/stores/deneme2
Normal file
@@ -0,0 +1,1128 @@
|
||||
/* ===========================================================
|
||||
GLOBAL CUSTOM CSS
|
||||
=========================================================== */
|
||||
.with-bg {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
.with-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: url('/images/Baggi-tekstilas-logolu.jpg') no-repeat center top;
|
||||
background-size: 400px auto;
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.with-bg > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.q-page {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.with-bg::before {
|
||||
background-size: 260px auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ÜST BLOKLAR (SABİT) ===== */
|
||||
.filter-sticky {
|
||||
position: sticky;
|
||||
top: 56px; /* q-header yüksekliği */
|
||||
z-index: 300;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.filter-collapsible {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ===== TABLO SCROLL ===== */
|
||||
.table-scroll {
|
||||
margin-top: 0; /* 🔹 Boşluğu kaldır */
|
||||
height: calc(100vh - 56px); /* 🔹 Header yüksekliği kadar kısalt */
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sticky-table .q-table__middle {
|
||||
overflow: visible !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.sticky-table .q-table__top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 220;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.sticky-table thead th {
|
||||
position: sticky;
|
||||
top: 40px;
|
||||
z-index: 210;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 🔹 Toggle bar */
|
||||
.sticky-bar {
|
||||
position: sticky;
|
||||
top: 0; /* tablo scroll başladığında en üstte kalsın */
|
||||
z-index: 230;
|
||||
background: #fff;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* ===== KOLON DARALTMA + WRAP ===== */
|
||||
.sticky-table thead th {
|
||||
resize: horizontal;
|
||||
overflow: auto;
|
||||
min-width: 80px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.sticky-table td {
|
||||
min-width: 80px;
|
||||
max-width: 400px;
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
line-height: 1.2rem;
|
||||
padding: 4px 8px !important;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ===== GÖRSEL ===== */
|
||||
.baggi-ppct {
|
||||
display: block;
|
||||
margin: 30px auto 0;
|
||||
max-width: 400px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.col-desc {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 0.75rem !important;
|
||||
line-height: 1.1rem;
|
||||
width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
min-width: 180px !important;
|
||||
}
|
||||
|
||||
/* ===== TABLO GÖRÜNÜM ===== */
|
||||
.custom-table { font-size: 0.8rem; }
|
||||
.custom-table th { background: #fff; font-weight: 800; color: #222; }
|
||||
.custom-table td { font-weight: 600; color: #333; }
|
||||
|
||||
.custom-subtable { font-size: 0.72rem; background: #fafafa; }
|
||||
.custom-subtable th { background: #f9f9f9; font-weight: 500; color: #555; }
|
||||
.custom-subtable td { font-weight: 400; color: #666; }
|
||||
|
||||
/* dar sütunlar için */
|
||||
.col-narrow {
|
||||
font-size: 0.72rem;
|
||||
padding: 2px 6px !important;
|
||||
max-width: 90px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ===== GRUP SATIRI ===== */
|
||||
.group-row {
|
||||
background: #f1f1f1 !important;
|
||||
font-weight: 700 !important;
|
||||
color: #222;
|
||||
border-top: 2px solid #ccc;
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
||||
/* ===== BALANCE CARD ===== */
|
||||
.balance-card {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.q-table td[data-col="belge_no"],
|
||||
.q-table td[data-col="Belge_No"],
|
||||
.q-table td[data-col="BELGE_NO"] {
|
||||
color: var(--q-primary) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
PERMISSIONS PAGE (FINAL)
|
||||
=========================================================== */
|
||||
|
||||
/* Toolbar */
|
||||
.permissions-toolbar {
|
||||
position: sticky;
|
||||
top: 42px; /* q-header yüksekliği */
|
||||
z-index: 300;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Table scroll alanı */
|
||||
.permissions-table-scroll {
|
||||
height: calc(100vh - 112px); /* header (56) + toolbar (56) */
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Tablo gövdesi */
|
||||
.permissions-table .q-table__middle {
|
||||
overflow: auto !important;
|
||||
max-height: none !important;
|
||||
padding-top: 0px; /* 🔑 Başlık yüksekliği kadar boşluk bırak */
|
||||
}
|
||||
|
||||
/* Sticky başlıklar – toolbar’ın altında */
|
||||
.permissions-table thead th {
|
||||
position: sticky;
|
||||
top:10px; /* toolbar altında hizalanır */
|
||||
z-index: 210;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Hücreler */
|
||||
.permissions-table td {
|
||||
min-width: 80px;
|
||||
max-width: 400px;
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
line-height: 1.2rem;
|
||||
padding: 4px 8px !important;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* İlk kolon (role) sabit */
|
||||
.permissions-table .permissions-sticky-col {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 205;
|
||||
background: #fff;
|
||||
box-shadow: 2px 0 4px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
1️⃣ ROOT & GLOBAL RESET
|
||||
=========================================================== */
|
||||
:root {
|
||||
--header-h: 0px;
|
||||
--filter-h: 72px;
|
||||
--save-h: 60px;
|
||||
--grid-header-h: 172px;
|
||||
--sub-header-h: 34px;
|
||||
--drawer-w: 240px;
|
||||
|
||||
/* Grid kolon genişlikleri */
|
||||
--col-model: 90px;
|
||||
--col-renk: 80px;
|
||||
--col-ana: 100px;
|
||||
--col-alt: 100px;
|
||||
--col-aciklama: 140px;
|
||||
--col-adet: 70px;
|
||||
--col-fiyat: 70px;
|
||||
--col-pb: 70px;
|
||||
--col-tutar: 70px;
|
||||
--col-termin: 142px; /* 🔹 termin tarihi kolon genişliği */
|
||||
|
||||
/* Beden blok ölçüleri */
|
||||
--grp-title-w: 90px;
|
||||
--grp-title-gap: 4px;
|
||||
--beden-w: 44px;
|
||||
--beden-h: 28px;
|
||||
--beden-count: 16;
|
||||
|
||||
/* Tema renkleri */
|
||||
--baggi-gold: #c9a227;
|
||||
--baggi-gold-pale: #fff9e6;
|
||||
--baggi-gold-light: #fff7d2;
|
||||
--baggi-cream: #fffef9;
|
||||
--baggi-gray-border: #bbb;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
background: #fff;
|
||||
color: #222;
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
#q-app, .q-page-container { margin: 0; padding: 0; }
|
||||
.q-layout__page { top: 0 !important; }
|
||||
/* ===========================================================
|
||||
2️⃣ PAGE STRUCTURE & SCROLL
|
||||
=========================================================== */
|
||||
.order-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--header-h));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.body--drawer-left-open .q-page-container {
|
||||
margin-left: var(--drawer-w);
|
||||
width: calc(100% - var(--drawer-w));
|
||||
}
|
||||
.body--drawer-left-closed .q-page-container {
|
||||
margin-left: 0; width: 100%;
|
||||
}
|
||||
|
||||
/* 🔸 Yatay scroll sadece grid alanında */
|
||||
.order-scroll-x {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 🔸 Scrollbar stili */
|
||||
.order-page::-webkit-scrollbar,
|
||||
.order-scroll-x::-webkit-scrollbar {
|
||||
height: 8px; width: 8px;
|
||||
}
|
||||
.order-page::-webkit-scrollbar-thumb,
|
||||
.order-scroll-x::-webkit-scrollbar-thumb {
|
||||
background: #c0a75e;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.order-page::-webkit-scrollbar-track,
|
||||
.order-scroll-x::-webkit-scrollbar-track {
|
||||
background: #f9f5e6;
|
||||
}
|
||||
/* ===========================================================
|
||||
3️⃣ STICKY STACK (HEADER + TOOLBARS)
|
||||
=========================================================== */
|
||||
.q-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.sticky-stack {
|
||||
position: sticky;
|
||||
top: var(--header-h);
|
||||
margin-top: 0 !important;
|
||||
z-index: 950;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* 🔹 Filtre bar */
|
||||
.filter-bar {
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 12px 24px;
|
||||
margin-top:0 !important;
|
||||
}
|
||||
|
||||
/* 🔹 Save toolbar */
|
||||
.save-toolbar {
|
||||
background: var(--baggi-gold-pale);
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 940;
|
||||
}
|
||||
.save-toolbar .label { font-weight: 700; color: #6a5314; }
|
||||
.save-toolbar .value { font-weight: 700; color: #000; }
|
||||
.save-toolbar .q-btn {
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
text-transform: none;
|
||||
}
|
||||
/* ===========================================================
|
||||
4️⃣ GRID HEADER (ANA BAŞLIK BLOKU)
|
||||
=========================================================== */
|
||||
.order-grid-header {
|
||||
position: sticky;
|
||||
top: calc(var(--header-h) + var(--filter-h) + var(--save-h));
|
||||
z-index: 700;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns:
|
||||
var(--col-model)
|
||||
var(--col-renk)
|
||||
var(--col-ana)
|
||||
var(--col-alt)
|
||||
var(--col-aciklama)
|
||||
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count)))
|
||||
var(--col-adet)
|
||||
var(--col-fiyat)
|
||||
var(--col-pb)
|
||||
var(--col-tutar)
|
||||
var(--col-termin);
|
||||
background: var(--baggi-cream);
|
||||
border-bottom: 2px solid var(--baggi-gray-border);
|
||||
box-shadow: 0 2px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* Sabit kolonlar */
|
||||
.order-grid-header .col-fixed {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
writing-mode: vertical-lr;
|
||||
transform: rotate(180deg);
|
||||
background: var(--baggi-gold-light);
|
||||
border: 1px solid #aaa;
|
||||
font-weight: 700;
|
||||
font-size: 12.5px;
|
||||
height: var(--grid-header-h);
|
||||
}
|
||||
|
||||
.order-grid-header .aciklama-col {
|
||||
background: #fff9c4;
|
||||
border-right: 2px solid #a6a6a6;
|
||||
}
|
||||
/* ===========================================================
|
||||
5️⃣ BEDEN BLOKLARI & SAĞ TOPLAM
|
||||
=========================================================== */
|
||||
.order-grid-header .beden-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: var(--grid-header-h);
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.order-grid-header .grp-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--beden-h);
|
||||
}
|
||||
|
||||
.order-grid-header .grp-title {
|
||||
width: var(--grp-title-w);
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.order-grid-header .grp-body {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: var(--beden-w);
|
||||
}
|
||||
.order-grid-header .grp-cell.hdr {
|
||||
width: var(--beden-w);
|
||||
height: var(--beden-h);
|
||||
border: 1px solid #bbb;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.order-grid-header .total-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
background: #fff59d;
|
||||
}
|
||||
.order-grid-header .total-cell {
|
||||
width: var(--col-adet);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
writing-mode: vertical-lr;
|
||||
transform: rotate(180deg);
|
||||
border-right: 1px solid #bbb;
|
||||
background: var(--baggi-gold-pale);
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* ===========================================================
|
||||
6️⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ
|
||||
=========================================================== */
|
||||
.order-sub-header {
|
||||
padding-right: 0 !important; /* 🔹 Ekstra sağ boşluğu kaldır */
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.order-sub-header {
|
||||
position: sticky;
|
||||
top: calc(
|
||||
var(--header-h)
|
||||
+ var(--filter-h)
|
||||
+ var(--save-h)
|
||||
+ var(--grid-header-h)
|
||||
);
|
||||
z-index: 650;
|
||||
|
||||
/* 🔹 Header ile birebir grid düzeni */
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns:
|
||||
var(--col-model)
|
||||
var(--col-renk)
|
||||
var(--col-ana)
|
||||
var(--col-alt)
|
||||
var(--col-aciklama)
|
||||
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
|
||||
var(--col-adet)
|
||||
var(--col-fiyat)
|
||||
var(--col-pb)
|
||||
var(--col-tutar)
|
||||
var(--col-termin);
|
||||
|
||||
align-items: center;
|
||||
justify-items: stretch;
|
||||
height: var(--sub-header-h);
|
||||
min-height: var(--sub-header-h);
|
||||
|
||||
/* 🔹 Görsel */
|
||||
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
|
||||
border-top: 1px solid #d6c06a;
|
||||
border-bottom: 1px solid #d6c06a;
|
||||
|
||||
/* 🔹 Hatalı hizalamaları engelle */
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding-right: 0 !important; /* ✅ sağ taşmayı önler */
|
||||
}
|
||||
|
||||
/* 🔹 Genişlik eşitleme */
|
||||
:root {
|
||||
--col-termin: 142px; /* ✅ q-input genişliğiyle birebir */
|
||||
}
|
||||
|
||||
/* 🔹 Sub-header hover efekti */
|
||||
.order-sub-header:hover {
|
||||
background: linear-gradient(90deg, #fff9cf 0%, #fff3b0 70%, #ffe88f 100%);
|
||||
}
|
||||
|
||||
|
||||
/* 🔹 Sol taraf (MODEL–AÇIKLAMA alanı) */
|
||||
.order-sub-header .sub-left {
|
||||
grid-column: 1 / span 5;
|
||||
font-weight: 800;
|
||||
padding-left: 6px;
|
||||
color: #2b1f05;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 🔹 Orta beden bloğu (header’la aynı yapı) */
|
||||
.order-sub-header .sub-center {
|
||||
grid-column: 6 / 7;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: var(--beden-w);
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
width: calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)));
|
||||
padding-left: var(--grp-title-w);
|
||||
margin-left: var(--grp-title-gap);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.order-sub-header .beden-cell {
|
||||
width: var(--beden-w);
|
||||
height: 100%;
|
||||
border: 1px solid #d8c16b;
|
||||
border-right: none;
|
||||
background: #fffdf3;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.order-sub-header .beden-cell:last-child {
|
||||
border-right: 1px solid #d8c16b;
|
||||
}
|
||||
|
||||
/* 🔹 Sağ taraf (adet–fiyat–pb–tutar–termin toplamları) */
|
||||
.order-sub-header .sub-right {
|
||||
grid-column: 7 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
padding-right: 6px;
|
||||
font-weight: 900;
|
||||
color: #3b2f09;
|
||||
line-height: 1.3;
|
||||
text-transform: uppercase;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
.order-sub-header:hover {
|
||||
background: linear-gradient(90deg,#fff9cf 0%,#fff3b0 70%,#ffe88f 100%);
|
||||
}
|
||||
|
||||
:root {
|
||||
--sub-header-h: 60px;
|
||||
}
|
||||
/* SUB-HEADER sağ yazıyı 3 beden kolonu sola kaydır */
|
||||
.order-sub-header .sub-right {
|
||||
transform: translateX(calc(-1 * var(--beden-w) * 4));
|
||||
}
|
||||
|
||||
/* Taşmayı engelle (ihtiyaten) */
|
||||
.order-sub-header {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
7️⃣ GRID BODY & SATIRLAR — TAM HİZALANMIŞ
|
||||
=========================================================== */
|
||||
.order-grid-body {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--col-model)
|
||||
var(--col-renk)
|
||||
var(--col-ana)
|
||||
var(--col-alt)
|
||||
var(--col-aciklama)
|
||||
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
|
||||
var(--col-adet)
|
||||
var(--col-fiyat)
|
||||
var(--col-pb)
|
||||
var(--col-tutar)
|
||||
var(--col-termin);}
|
||||
|
||||
.summary-row:hover {
|
||||
background: #fffce0;
|
||||
}
|
||||
.summary-row.is-editing {
|
||||
background: #fff3cd;
|
||||
outline: 2px solid #caa83f;
|
||||
}
|
||||
|
||||
.summary-row .cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--beden-h);
|
||||
padding: 4px 6px;
|
||||
font-size: 13px;
|
||||
color: #222;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.summary-row:nth-child(odd) { background: #fffef9; }
|
||||
|
||||
/* 🔹 Beden blok hizalaması */
|
||||
.summary-row .grp-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
transform: translateX(calc(var(--grp-title-w) - var(--beden-w)));
|
||||
}
|
||||
.summary-row .grp-row {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: var(--beden-w);
|
||||
}
|
||||
.summary-row .grp-row .cell.beden {
|
||||
width: var(--beden-w);
|
||||
height: var(--beden-h);
|
||||
border: 1px solid #ddd;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cell.beden.ghost {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
/* 🔹 Sağ kolonlar */
|
||||
.summary-row .cell.adet,
|
||||
.summary-row .cell.fiyat,
|
||||
.summary-row .cell.pb,
|
||||
.summary-row .cell.tutar,
|
||||
.summary-row .cell.termin {
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
border-left: none !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.summary-row .cell.tutar {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.summary-row .cell.termin {
|
||||
background: #fffef9;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: var(--col-termin);
|
||||
}
|
||||
.summary-row .cell.termin .q-input {
|
||||
width: 100%;
|
||||
max-width: 142px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.summary-row .cell.termin input {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
9️⃣ ORDER EDITOR (ALT FORM)
|
||||
=========================================================== */
|
||||
.editor {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
background: #fffef9;
|
||||
border-top: 1px solid #ddd;
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
}
|
||||
.editor::before {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 4px;
|
||||
background: linear-gradient(to right,#c9a227,#e5d28b,#fff7d2);
|
||||
margin-bottom: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.editor .q-btn:hover { background: #d2b04d; }
|
||||
.editor .q-input,
|
||||
.editor .q-select { margin-bottom: 8px; font-size: 14px; }
|
||||
.cell.termin .termin-label {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #222;
|
||||
background: #fffef9;
|
||||
border-left: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔟 RESPONSIVE + MİNÖR DÜZEN
|
||||
=========================================================== */
|
||||
@media (max-width: 1024px) {
|
||||
:root { --beden-w: 40px; --col-aciklama: 120px; }
|
||||
.order-grid-header .col-fixed { font-size: 11px; }
|
||||
.order-sub-header { font-size: 12.5px; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--beden-w: 36px;
|
||||
--col-model: 70px;
|
||||
--col-renk: 60px;
|
||||
--col-aciklama: 100px;
|
||||
}
|
||||
.order-page { font-size: 13px; }
|
||||
.order-grid-header .total-cell { font-size: 10.5px; }
|
||||
}
|
||||
|
||||
.summary-row .cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.summary-row .grp-area,
|
||||
.summary-row .grp-row,
|
||||
.summary-row .grp-row .cell.beden {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.summary-row .cell.aciklama {
|
||||
grid-column: 5 / 6 !important; /* sadece 5. kolon */
|
||||
position: relative !important;
|
||||
width: calc(var(--col-aciklama) + 92px) !important; /* 🔹 74px genişletme */
|
||||
margin-right: -92px !important; /* 🔹 bedenle tam hizalanır */
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 6px 12px !important;
|
||||
font-size: 13px !important;
|
||||
text-align: left !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
justify-content: flex-start !important;
|
||||
min-height: 36px !important;
|
||||
background: #fff !important;
|
||||
box-sizing: border-box !important;
|
||||
border-right: 1px solid #ccc !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
/* 🧩 Grid çizgi kontrastı güçlendirme */
|
||||
.summary-row .cell,
|
||||
.order-grid-header .col-fixed,
|
||||
.summary-row .grp-row .cell.beden {
|
||||
border-color: #bbb !important; /* 🔹 daha belirgin çizgi */
|
||||
}
|
||||
|
||||
.summary-row .cell:not(:last-child) {
|
||||
border-right: 1px solid #bdbdbd !important;
|
||||
}
|
||||
/* ===========================================================
|
||||
🧱 ALT GRID ÇİZGİLERİ – TÜM SATIRLAR İÇİN
|
||||
=========================================================== */
|
||||
.summary-row {
|
||||
border-bottom: 1px solid #ccc; /* 🔹 satır alt çizgisi */
|
||||
}
|
||||
|
||||
.summary-row:last-child {
|
||||
border-bottom: 2px solid #b7a33a; /* 🔹 son satırda Baggi gold tonu */
|
||||
}
|
||||
|
||||
/* 🔹 Hücrelerin alt çizgisi (beden dahil) */
|
||||
.summary-row .cell,
|
||||
.summary-row .grp-row .cell.beden {
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */
|
||||
.summary-row:hover .cell,
|
||||
.summary-row:hover .grp-row .cell.beden {
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
}
|
||||
.summary-row:hover {
|
||||
background: #fffce0;
|
||||
}
|
||||
|
||||
.summary-row.is-editing {
|
||||
background: #fff3cd;
|
||||
outline: 2px solid #caa83f;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.editor .q-btn:hover {
|
||||
background: #d2b04d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */
|
||||
.summary-row:hover .cell,
|
||||
.summary-row:hover .grp-row .cell.beden {
|
||||
border-bottom: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🎨 STOK RENKLERİ (LOW–MID–HIGH)
|
||||
=========================================================== */
|
||||
.stok-red {
|
||||
color: #e53935; /* 🔴 Kırmızı */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stok-yellow {
|
||||
color: #f9a825; /* 🟡 Sarı */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stok-green {
|
||||
color: #43a047; /* 🟢 Yeşil */
|
||||
font-weight: 600;
|
||||
}
|
||||
.q-banner.rounded-borders {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.order-gateway {
|
||||
background: linear-gradient(145deg, #fff 0%, #fafafa 100%);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.order-btn {
|
||||
font-size: 1.2rem;
|
||||
padding: 20px 40px;
|
||||
border-radius: 12px;
|
||||
min-width: 280px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.order-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
/* ===========================================================
|
||||
🧭 DRAWER AÇIKKEN GRID HİZALAMA FIX
|
||||
=========================================================== */
|
||||
|
||||
/* Drawer açıkken içerik kaymaması */
|
||||
.body--drawer-left-open .order-page {
|
||||
width: calc(100vw - var(--drawer-w)); /* viewport'tan drawer genişliği kadar düş */
|
||||
overflow-x: hidden; /* dış overflow’u kes */
|
||||
}
|
||||
|
||||
/* Scroll konteyner sadece grid içinde çalışsın */
|
||||
.order-scroll-x {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Scrollbar ve sağ boşluğu dengeler */
|
||||
.order-grid-header,
|
||||
.order-sub-header,
|
||||
.order-grid-body {
|
||||
min-width: fit-content;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* ===========================================================
|
||||
🧱 DRAWER AÇIKKEN TAM HİZALAMA FIX (v2)
|
||||
=========================================================== */
|
||||
|
||||
/* Drawer açıkken tüm üst bloklar sağdan taşmasın */
|
||||
.body--drawer-left-open .filter-bar,
|
||||
.body--drawer-left-open .save-toolbar,
|
||||
.body--drawer-left-open .order-grid-header,
|
||||
.body--drawer-left-open .order-sub-header,
|
||||
.body--drawer-left-open .order-grid-body {
|
||||
width: calc(100vw - var(--drawer-w)); /* drawer genişliği kadar daralt */
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Drawer kapalıyken tam genişlik */
|
||||
.body--drawer-left-closed .filter-bar,
|
||||
.body--drawer-left-closed .save-toolbar,
|
||||
.body--drawer-left-closed .order-grid-header,
|
||||
.body--drawer-left-closed .order-sub-header,
|
||||
.body--drawer-left-closed .order-grid-body {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Order grid sağ sınırı altın kenarlıkla bitir (optik kapanış) */
|
||||
.order-grid-header,
|
||||
.order-sub-header,
|
||||
.order-grid-body {
|
||||
border-right: 2px solid var(--baggi-gold);
|
||||
}
|
||||
/* ===========================================================
|
||||
🎯 SAĞ ALT BOŞLUK FİNAL FIX
|
||||
=========================================================== */
|
||||
|
||||
/* Drawer açıkken tüm grid konteynerleri sağdan tam sıfırla */
|
||||
.body--drawer-left-open .order-page,
|
||||
.body--drawer-left-open .filter-bar,
|
||||
.body--drawer-left-open .save-toolbar,
|
||||
.body--drawer-left-open .order-grid-header,
|
||||
.body--drawer-left-open .order-sub-header,
|
||||
.body--drawer-left-open .order-grid-body {
|
||||
width: calc(100vw - var(--drawer-w) - 8px); /* 🔹 scrollbar toleransı */
|
||||
padding-right: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* Son altın kenarlık hizasını koru */
|
||||
.order-grid-body {
|
||||
border-right: 2px solid var(--baggi-gold);
|
||||
}
|
||||
/* ===========================================================
|
||||
🎯 GRID SAĞ HİZALAMA (FILTER + SAVE + HEADER)
|
||||
=========================================================== */
|
||||
|
||||
/* Ana scroll container referansı */
|
||||
.order-scroll-x {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; /* hizalama sola */
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Filter ve Save barlar grid genişliğini takip etsin */
|
||||
.filter-bar,
|
||||
.save-toolbar,
|
||||
.order-grid-header,
|
||||
.order-sub-header {
|
||||
width: fit-content; /* içeriğe göre genişlik */
|
||||
min-width: 100%; /* minimum ekran kadar */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Grid body’nin genişliği kadar sağ hizalama */
|
||||
.order-grid-body {
|
||||
width: fit-content;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Sağ kenarda taşma veya padding olmasın */
|
||||
.filter-bar,
|
||||
.save-toolbar,
|
||||
.order-grid-header,
|
||||
.order-sub-header,
|
||||
.order-grid-body {
|
||||
margin-right: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
border-right: none !important; /* altın çizgi istemiyorsan kaldırılır */
|
||||
}
|
||||
|
||||
/* Drawer açık/kapalı fark etmeden */
|
||||
.body--drawer-left-open .order-scroll-x,
|
||||
.body--drawer-left-closed .order-scroll-x {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
ORDER LIST (ol-) — Sticky Stack
|
||||
=============================== */
|
||||
|
||||
:root {
|
||||
/* Quasar header yüksekliği */
|
||||
--ol-header-h: 56px;
|
||||
/* Filter bar yüksekliği (px) — inputlar tek satırsa 56 idealdir */
|
||||
--ol-filter-h: 96px;
|
||||
}
|
||||
|
||||
/* q-page tek scroller: header altından başlar */
|
||||
.ol-page {
|
||||
height: calc(100vh - var(--ol-header-h));
|
||||
overflow: auto; /* 🔑 tek scroll container */
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Filter bar: q-header’ın altında sticky */
|
||||
.ol-filter-bar {
|
||||
position: sticky;
|
||||
top: 0; /* 🔑 .ol-page scroller’ında en üst */
|
||||
z-index: 600;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 16px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||
min-height: var(--ol-filter-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* QTable: sticky thead, zebra aktif ve çakışma yok */
|
||||
.ol-table .q-table__middle {
|
||||
overflow: visible !important; /* sticky thead için güvenli */
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
/* thead sabitleme: filter bar’ın ALTINA oturur */
|
||||
.ol-table thead th {
|
||||
position: sticky;
|
||||
top: var(--ol-filter-h); /* 🔑 filter yüksekliği kadar boşluk */
|
||||
z-index: 500;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Zebra */
|
||||
.ol-table .q-table__body .q-tr:nth-child(odd) {
|
||||
background-color: #f7f7f7 !important;
|
||||
}
|
||||
.ol-table .q-table__body .q-tr:nth-child(even) {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.ol-table .q-table__body .q-tr:hover {
|
||||
background-color: #fff7d1 !important;
|
||||
transition: background-color .15s ease;
|
||||
}
|
||||
|
||||
/* Hücreler */
|
||||
.ol-table .q-td {
|
||||
font-size: .9rem;
|
||||
line-height: 1.3;
|
||||
padding: 6px 8px !important;
|
||||
}
|
||||
|
||||
/* Güvenli z-index hiyerarşisi */
|
||||
.q-header { z-index: 1000 !important; } /* header en üstte */
|
||||
.q-drawer { z-index: 950 !important; } /* drawer header’ın altında */
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
:root { --ol-filter-h: 64px; } /* input kırılıyorsa biraz artır */
|
||||
.ol-filter-bar { padding: 8px 12px; }
|
||||
}
|
||||
/* ===========================================================
|
||||
🟡 ORDERLIST ZEBRA FIX (v3)
|
||||
=========================================================== */
|
||||
|
||||
/* Her iki tr katmanını da hedefliyoruz (Quasar q-tr + native tr) */
|
||||
.ol-table tbody tr:nth-child(odd),
|
||||
.ol-table .q-table__body .q-tr:nth-child(odd) {
|
||||
background-color: #faf8ef !important; /* açık krem tonu */
|
||||
}
|
||||
|
||||
.ol-table tbody tr:nth-child(even),
|
||||
.ol-table .q-table__body .q-tr:nth-child(even) {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Hover tonu: hafif Baggi gold dokunuşu */
|
||||
.ol-table tbody tr:hover,
|
||||
.ol-table .q-table__body .q-tr:hover {
|
||||
background-color: #fff4cc !important;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
653
ui/src/stores/deneme3
Normal file
653
ui/src/stores/deneme3
Normal file
@@ -0,0 +1,653 @@
|
||||
// src/stores/orderentryStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
import { useAuthStore } from 'src/stores/authStore'
|
||||
import dayjs from 'src/boot/dayjs'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
/* ==========================================================
|
||||
Reaktif shared referanslar (bazı UI yardımcıları)
|
||||
========================================================== */
|
||||
const stockMap = ref({}) // { "48": 12, "50": 7, ... }
|
||||
const bedenStock = ref([]) // [{beden:'48', stok:12}, ...]
|
||||
const sizeCache = ref({}) // beden/stok cache (component tarafı çağırıyor)
|
||||
|
||||
/* ==========================================================
|
||||
STORE
|
||||
========================================================== */
|
||||
export const useOrderentryStore = defineStore('orderentry', {
|
||||
state: () => ({
|
||||
/* 🔹 Ana durumlar */
|
||||
orders: [], // grid kaynak array’i (summaryRows ile senkron)
|
||||
loading: false,
|
||||
selected: null, // UI’de seçili satır
|
||||
error: null,
|
||||
|
||||
/* 🔹 Cari */
|
||||
customers: [],
|
||||
selectedCustomer: null,
|
||||
|
||||
/* 🔹 Ürün zinciri */
|
||||
products: [],
|
||||
colors: [],
|
||||
secondColors: [],
|
||||
inventory: [],
|
||||
|
||||
selectedProduct: null,
|
||||
selectedColor: null,
|
||||
selectedColor2: null,
|
||||
|
||||
/* 🔹 Transaction & Storage */
|
||||
activeTransactionId: null,
|
||||
persistKey: 'bss_orderentry_data', // ♻️ kalıcı depolama key’i
|
||||
lastSnapshotKey: 'bss_orderentry_snapshot', // son-kaydedilen-sipariş
|
||||
|
||||
/* 🔹 Düzenleme durumu */
|
||||
editingIndex: -1,
|
||||
currentOrderId: null, // edit modunda header ID
|
||||
header: {}, // backend header modeli
|
||||
mode: 'new' // 'new' | 'edit'
|
||||
}),
|
||||
|
||||
getters: {
|
||||
/* 🔹 Toplam adet */
|
||||
totalQty(state) {
|
||||
return state.orders.reduce((sum, r) => sum + (Number(r.adet) || 0), 0)
|
||||
},
|
||||
/* 🔹 Toplam tutar (string fix2) */
|
||||
totalAmount(state) {
|
||||
const n = state.orders.reduce((s, r) => s + (Number(r.tutar) || 0), 0)
|
||||
return isNaN(n) ? '0.00' : n.toFixed(2)
|
||||
},
|
||||
/* 🔹 Müşteri bazlı gruplanmış (opsiyonel) */
|
||||
groupedByCustomer(state) {
|
||||
const out = {}
|
||||
for (const row of state.orders) {
|
||||
const k = row.musteri || '—'
|
||||
if (!out[k]) out[k] = []
|
||||
out[k].push(row)
|
||||
}
|
||||
return out
|
||||
},
|
||||
/* 🔹 2. renk var mı? */
|
||||
hasSecondColor(state) {
|
||||
return Array.isArray(state.secondColors) && state.secondColors.length > 0
|
||||
},
|
||||
/* 🔹 Envanter toplamı */
|
||||
totalInventoryQty(state) {
|
||||
return state.inventory.reduce((s, r) => s + (Number(r.kullanilabilir) || 0), 0)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
/* ==========================================================
|
||||
STORAGE — Kalıcı kayıt yardımcıları
|
||||
========================================================== */
|
||||
saveToStorage() {
|
||||
try {
|
||||
const payload = {
|
||||
orders: this.orders,
|
||||
header: this.header,
|
||||
currentOrderId: this.currentOrderId,
|
||||
selectedCustomer: this.selectedCustomer,
|
||||
activeTransactionId: this.activeTransactionId,
|
||||
mode: this.mode,
|
||||
savedAt: dayjs().toISOString()
|
||||
}
|
||||
localStorage.setItem(this.persistKey, JSON.stringify(payload))
|
||||
} catch (err) {
|
||||
console.warn('⚠️ localStorage kaydı başarısız:', err)
|
||||
}
|
||||
},
|
||||
|
||||
/* Kayıt sonrası görüntülenecek "snapshot".
|
||||
UI’yi temizlesen bile sayfa yenilenince bu snapshot geri yüklenebilir. */
|
||||
saveSnapshot(tag = 'post-submit') {
|
||||
try {
|
||||
const snap = {
|
||||
tag,
|
||||
orders: this.orders,
|
||||
header: this.header,
|
||||
currentOrderId: this.currentOrderId,
|
||||
selectedCustomer: this.selectedCustomer,
|
||||
mode: this.mode,
|
||||
savedAt: dayjs().toISOString()
|
||||
}
|
||||
localStorage.setItem(this.lastSnapshotKey, JSON.stringify(snap))
|
||||
} catch (e) {
|
||||
console.warn('⚠️ saveSnapshot hatası:', e)
|
||||
}
|
||||
},
|
||||
|
||||
loadFromStorage() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.persistKey)
|
||||
if (!raw) return
|
||||
const data = JSON.parse(raw)
|
||||
if (Array.isArray(data.orders)) this.orders = data.orders
|
||||
this.header = data.header || {}
|
||||
this.currentOrderId = data.currentOrderId || null
|
||||
this.selectedCustomer = data.selectedCustomer || null
|
||||
this.activeTransactionId = data.activeTransactionId || null
|
||||
this.mode = data.mode || 'new'
|
||||
console.log(`♻️ Storage yüklendi • mode:${this.mode} • rows:${this.orders.length}`)
|
||||
} catch (err) {
|
||||
console.warn('⚠️ localStorage okuma hatası:', err)
|
||||
}
|
||||
},
|
||||
|
||||
loadSnapshot() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.lastSnapshotKey)
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw)
|
||||
} catch (e) {
|
||||
console.warn('⚠️ loadSnapshot hatası:', e)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
clearStorage() {
|
||||
localStorage.removeItem(this.persistKey)
|
||||
// snapshot’ı silmiyoruz → kullanıcı isterse elle siler
|
||||
},
|
||||
/* ==========================================================
|
||||
TRANSACTION STATE
|
||||
========================================================== */
|
||||
setTransaction(id) {
|
||||
this.activeTransactionId = id
|
||||
this.saveToStorage()
|
||||
},
|
||||
|
||||
async initTransaction() {
|
||||
if (this.activeTransactionId) {
|
||||
console.log('🔹 Aktif transaction:', this.activeTransactionId)
|
||||
return this.activeTransactionId
|
||||
}
|
||||
try {
|
||||
const dummyId = Math.floor(100000 + Math.random() * 900000)
|
||||
this.activeTransactionId = dummyId
|
||||
this.saveToStorage()
|
||||
console.log('🧩 Dummy Transaction başlatıldı:', dummyId)
|
||||
return dummyId
|
||||
} catch (err) {
|
||||
console.error('❌ Dummy transaction başlatılamadı:', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
clearTransaction() {
|
||||
this.activeTransactionId = null
|
||||
this.saveToStorage()
|
||||
},
|
||||
|
||||
/* Orders’ı otomatik kaydeden watcher (component’ten çağrılır) */
|
||||
watchOrders() {
|
||||
watch(
|
||||
() => this.orders,
|
||||
() => {
|
||||
// her değişimde full storage yaz
|
||||
this.saveToStorage()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
},
|
||||
/* ==========================================================
|
||||
CRUD — Frontend grid’i ile senkron temel aksiyonlar
|
||||
========================================================== */
|
||||
|
||||
addRow(row) {
|
||||
if (!row) return
|
||||
this.orders.push({ ...row })
|
||||
this.saveToStorage()
|
||||
},
|
||||
|
||||
updateRow(idOrIndex, patch) {
|
||||
if (idOrIndex == null) return
|
||||
let idx = -1
|
||||
if (typeof idOrIndex === 'number') {
|
||||
idx = idOrIndex
|
||||
} else {
|
||||
// id ile bul
|
||||
idx = this.orders.findIndex(r => r.id === idOrIndex)
|
||||
}
|
||||
if (idx >= 0 && this.orders[idx]) {
|
||||
this.orders[idx] = { ...this.orders[idx], ...patch }
|
||||
this.saveToStorage()
|
||||
}
|
||||
},
|
||||
|
||||
removeRow(idOrIndex) {
|
||||
let idx = -1
|
||||
if (typeof idOrIndex === 'number') {
|
||||
idx = idOrIndex
|
||||
} else {
|
||||
idx = this.orders.findIndex(r => r.id === idOrIndex)
|
||||
}
|
||||
if (idx >= 0) {
|
||||
this.orders.splice(idx, 1)
|
||||
this.saveToStorage()
|
||||
}
|
||||
},
|
||||
/* ==========================================================
|
||||
PRICE / LIMIT — Minimum fiyat sorgusu (model + PB)
|
||||
Beklenen response: { price, priceTRY, rateToTRY }
|
||||
========================================================== */
|
||||
async fetchMinPrice(modelCode, pb) {
|
||||
if (!modelCode || !pb) return null
|
||||
try {
|
||||
const baseURL = 'http://localhost:8080'
|
||||
const res = await axios.get(`${baseURL}/api/min-price`, {
|
||||
params: { code: modelCode, pb }
|
||||
})
|
||||
const d = res?.data || null
|
||||
if (!d) return null
|
||||
|
||||
// normalize
|
||||
return {
|
||||
price: Number(d.price ?? d.Price ?? 0),
|
||||
priceTRY: Number(d.priceTRY ?? d.PriceTRY ?? d.price_try ?? 0),
|
||||
rateToTRY: Number(d.rateToTRY ?? d.RateToTRY ?? d.rate ?? 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ fetchMinPrice hata:', e)
|
||||
return null
|
||||
}
|
||||
},
|
||||
/* ==========================================================
|
||||
LOAD (EDIT MODE) — Sunucudan Siparişi Açma
|
||||
========================================================== */
|
||||
async openById(id) {
|
||||
if (!id) return
|
||||
this.loading = true
|
||||
try {
|
||||
const auth = useAuthStore()
|
||||
const res = await axios.get(`http://localhost:8080/api/order/get/${id}`, {
|
||||
headers: { Authorization: `Bearer ${auth.token}` }
|
||||
})
|
||||
const data = res.data || {}
|
||||
|
||||
// 🔹 sql.Null* flatten helper
|
||||
const flat = (v) => {
|
||||
if (v === null || v === undefined) return null
|
||||
if (typeof v === 'object' && 'Valid' in v) {
|
||||
return v.Valid
|
||||
? v.String ?? v.Float64 ?? v.Int32 ?? v.Time ?? null
|
||||
: null
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
🧾 HEADER MAPPING (73 kolon)
|
||||
============================================================ */
|
||||
const h = data.header || {}
|
||||
const header = {
|
||||
// Görünen alanlar
|
||||
OrderHeaderID: flat(h.OrderHeaderID) || '',
|
||||
OrderNumber: flat(h.OrderNumber) || '',
|
||||
OrderDate: flat(h.OrderDate)
|
||||
? String(flat(h.OrderDate)).substring(0, 10)
|
||||
: '',
|
||||
AverageDueDate: flat(h.AverageDueDate)
|
||||
? String(flat(h.AverageDueDate)).substring(0, 10)
|
||||
: '',
|
||||
Description: flat(h.Description) || '',
|
||||
CurrAccCode: flat(h.CurrAccCode) || '',
|
||||
DocCurrencyCode: flat(h.DocCurrencyCode) || 'TRY',
|
||||
|
||||
// Arka plan alanlar (backend roundtrip)
|
||||
OrderTypeCode: flat(h.OrderTypeCode) || 1,
|
||||
ProcessCode: flat(h.ProcessCode) || 'WS',
|
||||
IsCancelOrder: flat(h.IsCancelOrder) || 0,
|
||||
OrderTime: flat(h.OrderTime) || '',
|
||||
DocumentNumber: flat(h.DocumentNumber) || '',
|
||||
PaymentTerm: flat(h.PaymentTerm) || '',
|
||||
InternalDescription: flat(h.InternalDescription) || '',
|
||||
CurrAccTypeCode: flat(h.CurrAccTypeCode) || '',
|
||||
SubCurrAccID: flat(h.SubCurrAccID) || '',
|
||||
ContactID: flat(h.ContactID) || '',
|
||||
ShipmentMethodCode: flat(h.ShipmentMethodCode) || '',
|
||||
ShippingPostalAddressID: flat(h.ShippingPostalAddressID) || '',
|
||||
BillingPostalAddressID: flat(h.BillingPostalAddressID) || '',
|
||||
GuarantorContactID: flat(h.GuarantorContactID) || '',
|
||||
GuarantorContactID2: flat(h.GuarantorContactID2) || '',
|
||||
RoundsmanCode: flat(h.RoundsmanCode) || '',
|
||||
DeliveryCompanyCode: flat(h.DeliveryCompanyCode) || '',
|
||||
TaxTypeCode: flat(h.TaxTypeCode) || '',
|
||||
WithHoldingTaxTypeCode: flat(h.WithHoldingTaxTypeCode) || '',
|
||||
DOVCode: flat(h.DOVCode) || '',
|
||||
TaxExemptionCode: flat(h.TaxExemptionCode) || 0,
|
||||
CompanyCode: flat(h.CompanyCode) || 1,
|
||||
OfficeCode: flat(h.OfficeCode) || '101',
|
||||
StoreTypeCode: flat(h.StoreTypeCode) || 5,
|
||||
StoreCode: flat(h.StoreCode) || 0,
|
||||
POSTerminalID: flat(h.POSTerminalID) || 0,
|
||||
WarehouseCode: flat(h.WarehouseCode) || '1-0-12',
|
||||
ToWarehouseCode: flat(h.ToWarehouseCode) || '',
|
||||
OrdererCompanyCode: flat(h.OrdererCompanyCode) || 1,
|
||||
OrdererOfficeCode: flat(h.OrdererOfficeCode) || '101',
|
||||
OrdererStoreCode: flat(h.OrdererStoreCode) || '',
|
||||
GLTypeCode: flat(h.GLTypeCode) || '',
|
||||
LocalCurrencyCode: flat(h.LocalCurrencyCode) || 'TRY',
|
||||
ExchangeRate: flat(h.ExchangeRate) || 1,
|
||||
DiscountReasonCode: flat(h.DiscountReasonCode) || 0,
|
||||
SurplusOrderQtyToleranceRate: flat(h.SurplusOrderQtyToleranceRate) || 0,
|
||||
IncotermCode1: flat(h.IncotermCode1) || '',
|
||||
IncotermCode2: flat(h.IncotermCode2) || '',
|
||||
PaymentMethodCode: flat(h.PaymentMethodCode) || '',
|
||||
IsInclutedVat: flat(h.IsInclutedVat) || 0,
|
||||
IsCreditSale: flat(h.IsCreditSale) || 1,
|
||||
IsCreditableConfirmed: flat(h.IsCreditableConfirmed) || 1,
|
||||
CreditableConfirmedUser: flat(h.CreditableConfirmedUser) || '',
|
||||
CreditableConfirmedDate: flat(h.CreditableConfirmedDate) || '',
|
||||
ApplicationCode: flat(h.ApplicationCode) || 'Order',
|
||||
ApplicationID: flat(h.ApplicationID) || '',
|
||||
CreatedUserName: flat(h.CreatedUserName) || '',
|
||||
CreatedDate: flat(h.CreatedDate) || '',
|
||||
LastUpdatedUserName: flat(h.LastUpdatedUserName) || '',
|
||||
LastUpdatedDate: flat(h.LastUpdatedDate) || '',
|
||||
IsProposalBased: flat(h.IsProposalBased) || 0
|
||||
}
|
||||
|
||||
this.header = header
|
||||
this.currentOrderId = header.OrderHeaderID || id
|
||||
this.mode = 'edit'
|
||||
|
||||
// 🔹 Cari görünümü (QSelect)
|
||||
this.selectedCustomer = {
|
||||
value: header.CurrAccCode || '',
|
||||
label: `${header.CurrAccCode || ''} - ${flat(h.CurrAccDescription) || ''}`
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
📦 LINES MAPPING (57 kolon)
|
||||
============================================================ */
|
||||
this.orders = (data.lines || []).map((l, idx) => ({
|
||||
// Görünen alanlar
|
||||
id: flat(l.OrderLineID) || `row-${idx + 1}`,
|
||||
model: flat(l.ItemCode),
|
||||
renk: flat(l.ColorCode),
|
||||
renk2: flat(l.ItemDim2Code),
|
||||
fiyat: Number(flat(l.Price) || 0),
|
||||
pb: flat(l.DocCurrencyCode) || flat(l.PriceCurrencyCode) || 'USD',
|
||||
adet: Number(flat(l.Qty1) || 0),
|
||||
tutar: Number(flat(l.Price) || 0) * Number(flat(l.Qty1) || 0),
|
||||
aciklama: flat(l.LineDescription) || '',
|
||||
terminTarihi: flat(l.DeliveryDate)
|
||||
? String(flat(l.DeliveryDate)).substring(0, 10)
|
||||
: '',
|
||||
urunAnaGrubu: flat(l.ProductGroup) || '',
|
||||
urunAltGrubu: flat(l.ProductSubGroup) || '',
|
||||
grpKey: l.grpKey || 'tak',
|
||||
bedenMap: l.BedenMap || {},
|
||||
|
||||
// Backend roundtrip alanları
|
||||
SortOrder: flat(l.SortOrder) || 0,
|
||||
ItemTypeCode: flat(l.ItemTypeCode) || 1,
|
||||
ItemDim1Code: flat(l.ItemDim1Code) || '',
|
||||
ItemDim3Code: flat(l.ItemDim3Code) || '',
|
||||
Qty2: flat(l.Qty2) || 0,
|
||||
CancelQty1: flat(l.CancelQty1) || 0,
|
||||
CancelQty2: flat(l.CancelQty2) || 0,
|
||||
CancelDate: flat(l.CancelDate) || null,
|
||||
OrderCancelReasonCode: flat(l.OrderCancelReasonCode) || '',
|
||||
ClosedDate: flat(l.ClosedDate) || null,
|
||||
IsClosed: flat(l.IsClosed) || false,
|
||||
VatRate: flat(l.VatRate) || 10,
|
||||
PCTRate: flat(l.PCTRate) || 0,
|
||||
PriceCurrencyCode: flat(l.PriceCurrencyCode) || 'TRY',
|
||||
PriceExchangeRate: flat(l.PriceExchangeRate) || header.ExchangeRate || 1,
|
||||
CreatedUserName: flat(l.CreatedUserName) || '',
|
||||
CreatedDate: flat(l.CreatedDate) || '',
|
||||
LastUpdatedUserName: flat(l.LastUpdatedUserName) || '',
|
||||
LastUpdatedDate: flat(l.LastUpdatedDate) || '',
|
||||
SurplusOrderQtyToleranceRate:
|
||||
flat(l.SurplusOrderQtyToleranceRate) || 0
|
||||
}))
|
||||
|
||||
/* ============================================================
|
||||
💾 LOCAL STORAGE
|
||||
============================================================ */
|
||||
localStorage.setItem(
|
||||
`bssapp:order:last:${id}`,
|
||||
JSON.stringify({ header, lines: this.orders })
|
||||
)
|
||||
|
||||
console.log(`📦 Sipariş (${id}) yüklendi • rows:${this.orders.length}`)
|
||||
} catch (err) {
|
||||
console.error('❌ openById hatası:', err)
|
||||
this.error = err.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
,
|
||||
/* ==========================================================
|
||||
NEW TEMPLATE — Yeni sipariş başlatma
|
||||
========================================================== */
|
||||
newOrderTemplate() {
|
||||
const today = dayjs().format('YYYY-MM-DD')
|
||||
const due = dayjs().add(30, 'day').format('YYYY-MM-DD')
|
||||
|
||||
this.header = {
|
||||
OrderHeaderID: '',
|
||||
OrderTypeCode: 1,
|
||||
ProcessCode: 'WS',
|
||||
OrderNumber: '',
|
||||
OrderDate: today,
|
||||
AverageDueDate: due,
|
||||
Description: '',
|
||||
CurrAccCode: '',
|
||||
CurrAccDescription: '',
|
||||
DocCurrencyCode: 'USD',
|
||||
LocalCurrencyCode: 'TRY',
|
||||
ExchangeRate: 1,
|
||||
CompanyCode: 1,
|
||||
OfficeCode: '101',
|
||||
StoreTypeCode: 5,
|
||||
WarehouseCode: '1-0-12',
|
||||
IsCreditSale: true,
|
||||
CreatedUserName: '',
|
||||
CreatedDate: today,
|
||||
LastUpdatedUserName: '',
|
||||
LastUpdatedDate: today
|
||||
}
|
||||
|
||||
this.orders = []
|
||||
this.currentOrderId = null
|
||||
this.activeTransactionId = null
|
||||
this.selectedCustomer = null
|
||||
this.mode = 'new'
|
||||
this.error = null
|
||||
|
||||
// Temiz bir başlangıcı storage’a yaz
|
||||
this.saveToStorage()
|
||||
console.log('🧾 Yeni sipariş template yüklendi.')
|
||||
},
|
||||
/* ==========================================================
|
||||
SUBMIT — Create/Update (SQL tablo INSERT/UPDATE)
|
||||
➜ Kayıt sonrası: transaction kapanır AMA snapshot tutulur.
|
||||
========================================================== */
|
||||
async submitAll() {
|
||||
const auth = useAuthStore()
|
||||
const baseURL = 'http://localhost:8080'
|
||||
|
||||
const toNullable = (v, type = 'string') => {
|
||||
if (v === null || v === undefined || v === '') {
|
||||
if (type === 'number') return { Float64: 0, Valid: false }
|
||||
if (type === 'time') return { Time: null, Valid: false }
|
||||
return { String: '', Valid: false }
|
||||
}
|
||||
if (type === 'number') return { Float64: Number(v), Valid: true }
|
||||
if (type === 'time') return { Time: v, Valid: true }
|
||||
return { String: String(v), Valid: true }
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
// Header payload (backend’in beklediği Null* formatıyla)
|
||||
const h = this.header || {}
|
||||
const headerPayload = {
|
||||
OrderHeaderID: h.OrderHeaderID || this.currentOrderId || '',
|
||||
OrderTypeCode: toNullable(1, 'number'),
|
||||
ProcessCode: toNullable('WS'),
|
||||
OrderNumber: toNullable(h.OrderNumber),
|
||||
OrderDate: toNullable(h.OrderDate || dayjs().format('YYYY-MM-DD'), 'time'),
|
||||
AverageDueDate: toNullable(h.AverageDueDate || dayjs().add(30, 'day').format('YYYY-MM-DD'), 'time'),
|
||||
Description: toNullable(h.Description || ''),
|
||||
CurrAccCode: toNullable(h.CurrAccCode || this.selectedCustomer?.value || ''),
|
||||
CurrAccDescription: toNullable(h.CurrAccDescription || this.selectedCustomer?.label || ''),
|
||||
DocCurrencyCode: toNullable(h.DocCurrencyCode || 'USD'),
|
||||
LocalCurrencyCode: toNullable(h.LocalCurrencyCode || 'TRY'),
|
||||
ExchangeRate: toNullable(h.ExchangeRate || 1, 'number'),
|
||||
CompanyCode: toNullable(1, 'number'),
|
||||
OfficeCode: toNullable('101'),
|
||||
StoreTypeCode: toNullable(5, 'number'),
|
||||
WarehouseCode: toNullable(h.WarehouseCode || '1-0-12'),
|
||||
IsCreditSale: true,
|
||||
CreatedUserName: toNullable(auth.user?.Username || 'admin'),
|
||||
CreatedDate: toNullable(h.CreatedDate || dayjs().format('YYYY-MM-DD'), 'time'),
|
||||
LastUpdatedUserName: toNullable(auth.user?.Username || 'admin'),
|
||||
LastUpdatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time')
|
||||
}
|
||||
|
||||
// Lines payload
|
||||
const linesPayload = this.orders.map((l, idx) => ({
|
||||
OrderLineID: l.id || '',
|
||||
SortOrder: idx + 1,
|
||||
ItemTypeCode: toNullable(1, 'number'),
|
||||
ItemCode: toNullable(l.model),
|
||||
ColorCode: toNullable(l.renk),
|
||||
ItemDim1Code: toNullable(Object.keys(l.bedenMap?.[l.grpKey] || {})[0] || ''),
|
||||
ItemDim2Code: toNullable(l.renk2),
|
||||
Qty1: toNullable(Number(l.adet || 0), 'number'),
|
||||
Price: toNullable(Number(l.fiyat || 0), 'number'),
|
||||
DocCurrencyCode: toNullable(l.pb || 'USD'),
|
||||
VatRate: toNullable(10, 'number'),
|
||||
PCTRate: toNullable(0, 'number'),
|
||||
DeliveryDate: toNullable(l.terminTarihi || null, 'time'),
|
||||
LineDescription: toNullable(l.aciklama || ''),
|
||||
IsClosed: false,
|
||||
CreatedUserName: toNullable(auth.user?.Username || 'admin'),
|
||||
CreatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time'),
|
||||
LastUpdatedUserName: toNullable(auth.user?.Username || 'admin'),
|
||||
LastUpdatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time')
|
||||
}))
|
||||
|
||||
// Final payload
|
||||
const payload = {
|
||||
header: headerPayload,
|
||||
lines: linesPayload,
|
||||
user: auth.user?.Username || 'admin'
|
||||
}
|
||||
|
||||
let res
|
||||
if (this.currentOrderId) {
|
||||
// UPDATE
|
||||
res = await axios.post(`${baseURL}/api/order/update`, payload, {
|
||||
headers: { Authorization: `Bearer ${auth.token}` }
|
||||
})
|
||||
console.log('✅ UPDATE ok:', res.data)
|
||||
} else {
|
||||
// CREATE
|
||||
res = await axios.post(`${baseURL}/api/order/create`, payload, {
|
||||
headers: { Authorization: `Bearer ${auth.token}` }
|
||||
})
|
||||
console.log('✅ CREATE ok:', res.data)
|
||||
if (res.data?.orderID) {
|
||||
this.currentOrderId = res.data.orderID
|
||||
this.header.OrderHeaderID = res.data.orderID
|
||||
this.mode = 'edit'
|
||||
}
|
||||
}
|
||||
|
||||
// 🟩 Kayıt sonrası: snapshot’ı al ve storage’a da yaz
|
||||
this.saveSnapshot('post-submit')
|
||||
this.saveToStorage()
|
||||
|
||||
// 🧹 Transaction’ı kapat (UI temizliği ayrı fonksiyonda)
|
||||
this.clearTransaction()
|
||||
this.afterSubmit({ keepLocalStorage: true }) // 👈 önemli
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ submitAll hatası:', err)
|
||||
this.error = err.message
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
/* ==========================================================
|
||||
AFTER SUBMIT — UI temizliği (snapshot kalır!)
|
||||
keepLocalStorage=true → persistKey SİLİNMEZ
|
||||
========================================================== */
|
||||
afterSubmit(opts = { keepLocalStorage: true }) {
|
||||
try {
|
||||
// Snapshot zaten kaydedildi; istenirse persistKey’i bırak
|
||||
if (!opts?.keepLocalStorage) {
|
||||
localStorage.removeItem(this.persistKey)
|
||||
} else {
|
||||
// son hal zaten saveToStorage ile yazıldı — dokunma
|
||||
}
|
||||
|
||||
// UI temizliği (hafızada formu boşaltalım)
|
||||
// Ama edit’e dönmek istersen, snapshot/loadFromStorage ile geri getirirsin.
|
||||
this.orders = []
|
||||
// header’ı hafızadan temizliyoruz ama snapshot yerinde.
|
||||
this.header = {}
|
||||
this.selectedCustomer = null
|
||||
this.editingIndex = -1
|
||||
// currentOrderId’yi istersen koruyabilirsin; biz editte geri yüklüyoruz.
|
||||
// burada null’lıyoruz:
|
||||
this.currentOrderId = null
|
||||
this.mode = 'new'
|
||||
this.loading = false
|
||||
this.error = null
|
||||
|
||||
console.log('🧹 afterSubmit: UI temizlendi, snapshot storage’da.')
|
||||
} catch (err) {
|
||||
console.warn('⚠️ afterSubmit temizleme hatası:', err)
|
||||
}
|
||||
},
|
||||
|
||||
/* ==========================================================
|
||||
MANUAL UPDATE — mevcut header/lines yapılarına göre
|
||||
(İsteğe bağlı kullanılır)
|
||||
========================================================== */
|
||||
async updateOrder() {
|
||||
if (!this.currentOrderId) {
|
||||
console.warn('⚠️ currentOrderId yok, update yapılamaz.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const auth = useAuthStore()
|
||||
const payload = {
|
||||
header: this.header,
|
||||
lines: this.orders,
|
||||
username: auth.user?.Username || 'admin'
|
||||
}
|
||||
const res = await axios.post(
|
||||
'http://localhost:8080/api/order/update',
|
||||
payload,
|
||||
{ headers: { Authorization: `Bearer ${auth.token}` } }
|
||||
)
|
||||
console.log('✅ Güncelleme tamamlandı:', res.data)
|
||||
|
||||
// kayıt sonrası snapshot + persist
|
||||
this.saveSnapshot('manual-update')
|
||||
this.saveToStorage()
|
||||
} catch (err) {
|
||||
console.error('❌ updateOrder hatası:', err)
|
||||
this.error = err.message
|
||||
}
|
||||
}
|
||||
} // actions
|
||||
}) // defineStore
|
||||
// (opsiyonel) Bu referanslara component tarafından erişmek istersen:
|
||||
export const sharedOrderEntryRefs = {
|
||||
stockMap,
|
||||
bedenStock,
|
||||
sizeCache
|
||||
}
|
||||
50
ui/src/stores/downloadstHeadStore.js
Normal file
50
ui/src/stores/downloadstHeadStore.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// src/stores/downloadstHeadStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { download } from 'src/services/api'
|
||||
|
||||
export const useDownloadstHeadStore = defineStore('downloadstHead', {
|
||||
actions: {
|
||||
// 📄 Statement Header PDF indir / aç
|
||||
async handlestHeadDownload (
|
||||
accountCode,
|
||||
startDate,
|
||||
endDate,
|
||||
parislemler
|
||||
) {
|
||||
try {
|
||||
// ✅ Params (axios paramsSerializer array=repeat destekliyor)
|
||||
const params = {
|
||||
accountcode: accountCode,
|
||||
startdate: startDate,
|
||||
enddate: endDate
|
||||
}
|
||||
|
||||
if (Array.isArray(parislemler) && parislemler.length > 0) {
|
||||
params.parislemler = parislemler.filter(
|
||||
p => p !== undefined && p !== null && p !== ''
|
||||
)
|
||||
}
|
||||
|
||||
// 🔥 API CALL (TOKEN + BLOB + ERROR HANDLING OTOMATİK)
|
||||
const blob = await download(
|
||||
'/exportstamentheaderreport-pdf',
|
||||
params
|
||||
)
|
||||
|
||||
const pdfUrl = window.URL.createObjectURL(blob)
|
||||
window.open(pdfUrl, '_blank')
|
||||
|
||||
return { ok: true, message: '📄 PDF hazırlandı' }
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ PDF açma hatası:', err)
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
err?.message ||
|
||||
'❌ PDF açma hatası'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
51
ui/src/stores/downloadstpdfStore.js
Normal file
51
ui/src/stores/downloadstpdfStore.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// src/stores/downloadstpdfStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { download } from 'src/services/api'
|
||||
|
||||
export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
||||
actions: {
|
||||
/* ==========================================================
|
||||
📄 PDF İNDİR / AÇ
|
||||
========================================================== */
|
||||
async downloadPDF(accountCode, startDate, endDate, parislemler = []) {
|
||||
try {
|
||||
// 🔹 Query params
|
||||
const params = {
|
||||
accountcode: accountCode,
|
||||
startdate: startDate,
|
||||
enddate: endDate
|
||||
}
|
||||
|
||||
if (Array.isArray(parislemler) && parislemler.length > 0) {
|
||||
params.parislemler = parislemler.filter(
|
||||
p => p !== undefined && p !== null && p !== ''
|
||||
)
|
||||
}
|
||||
|
||||
// 🔥 MERKEZİ API — BLOB
|
||||
const blob = await download('/export-pdf', params)
|
||||
|
||||
// 🔹 Blob → URL
|
||||
const pdfUrl = window.URL.createObjectURL(
|
||||
new Blob([blob], { type: 'application/pdf' })
|
||||
)
|
||||
|
||||
// 🔹 Yeni sekmede aç
|
||||
window.open(pdfUrl, '_blank')
|
||||
|
||||
console.log('✅ PDF yeni sekmede açıldı')
|
||||
return { ok: true, message: '📄 PDF hazırlandı' }
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ PDF açma hatası:', err)
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
message:
|
||||
err?.message ||
|
||||
'❌ PDF alınamadı'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
15
ui/src/stores/example-store.js
Normal file
15
ui/src/stores/example-store.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useCounterStore = defineStore('counter', {
|
||||
state: () => ({
|
||||
counter: 0,
|
||||
}),
|
||||
getters: {
|
||||
doubleCount: (state) => state.counter * 2,
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
this.counter++;
|
||||
},
|
||||
},
|
||||
});
|
||||
20
ui/src/stores/index.js
Normal file
20
ui/src/stores/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineStore } from '#q-app/wrappers'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
* directly export the Store instantiation;
|
||||
*
|
||||
* The function below can be async too; either use
|
||||
* async/await or return a Promise which resolves
|
||||
* with the Store instance.
|
||||
*/
|
||||
|
||||
export default defineStore((/* { ssrContext } */) => {
|
||||
const pinia = createPinia()
|
||||
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
|
||||
return pinia
|
||||
})
|
||||
29
ui/src/stores/mailTestStore.js
Normal file
29
ui/src/stores/mailTestStore.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { post } from 'src/services/api'
|
||||
|
||||
export const useMailTestStore = defineStore('mailTest', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
lastResult: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async sendTestMail (to) {
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await post('/test-mail', {
|
||||
to
|
||||
})
|
||||
|
||||
this.lastResult = data
|
||||
return true
|
||||
} catch (err) {
|
||||
this.lastResult = err
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
38
ui/src/stores/mePasswordStore.js
Normal file
38
ui/src/stores/mePasswordStore.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// src/stores/mePasswordStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
export const useMePasswordStore = defineStore('mePassword', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
error: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async changePassword (currentPassword, newPassword) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
// 🔐 Token interceptor ile otomatik
|
||||
await api.post('/me/password', {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
|
||||
return true
|
||||
|
||||
} catch (e) {
|
||||
// 🔥 api.js normalize error
|
||||
this.error =
|
||||
e?.message ||
|
||||
'Şifre güncellenemedi'
|
||||
|
||||
throw e
|
||||
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
3340
ui/src/stores/orderentryStore.js
Normal file
3340
ui/src/stores/orderentryStore.js
Normal file
@@ -0,0 +1,3340 @@
|
||||
/* ===========================================================
|
||||
📦 orderentryStore.js (v3.4 CLEAN — AUTH + LOCAL PERSIST + AUTO RESUME)
|
||||
=========================================================== */
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
import dayjs from 'src/boot/dayjs'
|
||||
import { ref, toRaw, nextTick } from 'vue' // ✅ düzeltildi
|
||||
import { useAuthStore } from 'src/stores/authStore'
|
||||
|
||||
// ===========================================================
|
||||
// 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler)
|
||||
// ===========================================================
|
||||
/* ===========================================================
|
||||
🔹 BEDEN ŞEMALARI — STORE SOURCE OF TRUTH
|
||||
=========================================================== */
|
||||
// ⬆️ orderentryStore.js EN ÜSTÜNE
|
||||
// ===========================================================
|
||||
// 🔑 COMBO KEY CONTRACT (Frontend ↔ Backend) — v1
|
||||
// - trim + UPPER
|
||||
// - dim1 boşsa " "
|
||||
// - dim2 boşsa ""
|
||||
// ===========================================================
|
||||
const BEDEN_EMPTY = '_'
|
||||
|
||||
const norm = (v) => (v == null ? '' : String(v)).trim()
|
||||
const normUpper = (v) => norm(v).toUpperCase()
|
||||
|
||||
export function buildComboKey(row, beden) {
|
||||
const model = normUpper(row?.model || row?.ItemCode)
|
||||
const renk = normUpper(row?.renk || row?.ColorCode)
|
||||
const renk2 = normUpper(row?.renk2 || row?.ItemDim2Code)
|
||||
const bdn = normUpper(beden)
|
||||
const bedenFinal = bdn === '' ? BEDEN_EMPTY : bdn
|
||||
|
||||
// 🔒 KANONİK SIRA
|
||||
return `${model}||${renk}||${renk2}||${bedenFinal}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const BEDEN_SCHEMA = [
|
||||
{ key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] },
|
||||
{ key: 'yas', title: 'YAŞ', values: ['2','4','6','8','10','12','14'] },
|
||||
{ key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] },
|
||||
{ key: 'gom', title: 'GÖMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
|
||||
{ key: 'tak', title: 'TAKIM ELBİSE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
|
||||
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110CM', '115CM', '120CM', '125CM', '130CM', '135CM'] }
|
||||
]
|
||||
|
||||
export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
|
||||
m[g.key] = g
|
||||
return m
|
||||
}, {})
|
||||
|
||||
|
||||
export const stockMap = ref({})
|
||||
export const bedenStock = ref([])
|
||||
export const sizeCache = ref({})
|
||||
// ===========================================================
|
||||
// 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler)
|
||||
// ===========================================================
|
||||
// ========================
|
||||
// 🧰 GLOBAL DATE NORMALIZER
|
||||
// ========================
|
||||
|
||||
function newGuid() {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 🔑 Her beden satırı için deterministik clientKey üretimi
|
||||
function makeLineClientKey(row, grpKey, beden) {
|
||||
const base =
|
||||
row.clientRowKey ||
|
||||
row.clientKey ||
|
||||
row.id ||
|
||||
row._id ||
|
||||
row.tmpId ||
|
||||
`${row.model || ''}|${row.renk || ''}|${row.renk2 || ''}`
|
||||
|
||||
return `${base}::${grpKey}::${beden}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========================================================
|
||||
// 🧩 Pinia Store — ORDER ENTRY STORE (REV 2025-11-03.2)
|
||||
// ===========================================================
|
||||
export const useOrderEntryStore = defineStore('orderentry', {
|
||||
state: () => ({
|
||||
|
||||
isControlledSubmit: false,
|
||||
|
||||
allowRouteLeaveOnce: false,
|
||||
schemaMap: {},
|
||||
productCache: {},
|
||||
_lastSavedFingerprint: null,
|
||||
|
||||
activeNewHeaderId: localStorage.getItem("bss_active_new_header") || null,
|
||||
|
||||
loading: false,
|
||||
selected: null,
|
||||
error: null,
|
||||
|
||||
customers: [],
|
||||
selectedCustomer: null,
|
||||
|
||||
products: [],
|
||||
colors: [],
|
||||
secondColors: [],
|
||||
inventory: [],
|
||||
|
||||
selectedProduct: null,
|
||||
selectedColor: null,
|
||||
selectedColor2: null,
|
||||
|
||||
OrderHeaderID: null,
|
||||
|
||||
// Persist config
|
||||
persistKey: 'bss_orderentry_data',
|
||||
lastSnapshotKey: 'bss_orderentry_snapshot',
|
||||
|
||||
// Editor state
|
||||
editingKey: null,
|
||||
currentOrderId: null,
|
||||
mode: 'new',
|
||||
|
||||
// Grid state
|
||||
orders: [],
|
||||
header: {},
|
||||
summaryRows: [],
|
||||
|
||||
lastSavedAt: null,
|
||||
|
||||
// Guards
|
||||
preventPersist: false,
|
||||
_uiBusy: false,
|
||||
_unsavedChanges: false,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getDraftKey() {
|
||||
// NEW taslak → GLOBAL ama tekil
|
||||
return 'bss_orderentry_new_draft'
|
||||
},
|
||||
|
||||
getEditKey() {
|
||||
// EDIT → OrderHeaderID’ye bağlı
|
||||
const id = this.header?.OrderHeaderID
|
||||
return id ? `bss_orderentry_edit:${id}` : null
|
||||
}
|
||||
,
|
||||
hasUnsavedChanges(state) {
|
||||
try {
|
||||
return (
|
||||
state._lastSavedFingerprint !==
|
||||
state._persistFingerprint?.()
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
getPersistKey: (state) =>
|
||||
state.header?.OrderHeaderID
|
||||
? `${state.persistKey}:${state.header.OrderHeaderID}`
|
||||
: state.persistKey,
|
||||
|
||||
getSnapshotKey: (state) =>
|
||||
state.header?.OrderHeaderID
|
||||
? `${state.lastSnapshotKey}:${state.header.OrderHeaderID}`
|
||||
: state.lastSnapshotKey,
|
||||
|
||||
totalQty: (state) =>
|
||||
(state.orders || []).reduce((sum, r) => sum + (Number(r?.adet) || 0), 0),
|
||||
|
||||
hasAnyClosedLine(state) {
|
||||
return Array.isArray(state.summaryRows) &&
|
||||
state.summaryRows.some(r => r?.isClosed === true)
|
||||
},
|
||||
|
||||
totalAmount(state) {
|
||||
if (!Array.isArray(state.summaryRows)) return 0
|
||||
return state.summaryRows.reduce(
|
||||
(sum, r) => sum + Number(r?.tutar || 0),
|
||||
0
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
normalizeComboUI(row) {
|
||||
return buildComboKey(row, BEDEN_EMPTY)
|
||||
}
|
||||
|
||||
,
|
||||
/* ===========================================================
|
||||
🧩 initSchemaMap — BEDEN ŞEMA İNİT
|
||||
- TEK SOURCE OF TRUTH: BEDEN_SCHEMA
|
||||
=========================================================== */
|
||||
initSchemaMap() {
|
||||
if (this.schemaMap && Object.keys(this.schemaMap).length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = {}
|
||||
|
||||
for (const g of BEDEN_SCHEMA) {
|
||||
map[g.key] = {
|
||||
key: g.key,
|
||||
title: g.title,
|
||||
values: [...g.values]
|
||||
}
|
||||
}
|
||||
|
||||
this.schemaMap = map
|
||||
|
||||
console.log(
|
||||
'🧩 schemaMap INIT edildi:',
|
||||
Object.keys(this.schemaMap)
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
getRowKey(row) {
|
||||
if (!row) return null
|
||||
return row.OrderLineID || row.id || null
|
||||
}
|
||||
|
||||
|
||||
,
|
||||
updateHeaderTotals() {
|
||||
try {
|
||||
if (!Array.isArray(this.summaryRows)) return 0
|
||||
|
||||
const total = this.summaryRows.reduce(
|
||||
(sum, r) => sum + Number(r?.tutar || 0),
|
||||
0
|
||||
)
|
||||
|
||||
// Header sadece GÖSTERİM / BACKEND için
|
||||
if (this.header) {
|
||||
this.header.TotalAmount = Number(total.toFixed(2))
|
||||
}
|
||||
|
||||
return total
|
||||
} catch (err) {
|
||||
console.error('❌ updateHeaderTotals hata:', err)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
/* ===========================================================
|
||||
🚨 showInvalidVariantDialog — FINAL
|
||||
-----------------------------------------------------------
|
||||
✔ prItemVariant olmayan satırları listeler
|
||||
✔ Satıra tıkla → scroll + highlight
|
||||
✔ Kaydı BLOKLAYAN tek UI noktası
|
||||
=========================================================== */
|
||||
async showInvalidVariantDialog($q, invalidList = []) {
|
||||
if (!Array.isArray(invalidList) || invalidList.length === 0) return
|
||||
|
||||
return new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: '🚨 Tanımsız Ürün Kombinasyonları',
|
||||
message: `
|
||||
<div style="max-height:60vh;overflow:auto">
|
||||
${invalidList.map((v, i) => `
|
||||
<div
|
||||
class="invalid-row"
|
||||
data-clientkey="${v.clientKey}"
|
||||
style="
|
||||
padding:8px 10px;
|
||||
margin-bottom:6px;
|
||||
border-left:4px solid #c10015;
|
||||
background:#fff5f5;
|
||||
cursor:pointer;
|
||||
"
|
||||
>
|
||||
<div style="font-weight:600">
|
||||
#${i + 1} | Item: ${v.itemCode}
|
||||
</div>
|
||||
<div style="font-size:13px">
|
||||
Beden: ${v.dim1 || '(boş)'} |
|
||||
Renk: ${v.colorCode || '-'} |
|
||||
Qty: ${v.qty1}
|
||||
</div>
|
||||
<div style="font-size:12px;color:#c10015">
|
||||
Sebep: ${v.reason || 'Tanımsız ürün kombinasyonu'}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`,
|
||||
html: true,
|
||||
ok: {
|
||||
label: 'Düzelt',
|
||||
color: 'negative'
|
||||
},
|
||||
cancel: false,
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => resolve())
|
||||
.onDismiss(() => resolve())
|
||||
.onShown(() => {
|
||||
// Satıra tıklama → scroll + highlight
|
||||
const nodes = document.querySelectorAll('.invalid-row')
|
||||
nodes.forEach(n => {
|
||||
n.addEventListener('click', () => {
|
||||
const ck = n.getAttribute('data-clientkey')
|
||||
this.scrollToInvalidRow?.(ck)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
,
|
||||
/* ===========================================================
|
||||
🎯 scrollToInvalidRow — FINAL
|
||||
-----------------------------------------------------------
|
||||
✔ ClientKey bazlı scroll
|
||||
✔ Hem summaryRows hem orders destekli
|
||||
✔ Highlight otomatik kalkar
|
||||
=========================================================== */
|
||||
scrollToInvalidRow(clientKey) {
|
||||
if (!clientKey) return
|
||||
|
||||
// 1️⃣ Store içindeki satırı bul
|
||||
const idx = this.summaryRows?.findIndex(
|
||||
r => r.clientKey === clientKey
|
||||
)
|
||||
|
||||
if (idx === -1) {
|
||||
console.warn('❌ Satır bulunamadı:', clientKey)
|
||||
return
|
||||
}
|
||||
|
||||
// 2️⃣ DOM node
|
||||
const el = document.querySelector(
|
||||
`[data-clientkey="${clientKey}"]`
|
||||
)
|
||||
|
||||
if (!el) {
|
||||
console.warn('❌ DOM satırı bulunamadı:', clientKey)
|
||||
return
|
||||
}
|
||||
|
||||
// 3️⃣ Scroll
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
|
||||
// 4️⃣ Highlight
|
||||
el.classList.add('invalid-highlight')
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.remove('invalid-highlight')
|
||||
}, 2500)
|
||||
}
|
||||
,
|
||||
|
||||
async checkHeaderExists(orderHeaderID) {
|
||||
try {
|
||||
if (!orderHeaderID) return false
|
||||
|
||||
const res = await api.get(`/orders/check/${orderHeaderID}`)
|
||||
|
||||
// Backend “true/false” döner varsayımı
|
||||
return res?.data?.exists === true
|
||||
} catch (err) {
|
||||
console.warn("⚠ checkHeaderExists hata:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
async fetchOrderPdf(orderId) {
|
||||
try {
|
||||
const resp = await api.get(`/order/pdf/${orderId}`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
return resp.data
|
||||
} catch (err) {
|
||||
console.error("❌ fetchOrderPdf hata:", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
async downloadOrderPdf(id = null) {
|
||||
try {
|
||||
const orderId = id || this.header?.OrderHeaderID
|
||||
if (!orderId) {
|
||||
console.error('❌ PDF ID bulunamadı')
|
||||
return
|
||||
}
|
||||
|
||||
const res = await api.get(`/order/pdf/${orderId}`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
const blob = new Blob([res.data], { type: 'application/pdf' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
window.open(url, '_blank')
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60_000)
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ PDF açma hatası:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
,
|
||||
setActiveNewHeader(id) {
|
||||
this.activeNewHeaderId = id || null
|
||||
if (id) localStorage.setItem("bss_active_new_header", id)
|
||||
else localStorage.removeItem("bss_active_new_header")
|
||||
},
|
||||
|
||||
getActiveNewHeaderId() {
|
||||
return this.activeNewHeaderId || localStorage.getItem("bss_active_new_header")
|
||||
},
|
||||
|
||||
/* ===========================================================
|
||||
🧩 initFromRoute (v5.6 — groupedRows TOUCH YOK)
|
||||
-----------------------------------------------------------
|
||||
- Route ID ve bss_last_txn arasında en dolu snapshot'ı seçer
|
||||
- header + orders + summaryRows restore edilir
|
||||
- groupedRows hydrate edilmez / resetlenmez / dokunulmaz
|
||||
- Route ID farklıysa router.replace ile URL düzeltilir
|
||||
=========================================================== */
|
||||
async initFromRoute(orderId, router = null) { // ✅ NEW MODE → SADECE global draft
|
||||
if (this.mode === 'new') {
|
||||
const raw = localStorage.getItem(this.getDraftKey)
|
||||
if (raw) {
|
||||
try {
|
||||
const payload = JSON.parse(raw)
|
||||
this.header = payload.header || {}
|
||||
this.orders = payload.orders || []
|
||||
this.summaryRows = payload.summaryRows || this.orders
|
||||
console.log('♻️ NEW draft restore edildi (global)')
|
||||
return
|
||||
} catch {}
|
||||
}
|
||||
console.log('⚪ NEW draft yok, boş başlatılıyor')
|
||||
return
|
||||
}
|
||||
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
|
||||
this.initSchemaMap()
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🧩 [initFromRoute] orderId:', orderId)
|
||||
|
||||
const lastTxn = localStorage.getItem('bss_last_txn') || null
|
||||
|
||||
const readPayload = (id) => {
|
||||
if (!id) return null
|
||||
const raw = localStorage.getItem(`bss_orderentry_data:${id}`)
|
||||
if (!raw) return null
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fromRoute = readPayload(orderId)
|
||||
const fromLast = readPayload(lastTxn)
|
||||
|
||||
const hasData = (p) =>
|
||||
!!p && (
|
||||
(Array.isArray(p.orders) && p.orders.length > 0) ||
|
||||
(Array.isArray(p.summaryRows) && p.summaryRows.length > 0)
|
||||
)
|
||||
|
||||
let chosenId = null
|
||||
let chosenPayload = null
|
||||
|
||||
if (hasData(fromRoute)) {
|
||||
chosenId = orderId
|
||||
chosenPayload = fromRoute
|
||||
console.log('✅ [initFromRoute] Route ID snapshot seçildi:', chosenId)
|
||||
} else if (hasData(fromLast)) {
|
||||
chosenId = lastTxn
|
||||
chosenPayload = fromLast
|
||||
console.log('✅ [initFromRoute] lastTxn snapshot seçildi:', chosenId)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🚫 SNAPSHOT YOK → BOŞ BAŞLA
|
||||
-------------------------------------------------------- */
|
||||
if (!chosenId || !chosenPayload) {
|
||||
console.log('⚪ [initFromRoute] Snapshot yok, boş başlatılıyor')
|
||||
|
||||
this.header = {
|
||||
...(this.header || {}),
|
||||
OrderHeaderID: orderId || lastTxn || crypto.randomUUID()
|
||||
}
|
||||
|
||||
this.orders = []
|
||||
this.summaryRows = []
|
||||
|
||||
// ❗ groupedRows'a DOKUNMA
|
||||
return
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
✅ SNAPSHOT RESTORE (SAFE CLONE)
|
||||
-------------------------------------------------------- */
|
||||
this.header = {
|
||||
...(chosenPayload.header || {}),
|
||||
OrderHeaderID: chosenId
|
||||
}
|
||||
|
||||
const orders = Array.isArray(chosenPayload.orders)
|
||||
? [...chosenPayload.orders]
|
||||
: []
|
||||
|
||||
const summaryRows = Array.isArray(chosenPayload.summaryRows)
|
||||
? [...chosenPayload.summaryRows]
|
||||
: orders
|
||||
|
||||
this.orders = orders
|
||||
this.summaryRows = summaryRows
|
||||
|
||||
// ❗ groupedRows hydrate edilmez, resetlenmez
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🔁 lastTxn SENKRON
|
||||
-------------------------------------------------------- */
|
||||
try {
|
||||
localStorage.setItem('bss_last_txn', chosenId)
|
||||
} catch (e) {
|
||||
console.warn('⚠️ bss_last_txn yazılamadı:', e)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🔁 ROUTE DÜZELTME (GEREKİRSE)
|
||||
-------------------------------------------------------- */
|
||||
if (router && orderId && orderId !== chosenId) {
|
||||
console.log('🔁 [initFromRoute] Route ID düzeltiliyor →', chosenId)
|
||||
await router.replace({
|
||||
name: 'order-entry',
|
||||
params: { orderHeaderID: chosenId }
|
||||
})
|
||||
}
|
||||
|
||||
console.log(
|
||||
'✅ [initFromRoute] Restore tamam. Satır sayısı:',
|
||||
this.summaryRows.length
|
||||
)
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ [initFromRoute] hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
🆕 startNewOrder (v8.3 — FINAL & STABLE)
|
||||
=========================================================== */
|
||||
async startNewOrder({ $q }) {
|
||||
|
||||
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
|
||||
this.initSchemaMap()
|
||||
}
|
||||
|
||||
const headerId = crypto.randomUUID()
|
||||
|
||||
let orderNumber = `LOCAL-${dayjs().format("YYMMDD-HHmmss")}`
|
||||
|
||||
try {
|
||||
const res = await api.get("/order/new-number")
|
||||
if (res?.data?.OrderNumber) {
|
||||
orderNumber = res.data.OrderNumber
|
||||
}
|
||||
} catch {
|
||||
console.info('ℹ️ Backend order number yok, LOCAL kullanıldı')
|
||||
}
|
||||
|
||||
this.mode = 'new'
|
||||
this.isControlledSubmit = false
|
||||
this.allowRouteLeaveOnce = false
|
||||
|
||||
this.header = {
|
||||
OrderHeaderID: headerId,
|
||||
OrderNumber: orderNumber,
|
||||
OrderDate: new Date().toISOString().slice(0, 10),
|
||||
CurrAccCode: null,
|
||||
DocCurrencyCode: 'USD',
|
||||
PriceCurrencyCode: 'USD',
|
||||
PriceExchangeRate: 1
|
||||
}
|
||||
|
||||
this.orders = []
|
||||
this.summaryRows = []
|
||||
|
||||
// ✅ fingerprint bazlı sistem için reset
|
||||
this._lastSavedFingerprint = null
|
||||
|
||||
// ✅ NEW draft hemen yazılır
|
||||
this.persistLocalStorage?.()
|
||||
|
||||
return this.header
|
||||
}
|
||||
|
||||
|
||||
|
||||
,
|
||||
dedupeActiveLinesByCombo(lines) {
|
||||
const map = new Map()
|
||||
for (const ln of lines) {
|
||||
const key = buildComboKey({
|
||||
model: ln.ItemCode,
|
||||
renk: ln.ColorCode,
|
||||
renk2: ln.ItemDim2Code
|
||||
}, ln.ItemDim1Code)
|
||||
|
||||
if (!map.has(key)) {
|
||||
ln.ComboKey = key
|
||||
map.set(key, ln)
|
||||
continue
|
||||
}
|
||||
|
||||
const ex = map.get(key)
|
||||
ex.Qty1 = (Number(ex.Qty1) || 0) + (Number(ln.Qty1) || 0)
|
||||
|
||||
// OrderLineID boşsa doldur (editte önemli)
|
||||
if (!ex.OrderLineID && ln.OrderLineID) ex.OrderLineID = ln.OrderLineID
|
||||
}
|
||||
return Array.from(map.values())
|
||||
}
|
||||
|
||||
|
||||
,
|
||||
/* ===========================================================
|
||||
🧹 Core reset helper — sadece state'i sıfırlar
|
||||
=========================================================== */
|
||||
resetCoreState() {
|
||||
this.orders = []
|
||||
this.summaryRows = []
|
||||
this.groupedRows = []
|
||||
this.header = {}
|
||||
this.editingKey = null
|
||||
this.currentOrderId = null
|
||||
},resetForNewOrder() {
|
||||
// mevcut her şeyi temizle
|
||||
this.header = {
|
||||
OrderHeaderID: this.header?.OrderHeaderID || null,
|
||||
OrderDate: new Date().toISOString().slice(0,10),
|
||||
CurrAccCode: null,
|
||||
DocCurrencyCode: 'TRY',
|
||||
PriceCurrencyCode: 'TRY',
|
||||
// ihtiyaç duyduğun diğer default header alanları
|
||||
}
|
||||
|
||||
this.orders = []
|
||||
this.summaryRows = []
|
||||
this.productCache = {}
|
||||
this.stockMap = {}
|
||||
|
||||
this.setMode('new')
|
||||
}
|
||||
,
|
||||
resetForEdit() {
|
||||
// EDIT modda grid temizlenmez — sadece UI state resetlenir
|
||||
this.editingKey = null
|
||||
this.groupedRows = []
|
||||
this.mode = 'edit'
|
||||
}
|
||||
|
||||
,markAsSaved() {
|
||||
try {
|
||||
this._lastSavedFingerprint = this._persistFingerprint()
|
||||
console.log('✅ markAsSaved → fingerprint senkron')
|
||||
} catch (e) {
|
||||
console.warn('⚠️ markAsSaved hata:', e)
|
||||
}
|
||||
}
|
||||
,clearLocalSnapshot() {
|
||||
try {
|
||||
const id = this.header?.OrderHeaderID
|
||||
if (!id) return
|
||||
localStorage.removeItem(`bss_orderentry_data:${id}`)
|
||||
console.log('🧹 Local snapshot temizlendi:', id)
|
||||
} catch (e) {
|
||||
console.warn('⚠️ clearLocalSnapshot hata:', e)
|
||||
}
|
||||
},/* ===========================================================
|
||||
🧹 HARD CLEAN — ALL ORDERENTRY SNAPSHOTS
|
||||
=========================================================== */
|
||||
clearAllOrderSnapshots () {
|
||||
Object.keys(localStorage)
|
||||
.filter(k =>
|
||||
k.startsWith('bss_orderentry_data:') ||
|
||||
k.startsWith('bss_orderentry_edit:')
|
||||
)
|
||||
.forEach(k => {
|
||||
console.log('🧹 snapshot silindi:', k)
|
||||
localStorage.removeItem(k)
|
||||
})
|
||||
|
||||
localStorage.removeItem('bss_last_txn')
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
,
|
||||
/* ===========================================================
|
||||
🧹 Store Hard Reset — Submit Sonrası Temizlik (FIXED)
|
||||
- Grid, header, toplamlar, local state'ler sıfırlanır
|
||||
- persistKey / lastSnapshotKey NULL yapılmaz (config sabit kalır)
|
||||
- localStorage txn/snapshot temizliği güvenli yapılır
|
||||
=========================================================== */
|
||||
hardResetAfterSubmit() {
|
||||
try {
|
||||
// 🔑 mevcut id’yi yakala (local temizliği için)
|
||||
const id = this.header?.OrderHeaderID || null
|
||||
|
||||
/* -------------------------------------------------------
|
||||
1) Grid ve satırlar
|
||||
-------------------------------------------------------- */
|
||||
this.orders = []
|
||||
this.summaryRows = []
|
||||
this.groupedRows = []
|
||||
|
||||
/* -------------------------------------------------------
|
||||
2) Header & meta
|
||||
-------------------------------------------------------- */
|
||||
this.header = {}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
3) Mode & edit state
|
||||
-------------------------------------------------------- */
|
||||
this.mode = 'new'
|
||||
this.editingKey = null
|
||||
this.currentOrderId = null
|
||||
|
||||
/* -------------------------------------------------------
|
||||
4) Snapshot / transaction meta
|
||||
⚠️ persistKey / lastSnapshotKey store config → NULL YAPMA
|
||||
-------------------------------------------------------- */
|
||||
this.activeTransactionId = null
|
||||
this.submitted = false
|
||||
|
||||
// fingerprint / debounce meta varsa sıfırla
|
||||
this._lastSavedFingerprint = null
|
||||
this._lastPersistFingerprint = null
|
||||
if (this._persistTimeout) {
|
||||
clearTimeout(this._persistTimeout)
|
||||
this._persistTimeout = null
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
5) LocalStorage temizlik (opsiyonel ama submit sonrası doğru)
|
||||
-------------------------------------------------------- */
|
||||
try {
|
||||
if (id) {
|
||||
localStorage.removeItem(`bss_orderentry_data:${id}`)
|
||||
localStorage.removeItem(`bss_orderentry_snapshot:${id}`)
|
||||
}
|
||||
localStorage.removeItem('bss_last_txn')
|
||||
localStorage.removeItem('bss_active_new_header')
|
||||
} catch (e) {
|
||||
console.warn('⚠️ hardResetAfterSubmit localStorage temizliği hata:', e)
|
||||
}
|
||||
|
||||
console.log('🧹 Store resetlendi (submit sonrası).')
|
||||
} catch (err) {
|
||||
console.error('❌ hardResetAfterSubmit hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
✏️ openExistingForEdit (v12 — FINAL & CLEAN)
|
||||
-----------------------------------------------------------
|
||||
✔ Backend authoritative (orderlist açılışı local'i dikkate almaz)
|
||||
✔ mode=new → backend çağrısı YOK
|
||||
✔ normalizeOrderLines → grpKey + bedenMap garanti
|
||||
✔ isClosed varsa → view, yoksa → edit
|
||||
✔ Form sync opsiyonel
|
||||
✔ İlk açılışta snapshot yazılır (edit boyunca persist ile güncellenir)
|
||||
=========================================================== */
|
||||
async openExistingForEdit(
|
||||
orderId,
|
||||
{ $q = null, form = null, productCache = null } = {}
|
||||
) {
|
||||
// 🔑 schemaMap garanti
|
||||
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
|
||||
this.initSchemaMap?.()
|
||||
}
|
||||
|
||||
if (!orderId) return false
|
||||
|
||||
/* =======================================================
|
||||
🟦 NEW MODE — ASLA backend çağrısı yok
|
||||
======================================================= */
|
||||
if (this.mode === 'new') {
|
||||
console.log('⚪ openExistingForEdit skip (mode=new)')
|
||||
return false
|
||||
}
|
||||
|
||||
// productCache hem ref hem reactive olabilir → güvenli oku
|
||||
const pc =
|
||||
productCache?.value
|
||||
? productCache.value
|
||||
: (productCache && typeof productCache === 'object' ? productCache : {})
|
||||
|
||||
try {
|
||||
// geçici varsayım (sonra isClosed durumuna göre set edilecek)
|
||||
this.setMode?.('edit')
|
||||
|
||||
/* =======================================================
|
||||
🔹 BACKEND — authoritative load
|
||||
======================================================= */
|
||||
const res = await api.get(`/order/get/${orderId}`)
|
||||
const backend = res?.data
|
||||
|
||||
if (!backend?.header) {
|
||||
throw new Error('Backend header yok')
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
🔹 HEADER — SADECE BACKEND
|
||||
(orderlist açılışında local merge YOK)
|
||||
======================================================= */
|
||||
this.header = {
|
||||
...backend.header,
|
||||
OrderHeaderID: backend.header.OrderHeaderID || orderId
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
🔹 NORMALIZE LINES (TEK KAYNAK)
|
||||
normalizeOrderLines şu alanları üretmeli:
|
||||
✔ row.grpKey
|
||||
✔ row.bedenMap[grpKey]
|
||||
✔ row.isClosed (boolean)
|
||||
======================================================= */
|
||||
const normalized = this.normalizeOrderLines(
|
||||
backend.lines || [],
|
||||
this.header.DocCurrencyCode || 'USD',
|
||||
pc
|
||||
)
|
||||
|
||||
this.orders = Array.isArray(normalized) ? normalized : []
|
||||
this.summaryRows = [...this.orders]
|
||||
|
||||
/* =======================================================
|
||||
🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN)
|
||||
- herhangi bir isClosed=true → view
|
||||
- değilse → edit
|
||||
======================================================= */
|
||||
const hasClosedLine = (this.summaryRows || []).some(r => r?.isClosed === true)
|
||||
this.setMode?.(hasClosedLine ? 'view' : 'edit')
|
||||
|
||||
/* =======================================================
|
||||
🔹 FORM SYNC (opsiyonel)
|
||||
======================================================= */
|
||||
if (form) {
|
||||
Object.assign(form, this.header)
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
🔹 LOCAL SNAPSHOT (edit boyunca tutulacak temel)
|
||||
- Açılışta snapshot yaz
|
||||
- Sonraki değişikliklerde zaten persistLocalStorage çağrıları var
|
||||
======================================================= */
|
||||
this.persistLocalStorage?.()
|
||||
try {
|
||||
localStorage.setItem('bss_last_txn', String(orderId))
|
||||
} catch {}
|
||||
|
||||
console.log('✅ openExistingForEdit OK:', {
|
||||
id: orderId,
|
||||
rows: this.summaryRows.length,
|
||||
mode: this.mode,
|
||||
hasClosedLine
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('❌ openExistingForEdit hata:', err)
|
||||
|
||||
// new değilse uyar
|
||||
if (this.mode !== 'new') {
|
||||
$q?.notify?.({
|
||||
type: 'negative',
|
||||
message: 'Sipariş yüklenemedi'
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
/* ===========================================================
|
||||
♻️ hydrateFromLocalStorage (v5.5 — FIXED & CLEAN)
|
||||
-----------------------------------------------------------
|
||||
- Tek assign (double overwrite YOK)
|
||||
- groupedRows hydrate edilmez
|
||||
- mode ASLA set edilmez
|
||||
- header + rows güvenli restore
|
||||
=========================================================== */
|
||||
async hydrateFromLocalStorage(orderId, log = false) {if (this.mode === 'new') {
|
||||
return this.hydrateFromLocalStorageIfExists()
|
||||
}
|
||||
|
||||
try {
|
||||
const key = `bss_orderentry_data:${orderId}`
|
||||
const payload = JSON.parse(localStorage.getItem(key) || 'null')
|
||||
|
||||
if (!payload) {
|
||||
log && console.log('ℹ️ hydrate → snapshot yok:', orderId)
|
||||
return null
|
||||
}
|
||||
|
||||
// 🔑 source bilgisi (mode set edilmez)
|
||||
this.source = payload.source || 'local'
|
||||
|
||||
/* -------------------------------------------------------
|
||||
MSSQL tarih helper’ları
|
||||
-------------------------------------------------------- */
|
||||
const safeDateTime = v => {
|
||||
if (!v) return null
|
||||
const d = dayjs(v)
|
||||
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
|
||||
const safeDateOnly = v => {
|
||||
if (!v) return null
|
||||
const d = dayjs(v)
|
||||
return d.isValid() ? d.format('YYYY-MM-DD') : null
|
||||
}
|
||||
|
||||
const safeTimeOnly = v => {
|
||||
if (!v) return null
|
||||
const d = dayjs(v)
|
||||
return d.isValid() ? d.format('HH:mm:ss') : null
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
HEADER
|
||||
-------------------------------------------------------- */
|
||||
this.header = {
|
||||
...(payload.header || {}),
|
||||
OrderHeaderID: payload.header?.OrderHeaderID ?? orderId,
|
||||
OrderNumber : payload.header?.OrderNumber ?? null
|
||||
}
|
||||
|
||||
const h = this.header
|
||||
h.CreatedDate = safeDateTime(h.CreatedDate)
|
||||
h.LastUpdatedDate = safeDateTime(h.LastUpdatedDate)
|
||||
h.CreditableConfirmedDate = safeDateTime(h.CreditableConfirmedDate)
|
||||
h.OrderDate = safeDateOnly(h.OrderDate)
|
||||
h.OrderTime = safeTimeOnly(h.OrderTime)
|
||||
this.header = h
|
||||
|
||||
/* -------------------------------------------------------
|
||||
ROWS (TEK KAYNAK)
|
||||
-------------------------------------------------------- */
|
||||
const orders = Array.isArray(payload.orders)
|
||||
? payload.orders
|
||||
: []
|
||||
|
||||
this.orders = orders
|
||||
|
||||
this.summaryRows = Array.isArray(payload.summaryRows)
|
||||
? payload.summaryRows
|
||||
: orders
|
||||
|
||||
// ❌ groupedRows hydrate edilmez (computed olmalı)
|
||||
this.groupedRows = []
|
||||
|
||||
/* -------------------------------------------------------
|
||||
SNAPSHOT ÖZET
|
||||
-------------------------------------------------------- */
|
||||
const output = {
|
||||
type : payload.submitted === true ? 'submitted' : 'draft',
|
||||
source : this.source,
|
||||
headerId : orderId,
|
||||
orderNumber: this.header?.OrderNumber ?? null,
|
||||
rows : this.summaryRows.length,
|
||||
submitted :
|
||||
payload.submitted === true ||
|
||||
payload.header?.IsSubmitted === true
|
||||
}
|
||||
|
||||
log && console.log('♻️ hydrate sonuc (FIXED):', output)
|
||||
return output
|
||||
|
||||
} catch (err) {
|
||||
console.warn('⚠️ hydrateFromLocalStorage hata:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
,
|
||||
hydrateFromLocalStorageIfExists() {
|
||||
try {
|
||||
let raw = null
|
||||
|
||||
if (this.mode === 'new') {
|
||||
raw = localStorage.getItem(this.getDraftKey) // ✅
|
||||
}
|
||||
|
||||
if (this.mode === 'edit') {
|
||||
const key = this.getEditKey // ✅
|
||||
if (key) raw = localStorage.getItem(key)
|
||||
}
|
||||
|
||||
if (!raw) return false
|
||||
|
||||
const payload = JSON.parse(raw)
|
||||
|
||||
this.header = payload.header || {}
|
||||
this.orders = payload.orders || []
|
||||
this.summaryRows = payload.summaryRows || this.orders
|
||||
|
||||
console.log('♻️ hydrate OK:', this.mode)
|
||||
return true
|
||||
|
||||
} catch (err) {
|
||||
console.warn('hydrateFromLocalStorageIfExists hata:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
🔀 mergeOrders (local + backend)
|
||||
normalizeISO → kaldırıldı
|
||||
safe MSSQL helpers eklendi
|
||||
=========================================================== */
|
||||
mergeOrders(local, backend, preferLocal = true) {
|
||||
if (!backend && !local) return { header: {}, orders: [] }
|
||||
|
||||
const safeMerge = (base = {}, override = {}) => {
|
||||
const out = { ...base }
|
||||
for (const [k, v] of Object.entries(override || {})) {
|
||||
if (v === undefined || v === null) continue
|
||||
if (typeof v === 'string' && v.trim() === '') continue
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Header merge
|
||||
const header = safeMerge(backend?.header || {}, local?.header || {})
|
||||
header.OrderHeaderID =
|
||||
backend?.header?.OrderHeaderID ||
|
||||
local?.header?.OrderHeaderID ||
|
||||
header.OrderHeaderID ||
|
||||
null
|
||||
|
||||
const getKey = (r) =>
|
||||
(r.OrderLineID ||
|
||||
`${r.model || r.ItemCode}_${r.renk || r.ColorCode}_${r.renk2 || r.ColorCode2}`
|
||||
).toString().toUpperCase()
|
||||
|
||||
const map = new Map()
|
||||
|
||||
// Backend satırları
|
||||
for (const b of (backend?.lines || backend?.orders || [])) {
|
||||
map.set(getKey(b), { ...b, _src: 'backend' })
|
||||
}
|
||||
|
||||
// Local satırları merge et
|
||||
for (const l of (local?.orders || [])) {
|
||||
const k = getKey(l)
|
||||
if (map.has(k)) {
|
||||
const merged = safeMerge(map.get(k), l)
|
||||
merged._src = preferLocal ? 'local' : 'backend'
|
||||
map.set(k, merged)
|
||||
} else {
|
||||
map.set(k, { ...l, _src: 'local-only' })
|
||||
}
|
||||
}
|
||||
|
||||
const mergedOrders = Array.from(map.values())
|
||||
console.log(`🧩 mergeOrders → ${mergedOrders.length} satır birleşti (ID:${header.OrderHeaderID})`)
|
||||
|
||||
// ====================================================
|
||||
// 🕒 HEADER TARİHLERİNİ MSSQL FORMATINA NORMALİZE ET
|
||||
// ====================================================
|
||||
const safeDateTime = v => {
|
||||
if (!v) return null
|
||||
const d = dayjs(v)
|
||||
return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : null
|
||||
}
|
||||
|
||||
const safeDateOnly = v => {
|
||||
if (!v) return null
|
||||
const d = dayjs(v)
|
||||
return d.isValid() ? d.format("YYYY-MM-DD") : null
|
||||
}
|
||||
|
||||
const safeTimeOnly = v => {
|
||||
if (!v) return null
|
||||
const d = dayjs(v)
|
||||
return d.isValid() ? d.format("HH:mm:ss") : null
|
||||
}
|
||||
|
||||
header.CreatedDate = safeDateTime(header.CreatedDate)
|
||||
header.LastUpdatedDate = safeDateTime(header.LastUpdatedDate)
|
||||
header.CreditableConfirmedDate = safeDateTime(header.CreditableConfirmedDate)
|
||||
|
||||
header.OrderDate = safeDateOnly(header.OrderDate)
|
||||
header.OrderTime = safeTimeOnly(header.OrderTime)
|
||||
|
||||
return { header, orders: mergedOrders }
|
||||
|
||||
}
|
||||
,
|
||||
|
||||
markRowSource(row) {
|
||||
if (row._src === 'local-only') return '🟠 Offline'
|
||||
if (row._src === 'local') return '🔵 Local'
|
||||
return '⚪ Backend'
|
||||
}
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
🔄 mergeAndPersistBackendOrder (edit mode)
|
||||
=========================================================== */
|
||||
mergeAndPersistBackendOrder(orderId, backendPayload) {
|
||||
const key = `bss_orderentry_data:${orderId}`
|
||||
const localPayload = JSON.parse(localStorage.getItem(key) || 'null')
|
||||
|
||||
const merged = this.mergeOrders(localPayload, backendPayload, true)
|
||||
|
||||
localStorage.setItem(key, JSON.stringify({
|
||||
...merged,
|
||||
source: 'db',
|
||||
mode: 'edit',
|
||||
updatedAt: new Date().toISOString()
|
||||
}))
|
||||
|
||||
console.log(`💾 mergeAndPersistBackendOrder → ${orderId} localStorage’a yazıldı`)
|
||||
}
|
||||
,
|
||||
|
||||
persistLocalStorage() {
|
||||
try {
|
||||
if (this.preventPersist || this._uiBusy) return
|
||||
|
||||
const payload = {
|
||||
mode: this.mode,
|
||||
header: toRaw(this.header || {}),
|
||||
orders: toRaw(this.orders || []),
|
||||
summaryRows: toRaw(this.summaryRows || []),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
🟢 NEW MODE — GLOBAL TEK TASLAK
|
||||
=============================== */
|
||||
if (this.mode === 'new') {
|
||||
localStorage.setItem(this.getDraftKey, JSON.stringify(payload))
|
||||
|
||||
// 🔒 sadece aktif new header bilgisi
|
||||
this.setActiveNewHeader?.(this.header?.OrderHeaderID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
🔵 EDIT MODE — ID BAZLI
|
||||
=============================== */
|
||||
if (this.mode === 'edit') {
|
||||
const key = this.getEditKey
|
||||
if (!key) return
|
||||
localStorage.setItem(key, JSON.stringify(payload))
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.warn('persistLocalStorage error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
,
|
||||
clearEditSnapshotIfExists() {
|
||||
if (this.mode !== 'edit') return
|
||||
|
||||
const key = this.getEditKey // ✅
|
||||
if (!key) return
|
||||
|
||||
localStorage.removeItem(key)
|
||||
console.log('🧹 EDIT snapshot silindi:', key)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
,/* ===========================================================
|
||||
🧠 _persistFingerprint — kritik state’leri tek stringe indirger
|
||||
- X3: orders+header yetmez → mode, summaryRows, id/no, map’ler dahil
|
||||
=========================================================== */
|
||||
_persistFingerprint() {
|
||||
// 🔹 orders: çok büyürse pahalı olabilir ama snapshot tutarlılığı için önemli
|
||||
// (istersen burada sadece length + rowKey listesi gibi optimize ederiz)
|
||||
const ordersSnap = JSON.stringify(this.orders || [])
|
||||
|
||||
// 🔹 header: sadece kritik alanları al (tam header yerine daha stabil)
|
||||
const h = this.header || {}
|
||||
const headerSnap = JSON.stringify({
|
||||
OrderHeaderID: h.OrderHeaderID || '',
|
||||
OrderNumber: h.OrderNumber || '',
|
||||
CurrAccCode: h.CurrAccCode || '',
|
||||
DocCurrencyCode: h.DocCurrencyCode || '',
|
||||
ExchangeRate: h.ExchangeRate ?? null
|
||||
})
|
||||
|
||||
// 🔹 summaryRows: hash yerine şimdilik “length + rowKey listesi” (hafif + etkili)
|
||||
const sr = Array.isArray(this.summaryRows) ? this.summaryRows : []
|
||||
const summaryMeta = JSON.stringify({
|
||||
len: sr.length,
|
||||
keys: sr.map(r => this.getRowKey?.(r) || r?.key || r?.id || '').filter(Boolean)
|
||||
})
|
||||
|
||||
// 🔹 comboLineIds / lineIdMap gibi kritik map’ler
|
||||
// (sende hangisi varsa onu otomatik topluyoruz)
|
||||
const mapSnap = JSON.stringify({
|
||||
lineIdMap: this.lineIdMap || null,
|
||||
comboLineIds: this.comboLineIds || null,
|
||||
comboLineIdMap: this.comboLineIdMap || null,
|
||||
comboLineIdSet: this.comboLineIdSet ? Array.from(this.comboLineIdSet) : null
|
||||
})
|
||||
|
||||
// 🔹 mode
|
||||
const modeSnap = String(this.mode || 'new')
|
||||
|
||||
// ✅ Tek fingerprint
|
||||
return `${modeSnap}|${headerSnap}|${summaryMeta}|${mapSnap}|${ordersSnap}`
|
||||
}
|
||||
,
|
||||
/* ===========================================================
|
||||
🕒 _safePersistDebounced — snapshot değişmediği sürece yazmaz (X3)
|
||||
- fingerprint: mode + header(id/no) + summaryRows meta + lineIdMap/combo + orders
|
||||
=========================================================== */
|
||||
_safePersistDebounced(delay = 1200) {
|
||||
clearTimeout(this._persistTimeout)
|
||||
|
||||
this._persistTimeout = setTimeout(() => {
|
||||
try {
|
||||
// ✅ Persist guard’ları (varsa)
|
||||
if (this.preventPersist) return
|
||||
if (this._uiBusy) return
|
||||
|
||||
const fp = this._persistFingerprint()
|
||||
|
||||
if (fp === this._lastPersistFingerprint) {
|
||||
return
|
||||
}
|
||||
|
||||
this._lastPersistFingerprint = fp
|
||||
|
||||
this.persistLocalStorage()
|
||||
console.log(`🕒 Otomatik LocalStorage senkron (${this.orders?.length || 0} satır).`)
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Debounce persist hata:', err)
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
💰 fetchMinPrice — model/pb için min fiyat
|
||||
=========================================================== */
|
||||
async fetchMinPrice(model, currency, $q) {
|
||||
try {
|
||||
const res = await api.get('/min-price', {
|
||||
params: { model, currency }
|
||||
})
|
||||
const data = res?.data || {}
|
||||
console.log('💰 [store.fetchMinPrice] yanıt:', data)
|
||||
return {
|
||||
price: Number(data.price || 0),
|
||||
rateToTRY: Number(data.rateToTRY || 1),
|
||||
priceTRY: Number(data.priceTRY || 0)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ [store.fetchMinPrice] Min fiyat alınamadı:', err)
|
||||
$q?.notify?.({
|
||||
type: 'warning',
|
||||
message: 'Min. fiyat bilgisi alınamadı, kontrol atlandı ⚠️',
|
||||
position: 'top-right'
|
||||
})
|
||||
return { price: 0, rateToTRY: 1, priceTRY: 0 }
|
||||
}
|
||||
}
|
||||
,
|
||||
applyCurrencyToLines(newPB) {
|
||||
if (!newPB) return
|
||||
|
||||
// 🔹 Header
|
||||
if (this.header) {
|
||||
this.header.DocCurrencyCode = newPB
|
||||
this.header.PriceCurrencyCode = newPB
|
||||
}
|
||||
|
||||
// 🔹 Lines
|
||||
if (Array.isArray(this.orders)) {
|
||||
this.orders = this.orders.map(r => ({
|
||||
...r,
|
||||
pb: newPB,
|
||||
DocCurrencyCode: newPB,
|
||||
PriceCurrencyCode: newPB
|
||||
}))
|
||||
}
|
||||
|
||||
// 🔹 Summary
|
||||
if (Array.isArray(this.summaryRows)) {
|
||||
this.summaryRows = this.summaryRows.map(r => ({
|
||||
...r,
|
||||
pb: newPB,
|
||||
DocCurrencyCode: newPB,
|
||||
PriceCurrencyCode: newPB
|
||||
}))
|
||||
}
|
||||
|
||||
// ❗ totalAmount SET ETME
|
||||
// ✔️ TEK MERKEZ
|
||||
this.updateHeaderTotals?.()
|
||||
}
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
💠 HEADER SET & CURRENCY PROPAGATION
|
||||
=========================================================== */
|
||||
setHeaderFields(fields, opts = {}) {
|
||||
const {
|
||||
applyCurrencyToLines = false,
|
||||
immediatePersist = false
|
||||
} = opts
|
||||
|
||||
// 1️⃣ HEADER
|
||||
this.header = {
|
||||
...(this.header || {}),
|
||||
...fields
|
||||
}
|
||||
|
||||
// 2️⃣ SATIRLARA GERÇEKTEN YAY
|
||||
if (applyCurrencyToLines && Array.isArray(this.summaryRows)) {
|
||||
this.summaryRows = this.summaryRows.map(r => ({
|
||||
...r,
|
||||
pb: fields.DocCurrencyCode ?? r.pb,
|
||||
DocCurrencyCode: fields.DocCurrencyCode ?? r.DocCurrencyCode,
|
||||
PriceCurrencyCode: fields.PriceCurrencyCode ?? fields.DocCurrencyCode ?? r.PriceCurrencyCode
|
||||
}))
|
||||
}
|
||||
|
||||
// 3️⃣ STORE ORDERS REFERANSI
|
||||
this.orders = [...this.summaryRows]
|
||||
|
||||
|
||||
// 4️⃣ PERSIST
|
||||
if (immediatePersist) {
|
||||
this.persistLocalStorage('header-change')
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
applyHeaderCurrencyToOrders() {
|
||||
if (!Array.isArray(this.orders)) return
|
||||
|
||||
const doc = this.header?.DocCurrencyCode ?? null
|
||||
const prc = this.header?.PriceCurrencyCode ?? null
|
||||
const rate = this.header?.PriceExchangeRate ?? null
|
||||
|
||||
let cnt = 0
|
||||
|
||||
for (const r of this.orders) {
|
||||
if (doc) r.DocCurrencyCode = doc
|
||||
if (prc) r.PriceCurrencyCode = prc
|
||||
if (rate != null) r.PriceExchangeRate = rate
|
||||
cnt++
|
||||
}
|
||||
|
||||
console.log(`💱 ${cnt} satırda PB güncellendi → Doc:${doc} Price:${prc} Rate:${rate}`)
|
||||
}
|
||||
|
||||
|
||||
,/* ===========================================================
|
||||
📸 saveSnapshot — küçük debug snapshot
|
||||
=========================================================== */
|
||||
saveSnapshot(tag = 'snapshot') {
|
||||
try {
|
||||
const id = this.header?.OrderHeaderID
|
||||
if (!id) return
|
||||
|
||||
const key = `bss_orderentry_snapshot:${id}`
|
||||
|
||||
const snap = {
|
||||
tag,
|
||||
mode: this.mode,
|
||||
orders: toRaw(this.orders || []),
|
||||
header: toRaw(this.header || {}),
|
||||
savedAt: dayjs().toISOString()
|
||||
}
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(snap))
|
||||
console.log(`📸 Snapshot kaydedildi [${key}]`)
|
||||
} catch (err) {
|
||||
console.warn('⚠️ saveSnapshot hata:', err)
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
♻️ loadFromStorage — eski generic persist için
|
||||
=========================================================== */
|
||||
loadFromStorage(force = false) {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.getPersistKey)
|
||||
if (!raw) {
|
||||
console.info('ℹ️ LocalStorage boş, grid başlatılmadı.')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!force && this.mode === 'edit') {
|
||||
console.info('⚠️ Edit modda local restore atlandı (force=false).')
|
||||
return false
|
||||
}
|
||||
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
this.orders = Array.isArray(data.orders) ? data.orders : []
|
||||
this.header = data.header || {}
|
||||
this.currentOrderId = data.currentOrderId || null
|
||||
this.selectedCustomer = data.selectedCustomer || null
|
||||
|
||||
// 🔧 Temiz ID
|
||||
this.header.OrderHeaderID = data.header?.OrderHeaderID || null
|
||||
|
||||
this.mode = data.mode || 'new'
|
||||
this.lastSavedAt = data.savedAt || null
|
||||
|
||||
console.log(`♻️ Storage yüklendi • txn:${this.header.OrderHeaderID} (${this.orders.length} satır)`)
|
||||
|
||||
// Header PB -> satırlara
|
||||
this.applyHeaderCurrencyToOrders()
|
||||
this._safePersistDebounced(200)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.warn('⚠️ localStorage okuma hatası:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
clearStorage() {
|
||||
try {
|
||||
localStorage.removeItem(this.getPersistKey)
|
||||
console.log(`🗑️ LocalStorage temizlendi [${this.getPersistKey}]`)
|
||||
} catch (err) {
|
||||
console.warn('⚠️ clearStorage hatası:', err)
|
||||
}
|
||||
}
|
||||
,
|
||||
clearNewDraft() {
|
||||
localStorage.removeItem(this.getDraftKey) // ✅
|
||||
localStorage.removeItem('bss_last_txn')
|
||||
console.log('🧹 NEW taslak temizlendi')
|
||||
}
|
||||
|
||||
,
|
||||
// ===========================================================
|
||||
// 🔹 isSameCombo — STORE LEVEL (TEK KAYNAK)
|
||||
// - model ZORUNLU eşleşir
|
||||
// - renk / renk2 boşsa → joker
|
||||
// ===========================================================
|
||||
isSameCombo(a, b) {
|
||||
if (!a || !b) return false
|
||||
|
||||
const n = v => (v == null ? '' : String(v).trim().toUpperCase())
|
||||
|
||||
const A = { model: n(a.model), renk: n(a.renk), renk2: n(a.renk2) }
|
||||
const B = { model: n(b.model), renk: n(b.renk), renk2: n(b.renk2) }
|
||||
|
||||
if (!A.model || !B.model) return false
|
||||
|
||||
const renkOk = (A.renk === B.renk) || !A.renk || !B.renk
|
||||
const renk2Ok = (A.renk2 === B.renk2) || !A.renk2 || !B.renk2
|
||||
|
||||
return A.model === B.model && renkOk && renk2Ok
|
||||
},
|
||||
|
||||
|
||||
|
||||
// ===========================================================
|
||||
// 🔹 saveOrUpdateRowUnified (v6.6 — COMBO SAFE + FIXED STOCK+PRICE + UI)
|
||||
// - v6.5 korunur (stok+min fiyat + this.loadProductSizes)
|
||||
// - ✅ NEW MODE: dupIdx artık _deleteSignal satırlarını BAŞTAN hariç tutar
|
||||
// - EDIT MODE: sameCombo → update, combo değişti → delete + insert (korundu)
|
||||
// - lineIdMap koruması korunur
|
||||
// ===========================================================
|
||||
async saveOrUpdateRowUnified({
|
||||
form,
|
||||
recalcVat = null,
|
||||
resetEditor = null,
|
||||
stockMap = null,
|
||||
loadProductSizes = null,
|
||||
$q = null
|
||||
}) {
|
||||
try {
|
||||
console.log('🔥 saveOrUpdateRowUnified v6.6', {
|
||||
model: form?.model,
|
||||
mode: this.mode,
|
||||
editingKey: this.editingKey
|
||||
})
|
||||
|
||||
const getKey =
|
||||
typeof this.getRowKey === 'function'
|
||||
? this.getRowKey
|
||||
: (r => r?.clientKey || r?.id || r?.OrderLineID)
|
||||
|
||||
const rows = Array.isArray(this.summaryRows)
|
||||
? [...this.summaryRows]
|
||||
: []
|
||||
|
||||
/* =======================================================
|
||||
1️⃣ ZORUNLU KONTROLLER
|
||||
======================================================= */
|
||||
if (!form?.model) {
|
||||
$q?.notify?.({ type: 'warning', message: 'Model seçiniz' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!form.pb) {
|
||||
form.pb = this.header?.DocCurrencyCode || 'USD'
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
2️⃣ STOK KONTROLÜ (FIXED)
|
||||
- stok guard’dan önce this.loadProductSizes(form,true,$q)
|
||||
- opsiyonel callback loadProductSizes(true)
|
||||
- tek dialog + doğru await
|
||||
======================================================= */
|
||||
|
||||
// ✅ store fonksiyonu
|
||||
try {
|
||||
if (typeof this.loadProductSizes === 'function') {
|
||||
await this.loadProductSizes(form, true, $q)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠ this.loadProductSizes hata:', err)
|
||||
}
|
||||
|
||||
// ✅ dışarıdan callback geldiyse
|
||||
try {
|
||||
if (typeof loadProductSizes === 'function') {
|
||||
await loadProductSizes(true)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠ loadProductSizes hata:', err)
|
||||
}
|
||||
|
||||
const stockMapLocal = stockMap?.value || stockMap || {}
|
||||
const bedenLabels = form.bedenLabels || []
|
||||
const bedenValues = form.bedenler || []
|
||||
|
||||
const overLimit = []
|
||||
for (let i = 0; i < bedenLabels.length; i++) {
|
||||
const lbl = String(bedenLabels[i] ?? '').trim()
|
||||
const stok = Number(stockMapLocal?.[lbl] ?? 0)
|
||||
const girilen = Number(bedenValues?.[i] ?? 0)
|
||||
|
||||
if (stok > 0 && girilen > stok) {
|
||||
overLimit.push({ beden: lbl, stok, girilen })
|
||||
}
|
||||
}
|
||||
|
||||
if (overLimit.length && $q) {
|
||||
const msg = overLimit
|
||||
.map(x => `• <b>${x.beden}</b>: ${x.girilen} (Stok: ${x.stok})`)
|
||||
.join('<br>')
|
||||
|
||||
const stokOK = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Stok Uyarısı',
|
||||
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:<br><br>${msg}`,
|
||||
html: true,
|
||||
ok: { label: 'Devam', color: 'primary' },
|
||||
cancel: { label: 'İptal', color: 'negative' }
|
||||
})
|
||||
.onOk(() => resolve(true))
|
||||
.onCancel(() => resolve(false))
|
||||
.onDismiss(() => resolve(false))
|
||||
})
|
||||
|
||||
if (!stokOK) return false
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
3️⃣ FİYAT (MIN) KONTROLÜ (FIXED)
|
||||
======================================================= */
|
||||
let fiyatOK = true
|
||||
try {
|
||||
let minFiyat = 0
|
||||
|
||||
if (typeof this.fetchMinPrice === 'function') {
|
||||
const p = await this.fetchMinPrice(form.model, form.pb, $q)
|
||||
minFiyat = Number(p?.price || 0)
|
||||
} else if (Number(form.minFiyat || 0) > 0) {
|
||||
minFiyat = Number(form.minFiyat)
|
||||
}
|
||||
|
||||
const girilen = Number(form.fiyat || 0)
|
||||
|
||||
if (minFiyat > 0 && girilen > 0 && girilen < minFiyat && $q) {
|
||||
fiyatOK = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Fiyat Uyarısı',
|
||||
message:
|
||||
`<b>Min. Fiyat:</b> ${minFiyat} ${form.pb}<br>` +
|
||||
`<b>Girdiğiniz:</b> ${girilen} ${form.pb}`,
|
||||
html: true,
|
||||
ok: { label: 'Devam', color: 'primary' },
|
||||
cancel: { label: 'İptal', color: 'negative' }
|
||||
})
|
||||
.onOk(() => resolve(true))
|
||||
.onCancel(() => resolve(false))
|
||||
.onDismiss(() => resolve(false))
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠ Min fiyat hata:', err)
|
||||
}
|
||||
if (!fiyatOK) return false
|
||||
|
||||
/* =======================================================
|
||||
4️⃣ TOPLAM HESABI
|
||||
======================================================= */
|
||||
const adet = (form.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
|
||||
form.adet = adet
|
||||
form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2))
|
||||
|
||||
const newRow = toSummaryRowFromForm(form)
|
||||
|
||||
/* =======================================================
|
||||
5️⃣ EDIT MODE (editingKey ZORUNLU)
|
||||
======================================================= */
|
||||
if (this.editingKey) {
|
||||
const idx = rows.findIndex(r => getKey(r) === this.editingKey)
|
||||
if (idx === -1) {
|
||||
this.editingKey = null
|
||||
resetEditor?.(true)
|
||||
return false
|
||||
}
|
||||
|
||||
const prev = rows[idx]
|
||||
|
||||
if (this.isRowLocked?.(prev)) {
|
||||
$q?.notify?.({ type: 'warning', message: 'Satır kapalı' })
|
||||
this.editingKey = null
|
||||
resetEditor?.(true)
|
||||
return false
|
||||
}
|
||||
|
||||
// ✅ kritik: store-level
|
||||
const sameCombo = this.isSameCombo(prev, newRow)
|
||||
|
||||
const preservedLineIdMap =
|
||||
(prev?.lineIdMap && typeof prev.lineIdMap === 'object')
|
||||
? { ...prev.lineIdMap }
|
||||
: (newRow?.lineIdMap && typeof newRow.lineIdMap === 'object')
|
||||
? { ...newRow.lineIdMap }
|
||||
: {}
|
||||
|
||||
/* ===== SAME COMBO → UPDATE ===== */
|
||||
if (sameCombo) {
|
||||
rows[idx] = {
|
||||
...prev,
|
||||
...newRow,
|
||||
id: prev.id,
|
||||
OrderLineID: prev.OrderLineID || null,
|
||||
lineIdMap: preservedLineIdMap
|
||||
}
|
||||
|
||||
this.summaryRows = rows
|
||||
this.orders = rows
|
||||
|
||||
this.updateHeaderTotals?.()
|
||||
this.persistLocalStorage?.()
|
||||
|
||||
this.editingKey = null
|
||||
resetEditor?.(true)
|
||||
recalcVat?.()
|
||||
|
||||
$q?.notify?.({ type: 'positive', message: 'Satır güncellendi' })
|
||||
return true
|
||||
}
|
||||
|
||||
/* ===== COMBO CHANGED → DELETE + INSERT ===== */
|
||||
const grpKey =
|
||||
prev?.grpKey ||
|
||||
Object.keys(prev?.bedenMap || {})[0] ||
|
||||
'tak'
|
||||
|
||||
const emptyMap = {}
|
||||
const srcMap =
|
||||
(prev?.bedenMap?.[grpKey] && typeof prev.bedenMap[grpKey] === 'object')
|
||||
? prev.bedenMap[grpKey]
|
||||
: (preservedLineIdMap && typeof preservedLineIdMap === 'object')
|
||||
? preservedLineIdMap
|
||||
: null
|
||||
|
||||
if (srcMap) {
|
||||
for (const beden of Object.keys(srcMap)) emptyMap[beden] = 0
|
||||
} else {
|
||||
emptyMap['STD'] = 0
|
||||
}
|
||||
|
||||
const deleteRow = {
|
||||
...prev,
|
||||
id: `DEL::${prev.id || prev.OrderLineID || crypto.randomUUID()}`,
|
||||
_deleteSignal: true,
|
||||
adet: 0,
|
||||
Qty1: 0,
|
||||
tutar: 0,
|
||||
ComboKey: '',
|
||||
|
||||
OrderLineID: prev.OrderLineID || null,
|
||||
|
||||
grpKey,
|
||||
bedenMap: { [grpKey]: emptyMap },
|
||||
lineIdMap: preservedLineIdMap,
|
||||
comboLineIds: { ...(prev.comboLineIds || {}) }
|
||||
}
|
||||
|
||||
const insertedRow = {
|
||||
...newRow,
|
||||
id: crypto.randomUUID(),
|
||||
OrderLineID: null,
|
||||
lineIdMap: {}
|
||||
}
|
||||
|
||||
rows.splice(idx, 1, insertedRow)
|
||||
|
||||
this.summaryRows = rows
|
||||
this.orders = [...rows, deleteRow]
|
||||
|
||||
this.updateHeaderTotals?.()
|
||||
this.persistLocalStorage?.()
|
||||
|
||||
this.editingKey = null
|
||||
resetEditor?.(true)
|
||||
recalcVat?.()
|
||||
|
||||
$q?.notify?.({ type: 'positive', message: 'Kombinasyon değişti' })
|
||||
return true
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
6️⃣ NEW MODE (MERGE / INSERT) — COMBO SAFE
|
||||
- aynı combo → bedenMap merge (satır sayısı artmaz)
|
||||
- farklı combo → yeni satır
|
||||
- ✅ FIX: _deleteSignal satırlarını dup aramasında hariç tut
|
||||
======================================================= */
|
||||
const dupIdx = rows.findIndex(r =>
|
||||
!r?._deleteSignal &&
|
||||
this.isSameCombo(r, newRow)
|
||||
)
|
||||
|
||||
// helper: bedenMap çıkar (gruplu ya da düz)
|
||||
const extractMap = (row) => {
|
||||
const grpKey =
|
||||
row?.grpKey ||
|
||||
Object.keys(row?.bedenMap || {})[0] ||
|
||||
'GENEL'
|
||||
|
||||
const grouped = row?.bedenMap?.[grpKey]
|
||||
const flat = (row?.bedenMap && typeof row.bedenMap === 'object' && !grouped)
|
||||
? row.bedenMap
|
||||
: null
|
||||
|
||||
return { grpKey, map: (grouped || flat || {}) }
|
||||
}
|
||||
|
||||
if (dupIdx !== -1) {
|
||||
const prev = rows[dupIdx]
|
||||
|
||||
// delete satırına merge yapma (ek güvenlik)
|
||||
if (prev?._deleteSignal !== true) {
|
||||
const { grpKey: prevGrp, map: prevMap } = extractMap(prev)
|
||||
const { grpKey: newGrp, map: newMap } = extractMap(newRow)
|
||||
|
||||
// hangi grpKey kullanılacak?
|
||||
const grpKey = newRow?.grpKey || prevGrp || newGrp || 'GENEL'
|
||||
|
||||
// MERGE: bedenleri topluyoruz (override değil)
|
||||
const merged = { ...(prevMap || {}) }
|
||||
for (const [k, v] of Object.entries(newMap || {})) {
|
||||
const beden = (k == null || String(k).trim() === '') ? ' ' : String(k).trim()
|
||||
merged[beden] = Number(merged[beden] || 0) + Number(v || 0)
|
||||
}
|
||||
|
||||
// toplam adet/tutar recalc
|
||||
const totalAdet = Object.values(merged).reduce((a, b) => a + Number(b || 0), 0)
|
||||
const price = Number(newRow?.fiyat ?? prev?.fiyat ?? 0)
|
||||
const totalTutar = Number((totalAdet * price).toFixed(2))
|
||||
|
||||
rows[dupIdx] = {
|
||||
...prev,
|
||||
...newRow,
|
||||
|
||||
// kritik korumalar
|
||||
id: prev.id,
|
||||
OrderLineID: prev.OrderLineID || null,
|
||||
lineIdMap: { ...(prev.lineIdMap || {}) },
|
||||
|
||||
// MERGED bedenMap
|
||||
grpKey,
|
||||
bedenMap: { [grpKey]: merged },
|
||||
|
||||
// adet/tutar
|
||||
adet: totalAdet,
|
||||
tutar: totalTutar,
|
||||
|
||||
updatedAt: dayjs().toISOString()
|
||||
}
|
||||
|
||||
this.summaryRows = rows
|
||||
this.orders = rows
|
||||
|
||||
this.updateHeaderTotals?.()
|
||||
this.persistLocalStorage?.()
|
||||
resetEditor?.(true)
|
||||
recalcVat?.()
|
||||
|
||||
$q?.notify?.({ type: 'positive', message: 'Aynı kombinasyon bulundu, bedenler birleştirildi' })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// dup yoksa (veya dup delete satırıydı) → yeni satır
|
||||
rows.push({
|
||||
...newRow,
|
||||
id: newRow.id || crypto.randomUUID(),
|
||||
OrderLineID: null,
|
||||
lineIdMap: { ...(newRow.lineIdMap || {}) }
|
||||
})
|
||||
|
||||
this.summaryRows = rows
|
||||
this.orders = rows
|
||||
|
||||
this.updateHeaderTotals?.()
|
||||
this.persistLocalStorage?.()
|
||||
resetEditor?.(true)
|
||||
recalcVat?.()
|
||||
|
||||
$q?.notify?.({ type: 'positive', message: 'Yeni satır eklendi' })
|
||||
return true
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ saveOrUpdateRowUnified:', err)
|
||||
$q?.notify?.({ type: 'negative', message: 'Satır kaydı başarısız' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
,
|
||||
|
||||
/* ===========================================================
|
||||
🔄 setTransaction — yeni transaction ID set et
|
||||
=========================================================== */
|
||||
setTransaction(id, autoResume = true) {
|
||||
if (!id) return
|
||||
|
||||
// 🔧 temiz ID
|
||||
this.header.OrderHeaderID = id
|
||||
|
||||
localStorage.setItem('bss_last_txn', id)
|
||||
console.log('🔄 Transaction değiştirildi:', id)
|
||||
|
||||
if (autoResume) {
|
||||
const hasData = Array.isArray(this.orders) && this.orders.length > 0
|
||||
if (!hasData) {
|
||||
const ok = this.hydrateFromLocalStorage(id,true)
|
||||
if (ok) console.info('📦 Local kayıt geri yüklendi (boş grid için).')
|
||||
} else {
|
||||
console.log('🚫 Grid dolu, auto-resume atlandı (mevcut satırlar korundu).')
|
||||
}
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🧹 clearTransaction — sadece NEW MODE taslaklarını temizler
|
||||
=========================================================== */
|
||||
clearTransaction() {
|
||||
try {
|
||||
const id = this.header?.OrderHeaderID
|
||||
if (id) {
|
||||
localStorage.removeItem(`bss_orderentry_data:${id}`)
|
||||
}
|
||||
|
||||
this.orders = []
|
||||
this.summaryRows = []
|
||||
this.groupedRows = []
|
||||
this.header = {}
|
||||
this.mode = 'new'
|
||||
|
||||
localStorage.removeItem('bss_last_txn')
|
||||
|
||||
console.log('🧹 Transaction temizlendi')
|
||||
} catch (err) {
|
||||
console.warn('⚠️ clearTransaction hata:', err)
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
|
||||
// =======================================================
|
||||
// 🔒 KİLİT KONTROLÜ — Sadece EDIT modunda, backend satırı
|
||||
// =======================================================
|
||||
isRowLocked(row) {
|
||||
if (!row) return false
|
||||
// Sadece edit modunda,
|
||||
// ve backend'den gelen gerçek OrderLineID varsa,
|
||||
// ve IsClosed=1 ise satır kilitli
|
||||
return (
|
||||
this.mode === 'edit' &&
|
||||
!!row.OrderLineID &&
|
||||
row.isClosed === true
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
findExistingIndexByForm(form) {
|
||||
return this.orders.findIndex(r => this.isSameCombo(r, form))
|
||||
},
|
||||
|
||||
addRow(row) {
|
||||
if (!row) return
|
||||
|
||||
const existingIndex = this.orders.findIndex(r => {
|
||||
const sameId = r.id && row.id && r.id === row.id
|
||||
const sameCombo = this.isSameCombo(r, row)
|
||||
return sameId || sameCombo
|
||||
})
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const old = this.orders[existingIndex]
|
||||
this.orders[existingIndex] = {
|
||||
...old,
|
||||
adet: Number(row.adet ?? old.adet ?? 0),
|
||||
fiyat: Number(row.fiyat ?? old.fiyat ?? 0),
|
||||
tutar: Number(row.fiyat ?? old.fiyat ?? 0) * Number(row.adet ?? old.adet ?? 0),
|
||||
ItemDim1Code: row.ItemDim1Code || old.ItemDim1Code,
|
||||
aciklama: row.aciklama || old.aciklama,
|
||||
updatedAt: dayjs().toISOString()
|
||||
}
|
||||
console.log(`⚠️ Aynı kombinasyon bulundu, satır güncellendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`)
|
||||
} else {
|
||||
this.orders.push(toRaw(row))
|
||||
console.log(`➕ Yeni kombinasyon eklendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`)
|
||||
}
|
||||
|
||||
this.persistLocalStorage()
|
||||
this.saveSnapshot('after-add')
|
||||
},
|
||||
|
||||
updateRow(index, patch) {
|
||||
if (index < 0 || index >= this.orders.length) return
|
||||
this.orders[index] = {
|
||||
...this.orders[index],
|
||||
...toRaw(patch),
|
||||
updatedAt: dayjs().toISOString()
|
||||
}
|
||||
this.persistLocalStorage()
|
||||
this.saveSnapshot('after-update')
|
||||
console.log(`✏️ Satır güncellendi (store): #${index}`)
|
||||
},
|
||||
|
||||
|
||||
removeRow(index) {
|
||||
if (index < 0 || index >= this.orders.length) return
|
||||
|
||||
const removed = this.orders.splice(index, 1)
|
||||
if (Array.isArray(this.summaryRows)) {
|
||||
this.summaryRows.splice(index, 1)
|
||||
}
|
||||
|
||||
this.persistLocalStorage()
|
||||
this.saveSnapshot('after-remove')
|
||||
console.log(`🗑️ Satır silindi: ${removed[0]?.model || '(model yok)'}`)
|
||||
},
|
||||
removeSelectedRow(row, $q = null) {
|
||||
if (!row) return
|
||||
|
||||
// 1) Kilitli satır silinemez
|
||||
if (this.isRowLocked(row)) {
|
||||
$q?.notify?.({
|
||||
type: 'warning',
|
||||
message: '🔒 Bu satır (IsClosed=1) kapatılmış. Silinemez.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 2) Kullanıcıya onay sor
|
||||
return new Promise(resolve => {
|
||||
$q?.dialog({
|
||||
title: 'Satır Sil',
|
||||
message: `${row.model} / ${row.renk} / ${row.renk2} kombinasyonu silinsin mi?`,
|
||||
ok: { label: 'Evet', color: 'negative' },
|
||||
cancel: { label: 'Vazgeç' }
|
||||
})
|
||||
.onOk(() => {
|
||||
this.removeRowInternal(row)
|
||||
resolve(true)
|
||||
})
|
||||
.onCancel(() => resolve(false))
|
||||
})
|
||||
}
|
||||
,
|
||||
removeRowInternal(row) {
|
||||
if (!row) return false
|
||||
|
||||
// 1️⃣ Kilit kontrolü
|
||||
if (this.isRowLocked(row)) {
|
||||
console.warn('🔒 Kilitli satır silinemez.')
|
||||
return false
|
||||
}
|
||||
|
||||
const getKey =
|
||||
typeof this.getRowKey === 'function'
|
||||
? this.getRowKey
|
||||
: (r => r?.clientKey || r?.id || r?.OrderLineID)
|
||||
|
||||
const rowKey = getKey(row)
|
||||
if (!rowKey) return false
|
||||
|
||||
const idx = this.summaryRows.findIndex(r => getKey(r) === rowKey)
|
||||
if (idx === -1) return false
|
||||
|
||||
console.log('🗑️ X2 removeRowInternal →', row)
|
||||
|
||||
// 🔐 UI BUSY
|
||||
this._uiBusy = true
|
||||
this.preventPersist = true
|
||||
|
||||
try {
|
||||
// 2️⃣ UI’dan kaldır
|
||||
this.summaryRows.splice(idx, 1)
|
||||
|
||||
// orders = UI satırları (temiz kopya)
|
||||
this.orders = [...this.summaryRows]
|
||||
|
||||
// 3️⃣ EDIT MODE → DELETE SİNYALİ
|
||||
if (this.mode === 'edit') {
|
||||
const grpKey =
|
||||
row.grpKey ||
|
||||
Object.keys(row.bedenMap || {})[0] ||
|
||||
'tak'
|
||||
|
||||
// ✅ lineIdMap referansı (varsa)
|
||||
const lineIdMap =
|
||||
(row.lineIdMap && typeof row.lineIdMap === 'object')
|
||||
? { ...row.lineIdMap }
|
||||
: {}
|
||||
|
||||
const emptyMap = {}
|
||||
|
||||
// Öncelik: bedenMap[grpKey] → lineIdMap → fallback
|
||||
if (row.bedenMap && row.bedenMap[grpKey]) {
|
||||
for (const beden of Object.keys(row.bedenMap[grpKey] || {})) {
|
||||
emptyMap[beden] = 0
|
||||
}
|
||||
} else if (Object.keys(lineIdMap).length) {
|
||||
for (const beden of Object.keys(lineIdMap)) {
|
||||
emptyMap[beden] = 0
|
||||
}
|
||||
} else {
|
||||
emptyMap['STD'] = 0
|
||||
}
|
||||
|
||||
const deleteSignalRow = {
|
||||
...row,
|
||||
|
||||
// 🔴 UI KEY
|
||||
id: `DEL::${row.id || row.OrderLineID || crypto.randomUUID()}`,
|
||||
|
||||
// 🔴 BACKEND DELETE SIGNAL
|
||||
adet: 0,
|
||||
Qty1: 0,
|
||||
tutar: 0,
|
||||
|
||||
// 🔴 CRITICAL: duplicate guard'a girmesin
|
||||
ComboKey: '',
|
||||
|
||||
// 🔴 legacy tekil alan (varsa kalsın)
|
||||
OrderLineID: row.OrderLineID || null,
|
||||
|
||||
// ✅ CRITICAL
|
||||
grpKey,
|
||||
bedenMap: { [grpKey]: emptyMap },
|
||||
lineIdMap,
|
||||
comboLineIds: { ...(row.comboLineIds || {}) },
|
||||
|
||||
_deleteSignal: true
|
||||
}
|
||||
|
||||
console.log('📡 DELETE sinyali üretildi:', deleteSignalRow)
|
||||
|
||||
this.orders.push(deleteSignalRow)
|
||||
}
|
||||
|
||||
// 4️⃣ Totals (persist YOK)
|
||||
this.updateHeaderTotals?.()
|
||||
|
||||
} finally {
|
||||
// 🔓 GUARD KAPAT
|
||||
this.preventPersist = false
|
||||
this._uiBusy = false
|
||||
}
|
||||
|
||||
// 5️⃣ TEK VE KONTROLLÜ persist
|
||||
this.persistLocalStorage()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
📦 normalizeOrderLines (v9 — lineIdMap FIXED + AKSBİR SAFE)
|
||||
-----------------------------------------------------------
|
||||
✔ grpKey SADECE burada set edilir
|
||||
✔ detectBedenGroup SADECE store’da kullanılır
|
||||
✔ aksbir → ' ' bedeni = GERÇEK adet
|
||||
✔ backend satırlarında BEDEN → OrderLineID map’i üretilir
|
||||
=========================================================== */
|
||||
normalizeOrderLines(lines, pbFallback = 'USD') {
|
||||
if (!Array.isArray(lines)) return []
|
||||
|
||||
const merged = Object.create(null)
|
||||
|
||||
const makeBaseKey = (model, renk, renk2) =>
|
||||
`${model || ''}||${renk || ''}||${renk2 || ''}`
|
||||
|
||||
for (const raw of lines) {
|
||||
if (!raw) continue
|
||||
|
||||
const isClosed =
|
||||
raw.IsClosed === true ||
|
||||
raw.isClosed === true ||
|
||||
raw.IsClosed?.Bool === true
|
||||
|
||||
/* =======================================================
|
||||
1️⃣ UI / SNAPSHOT KAYNAKLI SATIR
|
||||
-------------------------------------------------------
|
||||
✔ ComboKey YOK
|
||||
✔ Sadece model / renk / renk2 bazında gruplanır
|
||||
======================================================= */
|
||||
if (raw.bedenMap && Object.keys(raw.bedenMap).length) {
|
||||
const model = (raw.model || raw.ItemCode || '').trim()
|
||||
const renk = (raw.renk || raw.ColorCode || '').trim()
|
||||
const renk2 = (raw.renk2 || raw.ItemDim2Code || '').trim()
|
||||
|
||||
// ❗ BEDEN YOK → bu SADECE üst seviye grup anahtarı
|
||||
const modelKey = `${model}||${renk}||${renk2}`
|
||||
|
||||
const grpKey = raw.grpKey || 'tak'
|
||||
const srcMap = raw.bedenMap[grpKey] || {}
|
||||
|
||||
const adet = Object.values(srcMap).reduce((a, b) => a + (Number(b) || 0), 0)
|
||||
const fiyat = Number(raw.fiyat || 0)
|
||||
const pb = raw.pb || raw.DocCurrencyCode || pbFallback
|
||||
const tutar = Number(raw.tutar ?? adet * fiyat)
|
||||
|
||||
merged[modelKey] ??= []
|
||||
merged[modelKey].push({
|
||||
...raw,
|
||||
grpKey,
|
||||
bedenMap: { [grpKey]: { ...srcMap } },
|
||||
adet,
|
||||
fiyat,
|
||||
pb,
|
||||
tutar,
|
||||
isClosed
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
/* =======================================================
|
||||
2️⃣ BACKEND / LEGACY SATIR (FIXED)
|
||||
-------------------------------------------------------
|
||||
✔ ComboKey YOK
|
||||
✔ Sadece model / renk / renk2 bazlı gruplanır
|
||||
✔ BEDEN sadece bedenMap + lineIdMap için kullanılır
|
||||
======================================================= */
|
||||
const model = (raw.Model || raw.ItemCode || '').trim()
|
||||
const renk = (raw.ColorCode || '').trim()
|
||||
const renk2 = (raw.ItemDim2Code || '').trim()
|
||||
|
||||
// ❗ BEDEN HARİÇ — üst seviye grup anahtarı
|
||||
const modelKey = `${model}||${renk}||${renk2}`
|
||||
|
||||
merged[modelKey] ??= []
|
||||
|
||||
const beden = (
|
||||
raw.ItemDim1Code == null || String(raw.ItemDim1Code).trim() === ''
|
||||
? ' '
|
||||
: String(raw.ItemDim1Code).trim().toUpperCase()
|
||||
)
|
||||
|
||||
const qty = Number(raw.Qty1 || raw.Qty || 0)
|
||||
|
||||
let entry = merged[modelKey][0]
|
||||
if (!entry) {
|
||||
entry = {
|
||||
id: raw.OrderLineID || crypto.randomUUID(),
|
||||
|
||||
model,
|
||||
renk,
|
||||
renk2,
|
||||
|
||||
urunAnaGrubu: raw.UrunAnaGrubu || 'GENEL',
|
||||
urunAltGrubu: raw.UrunAltGrubu || '',
|
||||
kategori: raw.Kategori || '',
|
||||
|
||||
aciklama: raw.LineDescription || '',
|
||||
fiyat: Number(raw.Price || 0),
|
||||
pb: raw.DocCurrencyCode || pbFallback,
|
||||
|
||||
__tmpMap: {}, // beden → qty
|
||||
lineIdMap: {}, // beden → OrderLineID
|
||||
|
||||
adet: 0,
|
||||
tutar: 0,
|
||||
|
||||
terminTarihi: raw.DeliveryDate || null,
|
||||
isClosed
|
||||
}
|
||||
|
||||
merged[modelKey].push(entry)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
🔑 BEDEN → OrderLineID (DETERMINISTIC & SAFE)
|
||||
-------------------------------------------------------- */
|
||||
const rawLineId =
|
||||
raw.OrderLineID ||
|
||||
raw.OrderLineId ||
|
||||
raw.orderLineID ||
|
||||
null
|
||||
|
||||
if (rawLineId) {
|
||||
entry.lineIdMap[beden] = String(rawLineId)
|
||||
}
|
||||
|
||||
if (qty > 0) {
|
||||
entry.__tmpMap[beden] = (entry.__tmpMap[beden] || 0) + qty
|
||||
entry.adet += qty
|
||||
entry.tutar += qty * entry.fiyat
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
3️⃣ FINAL — grpKey KESİN + AKSBİR FIX
|
||||
======================================================= */
|
||||
const out = []
|
||||
|
||||
for (const rows of Object.values(merged)) {
|
||||
for (const row of rows) {
|
||||
if (!row.__tmpMap) {
|
||||
out.push(row)
|
||||
continue
|
||||
}
|
||||
|
||||
const bedenList = Object.keys(row.__tmpMap)
|
||||
|
||||
// 🔒 TEK VE KESİN KARAR
|
||||
const grpKey = detectBedenGroup(
|
||||
bedenList,
|
||||
row.urunAnaGrubu,
|
||||
row.kategori
|
||||
)
|
||||
|
||||
row.grpKey = grpKey
|
||||
row.bedenMap = { [grpKey]: { ...row.__tmpMap } }
|
||||
|
||||
/* ===================================================
|
||||
🔒 AKSBİR — BOŞLUK BEDEN GERÇEK ADETİ ALIR
|
||||
❗ STD’ye dönme YOK
|
||||
❗ 0 yazma YOK
|
||||
=================================================== */
|
||||
if (grpKey === 'aksbir') {
|
||||
row.bedenMap[grpKey] ??= {}
|
||||
row.bedenMap[grpKey][' '] = Number(row.adet || 0)
|
||||
}
|
||||
|
||||
delete row.__tmpMap
|
||||
out.push(row)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`📦 normalizeOrderLines (v9 + lineIdMap) → ${out.length} satır`
|
||||
)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
/**
|
||||
* ===========================================================
|
||||
* loadProductSizes — FINAL v4.2 (EDITOR SAFE)
|
||||
* -----------------------------------------------------------
|
||||
* ✔ grpKey SADECE form.grpKey
|
||||
* ✔ schemaMap TEK OTORİTE
|
||||
* ✔ edit modda BEDEN LABEL DOKUNULMAZ
|
||||
* ✔ ' ' (boş beden) korunur
|
||||
* ===========================================================
|
||||
*/
|
||||
async loadProductSizes(form, forceRefresh = false, $q = null) {
|
||||
if (!form?.model) return
|
||||
|
||||
const store = this
|
||||
const prevBusy = !!store._uiBusy
|
||||
const prevPrevent = !!store.preventPersist
|
||||
store._uiBusy = true
|
||||
store.preventPersist = true
|
||||
|
||||
try {
|
||||
const grpKey = form.grpKey
|
||||
if (!grpKey) {
|
||||
console.warn('⛔ loadProductSizes iptal → grpKey yok')
|
||||
return
|
||||
}
|
||||
|
||||
const colorKey = form.renk || 'nocolor'
|
||||
const color2Key = form.renk2 || 'no2color'
|
||||
const cacheKey = `${form.model}_${colorKey}_${color2Key}_${grpKey}`
|
||||
|
||||
/* =======================================================
|
||||
♻️ CACHE (LABEL DOKUNMADAN)
|
||||
======================================================= */
|
||||
if (!forceRefresh && sizeCache.value?.[cacheKey]) {
|
||||
const cached = sizeCache.value[cacheKey]
|
||||
bedenStock.value = [...cached.stockArray]
|
||||
stockMap.value = { ...cached.stockMap }
|
||||
console.log(`♻️ loadProductSizes CACHE → ${grpKey}`)
|
||||
return
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
📡 API
|
||||
======================================================= */
|
||||
const params = { code: form.model }
|
||||
if (form.renk) params.color = form.renk
|
||||
if (form.renk2) params.color2 = form.renk2
|
||||
|
||||
const res = await api.get('/product-colorsize', { params })
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
|
||||
if (!data.length) {
|
||||
bedenStock.value = []
|
||||
stockMap.value = {}
|
||||
return
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
📦 STOK MAP (' ' KORUNUR)
|
||||
======================================================= */
|
||||
const apiStockMap = {}
|
||||
for (const x of data) {
|
||||
const key =
|
||||
x.item_dim1_code === null || x.item_dim1_code === ''
|
||||
? ' '
|
||||
: String(x.item_dim1_code)
|
||||
apiStockMap[key] = Number(x.kullanilabilir_envanter ?? 0)
|
||||
}
|
||||
|
||||
const finalStockMap = {}
|
||||
for (const lbl of form.bedenLabels) {
|
||||
finalStockMap[lbl] = apiStockMap[lbl] ?? 0
|
||||
}
|
||||
|
||||
stockMap.value = { ...finalStockMap }
|
||||
bedenStock.value = Object.entries(stockMap.value).map(
|
||||
([beden, stok]) => ({ beden, stok })
|
||||
)
|
||||
|
||||
/* =======================================================
|
||||
💾 CACHE
|
||||
======================================================= */
|
||||
sizeCache.value[cacheKey] = {
|
||||
labels: [...form.bedenLabels],
|
||||
stockArray: [...bedenStock.value],
|
||||
stockMap: { ...stockMap.value }
|
||||
}
|
||||
|
||||
console.log(`✅ loadProductSizes FINAL v4.2 → ${grpKey}`)
|
||||
} catch (err) {
|
||||
console.error('❌ loadProductSizes hata:', err)
|
||||
$q?.notify?.({ type: 'negative', message: 'Beden / stok alınamadı' })
|
||||
} finally {
|
||||
store._uiBusy = prevBusy
|
||||
store.preventPersist = prevPrevent
|
||||
console.log('🧩 Editor beden hydrate', {
|
||||
grpKey: form.grpKey,
|
||||
labels: form.bedenLabels,
|
||||
values: form.bedenler
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
,
|
||||
|
||||
// =======================================================
|
||||
// 🔸 TOPLAM HESAPLAMA (store içi) — X3 SAFE
|
||||
// -------------------------------------------------------
|
||||
// ✔ f.adet / f.tutar hesaplanır
|
||||
// ✔ store.totalAmount ASLA set edilmez
|
||||
// ✔ gerçek toplam → header.TotalAmount
|
||||
// =======================================================
|
||||
updateTotals(f) {
|
||||
// 1️⃣ Satır adet
|
||||
f.adet = (f.bedenler || []).reduce(
|
||||
(a, b) => a + Number(b || 0),
|
||||
0
|
||||
)
|
||||
|
||||
// 2️⃣ Satır tutar
|
||||
const fiyat = Number(f.fiyat) || 0
|
||||
f.tutar = Number((f.adet * fiyat).toFixed(2))
|
||||
|
||||
// 3️⃣ Header toplam (tek gerçek state)
|
||||
if (this.header) {
|
||||
const total = (this.summaryRows || []).reduce(
|
||||
(sum, r) => sum + Number(r?.tutar || 0),
|
||||
0
|
||||
)
|
||||
|
||||
this.header.TotalAmount = Number(total.toFixed(2))
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
,
|
||||
|
||||
// =======================================================
|
||||
// 🔸 GRUP ANAHTARI TESPİTİ
|
||||
// =======================================================
|
||||
activeGroupKeyForRow(row) {
|
||||
const g = (row?.urunAnaGrubu || '').toUpperCase()
|
||||
if (g.includes('TAKIM')) return 'tak'
|
||||
if (g.includes('PANTOLON')) return 'pan'
|
||||
if (g.includes('GÖMLEK')) return 'gom'
|
||||
if (g.includes('AYAKKABI')) return 'ayk'
|
||||
if (g.includes('YAŞ')) return 'yas'
|
||||
return 'tak'
|
||||
},
|
||||
/* =======================================================
|
||||
🔹 MODE YÖNETİMİ — new / edit arası geçiş
|
||||
======================================================= */
|
||||
setMode(mode) {
|
||||
if (!['new', 'edit', 'view'].includes(mode)) {
|
||||
console.warn('⚠️ Geçersiz mode:', mode)
|
||||
return
|
||||
}
|
||||
|
||||
this.mode = mode
|
||||
console.log(`🧭 Order mode set edildi → ${mode}`)
|
||||
}
|
||||
,
|
||||
/* ===========================================================
|
||||
🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE)
|
||||
-----------------------------------------------------------
|
||||
✔ NEW → INSERT, EDIT → UPDATE (tek karar noktası)
|
||||
✔ Controlled submit → route guard SUSAR
|
||||
✔ Snapshot temizliği route öncesi
|
||||
✔ Kaydet → edit replace → backend reload
|
||||
✔ Listeye giderken guard popup 1 kez bypass
|
||||
✔ ✅ PRE-VALIDATE → prItemVariant olmayan kombinasyonlar kaydı DURDURUR
|
||||
=========================================================== */
|
||||
async submitAllReal($q, router, form, summaryRows, productCache) {
|
||||
let serverOrderId = null
|
||||
let serverOrderNo = null
|
||||
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
// 🔒 Kontrollü submit → route leave guard susar
|
||||
this.isControlledSubmit = true
|
||||
|
||||
const isNew = this.mode === 'new'
|
||||
const { header, lines } = this.buildFinalOrderJson()
|
||||
|
||||
// =======================================================
|
||||
// 🧾 DEBUG — FRONTEND → BACKEND GİDEN PAYLOAD
|
||||
// =======================================================
|
||||
console.groupCollapsed(
|
||||
`%c📤 ORDER PAYLOAD (${this.mode})`,
|
||||
'color:#c9a873;font-weight:bold'
|
||||
)
|
||||
|
||||
console.log('HEADER:', JSON.parse(JSON.stringify(header)))
|
||||
|
||||
lines.forEach((l, i) => {
|
||||
console.log(`LINE[${i}]`, {
|
||||
OrderLineID: l.OrderLineID,
|
||||
ClientKey: l.ClientKey,
|
||||
ItemCode: l.ItemCode,
|
||||
ColorCode: l.ColorCode,
|
||||
ItemDim1Code: l.ItemDim1Code,
|
||||
ItemDim2Code: l.ItemDim2Code,
|
||||
ItemDim3Code: l.ItemDim3Code,
|
||||
Qty1: l.Qty1,
|
||||
ComboKey: l.ComboKey
|
||||
})
|
||||
})
|
||||
|
||||
console.groupEnd()
|
||||
|
||||
// =======================================================
|
||||
// 🧾 DEBUG (opsiyonel helper)
|
||||
// =======================================================
|
||||
this.debugOrderPayload?.(header, lines, 'PRE-VALIDATE')
|
||||
|
||||
// =======================================================
|
||||
// 🧪 PRE-VALIDATE — prItemVariant ön kontrol
|
||||
// - invalid varsa CREATE/UPDATE ÇALIŞMAZ
|
||||
// =======================================================
|
||||
const v = await api.post('/order/validate', { header, lines })
|
||||
const invalid = v?.data?.invalid || []
|
||||
|
||||
if (invalid.length > 0) {
|
||||
await this.showInvalidVariantDialog?.($q, invalid)
|
||||
return // ❌ create / update ÇALIŞMAZ
|
||||
}
|
||||
|
||||
console.log('📤 submitAllReal payload', {
|
||||
mode: this.mode,
|
||||
lines: lines.length,
|
||||
deletes: lines.filter(l => l._deleteSignal).length
|
||||
})
|
||||
|
||||
/* =======================================================
|
||||
🚀 API CALL — TEK NOKTA
|
||||
======================================================= */
|
||||
const resp = await api.post(
|
||||
isNew ? '/order/create' : '/order/update',
|
||||
{ header, lines }
|
||||
)
|
||||
|
||||
const data = resp?.data || {}
|
||||
|
||||
serverOrderId =
|
||||
data.orderID ||
|
||||
data.orderHeaderID ||
|
||||
data.id ||
|
||||
header?.OrderHeaderID
|
||||
|
||||
serverOrderNo =
|
||||
data.orderNumber ||
|
||||
data.orderNo ||
|
||||
header?.OrderNumber
|
||||
|
||||
if (!serverOrderId) {
|
||||
throw new Error('OrderHeaderID backend’den dönmedi')
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
🔁 MODE SWITCH → EDIT
|
||||
======================================================= */
|
||||
this.setMode('edit')
|
||||
|
||||
// Header patch (ID / No)
|
||||
this.header = {
|
||||
...this.header,
|
||||
OrderHeaderID: serverOrderId,
|
||||
OrderNumber: serverOrderNo
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
🧹 KRİTİK: Snapshot + Dirty temizliği
|
||||
❗ ROUTE değişmeden ÖNCE
|
||||
======================================================= */
|
||||
this.updateHeaderTotals?.()
|
||||
this.markAsSaved?.()
|
||||
|
||||
/* =======================================================
|
||||
🧹 KRİTİK: NEW → EDIT geçişinde TÜM SNAPSHOT TEMİZLENİR
|
||||
======================================================= */
|
||||
this.clearAllOrderSnapshots()
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `Sipariş kaydedildi: ${serverOrderNo || ''}`.trim()
|
||||
})
|
||||
|
||||
/* =======================================================
|
||||
🔀 ROUTE REPLACE (EDIT MODE)
|
||||
- aynı sayfa → param değişti
|
||||
- guard 1 kez bypass
|
||||
======================================================= */
|
||||
this.allowRouteLeaveOnce = true
|
||||
|
||||
await router.replace({
|
||||
name: 'order-entry',
|
||||
params: { orderHeaderID: serverOrderId },
|
||||
query: { mode: 'edit', source: 'backend' }
|
||||
})
|
||||
|
||||
/* =======================================================
|
||||
🔄 BACKEND RELOAD (TEK GERÇEK KAYNAK)
|
||||
======================================================= */
|
||||
await this.openExistingForEdit(serverOrderId, {
|
||||
$q,
|
||||
form,
|
||||
summaryRowsRef: summaryRows,
|
||||
productCache
|
||||
})
|
||||
|
||||
/* =======================================================
|
||||
❓ USER NEXT STEP
|
||||
======================================================= */
|
||||
const choice = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Sipariş Kaydedildi',
|
||||
options: {
|
||||
type: 'radio',
|
||||
model: 'continue',
|
||||
items: [
|
||||
{ label: '✏️ Düzenlemeye Devam', value: 'continue' },
|
||||
{ label: '🖨 Yazdır', value: 'print' },
|
||||
{ label: '📋 Listeye Dön', value: 'list' }
|
||||
]
|
||||
},
|
||||
ok: { label: 'Seç' },
|
||||
cancel: { label: 'Kapat' }
|
||||
})
|
||||
.onOk(v => resolve(v))
|
||||
.onCancel(() => resolve('continue'))
|
||||
})
|
||||
|
||||
/* =======================================================
|
||||
🧭 USER ROUTING
|
||||
======================================================= */
|
||||
if (choice === 'print') {
|
||||
const id = this.header?.OrderHeaderID || serverOrderId
|
||||
if (id) await this.downloadOrderPdf(id)
|
||||
return
|
||||
}
|
||||
|
||||
if (choice === 'list') {
|
||||
this.allowRouteLeaveOnce = true
|
||||
await router.push({ name: 'order-list' })
|
||||
return
|
||||
}
|
||||
|
||||
// continue → sayfada kal (hiçbir şey yapma)
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ submitAllReal:', err)
|
||||
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message:
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
'Kayıt sırasında hata'
|
||||
})
|
||||
|
||||
} finally {
|
||||
// 🔓 Guard’lar normale dönsün
|
||||
this.isControlledSubmit = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
/* =======================================================
|
||||
🧪 SUBMIT ALL TEST
|
||||
======================================================= */
|
||||
async submitAllTest($q = null) {
|
||||
try {
|
||||
const { header, lines } = this.buildFinalOrderJson()
|
||||
|
||||
console.log('🧾 TEST HEADER', Object.keys(header).length, 'alan')
|
||||
console.log(JSON.stringify(header, null, 2))
|
||||
|
||||
console.log('🧾 TEST LINES', lines.length, 'satır')
|
||||
console.log(JSON.stringify(lines, null, 2))
|
||||
|
||||
$q?.notify?.({
|
||||
type: 'info',
|
||||
message: `Header (${Object.keys(header).length}) + Lines (${lines.length}) gösterildi`,
|
||||
position: 'top'
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('❌ submitAllTest hata:', err)
|
||||
$q?.notify?.({
|
||||
type: 'negative',
|
||||
message: 'Gösterimde hata oluştu ❌',
|
||||
position: 'top'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/* =======================================================
|
||||
🧹 KAYIT SONRASI TEMİZLİK
|
||||
======================================================= */
|
||||
afterSubmit(opts = {
|
||||
keepLocalStorage: true,
|
||||
backendPayload: null,
|
||||
resetMode: true // 🔑 yeni
|
||||
}) {
|
||||
try {
|
||||
console.log('🧹 afterSubmit başlatıldı', opts)
|
||||
|
||||
if (opts?.backendPayload?.header?.OrderHeaderID) {
|
||||
this.mergeAndPersistBackendOrder(
|
||||
opts.backendPayload.header.OrderHeaderID,
|
||||
opts.backendPayload
|
||||
)
|
||||
}
|
||||
|
||||
if (!opts?.keepLocalStorage) {
|
||||
this.clearStorage()
|
||||
this.clearTransaction()
|
||||
} else {
|
||||
this.saveSnapshot()
|
||||
}
|
||||
|
||||
this.orders = []
|
||||
this.header = {}
|
||||
this.editingKey = null
|
||||
this.currentOrderId = null
|
||||
|
||||
// 🔐 MODE RESET OPSİYONEL
|
||||
if (opts.resetMode === true) {
|
||||
this.mode = 'new'
|
||||
}
|
||||
|
||||
console.log('✅ afterSubmit tamamlandı.')
|
||||
} catch (err) {
|
||||
console.error('❌ afterSubmit hata:', err)
|
||||
}
|
||||
}
|
||||
|
||||
,
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🟦 BUILD FINAL ORDER JSON — SAFE v26.1 (FINAL)
|
||||
-----------------------------------------------------------
|
||||
✔ ComboKey TEK OTORİTE → buildComboKey (bedenKey ile)
|
||||
✔ UI/Map placeholder: '_' (bedenKey)
|
||||
✔ DB/payload: '' (bedenPayload) → "_" ASLA GİTMEZ
|
||||
✔ payload içinde aynı ComboKey TEK satır
|
||||
✔ backend duplicate guard %100 uyumlu (ComboKey stabil)
|
||||
✔ Final assert: payload’da "_" yakalanırsa patlatır
|
||||
=========================================================== */
|
||||
buildFinalOrderJson () {
|
||||
const auth = useAuthStore()
|
||||
const u = auth?.user || {}
|
||||
const now = dayjs()
|
||||
|
||||
/* =========================
|
||||
HELPERS
|
||||
========================== */
|
||||
const toNum = v => Number(v) || 0
|
||||
const safeStr = v => (v == null ? '' : String(v).trim())
|
||||
|
||||
const formatDateOnly = v => (v ? dayjs(v).format('YYYY-MM-DD') : null)
|
||||
const formatTimeOnly = v => dayjs(v).format('HH:mm:ss')
|
||||
const formatDateTime = v => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : null)
|
||||
|
||||
// ✅ Payload beden normalize: "_" / "-" / "" => ''
|
||||
const normBeden = (v) => {
|
||||
const s = safeStr(v)
|
||||
if (s === '' || s === '_' || s === '-') return '' // payload empty
|
||||
return s
|
||||
}
|
||||
|
||||
/* =========================
|
||||
USER META
|
||||
========================== */
|
||||
const group = safeStr(u?.v3usergroup)
|
||||
const v3name = safeStr(u?.v3_username)
|
||||
const who = (group && v3name) ? `${group} ${v3name}` : (v3name || 'BSS')
|
||||
|
||||
const PCT_CODE_ZERO = '%0'
|
||||
const VAT_CODE_ZERO = '%0'
|
||||
|
||||
/* =========================
|
||||
HEADER
|
||||
========================== */
|
||||
const headerId = this.header?.OrderHeaderID || crypto.randomUUID()
|
||||
const docCurrency = safeStr(this.header?.DocCurrencyCode) || 'TRY'
|
||||
const exRate = toNum(this.header?.ExchangeRate) || 1
|
||||
|
||||
const avgDueSource =
|
||||
this.header?.AverageDueDate ||
|
||||
dayjs(this.header?.OrderDate || now).add(14, 'day')
|
||||
|
||||
const header = {
|
||||
...this.header,
|
||||
|
||||
OrderHeaderID: headerId,
|
||||
OrderDate: formatDateOnly(this.header?.OrderDate || now),
|
||||
OrderTime: formatTimeOnly(now),
|
||||
AverageDueDate: formatDateOnly(avgDueSource),
|
||||
|
||||
DocCurrencyCode: docCurrency,
|
||||
LocalCurrencyCode: safeStr(this.header?.LocalCurrencyCode) || 'TRY',
|
||||
ExchangeRate: exRate,
|
||||
|
||||
CreatedUserName:
|
||||
this.mode === 'edit'
|
||||
? (this.header?.CreatedUserName || who)
|
||||
: who,
|
||||
|
||||
CreatedDate:
|
||||
this.mode === 'edit'
|
||||
? formatDateTime(this.header?.CreatedDate || now)
|
||||
: formatDateTime(now),
|
||||
|
||||
LastUpdatedUserName: who,
|
||||
LastUpdatedDate: formatDateTime(now)
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
LINES — COMBOKEY AGGREGATE (TEK MAP)
|
||||
======================================================= */
|
||||
const lines = []
|
||||
const lineByCombo = new Map() // 🔒 KEY = ComboKey
|
||||
|
||||
const pushOrMerge = (row, ctx) => {
|
||||
const {
|
||||
grpKey,
|
||||
bedenKey, // ✅ sadece ComboKey / Map için ('_' olabilir)
|
||||
bedenPayload, // ✅ DB için ('' / 'S' / 'M' ...)
|
||||
qty,
|
||||
orderLineId,
|
||||
isDeleteSignal
|
||||
} = ctx
|
||||
|
||||
if (qty <= 0 && !isDeleteSignal) return
|
||||
|
||||
// ComboKey stabil kalsın diye bedenKey kullan
|
||||
const comboKey = buildComboKey(row, bedenKey)
|
||||
|
||||
const makeLine = () => ({
|
||||
OrderLineID: orderLineId || '',
|
||||
ClientKey: makeLineClientKey(row, grpKey, bedenKey),
|
||||
ComboKey: comboKey,
|
||||
|
||||
SortOrder: 0,
|
||||
ItemTypeCode: 1,
|
||||
|
||||
ItemCode: safeStr(row.model),
|
||||
ColorCode: safeStr(row.renk),
|
||||
|
||||
// ✅ PAYLOAD: "_" ASLA YOK
|
||||
ItemDim1Code: bedenPayload,
|
||||
|
||||
ItemDim2Code: safeStr(row.renk2),
|
||||
ItemDim3Code: '',
|
||||
|
||||
Qty1: isDeleteSignal ? 0 : qty,
|
||||
Qty2: 0,
|
||||
|
||||
CancelQty1: 0,
|
||||
CancelQty2: 0,
|
||||
|
||||
DeliveryDate: row.terminTarihi
|
||||
? formatDateTime(row.terminTarihi)
|
||||
: null,
|
||||
|
||||
PlannedDateOfLading: row.terminTarihi
|
||||
? formatDateOnly(row.terminTarihi)
|
||||
: null,
|
||||
|
||||
LineDescription: safeStr(row.aciklama),
|
||||
UsedBarcode: '',
|
||||
CostCenterCode: '',
|
||||
|
||||
VatCode: VAT_CODE_ZERO,
|
||||
VatRate: toNum(row.vatRate ?? row.VatRate ?? 0),
|
||||
|
||||
PCTCode: PCT_CODE_ZERO,
|
||||
PCTRate: 0,
|
||||
|
||||
LDisRate1: 0,
|
||||
LDisRate2: 0,
|
||||
LDisRate3: 0,
|
||||
LDisRate4: 0,
|
||||
LDisRate5: 0,
|
||||
|
||||
DocCurrencyCode: header.DocCurrencyCode,
|
||||
PriceCurrencyCode: header.DocCurrencyCode,
|
||||
PriceExchangeRate: toNum(header.ExchangeRate),
|
||||
Price: toNum(row.fiyat),
|
||||
|
||||
BaseProcessCode: 'WS',
|
||||
BaseOrderNumber: header.OrderNumber,
|
||||
BaseCustomerTypeCode: 0,
|
||||
BaseCustomerCode: header.CurrAccCode,
|
||||
BaseSubCurrAccID: null,
|
||||
BaseStoreCode: '',
|
||||
|
||||
OrderHeaderID: headerId,
|
||||
|
||||
CreatedUserName: who,
|
||||
CreatedDate: formatDateTime(row.CreatedDate || now),
|
||||
LastUpdatedUserName: who,
|
||||
LastUpdatedDate: formatDateTime(now),
|
||||
|
||||
SurplusOrderQtyToleranceRate: 0,
|
||||
WithHoldingTaxTypeCode: '',
|
||||
DOVCode: ''
|
||||
})
|
||||
|
||||
const existing = lineByCombo.get(comboKey)
|
||||
|
||||
if (!existing) {
|
||||
const ln = makeLine()
|
||||
lineByCombo.set(comboKey, ln)
|
||||
lines.push(ln)
|
||||
return
|
||||
}
|
||||
|
||||
/* DELETE */
|
||||
if (isDeleteSignal) {
|
||||
if (orderLineId && !existing.OrderLineID) {
|
||||
existing.OrderLineID = orderLineId
|
||||
}
|
||||
existing.Qty1 = 0
|
||||
return
|
||||
}
|
||||
|
||||
/* MERGE */
|
||||
existing.Qty1 += qty
|
||||
|
||||
if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) {
|
||||
existing.OrderLineID = orderLineId
|
||||
}
|
||||
|
||||
existing.Price = toNum(row.fiyat)
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
ORDER ROW LOOP
|
||||
======================================================= */
|
||||
for (const row of this.orders || []) {
|
||||
if (row?.isClosed === true) continue
|
||||
|
||||
const grpKey =
|
||||
row.grpKey ||
|
||||
Object.keys(row.bedenMap || {})[0] ||
|
||||
'GENEL'
|
||||
|
||||
const lineIdMap = row.lineIdMap || {}
|
||||
|
||||
const grouped = row.bedenMap?.[grpKey]
|
||||
const flat =
|
||||
(row.bedenMap && typeof row.bedenMap === 'object' && !grouped)
|
||||
? row.bedenMap
|
||||
: null
|
||||
|
||||
const map = grouped || flat
|
||||
const hasAnyBeden =
|
||||
map && typeof map === 'object' && Object.keys(map).length > 0
|
||||
|
||||
/* 🔹 BEDENSİZ / AKSBİR */
|
||||
if (!hasAnyBeden) {
|
||||
const qty = toNum(row.qty ?? row.Qty1 ?? row.miktar ?? 0)
|
||||
|
||||
// ✅ ComboKey stabil: bedenKey = '_'
|
||||
const bedenKey = '_'
|
||||
// ✅ Payload: boş string
|
||||
const bedenPayload = ''
|
||||
|
||||
let orderLineId = ''
|
||||
if (this.mode === 'edit') {
|
||||
// lineIdMap burada '_' ile tutuluyorsa onu da oku
|
||||
orderLineId =
|
||||
safeStr(lineIdMap?.[bedenKey]) ||
|
||||
safeStr(lineIdMap?.[bedenPayload]) ||
|
||||
safeStr(row.OrderLineID)
|
||||
}
|
||||
|
||||
pushOrMerge(row, {
|
||||
grpKey,
|
||||
bedenKey,
|
||||
bedenPayload,
|
||||
qty,
|
||||
orderLineId,
|
||||
isDeleteSignal: row._deleteSignal === true && !!orderLineId
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
/* 🔹 BEDENLİ */
|
||||
for (const [bedenRaw, qtyRaw] of Object.entries(map)) {
|
||||
const qty = toNum(qtyRaw)
|
||||
|
||||
// ✅ payload beden: '' / 'S' / 'M' ...
|
||||
const bedenPayload = normBeden(bedenRaw)
|
||||
// ✅ combokey beden: boşsa '_' ile stabil kalsın
|
||||
const bedenKey = bedenPayload || '_'
|
||||
|
||||
let orderLineId = ''
|
||||
if (this.mode === 'edit') {
|
||||
// lineIdMap anahtarı sizde hangi bedenle tutuluyorsa ikisini de dene
|
||||
orderLineId =
|
||||
safeStr(lineIdMap?.[bedenKey]) ||
|
||||
safeStr(lineIdMap?.[bedenPayload]) ||
|
||||
(Object.keys(map).length === 1
|
||||
? safeStr(row.OrderLineID)
|
||||
: '')
|
||||
}
|
||||
|
||||
pushOrMerge(row, {
|
||||
grpKey,
|
||||
bedenKey,
|
||||
bedenPayload,
|
||||
qty,
|
||||
orderLineId,
|
||||
isDeleteSignal: row._deleteSignal === true && !!orderLineId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================
|
||||
FINAL SORT
|
||||
======================================================= */
|
||||
lines.forEach((ln, i) => { ln.SortOrder = i + 1 })
|
||||
|
||||
/* =======================================================
|
||||
ASSERT — payload’da "_" OLAMAZ
|
||||
======================================================= */
|
||||
if (lines.some(l => (l.ItemDim1Code || '') === '_' )) {
|
||||
console.error('❌ Payload’da "_" yakalandı', lines.filter(l => l.ItemDim1Code === '_'))
|
||||
throw new Error('Payload ItemDim1Code "_" olamaz')
|
||||
}
|
||||
/* =======================================================
|
||||
🔍 DEBUG — BUILD FINAL ORDER JSON OUTPUT
|
||||
======================================================= */
|
||||
console.groupCollapsed('%c📦 BUILD FINAL ORDER JSON', 'color:#c9a873;font-weight:bold')
|
||||
|
||||
console.log('🧾 HEADER:', header)
|
||||
|
||||
console.table(
|
||||
lines.map((l, i) => ({
|
||||
i: i + 1,
|
||||
OrderLineID: l.OrderLineID,
|
||||
ClientKey: l.ClientKey,
|
||||
ComboKey: l.ComboKey,
|
||||
ItemCode: l.ItemCode,
|
||||
ColorCode: l.ColorCode,
|
||||
ItemDim1Code: JSON.stringify(l.ItemDim1Code), // <-- kritik
|
||||
ItemDim2Code: l.ItemDim2Code,
|
||||
Qty1: l.Qty1,
|
||||
Price: l.Price
|
||||
}))
|
||||
)
|
||||
|
||||
console.groupEnd()
|
||||
|
||||
return { header, lines }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
,/* ===========================================================
|
||||
✅ STORE ACTIONS — FIXED HELPERS
|
||||
- setRowErrorByClientKey
|
||||
- clearRowErrorByClientKey
|
||||
- applyTerminToRowsIfEmpty
|
||||
=========================================================== */
|
||||
setRowErrorByClientKey(clientKey, payload) {
|
||||
if (!clientKey) return
|
||||
if (!Array.isArray(this.summaryRows)) return
|
||||
|
||||
const row = this.summaryRows.find(r => r?.clientKey === clientKey)
|
||||
if (!row) return
|
||||
|
||||
row._error = {
|
||||
code: payload?.code,
|
||||
message: payload?.message
|
||||
}
|
||||
},
|
||||
|
||||
clearRowErrorByClientKey(clientKey) {
|
||||
if (!clientKey) return
|
||||
if (!Array.isArray(this.summaryRows)) return
|
||||
|
||||
const row = this.summaryRows.find(r => r?.clientKey === clientKey)
|
||||
if (!row) return
|
||||
|
||||
if (row._error) {
|
||||
delete row._error
|
||||
}
|
||||
},
|
||||
|
||||
applyTerminToRowsIfEmpty(dateStr) {
|
||||
if (!dateStr) return
|
||||
if (!Array.isArray(this.summaryRows)) return
|
||||
|
||||
// ❗ reassign YOK — patch/mutate
|
||||
for (const r of this.summaryRows) {
|
||||
if (!r?.terminTarihi || r.terminTarihi === '') {
|
||||
r.terminTarihi = dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// opsiyonel ama genelde doğru:
|
||||
this.persistLocalStorage?.()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} // actions sonu
|
||||
}) // defineStore sonu
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 BEDEN LABEL NORMALİZASYONU (exported helper)
|
||||
=========================================================== */
|
||||
export function normalizeBedenLabel(v) {
|
||||
if (v === null || v === undefined) return ' '
|
||||
let s = String(v).trim()
|
||||
if (s === '') return ' '
|
||||
// 44R, 50L vb. son ekleri at
|
||||
s = s.replace(/(^\d+)\s*[A-Z]?$/i, '$1')
|
||||
s = s.toUpperCase()
|
||||
|
||||
// harfli bedenlerin normalizasyonu
|
||||
const map = {
|
||||
'XS': 'XS', 'S': 'S', 'M': 'M', 'L': 'L', 'XL': 'XL',
|
||||
'XXL': '2XL', '2XL': '2XL', '3XL': '3XL', '4XL': '4XL',
|
||||
'5XL': '5XL', '6XL': '6XL', '7XL': '7XL', 'STD': 'STD'
|
||||
}
|
||||
if (map[s]) return map[s]
|
||||
|
||||
// tamamen sayıysa string olarak döndür
|
||||
if (/^\d+$/.test(s)) return s
|
||||
|
||||
// virgüllü değer geldiyse ilkini al
|
||||
if (s.includes(',')) return s.split(',')[0].trim()
|
||||
return s
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 BEDEN GRUBU ALGILAMA HELPER’I
|
||||
-----------------------------------------------------------
|
||||
Gelen beden listesini, ürün grubu/kategori bilgisine göre
|
||||
doğru grup anahtarına dönüştürür (ayk, yas, pan, gom, tak, aksbir).
|
||||
-----------------------------------------------------------
|
||||
=========================================================== */
|
||||
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
|
||||
const list = Array.isArray(bedenList) && bedenList.length > 0
|
||||
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
|
||||
: [' ']
|
||||
|
||||
const ana = (urunAnaGrubu || '')
|
||||
.toUpperCase()
|
||||
.trim()
|
||||
.replace(/\(.*?\)/g, '')
|
||||
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
|
||||
const kat = (urunKategori || '').toUpperCase().trim()
|
||||
// 🔸 Aksesuar ise "aksbir"
|
||||
const aksesuarGruplari = [
|
||||
'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP','ÇORAP',
|
||||
'FULAR','MENDIL','MENDİL','KASKOL','ASKI',
|
||||
'YAKA','KOL DUGMESI','KOL DÜĞMESİ'
|
||||
]
|
||||
const giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT']
|
||||
// 🔸 Pantolon özel durumu
|
||||
if (
|
||||
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
|
||||
!giyimGruplari.some(g => ana.includes(g))
|
||||
) return 'aksbir'
|
||||
|
||||
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
|
||||
// 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı
|
||||
const allNumeric = list.every(v => /^\d+$/.test(v))
|
||||
if (allNumeric) {
|
||||
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
|
||||
const diffs = nums.slice(1).map((v, i) => v - nums[i])
|
||||
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
|
||||
}
|
||||
|
||||
// 🔸 Yaş grubu (çocuk/garson)
|
||||
if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas'
|
||||
|
||||
// 🔸 Harfli beden varsa doğrudan "gom" (gömlek, üst giyim)
|
||||
const harfliBedenler = ['XS','S','M','L','XL','XXL','3XL','4XL']
|
||||
if (list.some(b => harfliBedenler.includes(b))) return 'gom'
|
||||
|
||||
|
||||
|
||||
|
||||
// 🔸 Varsayılan: takım elbise
|
||||
return 'tak'
|
||||
}
|
||||
|
||||
export function toSummaryRowFromForm(form) {
|
||||
if (!form) return null
|
||||
|
||||
const grpKey = form.grpKey || 'tak'
|
||||
const bedenMap = {}
|
||||
|
||||
const labels = Array.isArray(form.bedenLabels) ? form.bedenLabels : []
|
||||
const values = Array.isArray(form.bedenler) ? form.bedenler : []
|
||||
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
const rawLbl = labels[i]
|
||||
const lbl =
|
||||
rawLbl == null || String(rawLbl).trim() === ''
|
||||
? ' '
|
||||
: String(rawLbl).trim()
|
||||
|
||||
const val = Number(values[i] || 0)
|
||||
if (val > 0) {
|
||||
bedenMap[lbl] = val
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: form.id || newGuid(),
|
||||
OrderLineID: form.OrderLineID || null,
|
||||
|
||||
model: form.model || '',
|
||||
renk: form.renk || '',
|
||||
renk2: form.renk2 || '',
|
||||
|
||||
urunAnaGrubu: form.urunAnaGrubu || '',
|
||||
urunAltGrubu: form.urunAltGrubu || '',
|
||||
aciklama: form.aciklama || '',
|
||||
|
||||
fiyat: Number(form.fiyat || 0),
|
||||
pb: form.pb || 'USD',
|
||||
|
||||
adet: Number(form.adet || 0),
|
||||
tutar: Number(form.tutar || 0),
|
||||
|
||||
grpKey,
|
||||
bedenMap: {
|
||||
[grpKey]: { ...bedenMap }
|
||||
},
|
||||
|
||||
terminTarihi: (form.terminTarihi || '').substring(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* ===========================================================
|
||||
🔹 TOPLAM HESAPLAMA (EXPORT)
|
||||
-----------------------------------------------------------
|
||||
Hem store içinde hem de component tarafında kullanılabilir.
|
||||
=========================================================== */
|
||||
export function updateTotals(f) {
|
||||
f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
|
||||
const fiyat = Number(f.fiyat) || 0
|
||||
f.tutar = (f.adet * fiyat).toFixed(2)
|
||||
return f
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
🔹 EXPORT SET — Tek Merkezli Dışa Aktarımlar
|
||||
=========================================================== */
|
||||
|
||||
/**
|
||||
* 🧩 Shared Reactive Refs
|
||||
* -----------------------------------------------------------
|
||||
* import { sharedOrderEntryRefs } from 'src/stores/orderentryStore'
|
||||
* const { stockMap, bedenStock, sizeCache } = sharedOrderEntryRefs
|
||||
*/
|
||||
export const sharedOrderEntryRefs = {
|
||||
stockMap,
|
||||
bedenStock,
|
||||
sizeCache,
|
||||
|
||||
|
||||
}
|
||||
|
||||
182
ui/src/stores/permissionStore.js
Normal file
182
ui/src/stores/permissionStore.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
|
||||
state: () => ({
|
||||
|
||||
// API route yetkileri
|
||||
routes: [],
|
||||
|
||||
// module+action matrix
|
||||
matrix: [],
|
||||
|
||||
loaded: false
|
||||
}),
|
||||
|
||||
|
||||
getters: {
|
||||
|
||||
/* ================= ADMIN ================= */
|
||||
|
||||
isAdmin () {
|
||||
const auth = useAuthStore()
|
||||
return auth.isAdmin === true
|
||||
},
|
||||
|
||||
|
||||
/* ================= API ROUTE ================= */
|
||||
|
||||
hasApiPermission: (state) => (apiPathOrPerm) => {
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (auth.isAdmin) return true
|
||||
if (!state.loaded) return false
|
||||
if (!apiPathOrPerm) return true
|
||||
|
||||
|
||||
// ============================
|
||||
// 1️⃣ MODULE:ACTION GELDİYSE
|
||||
// ============================
|
||||
|
||||
if (apiPathOrPerm.includes(':')) {
|
||||
|
||||
const [module, action] = apiPathOrPerm.split(':')
|
||||
|
||||
return state.matrix.some(p =>
|
||||
p.module === module &&
|
||||
p.action === action &&
|
||||
p.allowed === true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ============================
|
||||
// 2️⃣ API PATH GELDİYSE
|
||||
// ===========================
|
||||
|
||||
|
||||
const apiPath = apiPathOrPerm
|
||||
|
||||
|
||||
// exact match
|
||||
if (state.routes.some(p =>
|
||||
p.route === apiPath && p.can_access
|
||||
)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// /{id} normalize
|
||||
const normalized = apiPath
|
||||
.replace(/\/\d+/g, '/{id}')
|
||||
|
||||
|
||||
if (state.routes.some(p =>
|
||||
p.route === normalized && p.can_access
|
||||
)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// prefix
|
||||
return state.routes.some(p =>
|
||||
p.can_access && apiPath.startsWith(p.route)
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
/* ================= MODULE ================= */
|
||||
|
||||
hasModule: (state) => (module) => {
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (auth.isAdmin) return true
|
||||
if (!state.loaded) return false
|
||||
|
||||
return state.matrix.some(p =>
|
||||
p.module === module &&
|
||||
p.allowed === true
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
/* ================= ACTION ================= */
|
||||
|
||||
hasPermission: (state) => (module, action) => {
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (auth.isAdmin) return true
|
||||
if (!state.loaded) return false
|
||||
|
||||
return state.matrix.some(p =>
|
||||
p.module === module &&
|
||||
p.action === action &&
|
||||
p.allowed === true
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
actions: {
|
||||
|
||||
async fetchPermissions () {
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (auth.isAdmin) {
|
||||
|
||||
this.routes = []
|
||||
this.matrix = []
|
||||
this.loaded = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
|
||||
// API ROUTES
|
||||
const routesRes = await api.get('/permissions/routes')
|
||||
this.routes = routesRes.data || []
|
||||
|
||||
|
||||
// EFFECTIVE MATRIX
|
||||
const effRes = await api.get('/permissions/effective')
|
||||
this.matrix = effRes.data || []
|
||||
console.group('🔐 PERMISSION DEBUG')
|
||||
|
||||
console.log('API ROUTES:', this.routes)
|
||||
console.log('EFFECTIVE MATRIX:', this.matrix)
|
||||
|
||||
console.groupEnd()
|
||||
|
||||
|
||||
} catch (err) {
|
||||
|
||||
console.error('❌ Permission load failed', err)
|
||||
|
||||
this.routes = []
|
||||
this.matrix = []
|
||||
|
||||
|
||||
} finally {
|
||||
|
||||
this.loaded = true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
clear () {
|
||||
|
||||
this.routes = []
|
||||
this.matrix = []
|
||||
this.loaded = false
|
||||
}
|
||||
}
|
||||
})
|
||||
66
ui/src/stores/statementdetailStore.js
Normal file
66
ui/src/stores/statementdetailStore.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// src/stores/statementdetailStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
export const useStatementdetailStore = defineStore('statementdetail', {
|
||||
state: () => ({
|
||||
details: [],
|
||||
loading: false,
|
||||
error: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async loadDetails ({ accountCode, startDate, endDate, parislemler }) {
|
||||
if (!accountCode) {
|
||||
this.error = 'Geçerli bir cari kod seçilmedi.'
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
// ✅ Params (arrayFormat=repeat global)
|
||||
const params = {
|
||||
startdate: startDate,
|
||||
enddate: endDate
|
||||
}
|
||||
|
||||
if (Array.isArray(parislemler) && parislemler.length > 0) {
|
||||
params.parislemler = parislemler.filter(
|
||||
p => p !== undefined && p !== null && p !== ''
|
||||
)
|
||||
}
|
||||
|
||||
// 🔐 TOKEN + SERIALIZER + ERROR HANDLING OTOMATİK
|
||||
const res = await api.get(
|
||||
`/statements/${accountCode}/details`,
|
||||
{ params }
|
||||
)
|
||||
|
||||
this.details = res.data || []
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Details yüklenemedi:', err)
|
||||
this.error =
|
||||
err?.data?.message ||
|
||||
err?.message ||
|
||||
'Detaylar yüklenemedi'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
getDetailsByBelge (belgeNo) {
|
||||
return this.details.filter(
|
||||
d => d.belge_ref_numarasi === belgeNo
|
||||
)
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.details = []
|
||||
this.loading = false
|
||||
this.error = null
|
||||
}
|
||||
}
|
||||
})
|
||||
163
ui/src/stores/statementheaderStore.js
Normal file
163
ui/src/stores/statementheaderStore.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// src/stores/statementheaderStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
import qs from 'qs'
|
||||
import dayjs from 'src/boot/dayjs'
|
||||
|
||||
export const useStatementheaderStore = defineStore('statementheader', {
|
||||
state: () => ({
|
||||
headers: [], // Ana tablo verileri
|
||||
details: {}, // Alt tablolar (belge bazlı)
|
||||
loading: false, // Yükleme durumu
|
||||
groupOpen: {} // Para birimi bazlı aç/kapa durumu
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 🔹 Benzersiz para birimleri listesi
|
||||
currencies(state) {
|
||||
const set = new Set()
|
||||
for (const r of state.headers) {
|
||||
set.add(r.para_birimi || '—')
|
||||
}
|
||||
return Array.from(set).sort()
|
||||
},
|
||||
|
||||
// 🔹 Her para birimi için toplam borç / alacak / bakiye
|
||||
totalsByCurrency(state) {
|
||||
const out = {}
|
||||
|
||||
for (const r of state.headers) {
|
||||
const k = r.para_birimi || '—'
|
||||
if (!out[k]) {
|
||||
out[k] = { borc: 0, alacak: 0, bakiye: 0, count: 0 }
|
||||
}
|
||||
|
||||
out[k].borc += Number(r.borc) || 0
|
||||
out[k].alacak += Number(r.alacak) || 0
|
||||
out[k].bakiye += Number(r.bakiye) || 0
|
||||
out[k].count += 1
|
||||
}
|
||||
|
||||
return out
|
||||
},
|
||||
|
||||
// 🔹 QTable için satırlar (group + data)
|
||||
groupedRows: (state) => {
|
||||
const grouped = {}
|
||||
|
||||
for (const row of state.headers) {
|
||||
const k = row.para_birimi || '—'
|
||||
if (!grouped[k]) grouped[k] = []
|
||||
grouped[k].push(row)
|
||||
}
|
||||
|
||||
const output = []
|
||||
|
||||
for (const [currency, rows] of Object.entries(grouped)) {
|
||||
if (!rows.length) continue
|
||||
|
||||
// 📅 Tarihe göre sırala
|
||||
const sorted = [...rows].sort(
|
||||
(a, b) => new Date(a.belge_tarihi) - new Date(b.belge_tarihi)
|
||||
)
|
||||
|
||||
const lastRow = sorted.at(-1)
|
||||
const lastBalance =
|
||||
lastRow && lastRow.bakiye != null
|
||||
? Number(lastRow.bakiye)
|
||||
: 0
|
||||
|
||||
// 🔹 Grup satırı
|
||||
output.push({
|
||||
_type: 'group',
|
||||
para_birimi: currency,
|
||||
sonBakiye: lastBalance
|
||||
})
|
||||
|
||||
// 🔹 Alt satırlar
|
||||
if (state.groupOpen[currency] !== false) {
|
||||
sorted.forEach(r => {
|
||||
output.push({ ...r, _type: 'data' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/* ==========================================================
|
||||
🔄 ANA STATEMENT LİSTESİ
|
||||
========================================================== */
|
||||
async loadStatements(params = {}) {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const { data } = await api.get(
|
||||
'/statements',
|
||||
{
|
||||
params,
|
||||
paramsSerializer: p =>
|
||||
qs.stringify(p, { arrayFormat: 'repeat' })
|
||||
}
|
||||
)
|
||||
|
||||
this.headers = Array.isArray(data) ? data : []
|
||||
|
||||
|
||||
// 🔹 Yeni gelen para birimleri default açık
|
||||
for (const k of this.currencies) {
|
||||
if (!(k in this.groupOpen)) {
|
||||
this.groupOpen[k] = true
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Statements yüklenemedi:', err)
|
||||
this.headers = []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/* ==========================================================
|
||||
📄 BELGE DETAYLARI
|
||||
========================================================== */
|
||||
async loadDetails(belgeNo) {
|
||||
if (!belgeNo || this.details[belgeNo]) return
|
||||
|
||||
try {
|
||||
const { data } = await api.get(
|
||||
`/statements/${belgeNo}/details`
|
||||
)
|
||||
|
||||
this.details[belgeNo] = Array.isArray(data) ? data : []
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Details yüklenemedi:', err)
|
||||
this.details[belgeNo] = []
|
||||
}
|
||||
},
|
||||
|
||||
/* ==========================================================
|
||||
🔘 GRUP AÇ / KAPA
|
||||
========================================================== */
|
||||
toggleGroup(currency) {
|
||||
const key = currency || '—'
|
||||
this.groupOpen[key] = !this.groupOpen[key]
|
||||
},
|
||||
|
||||
openAllGroups() {
|
||||
for (const k of this.currencies) {
|
||||
this.groupOpen[k] = true
|
||||
}
|
||||
},
|
||||
|
||||
closeAllGroups() {
|
||||
for (const k of this.currencies) {
|
||||
this.groupOpen[k] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
13
ui/src/stores/store-flag.d.ts
vendored
Normal file
13
ui/src/stores/store-flag.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
WARNING: DO NOT MODIFY OR DELETE
|
||||
This file is auto-generated by Quasar CLI
|
||||
It's recommended to NOT .gitignore it
|
||||
You don't have to use TypeScript in your project, don't worry
|
||||
*/
|
||||
import "quasar/dist/types/feature-flag.d.ts";
|
||||
|
||||
declare module "quasar/dist/types/feature-flag.d.ts" {
|
||||
interface QuasarFeatureFlags {
|
||||
store: true;
|
||||
}
|
||||
}
|
||||
31
ui/src/stores/userPermissionStore.js
Normal file
31
ui/src/stores/userPermissionStore.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
export const useUserPermissionStore = defineStore('userPerm', {
|
||||
|
||||
state: () => ({
|
||||
rows: [],
|
||||
loading: false,
|
||||
saving: false
|
||||
}),
|
||||
|
||||
actions: {
|
||||
|
||||
async fetch (id) {
|
||||
this.loading = true
|
||||
|
||||
const res = await api.get(`/users/${id}/permissions`)
|
||||
this.rows = res.data || []
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async save (id) {
|
||||
this.saving = true
|
||||
|
||||
await api.post(`/users/${id}/permissions`, this.rows)
|
||||
|
||||
this.saving = false
|
||||
}
|
||||
}
|
||||
})
|
||||
53
ui/src/stores/userSyncStore.js
Normal file
53
ui/src/stores/userSyncStore.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// src/stores/userSyncStore.js
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserSyncStore = defineStore('userSync', {
|
||||
state: () => ({
|
||||
pgUsers: [], // Postgre kullanıcıları
|
||||
msUsers: [], // MSSQL kullanıcıları
|
||||
loading: false,
|
||||
selectedPg: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// 🧩 Dummy veri ile başlat
|
||||
async loadDummy() {
|
||||
this.loading = true
|
||||
await new Promise(r => setTimeout(r, 600)) // küçük gecikme
|
||||
this.pgUsers = [
|
||||
{ id: 1, code: 'mehmetk', full_name: 'Mehmet Keçeci', email: 'm@b.com', mssql_username: 'MKECECI', sync_status: 'synced', is_active: true },
|
||||
{ id: 2, code: 'ayse', full_name: 'Ayşe Yılmaz', email: 'a@y.com', mssql_username: null, sync_status: 'pending', is_active: true },
|
||||
{ id: 3, code: 'ali', full_name: 'Ali Demir', email: 'a@d.com', mssql_username: 'ALI.D', sync_status: 'blocked', is_active: false }
|
||||
]
|
||||
this.msUsers = [
|
||||
{ username: 'MKECECI', first_name: 'Mehmet', last_name: 'Keçeci', email: 'm@b.com', is_blocked: 0 },
|
||||
{ username: 'ALI.D', first_name: 'Ali', last_name: 'Demir', email: 'a@d.com', is_blocked: 1 },
|
||||
{ username: 'YENIUSR', first_name: 'Yeni', last_name: 'Kullanıcı', email: 'y@b.com', is_blocked: 0 }
|
||||
]
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async syncNow() {
|
||||
this.loading = true
|
||||
console.log('🔄 Dummy sync tetiklendi...')
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async map(pgId, msUsername) {
|
||||
const user = this.pgUsers.find(u => u.id === pgId)
|
||||
if (user) {
|
||||
user.mssql_username = msUsername
|
||||
user.sync_status = 'manual'
|
||||
}
|
||||
},
|
||||
|
||||
async unmap(pgId) {
|
||||
const user = this.pgUsers.find(u => u.id === pgId)
|
||||
if (user) {
|
||||
user.mssql_username = null
|
||||
user.sync_status = 'pending'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
54
ui/src/utils/flatten.js
Normal file
54
ui/src/utils/flatten.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// src/utils/flatten.js
|
||||
|
||||
/**
|
||||
* 🔹 sql.Null* türlerini sadeleştirir.
|
||||
* Backend'den gelen objelerde genellikle { String, Int32, Float64, Time, Bool, Valid } alanları vardır.
|
||||
* Bu yardımcı, bunları otomatik olarak primitive değerlere dönüştürür.
|
||||
* Örnek:
|
||||
* { String: "ABC", Valid: true } → "ABC"
|
||||
* { Float64: 10.5, Valid: true } → 10.5
|
||||
* { Bool: false, Valid: true } → false
|
||||
* { Valid: false } → null
|
||||
*/
|
||||
export function flat(value) {
|
||||
if (value === null || value === undefined) return null
|
||||
|
||||
// Eğer sql.Null* objesi geldiyse
|
||||
if (typeof value === 'object' && 'Valid' in value) {
|
||||
if (!value.Valid) return null
|
||||
|
||||
// Farklı tiplerde gelen alanları sırayla dene
|
||||
if ('String' in value) return value.String
|
||||
if ('Float64' in value) return value.Float64
|
||||
if ('Int32' in value) return value.Int32
|
||||
if ('Int16' in value) return value.Int16
|
||||
if ('Bool' in value) return value.Bool
|
||||
if ('Time' in value) {
|
||||
try {
|
||||
// Tarih objesi veya ISO string olabilir
|
||||
const t = value.Time
|
||||
return typeof t === 'string' ? t : new Date(t).toISOString()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Düz değer (primitive)
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔹 Objeyi tamamen düzleştirir (tüm alanlarda flat uygular)
|
||||
* Örnek:
|
||||
* flattenObject({ A: { String: "x", Valid: true }, B: 5 })
|
||||
* → { A: "x", B: 5 }
|
||||
*/
|
||||
export function flattenObject(obj = {}) {
|
||||
const result = {}
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
result[key] = flat(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
86
ui/src/utils/formatters.js
Normal file
86
ui/src/utils/formatters.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/* ===========================================================
|
||||
📦 src/utils/formatters.js
|
||||
Tarih, para, sayı, yüzde formatlama yardımcıları
|
||||
=========================================================== */
|
||||
|
||||
/**
|
||||
* 📅 formatDateInput(value)
|
||||
* SQL veya ISO datetime'i "yyyy-MM-dd" formatına çevirir.
|
||||
* <input type="date"> için uygundur.
|
||||
* Örnek: "2025-11-07 00:00:00" → "2025-11-07"
|
||||
*/
|
||||
export function formatDateInput(value) {
|
||||
if (!value) return ''
|
||||
try {
|
||||
if (typeof value === 'string') {
|
||||
// "2025-11-07 00:00:00" → "2025-11-07"
|
||||
if (value.includes(' ')) return value.split(' ')[0]
|
||||
// "2025-11-07T00:00:00Z" → "2025-11-07"
|
||||
if (value.includes('T')) return value.split('T')[0]
|
||||
// zaten yyyy-MM-dd ise aynen dön
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString().split('T')[0]
|
||||
}
|
||||
return ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 📅 formatDateDisplay(value)
|
||||
* "2025-10-31" veya "2025-10-31 00:00:00" → "31.10.2025"
|
||||
* Sadece ekranda gösterim için.
|
||||
*/
|
||||
export function formatDateDisplay(value) {
|
||||
if (!value) return ''
|
||||
try {
|
||||
const d = new Date(value)
|
||||
if (isNaN(d)) return ''
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const yyyy = d.getFullYear()
|
||||
return `${dd}.${mm}.${yyyy}`
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 💰 formatMoney(value, currency)
|
||||
* Sayıyı belirtilen para birimiyle biçimlendirir.
|
||||
* Varsayılan: TRY
|
||||
* Örnek: formatMoney(12500, 'USD')
|
||||
*/
|
||||
export function formatMoney(value, currency = 'TRY') {
|
||||
const n = Number(value || 0)
|
||||
return new Intl.NumberFormat('tr-TR', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔢 formatNumber(value, fraction)
|
||||
* Genel sayı biçimlendirme (küsuratlı veya küsuratsız)
|
||||
*/
|
||||
export function formatNumber(value, fraction = 2) {
|
||||
const n = Number(value || 0)
|
||||
return n.toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: fraction,
|
||||
maximumFractionDigits: fraction,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 💬 formatPercent(value)
|
||||
* 0.1 → "%10.00" şeklinde gösterir
|
||||
*/
|
||||
export function formatPercent(value) {
|
||||
const n = Number(value || 0)
|
||||
return `${(n * 100).toFixed(2)}%`
|
||||
}
|
||||
Reference in New Issue
Block a user