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

7
ui/src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<router-view />
</template>
<script setup>
//
</script>

View 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
View File

21
ui/src/boot/axios.js Normal file
View 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 keyin
if (token) {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${token}`
}
return config
})
})

14
ui/src/boot/dayjs.js Normal file
View 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'
// 🔹 Pluginleri aktif et
dayjs.extend(customParseFormat)
dayjs.extend(relativeTime)
dayjs.extend(localizedFormat)
dayjs.locale('tr')
export default dayjs

View 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>

View 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
View 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 (MODELAÇ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 (headerla 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 (adetfiyatpbtutartermin 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İ (LOWMIDHIGH)
=========================================================== */
.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ış overflowu 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 bodynin 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;
}

View 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ı

View File

@@ -0,0 +1,7 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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>

View 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
View 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
View 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ı scrollda
======================================================== -->
<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="ı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 ıklaması"
filled
dense
autogrow
maxlength="1500"
counter
placeholder="Siparişe genel ı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 (v22Final) — 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
- UIda 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 lookuplar
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 headera 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ş ı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ç: gatewaye 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
)
// backendden 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 stokMapinde 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 “4658 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/kategoriye 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 (fallbackli)
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>

View 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
View 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 </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>
<!-- 📝 ı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>
<!-- 🔗 -->
<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 </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>

View File

@@ -0,0 +1 @@
<template></template>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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ı /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] Storedan 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
View 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
View File

@@ -0,0 +1,383 @@
<template>
<q-page class="user-detail-page">
<!-- LOADING -->
<q-inner-loading :showing="loading">
<q-spinner size="48px" />
</q-inner-loading>
<!-- ================= STICKY HEADER ================= -->
<div class="sticky-stack">
<div class="filter-bar row q-col-gutter-md q-mb-sm">
<div class="col-3">
<div class="text-caption text-grey-7 q-mb-xs">Kullanıcı Kodu</div>
<q-input v-model="form.code" dense filled />
</div>
<div class="col-4">
<div class="text-caption text-grey-7 q-mb-xs">Ad Soyad</div>
<q-input v-model="form.full_name" dense filled />
</div>
<div class="col-2 flex items-end">
<q-toggle v-model="form.is_active" label="Aktif" color="primary" />
</div>
<q-badge
:color="hasPassword ? 'positive' : 'grey'"
class="q-ml-sm"
>
{{ hasPassword ? 'Parola Var' : 'Parola Yok' }}
</q-badge>
</div>
<div class="save-toolbar">
<div class="text-subtitle2 text-weight-bold">
{{ pageTitle }}
</div>
<div>
<q-btn
:label="saveLabel"
color="primary"
icon="save"
:loading="saving"
@click="onSave"
/>
<q-btn
label="LİSTEYE DÖN"
flat
icon="arrow_back"
class="q-ml-sm"
@click="goList"
/>
</div>
</div>
</div>
<!-- ================= BODY ================= -->
<div class="q-pa-md">
<!-- 🔐 PASSWORD ACTIONS -->
<q-card flat bordered class="q-mb-md">
<q-card-section class="row items-center justify-between">
<div>
<div class="text-subtitle2 text-weight-bold">Parola İşlemleri</div>
<div class="text-caption text-grey-7">
Kullanıcıya parola oluşturma / sıfırlama bağlantısı e-posta ile gönderilir.
</div>
<div class="text-caption q-mt-xs">
<span class="text-grey-7">E-posta:</span>
<span class="text-weight-medium q-ml-xs">{{ form.email || '-' }}</span>
</div>
<div v-if="lastPasswordMailSentAt" class="text-caption q-mt-xs text-grey-7">
Son gönderim: {{ lastPasswordMailSentAt }}
</div>
</div>
<div class="row items-center">
<q-btn
label="PAROLA MAİLİ GÖNDER"
color="primary"
icon="mail"
:disable="!canSendPasswordMail"
:loading="sendingPasswordMail"
@click="confirmSendPasswordMail"
/>
</div>
</q-card-section>
</q-card>
<!-- USER FORM -->
<q-card flat bordered>
<q-card-section>
<div class="row q-col-gutter-md">
<!-- EMAIL -->
<div class="col-4">
<div class="text-caption text-grey-7 q-mb-xs">E-Posta</div>
<q-input
v-model="form.email"
dense
filled
type="email"
:rules="[emailRule]"
lazy-rules
/>
</div>
<!-- PHONE -->
<div class="col-4">
<div class="text-caption text-grey-7 q-mb-xs">Telefon</div>
<q-input
v-model="form.mobile"
dense
filled
placeholder="+90XXXXXXXXXX"
mask="+#############"
fill-mask
:rules="[phoneRule]"
lazy-rules
/>
</div>
<!-- ADDRESS -->
<div class="col-4">
<div class="text-caption text-grey-7 q-mb-xs">Adres</div>
<q-input
v-model="form.address"
type="textarea"
dense
filled
autogrow
/>
</div>
<!-- ROLES -->
<div class="col-6">
<div class="text-caption text-grey-7 q-mb-xs">Roller</div>
<q-select
v-model="form.roles"
:options="roleOptions"
option-label="label"
option-value="value"
emit-value
map-options
multiple
use-input
use-chips
dense
filled
behavior="menu"
>
<template #option="scope">
<q-item v-bind="scope.itemProps" clickable>
<q-item-section avatar>
<q-checkbox
:model-value="scope.selected"
@update:model-value="scope.toggleOption(scope.opt)"
/>
</q-item-section>
<q-item-section>
{{ scope.opt.label }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<!-- DEPARTMENT -->
<div class="col-3">
<div class="text-caption text-grey-7 q-mb-xs">Departman</div>
<q-select
v-model="form.departments"
:options="departmentOptions"
option-label="label"
option-value="value"
emit-value
map-options
use-input
dense
filled
/>
</div>
<!-- PIYASALAR -->
<div class="col-3">
<div class="text-caption text-grey-7 q-mb-xs">Piyasalar</div>
<q-select
v-model="form.piyasalar"
:options="piyasaOptions"
option-label="label"
option-value="value"
emit-value
map-options
multiple
use-input
use-chips
dense
filled
behavior="menu"
>
<template #option="scope">
<q-item v-bind="scope.itemProps" clickable>
<q-item-section avatar>
<q-checkbox
:model-value="scope.selected"
@update:model-value="scope.toggleOption(scope.opt)"
/>
</q-item-section>
<q-item-section>
{{ scope.opt.label }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<!-- NEBIM -->
<div class="col-12">
<div class="text-caption text-grey-7 q-mb-xs">Nebim Kullanıcıları</div>
<q-select
v-model="form.nebim_users"
:options="nebimUserOptions"
option-label="label"
option-value="value"
emit-value
map-options
use-input
dense
filled
/>
</div>
</div>
</q-card-section>
</q-card>
</div>
</q-page>
</template>
<script setup>
import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { storeToRefs } from 'pinia'
import { useUserDetailStore } from 'src/stores/UserDetailStore'
import { usePermission } from 'src/composables/usePermission'
const { canRead, canWrite, canUpdate } = usePermission()
const canReadOrder = canRead('order')
const canWriteOrder = canWrite('order')
const canUpdateOrder = canUpdate('order')
const $q = useQuasar()
const route = useRoute()
const router = useRouter()
const store = useUserDetailStore()
/* 🔒 REAKTİVİTE */
const {
form,
loading,
saving,
roleOptions,
departmentOptions,
piyasaOptions,
nebimUserOptions,
sendingPasswordMail,
lastPasswordMailSentAt
} = storeToRefs(store)
const codeRule = v => !!v || 'Kullanıcı kodu zorunludur'
/* ================= MODE ================= */
const mode = computed(() => route.meta.mode || 'edit')
const isNew = computed(() => mode.value === 'new')
const isEdit = computed(() => mode.value === 'edit')
const isView = computed(() => mode.value === 'view')
const userId = computed(() => (isEdit.value || isView.value) ? Number(route.params.id) : null)
const hasPassword = computed(() => store.hasPassword)
const pageTitle = computed(() => (isNew.value ? 'Yeni Kullanıcı' : 'Kullanıcı Düzenleme'))
const saveLabel = computed(() => (isNew.value ? 'KAYDET' : 'GÜNCELLE'))
/* ================= VALIDATION ================= */
const emailRule = v =>
!v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Geçerli bir e-posta giriniz'
const phoneRule = v =>
!v || /^\+\d{10,15}$/.test(v.replace(/_/g, '')) || 'Telefon +90XXXXXXXXXX formatında olmalı'
const canSendPasswordMail = computed(() => {
if (isNew.value) return false // önce kullanıcı oluşmalı
if (!userId.value) return false
if (!form.value.is_active) return false // pasif kullanıcıya mail yok
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((form.value.email || '').trim())
})
/* ================= LIFECYCLE ================= */
watch(
() => userId.value,
async (id) => {
await store.fetchLookups()
if (!id) {
store.resetForm()
return
}
await store.fetchUser(id)
},
{ immediate: true }
)
/* ================= ACTIONS ================= */
async function onSave () {
try {
console.log('🟢 onSave() START', { mode: mode.value })
if (form.value.mobile) {
form.value.mobile = form.value.mobile.replace(/_/g, '').trim()
}
let id
if (isNew.value) {
id = await store.createUser()
console.log('➡️ CREATE → EDIT MODE id=', id)
// 🔄 EDIT MODEA GEÇ
router.replace({
name: 'user-edit',
params: { id }
})
} else {
await store.saveUser(userId.value)
router.push({ name: 'user-list' })
}
$q.notify({ type: 'positive', message: 'İşlem başarılı' })
} catch (e) {
console.error('❌ onSave ERROR', e)
$q.notify({ type: 'negative', message: store.error || 'İşlem başarısız' })
}
}
function goList () {
router.push({ name: 'user-list' })
}
function confirmSendPasswordMail () {
$q.dialog({
title: 'Parola maili gönderilsin mi?',
message: `${form.value.email} adresine parola oluşturma/sıfırlama bağlantısı gönderilecek.`,
cancel: true,
persistent: true
}).onOk(async () => {
await sendPasswordMail()
})
}
async function sendPasswordMail () {
try {
await store.sendPasswordMail(userId.value)
$q.notify({ type: 'positive', message: 'Parola maili gönderildi' })
} catch {
$q.notify({ type: 'negative', message: store.error || 'Mail gönderilemedi' })
}
}
</script>

View 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
View 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>

View 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
View 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>

View 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ı /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] Storedan 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
View 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
View 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
View 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
View 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

View File

@@ -0,0 +1,62 @@
// src/services/orderService.js
import { get, post, put } from './api'
/**
* 🔹 Tek bir siparişi IDye 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
}
}

View 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
}
}
}
}
})

View 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 (DBsiz): 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 || []
}
}
})

View 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
}
}
}
})

View 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
}
}
}
})

View 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
View 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
View 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: '',
// 🔹 Frontende ö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 indexi
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 refleri 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 indexini bulur
const findExistingIndexByForm = () =>
summaryRows.value.findIndex(r => isSameCombo(r, form))
/* ===========================================================
🔹 Ürün Ana Grubu Bazında Gruplanmış Satırlar
groupedRows computed fonksiyonu, satırları urunAnaGrubuna 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 summaryRowsa 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 3944)
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 “4658 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ı LocalStoragedan yüklendi:', summaryRows.value.length)
} else {
console.log(' LocalStorage boş, grid başlatılmadı.')
}
})
// 🔄 Store değişiklikleri anlık olarak gride 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 endpointinden 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
Backendde 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 {
// Storedaki fetchMinPrice fonksiyonu backendden 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 ERPden renk+model bazlı beden ve stok bilgisini çeker.
Ayrıca MSSQL stoklarıyla merge eder ve cacheler.
=========================================================== */
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)
// 💾 Cacheden veri varsa ve forceRefresh=false ise cache kullan
if (!forceRefresh && sizeCache.value[key]) {
console.log(`💾 Cacheden 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 patternini 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 summaryRowsa 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, // formdaki 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 editingi 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)
Griddeki 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 maplerini
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
storedan 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 idye göre, yoksa indexe 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 formun 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
LocalStoragea 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 → forma 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 UIda 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 stokMapinde 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, backendden 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 (12)
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
View 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 (MODELAÇ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 (headerla 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 (adetfiyatpbtutartermin 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İ (LOWMIDHIGH)
=========================================================== */
.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ış overflowu 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 bodynin 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
View 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 arrayi (summaryRows ile senkron)
loading: false,
selected: null, // UIde 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 keyi
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".
UIyi 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 (componentten çağrılır) */
watchOrders() {
watch(
() => this.orders,
() => {
// her değişimde full storage yaz
this.saveToStorage()
},
{ deep: true }
)
},
/* ==========================================================
CRUD — Frontend gridi 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ı storagea 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 (backendin 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 storagea 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 persistKeyi 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 edite 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
// currentOrderIdyi istersen koruyabilirsin; biz editte geri yüklüyoruz.
// burada nulllıyoruz:
this.currentOrderId = null
this.mode = 'new'
this.loading = false
this.error = null
console.log('🧹 afterSubmit: UI temizlendi, snapshot storageda.')
} 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
}

View 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ı'
}
}
}
}
})

View 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ı'
}
}
}
}
})

View 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
View 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
})

View 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
}
}
}
})

View 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
}
}
}
})

View 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 → OrderHeaderIDye 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 idyi 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 helperları
-------------------------------------------------------- */
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} localStoragea 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 stateleri tek stringe indirger
- X3: orders+header yetmez → mode, summaryRows, id/no, mapler 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 mapler
// (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 guardları (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 guarddan ö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⃣ UIdan 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 storeda kullanılır
✔ aksbir → ' ' bedeni = GERÇEK adet
✔ backend satırlarında BEDEN → OrderLineID mapi ü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
❗ STDye 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 backendden 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 {
// 🔓 Guardlar 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: payloadda "_" 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 — payloadda "_" OLAMAZ
======================================================= */
if (lines.some(l => (l.ItemDim1Code || '') === '_' )) {
console.error('❌ Payloadda "_" 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 HELPERI
-----------------------------------------------------------
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,
}

View 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
}
}
})

View 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
}
}
})

View 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
View 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;
}
}

View 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
}
}
})

View 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
View 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
}

View 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)}%`
}