Files
bssapp/ui/src/stores/orderentryStore.js

3485 lines
110 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===========================================================
📦 orderentryStore.js (v3.4 CLEAN — AUTH + LOCAL PERSIST + AUTO RESUME)
=========================================================== */
import { defineStore } from 'pinia'
import api, { extractApiErrorDetail } 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: 'YAS', 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: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
{ key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] }
]
export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
m[g.key] = g
return m
}, {})
export const stockMap = ref({})
export const bedenStock = ref([])
export const sizeCache = ref({})
// ===========================================================
// 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler)
// ===========================================================
// ========================
// 🧰 GLOBAL DATE NORMALIZER
// ========================
function newGuid() {
return crypto.randomUUID()
}
// 🔑 Her beden satırı için deterministik clientKey üretimi
function makeLineClientKey(row, grpKey, beden) {
const base =
row.clientRowKey ||
row.clientKey ||
row.id ||
row._id ||
row.tmpId ||
`${row.model || ''}|${row.renk || ''}|${row.renk2 || ''}`
return `${base}::${grpKey}::${beden}`
}
// ===========================================================
// 🧩 Pinia Store — ORDER ENTRY STORE (REV 2025-11-03.2)
// ===========================================================
export const useOrderEntryStore = defineStore('orderentry', {
state: () => ({
isControlledSubmit: false,
allowRouteLeaveOnce: false,
schemaMap: {},
productCache: {},
_lastSavedFingerprint: null,
activeNewHeaderId: localStorage.getItem("bss_active_new_header") || null,
loading: false,
selected: null,
error: null,
customers: [],
selectedCustomer: null,
products: [],
colors: [],
secondColors: [],
inventory: [],
selectedProduct: null,
selectedColor: null,
selectedColor2: null,
OrderHeaderID: null,
// Persist config
persistKey: 'bss_orderentry_data',
lastSnapshotKey: 'bss_orderentry_snapshot',
// Editor state
editingKey: null,
currentOrderId: null,
mode: 'new',
// Grid state
orders: [],
header: {},
summaryRows: [],
lastSavedAt: null,
// Guards
preventPersist: false,
_uiBusy: false,
_unsavedChanges: false,
}),
getters: {
getDraftKey() {
// NEW taslak → GLOBAL ama tekil
return 'bss_orderentry_new_draft'
},
getEditKey() {
// EDIT → OrderHeaderID’ye bağlı
const id = this.header?.OrderHeaderID
return id ? `bss_orderentry_edit:${id}` : null
}
,
hasUnsavedChanges(state) {
try {
return (
state._lastSavedFingerprint !==
state._persistFingerprint?.()
)
} catch {
return false
}
},
getPersistKey: (state) =>
state.header?.OrderHeaderID
? `${state.persistKey}:${state.header.OrderHeaderID}`
: state.persistKey,
getSnapshotKey: (state) =>
state.header?.OrderHeaderID
? `${state.lastSnapshotKey}:${state.header.OrderHeaderID}`
: state.lastSnapshotKey,
totalQty: (state) =>
(state.orders || []).reduce((sum, r) => sum + (Number(r?.adet) || 0), 0),
hasAnyClosedLine(state) {
return Array.isArray(state.summaryRows) &&
state.summaryRows.some(r => r?.isClosed === true)
},
totalAmount(state) {
if (!Array.isArray(state.summaryRows)) return 0
return state.summaryRows.reduce(
(sum, r) => sum + Number(r?.tutar || 0),
0
)
}
},
actions: {
normalizeComboUI(row) {
return buildComboKey(row, BEDEN_EMPTY)
}
,
/* ===========================================================
🧩 initSchemaMap — BEDEN ŞEMA İNİT
- TEK SOURCE OF TRUTH: BEDEN_SCHEMA
=========================================================== */
initSchemaMap() {
if (this.schemaMap && Object.keys(this.schemaMap).length > 0) {
return
}
const map = {}
for (const g of BEDEN_SCHEMA) {
map[g.key] = {
key: g.key,
title: g.title,
values: [...g.values]
}
}
this.schemaMap = map
console.log(
'🧩 schemaMap INIT edildi:',
Object.keys(this.schemaMap)
)
},
getRowKey(row) {
if (!row) return null
return row.OrderLineID || row.id || null
}
,
updateHeaderTotals() {
try {
if (!Array.isArray(this.summaryRows)) return 0
const total = this.summaryRows.reduce(
(sum, r) => sum + Number(r?.tutar || 0),
0
)
// Header sadece GÃSTERİM / BACKEND için
if (this.header) {
this.header.TotalAmount = Number(total.toFixed(2))
}
return total
} catch (err) {
console.error('❌ updateHeaderTotals hata:', err)
return 0
}
}
,
/* ===========================================================
🚨 showInvalidVariantDialog — FINAL
-----------------------------------------------------------
✔ prItemVariant olmayan satırları listeler
✔ Satıra tıkla → scroll + highlight
✔ Kaydı BLOKLAYAN tek UI noktası
=========================================================== */
async showInvalidVariantDialog($q, invalidList = []) {
if (!Array.isArray(invalidList) || invalidList.length === 0) return
return new Promise(resolve => {
const dlg = $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())
// Quasar v2 chain API'de onShown yok; dialog DOM'u render olduktan sonra baÄŸla.
setTimeout(() => {
if (!dlg) return
const nodes = document.querySelectorAll('.invalid-row')
nodes.forEach(n => {
n.addEventListener('click', () => {
const ck = n.getAttribute('data-clientkey')
this.scrollToInvalidRow?.(ck)
})
})
}, 0)
})
}
,
/* ===========================================================
🎯 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) {
const detail = await extractApiErrorDetail(err)
const status = err?.status || err?.response?.status || '-'
console.error(`❌ fetchOrderPdf hata [${status}] order=${orderId}: ${detail}`)
throw new Error(detail)
}
}
,
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) {
const detail = await extractApiErrorDetail(err)
const orderId = id || this.header?.OrderHeaderID || '-'
const status = err?.status || err?.response?.status || '-'
console.error(`❌ PDF açma hatası [${status}] order=${orderId}: ${detail}`)
throw new Error(detail)
}
}
,
setActiveNewHeader(id) {
this.activeNewHeaderId = id || null
if (id) localStorage.setItem("bss_active_new_header", id)
else localStorage.removeItem("bss_active_new_header")
},
getActiveNewHeaderId() {
return this.activeNewHeaderId || localStorage.getItem("bss_active_new_header")
},
/* ===========================================================
🧩 initFromRoute (v5.6 — groupedRows TOUCH YOK)
-----------------------------------------------------------
- Route ID ve bss_last_txn arasında en dolu snapshot'ı seçer
- header + orders + summaryRows restore edilir
- groupedRows hydrate edilmez / resetlenmez / dokunulmaz
- Route ID farklıysa router.replace ile URL düzeltilir
=========================================================== */
async initFromRoute(orderId, router = null) { // ✅ NEW MODE → SADECE global draft
if (this.mode === 'new') {
const raw = localStorage.getItem(this.getDraftKey)
if (raw) {
try {
const payload = JSON.parse(raw)
this.header = payload.header || {}
this.orders = payload.orders || []
this.summaryRows = payload.summaryRows || this.orders
console.log('♻️ NEW draft restore edildi (global)')
return
} catch {}
}
console.log('⚪ NEW draft yok, boş başlatılıyor')
return
}
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
this.initSchemaMap()
}
try {
console.log('🧩 [initFromRoute] orderId:', orderId)
const lastTxn = localStorage.getItem('bss_last_txn') || null
const readPayload = (id) => {
if (!id) return null
const raw = localStorage.getItem(`bss_orderentry_data:${id}`)
if (!raw) return null
try {
return JSON.parse(raw)
} catch {
return null
}
}
const fromRoute = readPayload(orderId)
const fromLast = readPayload(lastTxn)
const hasData = (p) =>
!!p && (
(Array.isArray(p.orders) && p.orders.length > 0) ||
(Array.isArray(p.summaryRows) && p.summaryRows.length > 0)
)
let chosenId = null
let chosenPayload = null
if (hasData(fromRoute)) {
chosenId = orderId
chosenPayload = fromRoute
console.log('✅ [initFromRoute] Route ID snapshot seçildi:', chosenId)
} else if (hasData(fromLast)) {
chosenId = lastTxn
chosenPayload = fromLast
console.log('✅ [initFromRoute] lastTxn snapshot seçildi:', chosenId)
}
/* -------------------------------------------------------
🚫 SNAPSHOT YOK → BOŞ BAŞLA
-------------------------------------------------------- */
if (!chosenId || !chosenPayload) {
console.log('⚪ [initFromRoute] Snapshot yok, boş başlatılıyor')
this.header = {
...(this.header || {}),
OrderHeaderID: orderId || lastTxn || crypto.randomUUID()
}
this.orders = []
this.summaryRows = []
// ❗ groupedRows'a DOKUNMA
return
}
/* -------------------------------------------------------
✅ SNAPSHOT RESTORE (SAFE CLONE)
-------------------------------------------------------- */
this.header = {
...(chosenPayload.header || {}),
OrderHeaderID: chosenId
}
const orders = Array.isArray(chosenPayload.orders)
? [...chosenPayload.orders]
: []
const summaryRows = Array.isArray(chosenPayload.summaryRows)
? [...chosenPayload.summaryRows]
: orders
this.orders = orders
this.summaryRows = summaryRows
// ❗ groupedRows hydrate edilmez, resetlenmez
/* -------------------------------------------------------
🔁 lastTxn SENKRON
-------------------------------------------------------- */
try {
localStorage.setItem('bss_last_txn', chosenId)
} catch (e) {
console.warn('⚠️ bss_last_txn yazılamadı:', e)
}
/* -------------------------------------------------------
🔁 ROUTE DÜZELTME (GEREKİRSE)
-------------------------------------------------------- */
if (router && orderId && orderId !== chosenId) {
console.log('🔁 [initFromRoute] Route ID düzeltiliyor →', chosenId)
await router.replace({
name: 'order-entry',
params: { orderHeaderID: chosenId }
})
}
console.log(
'✅ [initFromRoute] Restore tamam. Satır sayısı:',
this.summaryRows.length
)
} catch (err) {
console.error('❌ [initFromRoute] hata:', err)
}
}
,
/* ===========================================================
🆕 startNewOrder (v8.3 — FINAL & STABLE)
=========================================================== */
async startNewOrder({ $q }) {
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
this.initSchemaMap()
}
const headerId = crypto.randomUUID()
let orderNumber = `LOCAL-${dayjs().format("YYMMDD-HHmmss")}`
try {
const res = await api.get("/order/new-number")
if (res?.data?.OrderNumber) {
orderNumber = res.data.OrderNumber
}
} catch {
console.info('ℹ️ Backend order number yok, LOCAL kullanıldı')
}
this.mode = 'new'
this.isControlledSubmit = false
this.allowRouteLeaveOnce = false
this.header = {
OrderHeaderID: headerId,
OrderNumber: orderNumber,
OrderDate: new Date().toISOString().slice(0, 10),
CurrAccCode: null,
DocCurrencyCode: 'USD',
PriceCurrencyCode: 'USD',
PriceExchangeRate: 1
}
this.orders = []
this.summaryRows = []
// ✅ fingerprint bazlı sistem için reset
this._lastSavedFingerprint = null
// ✅ NEW draft hemen yazılır
this.persistLocalStorage?.()
return this.header
}
,
dedupeActiveLinesByCombo(lines) {
const map = new Map()
for (const ln of lines) {
const key = buildComboKey({
model: ln.ItemCode,
renk: ln.ColorCode,
renk2: ln.ItemDim2Code
}, ln.ItemDim1Code)
if (!map.has(key)) {
ln.ComboKey = key
map.set(key, ln)
continue
}
const ex = map.get(key)
ex.Qty1 = (Number(ex.Qty1) || 0) + (Number(ln.Qty1) || 0)
// OrderLineID boşsa doldur (editte önemli)
if (!ex.OrderLineID && ln.OrderLineID) ex.OrderLineID = ln.OrderLineID
}
return Array.from(map.values())
}
,
/* ===========================================================
🧹 Core reset helper — sadece state'i sıfırlar
=========================================================== */
resetCoreState() {
this.orders = []
this.summaryRows = []
this.groupedRows = []
this.header = {}
this.editingKey = null
this.currentOrderId = null
},resetForNewOrder() {
// mevcut her ÅŸeyi temizle
this.header = {
OrderHeaderID: this.header?.OrderHeaderID || null,
OrderDate: new Date().toISOString().slice(0,10),
CurrAccCode: null,
DocCurrencyCode: 'TRY',
PriceCurrencyCode: 'TRY',
// ihtiyaç duyduğun diğer default header alanları
}
this.orders = []
this.summaryRows = []
this.productCache = {}
this.stockMap = {}
this.setMode('new')
}
,
resetForEdit() {
// EDIT modda grid temizlenmez — sadece UI state resetlenir
this.editingKey = null
this.groupedRows = []
this.mode = 'edit'
}
,markAsSaved() {
try {
this._lastSavedFingerprint = this._persistFingerprint()
console.log('✅ markAsSaved → fingerprint senkron')
} catch (e) {
console.warn('⚠️ markAsSaved hata:', e)
}
}
,clearLocalSnapshot() {
try {
const id = this.header?.OrderHeaderID
if (!id) return
localStorage.removeItem(`bss_orderentry_data:${id}`)
console.log('🧹 Local snapshot temizlendi:', id)
} catch (e) {
console.warn('⚠️ clearLocalSnapshot hata:', e)
}
},/* ===========================================================
🧹 HARD CLEAN — ALL ORDERENTRY SNAPSHOTS
=========================================================== */
clearAllOrderSnapshots () {
Object.keys(localStorage)
.filter(k =>
k.startsWith('bss_orderentry_data:') ||
k.startsWith('bss_orderentry_edit:')
)
.forEach(k => {
console.log('🧹 snapshot silindi:', k)
localStorage.removeItem(k)
})
localStorage.removeItem('bss_last_txn')
}
,
/* ===========================================================
🧹 Store Hard Reset — Submit Sonrası Temizlik (FIXED)
- Grid, header, toplamlar, local state'ler sıfırlanır
- persistKey / lastSnapshotKey NULL yapılmaz (config sabit kalır)
- localStorage txn/snapshot temizliği güvenli yapılır
=========================================================== */
hardResetAfterSubmit() {
try {
// 🔑 mevcut id’yi yakala (local temizliği için)
const id = this.header?.OrderHeaderID || null
/* -------------------------------------------------------
1) Grid ve satırlar
-------------------------------------------------------- */
this.orders = []
this.summaryRows = []
this.groupedRows = []
/* -------------------------------------------------------
2) Header & meta
-------------------------------------------------------- */
this.header = {}
/* -------------------------------------------------------
3) Mode & edit state
-------------------------------------------------------- */
this.mode = 'new'
this.editingKey = null
this.currentOrderId = null
/* -------------------------------------------------------
4) Snapshot / transaction meta
⚠️ persistKey / lastSnapshotKey store config → NULL YAPMA
-------------------------------------------------------- */
this.activeTransactionId = null
this.submitted = false
// fingerprint / debounce meta varsa sıfırla
this._lastSavedFingerprint = null
this._lastPersistFingerprint = null
if (this._persistTimeout) {
clearTimeout(this._persistTimeout)
this._persistTimeout = null
}
/* -------------------------------------------------------
5) LocalStorage temizlik (opsiyonel ama submit sonrası doğru)
-------------------------------------------------------- */
try {
if (id) {
localStorage.removeItem(`bss_orderentry_data:${id}`)
localStorage.removeItem(`bss_orderentry_snapshot:${id}`)
}
localStorage.removeItem('bss_last_txn')
localStorage.removeItem('bss_active_new_header')
} catch (e) {
console.warn('⚠️ hardResetAfterSubmit localStorage temizliği hata:', e)
}
console.log('🧹 Store resetlendi (submit sonrası).')
} catch (err) {
console.error('❌ hardResetAfterSubmit hata:', err)
}
}
,
/* ===========================================================
✏️ openExistingForEdit (v12 — FINAL & CLEAN)
-----------------------------------------------------------
✔ Backend authoritative (orderlist açılışı local'i dikkate almaz)
✔ mode=new → backend çağrısı YOK
✔ normalizeOrderLines → grpKey + bedenMap garanti
✔ isClosed varsa → view, yoksa → edit
✔ Form sync opsiyonel
✔ İlk açılışta snapshot yazılır (edit boyunca persist ile güncellenir)
=========================================================== */
async openExistingForEdit(
orderId,
{ $q = null, form = null, productCache = null } = {}
) {
// 🔑 schemaMap garanti
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
this.initSchemaMap?.()
}
if (!orderId) return false
/* =======================================================
🟦 NEW MODE — ASLA backend çağrısı yok
======================================================= */
if (this.mode === 'new') {
console.log('⚪ openExistingForEdit skip (mode=new)')
return false
}
// productCache hem ref hem reactive olabilir → güvenli oku
const pc =
productCache?.value
? productCache.value
: (productCache && typeof productCache === 'object' ? productCache : {})
try {
// geçici varsayım (sonra isClosed durumuna göre set edilecek)
this.setMode?.('edit')
/* =======================================================
🔹 BACKEND — authoritative load
======================================================= */
const res = await api.get(`/order/get/${orderId}`)
const backend = res?.data
if (!backend?.header) {
throw new Error('Backend header yok')
}
/* =======================================================
🔹 HEADER — SADECE BACKEND
(orderlist açılışında local merge YOK)
======================================================= */
this.header = {
...backend.header,
OrderHeaderID: backend.header.OrderHeaderID || orderId
}
/* =======================================================
🔹 NORMALIZE LINES (TEK KAYNAK)
normalizeOrderLines şu alanları üretmeli:
✔ row.grpKey
✔ row.bedenMap[grpKey]
✔ row.isClosed (boolean)
======================================================= */
const normalized = this.normalizeOrderLines(
backend.lines || [],
this.header.DocCurrencyCode || 'USD',
pc
)
this.orders = Array.isArray(normalized) ? normalized : []
this.summaryRows = [...this.orders]
/* =======================================================
🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN)
- herhangi bir isClosed=true → view
- değilse → edit
======================================================= */
const hasClosedLine = (this.summaryRows || []).some(r => r?.isClosed === true)
this.setMode?.(hasClosedLine ? 'view' : 'edit')
/* =======================================================
🔹 FORM SYNC (opsiyonel)
======================================================= */
if (form) {
Object.assign(form, this.header)
}
/* =======================================================
🔹 LOCAL SNAPSHOT (edit boyunca tutulacak temel)
- Açılışta snapshot yaz
- Sonraki değişikliklerde zaten persistLocalStorage çağrıları var
======================================================= */
this.persistLocalStorage?.()
try {
localStorage.setItem('bss_last_txn', String(orderId))
} catch {}
console.log('✅ openExistingForEdit OK:', {
id: orderId,
rows: this.summaryRows.length,
mode: this.mode,
hasClosedLine
})
return true
} catch (err) {
console.error('❌ openExistingForEdit hata:', err)
// new deÄŸilse uyar
if (this.mode !== 'new') {
$q?.notify?.({
type: 'negative',
message: 'Sipariş yüklenemedi'
})
}
return false
}
}
,
/* ===========================================================
♻️ hydrateFromLocalStorage (v5.5 — FIXED & CLEAN)
-----------------------------------------------------------
- Tek assign (double overwrite YOK)
- groupedRows hydrate edilmez
- mode ASLA set edilmez
- header + rows güvenli restore
=========================================================== */
async hydrateFromLocalStorage(orderId, log = false) {if (this.mode === 'new') {
return this.hydrateFromLocalStorageIfExists()
}
try {
const key = `bss_orderentry_data:${orderId}`
const payload = JSON.parse(localStorage.getItem(key) || 'null')
if (!payload) {
log && console.log('ℹ️ hydrate → snapshot yok:', orderId)
return null
}
// 🔑 source bilgisi (mode set edilmez)
this.source = payload.source || 'local'
/* -------------------------------------------------------
MSSQL tarih helper’ları
-------------------------------------------------------- */
const safeDateTime = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : null
}
const safeDateOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format('YYYY-MM-DD') : null
}
const safeTimeOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format('HH:mm:ss') : null
}
/* -------------------------------------------------------
HEADER
-------------------------------------------------------- */
this.header = {
...(payload.header || {}),
OrderHeaderID: payload.header?.OrderHeaderID ?? orderId,
OrderNumber : payload.header?.OrderNumber ?? null
}
const h = this.header
h.CreatedDate = safeDateTime(h.CreatedDate)
h.LastUpdatedDate = safeDateTime(h.LastUpdatedDate)
h.CreditableConfirmedDate = safeDateTime(h.CreditableConfirmedDate)
h.OrderDate = safeDateOnly(h.OrderDate)
h.OrderTime = safeTimeOnly(h.OrderTime)
this.header = h
/* -------------------------------------------------------
ROWS (TEK KAYNAK)
-------------------------------------------------------- */
const orders = Array.isArray(payload.orders)
? payload.orders
: []
this.orders = orders
this.summaryRows = Array.isArray(payload.summaryRows)
? payload.summaryRows
: orders
// ❌ groupedRows hydrate edilmez (computed olmalı)
this.groupedRows = []
/* -------------------------------------------------------
SNAPSHOT ÃZET
-------------------------------------------------------- */
const output = {
type : payload.submitted === true ? 'submitted' : 'draft',
source : this.source,
headerId : orderId,
orderNumber: this.header?.OrderNumber ?? null,
rows : this.summaryRows.length,
submitted :
payload.submitted === true ||
payload.header?.IsSubmitted === true
}
log && console.log('♻️ hydrate sonuc (FIXED):', output)
return output
} catch (err) {
console.warn('⚠️ hydrateFromLocalStorage hata:', err)
return null
}
}
,
hydrateFromLocalStorageIfExists() {
try {
let raw = null
if (this.mode === 'new') {
raw = localStorage.getItem(this.getDraftKey) // ✅
}
if (this.mode === 'edit') {
const key = this.getEditKey // ✅
if (key) raw = localStorage.getItem(key)
}
if (!raw) return false
const payload = JSON.parse(raw)
this.header = payload.header || {}
this.orders = payload.orders || []
this.summaryRows = payload.summaryRows || this.orders
console.log('♻️ hydrate OK:', this.mode)
return true
} catch (err) {
console.warn('hydrateFromLocalStorageIfExists hata:', err)
return false
}
}
,
/* ===========================================================
🔀 mergeOrders (local + backend)
normalizeISO → kaldırıldı
safe MSSQL helpers eklendi
=========================================================== */
mergeOrders(local, backend, preferLocal = true) {
if (!backend && !local) return { header: {}, orders: [] }
const safeMerge = (base = {}, override = {}) => {
const out = { ...base }
for (const [k, v] of Object.entries(override || {})) {
if (v === undefined || v === null) continue
if (typeof v === 'string' && v.trim() === '') continue
out[k] = v
}
return out
}
// Header merge
const header = safeMerge(backend?.header || {}, local?.header || {})
header.OrderHeaderID =
backend?.header?.OrderHeaderID ||
local?.header?.OrderHeaderID ||
header.OrderHeaderID ||
null
const getKey = (r) =>
(r.OrderLineID ||
`${r.model || r.ItemCode}_${r.renk || r.ColorCode}_${r.renk2 || r.ColorCode2}`
).toString().toUpperCase()
const map = new Map()
// Backend satırları
for (const b of (backend?.lines || backend?.orders || [])) {
map.set(getKey(b), { ...b, _src: 'backend' })
}
// Local satırları merge et
for (const l of (local?.orders || [])) {
const k = getKey(l)
if (map.has(k)) {
const merged = safeMerge(map.get(k), l)
merged._src = preferLocal ? 'local' : 'backend'
map.set(k, merged)
} else {
map.set(k, { ...l, _src: 'local-only' })
}
}
const mergedOrders = Array.from(map.values())
console.log(`🧩 mergeOrders → ${mergedOrders.length} satır birleşti (ID:${header.OrderHeaderID})`)
// ====================================================
// 🕒 HEADER TARİHLERİNİ MSSQL FORMATINA NORMALİZE ET
// ====================================================
const safeDateTime = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : null
}
const safeDateOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format("YYYY-MM-DD") : null
}
const safeTimeOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format("HH:mm:ss") : null
}
header.CreatedDate = safeDateTime(header.CreatedDate)
header.LastUpdatedDate = safeDateTime(header.LastUpdatedDate)
header.CreditableConfirmedDate = safeDateTime(header.CreditableConfirmedDate)
header.OrderDate = safeDateOnly(header.OrderDate)
header.OrderTime = safeTimeOnly(header.OrderTime)
return { header, orders: mergedOrders }
}
,
markRowSource(row) {
if (row._src === 'local-only') return '🟠 Offline'
if (row._src === 'local') return '🔵 Local'
return '⚪ Backend'
}
,
/* ===========================================================
🔄 mergeAndPersistBackendOrder (edit mode)
=========================================================== */
mergeAndPersistBackendOrder(orderId, backendPayload) {
const key = `bss_orderentry_data:${orderId}`
const localPayload = JSON.parse(localStorage.getItem(key) || 'null')
const merged = this.mergeOrders(localPayload, backendPayload, true)
localStorage.setItem(key, JSON.stringify({
...merged,
source: 'db',
mode: 'edit',
updatedAt: new Date().toISOString()
}))
console.log(`💾 mergeAndPersistBackendOrder → ${orderId} localStorage’a yazıldı`)
}
,
persistLocalStorage() {
try {
if (this.preventPersist || this._uiBusy) return
const payload = {
mode: this.mode,
header: toRaw(this.header || {}),
orders: toRaw(this.orders || []),
summaryRows: toRaw(this.summaryRows || []),
updatedAt: new Date().toISOString()
}
/* ===============================
🟢 NEW MODE — GLOBAL TEK TASLAK
=============================== */
if (this.mode === 'new') {
localStorage.setItem(this.getDraftKey, JSON.stringify(payload))
// 🔒 sadece aktif new header bilgisi
this.setActiveNewHeader?.(this.header?.OrderHeaderID)
return
}
/* ===============================
🔵 EDIT MODE — ID BAZLI
=============================== */
if (this.mode === 'edit') {
const key = this.getEditKey
if (!key) return
localStorage.setItem(key, JSON.stringify(payload))
}
} catch (e) {
console.warn('persistLocalStorage error:', e)
}
}
,
clearEditSnapshotIfExists() {
if (this.mode !== 'edit') return
const key = this.getEditKey // ✅
if (!key) return
localStorage.removeItem(key)
console.log('🧹 EDIT snapshot silindi:', key)
}
,/* ===========================================================
🧠 _persistFingerprint — kritik state’leri tek stringe indirger
- X3: orders+header yetmez → mode, summaryRows, id/no, map’ler dahil
=========================================================== */
_persistFingerprint() {
// 🔹 orders: çok büyürse pahalı olabilir ama snapshot tutarlılığı için önemli
// (istersen burada sadece length + rowKey listesi gibi optimize ederiz)
const ordersSnap = JSON.stringify(this.orders || [])
// 🔹 header: sadece kritik alanları al (tam header yerine daha stabil)
const h = this.header || {}
const headerSnap = JSON.stringify({
OrderHeaderID: h.OrderHeaderID || '',
OrderNumber: h.OrderNumber || '',
CurrAccCode: h.CurrAccCode || '',
DocCurrencyCode: h.DocCurrencyCode || '',
ExchangeRate: h.ExchangeRate ?? null
})
// 🔹 summaryRows: hash yerine şimdilik “length + rowKey listesi” (hafif + etkili)
const sr = Array.isArray(this.summaryRows) ? this.summaryRows : []
const summaryMeta = JSON.stringify({
len: sr.length,
keys: sr.map(r => this.getRowKey?.(r) || r?.key || r?.id || '').filter(Boolean)
})
// 🔹 comboLineIds / lineIdMap gibi kritik map’ler
// (sende hangisi varsa onu otomatik topluyoruz)
const mapSnap = JSON.stringify({
lineIdMap: this.lineIdMap || null,
comboLineIds: this.comboLineIds || null,
comboLineIdMap: this.comboLineIdMap || null,
comboLineIdSet: this.comboLineIdSet ? Array.from(this.comboLineIdSet) : null
})
// 🔹 mode
const modeSnap = String(this.mode || 'new')
// ✅ Tek fingerprint
return `${modeSnap}|${headerSnap}|${summaryMeta}|${mapSnap}|${ordersSnap}`
}
,
/* ===========================================================
🕒 _safePersistDebounced — snapshot değişmediği sürece yazmaz (X3)
- fingerprint: mode + header(id/no) + summaryRows meta + lineIdMap/combo + orders
=========================================================== */
_safePersistDebounced(delay = 1200) {
clearTimeout(this._persistTimeout)
this._persistTimeout = setTimeout(() => {
try {
// ✅ Persist guard’ları (varsa)
if (this.preventPersist) return
if (this._uiBusy) return
const fp = this._persistFingerprint()
if (fp === this._lastPersistFingerprint) {
return
}
this._lastPersistFingerprint = fp
this.persistLocalStorage()
console.log(`🕒 Otomatik LocalStorage senkron (${this.orders?.length || 0} satır).`)
} catch (err) {
console.warn('⚠️ Debounce persist hata:', err)
}
}, delay)
}
,
/* ===========================================================
💰 fetchMinPrice — model/pb için min fiyat
=========================================================== */
async fetchMinPrice(model, currency, $q) {
try {
const res = await api.get('/min-price', {
params: { model, currency }
})
const data = res?.data || {}
console.log('💰 [store.fetchMinPrice] yanıt:', data)
return {
price: Number(data.price || 0),
rateToTRY: Number(data.rateToTRY || 1),
priceTRY: Number(data.priceTRY || 0)
}
} catch (err) {
console.error('❌ [store.fetchMinPrice] Min fiyat alınamadı:', err)
$q?.notify?.({
type: 'warning',
message: 'Min. fiyat bilgisi alınamadı, kontrol atlandı ⚠️',
position: 'top-right'
})
return { price: 0, rateToTRY: 1, priceTRY: 0 }
}
}
,
applyCurrencyToLines(newPB) {
if (!newPB) return
// 🔹 Header
if (this.header) {
this.header.DocCurrencyCode = newPB
this.header.PriceCurrencyCode = newPB
}
// 🔹 Lines
if (Array.isArray(this.orders)) {
this.orders = this.orders.map(r => ({
...r,
pb: newPB,
DocCurrencyCode: newPB,
PriceCurrencyCode: newPB
}))
}
// 🔹 Summary
if (Array.isArray(this.summaryRows)) {
this.summaryRows = this.summaryRows.map(r => ({
...r,
pb: newPB,
DocCurrencyCode: newPB,
PriceCurrencyCode: newPB
}))
}
// ❗ totalAmount SET ETME
// ✔️ TEK MERKEZ
this.updateHeaderTotals?.()
}
,
/* ===========================================================
💠 HEADER SET & CURRENCY PROPAGATION
=========================================================== */
setHeaderFields(fields, opts = {}) {
const {
applyCurrencyToLines = false,
immediatePersist = false
} = opts
// 1️⃣ HEADER
this.header = {
...(this.header || {}),
...fields
}
// 2️⃣ SATIRLARA GERÇEKTEN YAY
if (applyCurrencyToLines && Array.isArray(this.summaryRows)) {
this.summaryRows = this.summaryRows.map(r => ({
...r,
pb: fields.DocCurrencyCode ?? r.pb,
DocCurrencyCode: fields.DocCurrencyCode ?? r.DocCurrencyCode,
PriceCurrencyCode: fields.PriceCurrencyCode ?? fields.DocCurrencyCode ?? r.PriceCurrencyCode
}))
}
// 3️⃣ STORE ORDERS REFERANSI
this.orders = [...this.summaryRows]
// 4️⃣ PERSIST
if (immediatePersist) {
this.persistLocalStorage('header-change')
}
}
,
applyHeaderCurrencyToOrders() {
if (!Array.isArray(this.orders)) return
const doc = this.header?.DocCurrencyCode ?? null
const prc = this.header?.PriceCurrencyCode ?? null
const rate = this.header?.PriceExchangeRate ?? null
let cnt = 0
for (const r of this.orders) {
if (doc) r.DocCurrencyCode = doc
if (prc) r.PriceCurrencyCode = prc
if (rate != null) r.PriceExchangeRate = rate
cnt++
}
console.log(`💱 ${cnt} satırda PB güncellendi → Doc:${doc} Price:${prc} Rate:${rate}`)
}
,/* ===========================================================
📸 saveSnapshot — küçük debug snapshot
=========================================================== */
saveSnapshot(tag = 'snapshot') {
try {
const id = this.header?.OrderHeaderID
if (!id) return
const key = `bss_orderentry_snapshot:${id}`
const snap = {
tag,
mode: this.mode,
orders: toRaw(this.orders || []),
header: toRaw(this.header || {}),
savedAt: dayjs().toISOString()
}
localStorage.setItem(key, JSON.stringify(snap))
console.log(`📸 Snapshot kaydedildi [${key}]`)
} catch (err) {
console.warn('⚠️ saveSnapshot hata:', err)
}
}
,
/* ===========================================================
♻️ loadFromStorage — eski generic persist için
=========================================================== */
loadFromStorage(force = false) {
try {
const raw = localStorage.getItem(this.getPersistKey)
if (!raw) {
console.info('ℹ️ LocalStorage boş, grid başlatılmadı.')
return false
}
if (!force && this.mode === 'edit') {
console.info('⚠️ Edit modda local restore atlandı (force=false).')
return false
}
const data = JSON.parse(raw)
this.orders = Array.isArray(data.orders) ? data.orders : []
this.header = data.header || {}
this.currentOrderId = data.currentOrderId || null
this.selectedCustomer = data.selectedCustomer || null
// 🔧 Temiz ID
this.header.OrderHeaderID = data.header?.OrderHeaderID || null
this.mode = data.mode || 'new'
this.lastSavedAt = data.savedAt || null
console.log(`♻️ Storage yüklendi • txn:${this.header.OrderHeaderID} (${this.orders.length} satır)`)
// Header PB -> satırlara
this.applyHeaderCurrencyToOrders()
this._safePersistDebounced(200)
return data
} catch (err) {
console.warn('⚠️ localStorage okuma hatası:', err)
return false
}
}
,
clearStorage() {
try {
localStorage.removeItem(this.getPersistKey)
console.log(`🗑️ LocalStorage temizlendi [${this.getPersistKey}]`)
} catch (err) {
console.warn('⚠️ clearStorage hatası:', err)
}
}
,
clearNewDraft() {
localStorage.removeItem(this.getDraftKey) // ✅
localStorage.removeItem('bss_last_txn')
console.log('🧹 NEW taslak temizlendi')
}
,
// ===========================================================
// 🔹 isSameCombo — STORE LEVEL (TEK KAYNAK)
// - model ZORUNLU eÅŸleÅŸir
// - renk / renk2 boşsa → joker
// ===========================================================
isSameCombo(a, b) {
if (!a || !b) return false
const n = v => (v == null ? '' : String(v).trim().toUpperCase())
const A = { model: n(a.model), renk: n(a.renk), renk2: n(a.renk2) }
const B = { model: n(b.model), renk: n(b.renk), renk2: n(b.renk2) }
if (!A.model || !B.model) return false
const renkOk = (A.renk === B.renk) || !A.renk || !B.renk
const renk2Ok = (A.renk2 === B.renk2) || !A.renk2 || !B.renk2
return A.model === B.model && renkOk && renk2Ok
},
// ===========================================================
// 🔹 saveOrUpdateRowUnified (v6.6 — COMBO SAFE + FIXED STOCK+PRICE + UI)
// - v6.5 korunur (stok+min fiyat + this.loadProductSizes)
// - ✅ NEW MODE: dupIdx artık _deleteSignal satırlarını BAŞTAN hariç tutar
// - EDIT MODE: sameCombo → update, combo değişti → delete + insert (korundu)
// - lineIdMap koruması korunur
// ===========================================================
async saveOrUpdateRowUnified({
form,
recalcVat = null,
resetEditor = null,
stockMap = null,
loadProductSizes = null,
$q = null
}) {
try {
console.log('🔥 saveOrUpdateRowUnified v6.6', {
model: form?.model,
mode: this.mode,
editingKey: this.editingKey
})
const getKey =
typeof this.getRowKey === 'function'
? this.getRowKey
: (r => r?.clientKey || r?.id || r?.OrderLineID)
const rows = Array.isArray(this.summaryRows)
? [...this.summaryRows]
: []
/* =======================================================
¸âƒ£ ZORUNLU KONTROLLER
======================================================= */
if (!form?.model) {
$q?.notify?.({ type: 'warning', message: 'Model seçiniz' })
return false
}
if (!form.pb) {
form.pb = this.header?.DocCurrencyCode || 'USD'
}
/* =======================================================
¸âƒ£ STOK KONTROLÜ (FIXED)
- stok guard’dan önce this.loadProductSizes(form,true,$q)
- opsiyonel callback loadProductSizes(true)
- tek dialog + doÄŸru await
======================================================= */
// ✅ store fonksiyonu
try {
if (typeof this.loadProductSizes === 'function') {
await this.loadProductSizes(form, true, $q)
}
} catch (err) {
console.warn('âš  this.loadProductSizes hata:', err)
}
// ✅ dışarıdan callback geldiyse
try {
if (typeof loadProductSizes === 'function') {
await loadProductSizes(true)
}
} catch (err) {
console.warn('âš  loadProductSizes hata:', err)
}
const stockMapLocal = stockMap?.value || stockMap || {}
const bedenLabels = form.bedenLabels || []
const bedenValues = form.bedenler || []
const overLimit = []
for (let i = 0; i < bedenLabels.length; i++) {
const lbl = String(bedenLabels[i] ?? '').trim()
const stok = Number(stockMapLocal?.[lbl] ?? 0)
const girilen = Number(bedenValues?.[i] ?? 0)
if (stok > 0 && girilen > stok) {
overLimit.push({ beden: lbl, stok, girilen })
}
}
if (overLimit.length && $q) {
const msg = overLimit
.map(x => `• <b>${x.beden}</b>: ${x.girilen} (Stok: ${x.stok})`)
.join('<br>')
const stokOK = await new Promise(resolve => {
$q.dialog({
title: 'Stok Uyarısı',
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:<br><br>${msg}`,
html: true,
ok: { label: 'Devam', color: 'primary' },
cancel: { label: 'İptal', color: 'negative' }
})
.onOk(() => resolve(true))
.onCancel(() => resolve(false))
.onDismiss(() => resolve(false))
})
if (!stokOK) return false
}
/* =======================================================
¸âƒ£ 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
/* =======================================================
¸âƒ£ 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)
/* =======================================================
¸âƒ£ 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
}
/* =======================================================
¸âƒ£ 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() === '')
? ' '
: normalizeBedenLabel(String(k))
merged[beden] = Number(merged[beden] || 0) + Number(v || 0)
}
// toplam adet/tutar recalc
const totalAdet = Object.values(merged).reduce((a, b) => a + Number(b || 0), 0)
const price = Number(newRow?.fiyat ?? prev?.fiyat ?? 0)
const totalTutar = Number((totalAdet * price).toFixed(2))
rows[dupIdx] = {
...prev,
...newRow,
// kritik korumalar
id: prev.id,
OrderLineID: prev.OrderLineID || null,
lineIdMap: { ...(prev.lineIdMap || {}) },
// MERGED bedenMap
grpKey,
bedenMap: { [grpKey]: merged },
// adet/tutar
adet: totalAdet,
tutar: totalTutar,
updatedAt: dayjs().toISOString()
}
this.summaryRows = rows
this.orders = rows
this.updateHeaderTotals?.()
this.persistLocalStorage?.()
resetEditor?.(true)
recalcVat?.()
$q?.notify?.({ type: 'positive', message: 'Aynı kombinasyon bulundu, bedenler birleştirildi' })
return true
}
}
// dup yoksa (veya dup delete satırıydı) → yeni satır
rows.push({
...newRow,
id: newRow.id || crypto.randomUUID(),
OrderLineID: null,
lineIdMap: { ...(newRow.lineIdMap || {}) }
})
this.summaryRows = rows
this.orders = rows
this.updateHeaderTotals?.()
this.persistLocalStorage?.()
resetEditor?.(true)
recalcVat?.()
$q?.notify?.({ type: 'positive', message: 'Yeni satır eklendi' })
return true
} catch (err) {
console.error('❌ saveOrUpdateRowUnified:', err)
$q?.notify?.({ type: 'negative', message: 'Satır kaydı başarısız' })
return false
}
}
,
/* ===========================================================
🔄 setTransaction — yeni transaction ID set et
=========================================================== */
setTransaction(id, autoResume = true) {
if (!id) return
// 🔧 temiz ID
this.header.OrderHeaderID = id
localStorage.setItem('bss_last_txn', id)
console.log('🔄 Transaction değiştirildi:', id)
if (autoResume) {
const hasData = Array.isArray(this.orders) && this.orders.length > 0
if (!hasData) {
const ok = this.hydrateFromLocalStorage(id,true)
if (ok) console.info('📦 Local kayıt geri yüklendi (boş grid için).')
} else {
console.log('🚫 Grid dolu, auto-resume atlandı (mevcut satırlar korundu).')
}
}
}
,
/* ===========================================================
🧹 clearTransaction — sadece NEW MODE taslaklarını temizler
=========================================================== */
clearTransaction() {
try {
const id = this.header?.OrderHeaderID
if (id) {
localStorage.removeItem(`bss_orderentry_data:${id}`)
}
this.orders = []
this.summaryRows = []
this.groupedRows = []
this.header = {}
this.mode = 'new'
localStorage.removeItem('bss_last_txn')
console.log('🧹 Transaction temizlendi')
} catch (err) {
console.warn('⚠️ clearTransaction hata:', err)
}
}
,
// =======================================================
// 🔒 KİLİT KONTROLÜ — Sadece EDIT modunda, backend satırı
// =======================================================
isRowLocked(row) {
if (!row) return false
// Sadece edit modunda,
// ve backend'den gelen gerçek OrderLineID varsa,
// ve IsClosed=1 ise satır kilitli
return (
this.mode === 'edit' &&
!!row.OrderLineID &&
row.isClosed === true
)
},
findExistingIndexByForm(form) {
return this.orders.findIndex(r => this.isSameCombo(r, form))
},
addRow(row) {
if (!row) return
const existingIndex = this.orders.findIndex(r => {
const sameId = r.id && row.id && r.id === row.id
const sameCombo = this.isSameCombo(r, row)
return sameId || sameCombo
})
if (existingIndex !== -1) {
const old = this.orders[existingIndex]
this.orders[existingIndex] = {
...old,
adet: Number(row.adet ?? old.adet ?? 0),
fiyat: Number(row.fiyat ?? old.fiyat ?? 0),
tutar: Number(row.fiyat ?? old.fiyat ?? 0) * Number(row.adet ?? old.adet ?? 0),
ItemDim1Code: row.ItemDim1Code || old.ItemDim1Code,
aciklama: row.aciklama || old.aciklama,
updatedAt: dayjs().toISOString()
}
console.log(`⚠️ Aynı kombinasyon bulundu, satır güncellendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`)
} else {
this.orders.push(toRaw(row))
console.log(`➕ Yeni kombinasyon eklendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`)
}
this.persistLocalStorage()
this.saveSnapshot('after-add')
},
updateRow(index, patch) {
if (index < 0 || index >= this.orders.length) return
this.orders[index] = {
...this.orders[index],
...toRaw(patch),
updatedAt: dayjs().toISOString()
}
this.persistLocalStorage()
this.saveSnapshot('after-update')
console.log(`✏️ Satır güncellendi (store): #${index}`)
},
removeRow(index) {
if (index < 0 || index >= this.orders.length) return
const removed = this.orders.splice(index, 1)
if (Array.isArray(this.summaryRows)) {
this.summaryRows.splice(index, 1)
}
this.persistLocalStorage()
this.saveSnapshot('after-remove')
console.log(`🗑️ Satır silindi: ${removed[0]?.model || '(model yok)'}`)
},
removeSelectedRow(row, $q = null) {
if (!row) return
// 1) Kilitli satır silinemez
if (this.isRowLocked(row)) {
$q?.notify?.({
type: 'warning',
message: '🔒 Bu satır (IsClosed=1) kapatılmış. Silinemez.'
})
return false
}
// 2) Kullanıcıya onay sor
return new Promise(resolve => {
$q?.dialog({
title: 'Satır Sil',
message: `${row.model} / ${row.renk} / ${row.renk2} kombinasyonu silinsin mi?`,
ok: { label: 'Evet', color: 'negative' },
cancel: { label: 'Vazgeç' }
})
.onOk(() => {
this.removeRowInternal(row)
resolve(true)
})
.onCancel(() => resolve(false))
})
}
,
removeRowInternal(row) {
if (!row) return false
// 1️⃣ Kilit kontrolü
if (this.isRowLocked(row)) {
console.warn('🔒 Kilitli satır silinemez.')
return false
}
const getKey =
typeof this.getRowKey === 'function'
? this.getRowKey
: (r => r?.clientKey || r?.id || r?.OrderLineID)
const rowKey = getKey(row)
if (!rowKey) return false
const idx = this.summaryRows.findIndex(r => getKey(r) === rowKey)
if (idx === -1) return false
console.log('🗑️ X2 removeRowInternal →', row)
// 🔐 UI BUSY
this._uiBusy = true
this.preventPersist = true
try {
// 2️⃣ UI’dan kaldır
this.summaryRows.splice(idx, 1)
// orders = UI satırları (temiz kopya)
this.orders = [...this.summaryRows]
// 3️⃣ EDIT MODE → DELETE SİNYALİ
if (this.mode === 'edit') {
const grpKey =
row.grpKey ||
Object.keys(row.bedenMap || {})[0] ||
'tak'
// ✅ lineIdMap referansı (varsa)
const lineIdMap =
(row.lineIdMap && typeof row.lineIdMap === 'object')
? { ...row.lineIdMap }
: {}
const emptyMap = {}
// Ãncelik: bedenMap[grpKey] → lineIdMap → fallback
if (row.bedenMap && row.bedenMap[grpKey]) {
for (const beden of Object.keys(row.bedenMap[grpKey] || {})) {
emptyMap[beden] = 0
}
} else if (Object.keys(lineIdMap).length) {
for (const beden of Object.keys(lineIdMap)) {
emptyMap[beden] = 0
}
} else {
emptyMap['STD'] = 0
}
const deleteSignalRow = {
...row,
// 🔴 UI KEY
id: `DEL::${row.id || row.OrderLineID || crypto.randomUUID()}`,
// 🔴 BACKEND DELETE SIGNAL
adet: 0,
Qty1: 0,
tutar: 0,
// 🔴 CRITICAL: duplicate guard'a girmesin
ComboKey: '',
// 🔴 legacy tekil alan (varsa kalsın)
OrderLineID: row.OrderLineID || null,
// ✅ CRITICAL
grpKey,
bedenMap: { [grpKey]: emptyMap },
lineIdMap,
comboLineIds: { ...(row.comboLineIds || {}) },
_deleteSignal: true
}
console.log('📡 DELETE sinyali üretildi:', deleteSignalRow)
this.orders.push(deleteSignalRow)
}
// 4️⃣ Totals (persist YOK)
this.updateHeaderTotals?.()
} finally {
// 🔓 GUARD KAPAT
this.preventPersist = false
this._uiBusy = false
}
// 5️⃣ TEK VE KONTROLLÜ persist
this.persistLocalStorage()
return true
}
,
/* ===========================================================
📦 normalizeOrderLines (v9 — lineIdMap FIXED + AKSBİR SAFE)
-----------------------------------------------------------
✔ grpKey SADECE burada set edilir
✔ detectBedenGroup SADECE store’da kullanılır
✔ aksbir → ' ' bedeni = GERÇEK adet
✔ backend satırlarında BEDEN → OrderLineID map’i üretilir
=========================================================== */
normalizeOrderLines(lines, pbFallback = 'USD') {
if (!Array.isArray(lines)) return []
const merged = Object.create(null)
const makeBaseKey = (model, renk, renk2) =>
`${model || ''}||${renk || ''}||${renk2 || ''}`
for (const raw of lines) {
if (!raw) continue
const isClosed =
raw.IsClosed === true ||
raw.isClosed === true ||
raw.IsClosed?.Bool === true
/* =======================================================
¸âƒ£ 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
}
/* =======================================================
¸âƒ£ 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 bedenRaw =
raw.ItemDim1Code == null
? ''
: String(raw.ItemDim1Code).trim()
const beden = bedenRaw === '' ? ' ' : normalizeBedenLabel(bedenRaw)
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
}
}
/* =======================================================
¸âƒ£ 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
)
const cleanedMap = { ...row.__tmpMap }
const hasNonBlankBeden = Object.keys(cleanedMap)
.some(k => String(k).trim() !== '')
// Gomlek/takim/pantolon gibi gruplarda bos beden sadece legacy kirli veri olabilir.
// Gecerli bedenler varsa bos bedeni payload/UI'dan temizliyoruz.
if (grpKey !== 'aksbir' && hasNonBlankBeden) {
delete cleanedMap[' ']
delete cleanedMap['']
if (row.lineIdMap && typeof row.lineIdMap === 'object') {
delete row.lineIdMap[' ']
delete row.lineIdMap['']
}
}
row.grpKey = grpKey
row.bedenMap = { [grpKey]: { ...cleanedMap } }
row.adet = Object.values(cleanedMap).reduce((a, b) => a + (Number(b) || 0), 0)
row.tutar = Number((row.adet * Number(row.fiyat || 0)).toFixed(2))
/* ===================================================
🔒 AKSBİR — BOŞLUK BEDEN GERÇEK ADETİ ALIR
❗ STD’ye dönme YOK
❗ 0 yazma YOK
=================================================== */
if (grpKey === 'aksbir') {
row.bedenMap[grpKey] ??= {}
row.bedenMap[grpKey][' '] = Number(row.adet || 0)
}
delete row.__tmpMap
out.push(row)
}
}
console.log(
`📦 normalizeOrderLines (v9 + lineIdMap) → ${out.length} satır`
)
return out
}
,
/**
* ===========================================================
* loadProductSizes — FINAL v4.2 (EDITOR SAFE)
* -----------------------------------------------------------
* ✔ grpKey SADECE form.grpKey
* ✔ schemaMap TEK OTORİTE
* ✔ edit modda BEDEN LABEL DOKUNULMAZ
* ✔ ' ' (boş beden) korunur
* ===========================================================
*/
async loadProductSizes(form, forceRefresh = false, $q = null) {
if (!form?.model) return
const store = this
const prevBusy = !!store._uiBusy
const prevPrevent = !!store.preventPersist
store._uiBusy = true
store.preventPersist = true
try {
const grpKey = form.grpKey
if (!grpKey) {
console.warn('⛔ loadProductSizes iptal → grpKey yok')
return
}
const colorKey = form.renk || 'nocolor'
const color2Key = form.renk2 || 'no2color'
const cacheKey = `${form.model}_${colorKey}_${color2Key}_${grpKey}`
/* =======================================================
♻️ CACHE (LABEL DOKUNMADAN)
======================================================= */
if (!forceRefresh && sizeCache.value?.[cacheKey]) {
const cached = sizeCache.value[cacheKey]
bedenStock.value = [...cached.stockArray]
stockMap.value = { ...cached.stockMap }
console.log(`♻️ loadProductSizes CACHE → ${grpKey}`)
return
}
/* =======================================================
📡 API
======================================================= */
const params = { code: form.model }
if (form.renk) params.color = form.renk
if (form.renk2) params.color2 = form.renk2
const res = await api.get('/product-colorsize', { params })
const data = Array.isArray(res?.data) ? res.data : []
if (!data.length) {
bedenStock.value = []
stockMap.value = {}
return
}
/* =======================================================
📦 STOK MAP (' ' KORUNUR)
======================================================= */
const apiStockMap = {}
for (const x of data) {
const key =
x.item_dim1_code === null || x.item_dim1_code === ''
? ' '
: String(x.item_dim1_code)
apiStockMap[key] = Number(x.kullanilabilir_envanter ?? 0)
}
const finalStockMap = {}
for (const lbl of form.bedenLabels) {
finalStockMap[lbl] = apiStockMap[lbl] ?? 0
}
stockMap.value = { ...finalStockMap }
bedenStock.value = Object.entries(stockMap.value).map(
([beden, stok]) => ({ beden, stok })
)
/* =======================================================
💾 CACHE
======================================================= */
sizeCache.value[cacheKey] = {
labels: [...form.bedenLabels],
stockArray: [...bedenStock.value],
stockMap: { ...stockMap.value }
}
console.log(`✅ loadProductSizes FINAL v4.2 → ${grpKey}`)
} catch (err) {
console.error('❌ loadProductSizes hata:', err)
$q?.notify?.({ type: 'negative', message: 'Beden / stok alınamadı' })
} finally {
store._uiBusy = prevBusy
store.preventPersist = prevPrevent
console.log('🧩 Editor beden hydrate', {
grpKey: form.grpKey,
labels: form.bedenLabels,
values: form.bedenler
})
}
}
,
// =======================================================
// 🔸 TOPLAM HESAPLAMA (store içi) — X3 SAFE
// -------------------------------------------------------
// ✔ f.adet / f.tutar hesaplanır
// ✔ store.totalAmount ASLA set edilmez
// ✔ gerçek toplam → header.TotalAmount
// =======================================================
updateTotals(f) {
// 1️⃣ Satır adet
f.adet = (f.bedenler || []).reduce(
(a, b) => a + Number(b || 0),
0
)
// 2️⃣ Satır tutar
const fiyat = Number(f.fiyat) || 0
f.tutar = Number((f.adet * fiyat).toFixed(2))
// 3️⃣ Header toplam (tek gerçek state)
if (this.header) {
const total = (this.summaryRows || []).reduce(
(sum, r) => sum + Number(r?.tutar || 0),
0
)
this.header.TotalAmount = Number(total.toFixed(2))
}
return f
}
,
// =======================================================
// 🔸 GRUP ANAHTARI TESPİTİ
// =======================================================
activeGroupKeyForRow(row) {
const g = (row?.urunAnaGrubu || '').toUpperCase()
if (g.includes('TAKIM')) return 'tak'
if (g.includes('PANTOLON')) return 'pan'
if (g.includes('GOMLEK')) return 'gom'
if (g.includes('AYAKKABI')) return 'ayk'
if (g.includes('YAS')) 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')
// =======================================================
// 🧩 DUMMY CURRENCY PAYLOAD (model genişletmeden)
// - trOrderLineCurrency için gerekli alanları satıra basar
// - örnek satırdaki gibi: PriceVI/AmountVI = KDV dahil, Price/Amount = KDV hariç
// =======================================================
const r2 = (n) => Number((Number(n) || 0).toFixed(2))
const r4 = (n) => Number((Number(n) || 0).toFixed(4))
for (const ln of lines) {
const qty = Number(ln?.Qty1 || 0)
const unitBase = Number(ln?.Price || 0) // KDV hariç birim
const vatRate = Number(ln?.VatRate || 0)
const exRate = Number(ln?.PriceExchangeRate || header?.ExchangeRate || 1) || 1
const taxBase = r2(unitBase * qty) // Amount
const vat = r2((taxBase * vatRate) / 100) // Vat
const net = r2(taxBase + vat) // AmountVI / NetAmount
const unitWithVat = qty > 0 ? r4(net / qty) : r4(unitBase * (1 + vatRate / 100))
const docCurrency = String(ln?.DocCurrencyCode || header?.DocCurrencyCode || 'TRY').trim() || 'TRY'
// Backend model alanları
ln.RelationCurrencyCode = docCurrency
ln.DocPrice = unitWithVat
ln.DocAmount = net
ln.LocalPrice = unitBase
ln.LocalAmount = taxBase
ln.LineDiscount = Number(ln?.LineDiscount || 0)
ln.TotalDiscount = Number(ln?.TotalDiscount || 0)
ln.TaxBase = taxBase
ln.Pct = Number(ln?.Pct || 0)
ln.VatAmount = vat
ln.VatDeducation = 0
ln.NetAmount = net
// SQL kolonu isimleriyle dummy alias (decoder ignore etse de payload'da görünür)
ln.CurrencyCode = docCurrency
ln.ExchangeRate = exRate
ln.PriceVI = unitWithVat
ln.AmountVI = net
ln.Amount = taxBase
ln.LDiscount1 = Number(ln?.LDiscount1 || 0)
ln.TDiscount1 = Number(ln?.TDiscount1 || 0)
ln.Vat = vat
}
// =======================================================
// 🧪 PRE-VALIDATE — prItemVariant ön kontrol
// - invalid varsa CREATE/UPDATE ÇALIŞMAZ
// =======================================================
const v = await api.post('/order/validate', { header, lines })
const invalid = v?.data?.invalid || []
if (invalid.length > 0) {
await this.showInvalidVariantDialog?.($q, invalid)
return // ❌ create / update ÇALIŞMAZ
}
console.log('📤 submitAllReal payload', {
mode: this.mode,
lines: lines.length,
deletes: lines.filter(l => l._deleteSignal).length
})
/* =======================================================
🚀 API CALL — TEK NOKTA
======================================================= */
const resp = await api.post(
isNew ? '/order/create' : '/order/update',
{ header, lines }
)
const data = resp?.data || {}
serverOrderId =
data.orderID ||
data.orderHeaderID ||
data.id ||
header?.OrderHeaderID
serverOrderNo =
data.orderNumber ||
data.orderNo ||
header?.OrderNumber
if (!serverOrderId) {
throw new Error('OrderHeaderID backend’den dönmedi')
}
/* =======================================================
🔁 MODE SWITCH → EDIT
======================================================= */
this.setMode('edit')
// Header patch (ID / No)
this.header = {
...this.header,
OrderHeaderID: serverOrderId,
OrderNumber: serverOrderNo
}
/* =======================================================
🧹 KRİTİK: Snapshot + Dirty temizliği
❗ ROUTE deÄŸiÅŸmeden ÃNCE
======================================================= */
this.updateHeaderTotals?.()
this.markAsSaved?.()
/* =======================================================
🧹 KRİTİK: NEW → EDIT geçişinde TÜM SNAPSHOT TEMİZLENİR
======================================================= */
this.clearAllOrderSnapshots()
$q.notify({
type: 'positive',
message: `SipariÅŸ kaydedildi: ${serverOrderNo || ''}`.trim()
})
/* =======================================================
🔀 ROUTE REPLACE (EDIT MODE)
- aynı sayfa → param değişti
- guard 1 kez bypass
======================================================= */
this.allowRouteLeaveOnce = true
await router.replace({
name: 'order-entry',
params: { orderHeaderID: serverOrderId },
query: { mode: 'edit', source: 'backend' }
})
/* =======================================================
🔄 BACKEND RELOAD (TEK GERÇEK KAYNAK)
======================================================= */
await this.openExistingForEdit(serverOrderId, {
$q,
form,
summaryRowsRef: summaryRows,
productCache
})
/* =======================================================
❓ USER NEXT STEP
======================================================= */
const choice = await new Promise(resolve => {
$q.dialog({
title: 'SipariÅŸ Kaydedildi',
options: {
type: 'radio',
model: 'continue',
items: [
{ label: '✏️ Düzenlemeye Devam', value: 'continue' },
{ label: '🖨 Yazdır', value: 'print' },
{ label: '📋 Listeye Dön', value: 'list' }
]
},
ok: { label: 'Seç' },
cancel: { label: 'Kapat' }
})
.onOk(v => resolve(v))
.onCancel(() => resolve('continue'))
})
/* =======================================================
🧭 USER ROUTING
======================================================= */
if (choice === 'print') {
const id = this.header?.OrderHeaderID || serverOrderId
if (id) {
try {
await this.downloadOrderPdf(id)
} catch (pdfErr) {
console.error('⚠️ PDF açılamadı, kayıt başarılı:', pdfErr)
$q.notify({
type: 'warning',
message:
pdfErr?.message ||
'Sipariş kaydedildi fakat PDF açılamadı.'
})
}
}
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?.detail ||
err?.response?.data?.message ||
err?.message ||
'Kayıt sırasında hata'
})
} finally {
// 🔓 Guard’lar normale dönsün
this.isControlledSubmit = false
this.loading = false
}
}
,
/* =======================================================
🧪 SUBMIT ALL TEST
======================================================= */
async submitAllTest($q = null) {
try {
const { header, lines } = this.buildFinalOrderJson()
console.log('🧾 TEST HEADER', Object.keys(header).length, 'alan')
console.log(JSON.stringify(header, null, 2))
console.log('🧾 TEST LINES', lines.length, 'satır')
console.log(JSON.stringify(lines, null, 2))
$q?.notify?.({
type: 'info',
message: `Header (${Object.keys(header).length}) + Lines (${lines.length}) gösterildi`,
position: 'top'
})
} catch (err) {
console.error('❌ submitAllTest hata:', err)
$q?.notify?.({
type: 'negative',
message: 'Gösterimde hata oluştu ❌',
position: 'top'
})
}
},
/* =======================================================
🧹 KAYIT SONRASI TEMİZLİK
======================================================= */
afterSubmit(opts = {
keepLocalStorage: true,
backendPayload: null,
resetMode: true // 🔑 yeni
}) {
try {
console.log('🧹 afterSubmit başlatıldı', opts)
if (opts?.backendPayload?.header?.OrderHeaderID) {
this.mergeAndPersistBackendOrder(
opts.backendPayload.header.OrderHeaderID,
opts.backendPayload
)
}
if (!opts?.keepLocalStorage) {
this.clearStorage()
this.clearTransaction()
} else {
this.saveSnapshot()
}
this.orders = []
this.header = {}
this.editingKey = null
this.currentOrderId = null
// 🔐 MODE RESET OPSİYONEL
if (opts.resetMode === true) {
this.mode = 'new'
}
console.log('✅ afterSubmit tamamlandı.')
} catch (err) {
console.error('❌ afterSubmit hata:', err)
}
}
,
/* ===========================================================
🟦 BUILD FINAL ORDER JSON — SAFE v26.1 (FINAL)
-----------------------------------------------------------
✔ ComboKey TEK OTORİTE → buildComboKey (bedenKey ile)
✔ UI/Map placeholder: '_' (bedenKey)
✔ DB/payload: '' (bedenPayload) → "_" ASLA GİTMEZ
✔ payload içinde aynı ComboKey TEK satır
✔ backend duplicate guard %100 uyumlu (ComboKey stabil)
✔ Final assert: payload’da "_" yakalanırsa patlatır
=========================================================== */
buildFinalOrderJson () {
const auth = useAuthStore()
const u = auth?.user || {}
const now = dayjs()
/* =========================
HELPERS
========================== */
const toNum = v => Number(v) || 0
const safeStr = v => (v == null ? '' : String(v).trim())
const formatDateOnly = v => (v ? dayjs(v).format('YYYY-MM-DD') : null)
const formatTimeOnly = v => dayjs(v).format('HH:mm:ss')
const formatDateTime = v => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : null)
// ✅ Payload beden normalize: "_" / "-" / "" => ''
const normBeden = (v) => {
const s = safeStr(v)
if (s === '' || s === '_' || s === '-') return '' // payload empty
return s
}
/* =========================
USER META
========================== */
const group = safeStr(u?.v3usergroup)
const v3name = safeStr(u?.v3_username)
const who = (group && v3name) ? `${group} ${v3name}` : (v3name || 'BSS')
const PCT_CODE_ZERO = '%0'
const VAT_CODE_ZERO = '%0'
/* =========================
HEADER
========================== */
const headerId = this.header?.OrderHeaderID || crypto.randomUUID()
const docCurrency = safeStr(this.header?.DocCurrencyCode) || 'TRY'
const exRate = toNum(this.header?.ExchangeRate) || 1
const avgDueSource =
this.header?.AverageDueDate ||
dayjs(this.header?.OrderDate || now).add(14, 'day')
const header = {
...this.header,
OrderHeaderID: headerId,
OrderDate: formatDateOnly(this.header?.OrderDate || now),
OrderTime: formatTimeOnly(now),
AverageDueDate: formatDateOnly(avgDueSource),
DocCurrencyCode: docCurrency,
LocalCurrencyCode: safeStr(this.header?.LocalCurrencyCode) || 'TRY',
ExchangeRate: exRate,
CreatedUserName:
this.mode === 'edit'
? (this.header?.CreatedUserName || who)
: who,
CreatedDate:
this.mode === 'edit'
? formatDateTime(this.header?.CreatedDate || now)
: formatDateTime(now),
LastUpdatedUserName: who,
LastUpdatedDate: formatDateTime(now)
}
/* =======================================================
LINES — COMBOKEY AGGREGATE (TEK MAP)
======================================================= */
const lines = []
const lineByCombo = new Map() // 🔒 KEY = ComboKey
const pushOrMerge = (row, ctx) => {
const {
grpKey,
bedenKey, // ✅ sadece ComboKey / Map için ('_' olabilir)
bedenPayload, // ✅ DB için ('' / 'S' / 'M' ...)
qty,
orderLineId,
isDeleteSignal
} = ctx
if (qty <= 0 && !isDeleteSignal) return
// ComboKey stabil kalsın diye bedenKey kullan
const comboKey = buildComboKey(row, bedenKey)
const makeLine = () => ({
OrderLineID: orderLineId || '',
ClientKey: makeLineClientKey(row, grpKey, bedenKey),
ComboKey: comboKey,
SortOrder: 0,
ItemTypeCode: 1,
ItemCode: safeStr(row.model),
ColorCode: safeStr(row.renk),
// ✅ PAYLOAD: "_" ASLA YOK
ItemDim1Code: bedenPayload,
ItemDim2Code: safeStr(row.renk2),
ItemDim3Code: '',
Qty1: isDeleteSignal ? 0 : qty,
Qty2: 0,
CancelQty1: 0,
CancelQty2: 0,
DeliveryDate: row.terminTarihi
? formatDateTime(row.terminTarihi)
: null,
PlannedDateOfLading: row.terminTarihi
? formatDateOnly(row.terminTarihi)
: null,
LineDescription: safeStr(row.aciklama),
UsedBarcode: '',
CostCenterCode: '',
VatCode: VAT_CODE_ZERO,
VatRate: toNum(row.vatRate ?? row.VatRate ?? 0),
PCTCode: PCT_CODE_ZERO,
PCTRate: 0,
LDisRate1: 0,
LDisRate2: 0,
LDisRate3: 0,
LDisRate4: 0,
LDisRate5: 0,
DocCurrencyCode: header.DocCurrencyCode,
PriceCurrencyCode: header.DocCurrencyCode,
PriceExchangeRate: toNum(header.ExchangeRate),
Price: toNum(row.fiyat),
BaseProcessCode: 'WS',
BaseOrderNumber: header.OrderNumber,
BaseCustomerTypeCode: 0,
BaseCustomerCode: header.CurrAccCode,
BaseSubCurrAccID: null,
BaseStoreCode: '',
OrderHeaderID: headerId,
CreatedUserName: who,
CreatedDate: formatDateTime(row.CreatedDate || now),
LastUpdatedUserName: who,
LastUpdatedDate: formatDateTime(now),
SurplusOrderQtyToleranceRate: 0,
WithHoldingTaxTypeCode: '',
DOVCode: ''
})
const existing = lineByCombo.get(comboKey)
if (!existing) {
const ln = makeLine()
lineByCombo.set(comboKey, ln)
lines.push(ln)
return
}
/* DELETE */
if (isDeleteSignal) {
if (orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId
}
existing.Qty1 = 0
return
}
/* MERGE */
existing.Qty1 += qty
if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId
}
existing.Price = toNum(row.fiyat)
}
/* =======================================================
ORDER ROW LOOP
======================================================= */
for (const row of this.orders || []) {
if (row?.isClosed === true) continue
const grpKey =
row.grpKey ||
Object.keys(row.bedenMap || {})[0] ||
'GENEL'
const lineIdMap = row.lineIdMap || {}
const grouped = row.bedenMap?.[grpKey]
const flat =
(row.bedenMap && typeof row.bedenMap === 'object' && !grouped)
? row.bedenMap
: null
const map = grouped || flat
const hasAnyBeden =
map && typeof map === 'object' && Object.keys(map).length > 0
/* 🔹 BEDENSİZ / AKSBİR */
if (!hasAnyBeden) {
const allowBlankPayload =
grpKey === 'aksbir' || row._deleteSignal === true
if (!allowBlankPayload) {
continue
}
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(lineIdMap?.[' ']) ||
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 isBlankBeden = safeStr(bedenRaw) === ''
if (
isBlankBeden &&
grpKey !== 'aksbir' &&
row._deleteSignal !== true
) {
continue
}
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]) ||
safeStr(lineIdMap?.[' ']) ||
(Object.keys(map).length === 1
? safeStr(row.OrderLineID)
: '')
}
pushOrMerge(row, {
grpKey,
bedenKey,
bedenPayload,
qty,
orderLineId,
isDeleteSignal: row._deleteSignal === true && !!orderLineId
})
}
}
/* =======================================================
FINAL SORT
======================================================= */
lines.forEach((ln, i) => { ln.SortOrder = i + 1 })
/* =======================================================
ASSERT — payload’da "_" OLAMAZ
======================================================= */
if (lines.some(l => (l.ItemDim1Code || '') === '_' )) {
console.error('❌ Payload’da "_" yakalandı', lines.filter(l => l.ItemDim1Code === '_'))
throw new Error('Payload ItemDim1Code "_" olamaz')
}
/* =======================================================
🔍 DEBUG — BUILD FINAL ORDER JSON OUTPUT
======================================================= */
console.groupCollapsed('%c📦 BUILD FINAL ORDER JSON', 'color:#c9a873;font-weight:bold')
console.log('🧾 HEADER:', header)
console.table(
lines.map((l, i) => ({
i: i + 1,
OrderLineID: l.OrderLineID,
ClientKey: l.ClientKey,
ComboKey: l.ComboKey,
ItemCode: l.ItemCode,
ColorCode: l.ColorCode,
ItemDim1Code: JSON.stringify(l.ItemDim1Code), // <-- kritik
ItemDim2Code: l.ItemDim2Code,
Qty1: l.Qty1,
Price: l.Price
}))
)
console.groupEnd()
return { header, lines }
}
,/* ===========================================================
✅ STORE ACTIONS — FIXED HELPERS
- setRowErrorByClientKey
- clearRowErrorByClientKey
- applyTerminToRowsIfEmpty
=========================================================== */
setRowErrorByClientKey(clientKey, payload) {
if (!clientKey) return
if (!Array.isArray(this.summaryRows)) return
const row = this.summaryRows.find(r => r?.clientKey === clientKey)
if (!row) return
row._error = {
code: payload?.code,
message: payload?.message
}
},
clearRowErrorByClientKey(clientKey) {
if (!clientKey) return
if (!Array.isArray(this.summaryRows)) return
const row = this.summaryRows.find(r => r?.clientKey === clientKey)
if (!row) return
if (row._error) {
delete row._error
}
},
applyTerminToRowsIfEmpty(dateStr) {
if (!dateStr) return
if (!Array.isArray(this.summaryRows)) return
// ❗ reassign YOK — patch/mutate
for (const r of this.summaryRows) {
if (!r?.terminTarihi || r.terminTarihi === '') {
r.terminTarihi = dateStr
}
}
// opsiyonel ama genelde doÄŸru:
this.persistLocalStorage?.()
}
} // actions sonu
}) // defineStore sonu
/* ===========================================================
Size Label Normalization (frontend helper)
=========================================================== */
function safeTrimUpperJs(v) {
return (v == null ? '' : String(v)).trim().toUpperCase()
}
function normalizeTextForMatch(v) {
return safeTrimUpperJs(v)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
function parseNumericSizeJs(v) {
const s = safeTrimUpperJs(v)
if (s === '' || !/^\d+$/.test(s)) return null
const n = Number.parseInt(s, 10)
return Number.isNaN(n) ? null : n
}
export function normalizeBedenLabel(v) {
let s = (v == null ? '' : String(v)).trim()
if (s === '') return ' '
s = s.toUpperCase()
// Backend parity: normalize common "standard size" aliases.
switch (s) {
case 'STD':
case 'STANDART':
case 'STANDARD':
case 'ONE SIZE':
case 'ONESIZE':
return 'STD'
}
// Backend parity: only values ending with CM are converted to numeric part.
if (s.endsWith('CM')) {
const num = s.slice(0, -2).trim()
if (num !== '') return num
}
switch (s) {
case 'XS':
case 'S':
case 'M':
case 'L':
case 'XL':
case '2XL':
case '3XL':
case '4XL':
case '5XL':
case '6XL':
case '7XL':
return s
}
return s
}
/* ===========================================================
Size Group Detection
- Core logic aligned with backend detectBedenGroupGo
- Keeps frontend aksbir bucket for accessory lines
=========================================================== */
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
const list = Array.isArray(bedenList) ? bedenList : []
const ana = normalizeTextForMatch(urunAnaGrubu)
const alt = normalizeTextForMatch(urunKategori)
// Frontend compatibility: accessory-only products should stay in aksbir.
const accessoryGroups = [
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER', 'CORAP',
'FULAR', 'MENDIL', 'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI'
]
const clothingGroups = ['GOMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT']
if (
accessoryGroups.some(g => ana.includes(g) || alt.includes(g)) &&
!clothingGroups.some(g => ana.includes(g))
) {
return 'aksbir'
}
if (ana.includes('AYAKKABI') || alt.includes('AYAKKABI')) {
return 'ayk'
}
let hasYasNumeric = false
let hasAykNumeric = false
let hasPanNumeric = false
for (const raw of list) {
const b = safeTrimUpperJs(raw)
switch (b) {
case 'XS':
case 'S':
case 'M':
case 'L':
case 'XL':
case '2XL':
case '3XL':
case '4XL':
case '5XL':
case '6XL':
case '7XL':
return 'gom'
}
const n = parseNumericSizeJs(b)
if (n == null) continue
if (n >= 2 && n <= 14) hasYasNumeric = true
if (n >= 39 && n <= 45) hasAykNumeric = true
if (n >= 38 && n <= 68) hasPanNumeric = true
}
if (hasAykNumeric) return 'ayk'
if (ana.includes('PANTOLON')) return 'pan'
if (hasPanNumeric) return 'pan'
if (alt.includes('COCUK') || alt.includes('GARSON')) return 'yas'
if (hasYasNumeric) return 'yas'
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() === ''
? ' '
: normalizeBedenLabel(String(rawLbl))
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,
}