/* ===========================================================
📦 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: `
${invalidList.map((v, i) => `
#${i + 1} | Item: ${v.itemCode}
Beden: ${v.dim1 || '(boÅŸ)'} |
Renk: ${v.colorCode || '-'} |
Qty: ${v.qty1}
Sebep: ${v.reason || 'Tanımsız ürün kombinasyonu'}
`).join('')}
`,
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]
: []
/* =======================================================
1ï¸âƒ£ ZORUNLU KONTROLLER
======================================================= */
if (!form?.model) {
$q?.notify?.({ type: 'warning', message: 'Model seçiniz' })
return false
}
if (!form.pb) {
form.pb = this.header?.DocCurrencyCode || 'USD'
}
/* =======================================================
2ï¸âƒ£ STOK KONTROLÜ (FIXED)
- stok guard’dan önce this.loadProductSizes(form,true,$q)
- opsiyonel callback loadProductSizes(true)
- tek dialog + doÄŸru await
======================================================= */
// ✅ store fonksiyonu
try {
if (typeof this.loadProductSizes === 'function') {
await this.loadProductSizes(form, true, $q)
}
} catch (err) {
console.warn('âš this.loadProductSizes hata:', err)
}
// ✅ dışarıdan callback geldiyse
try {
if (typeof loadProductSizes === 'function') {
await loadProductSizes(true)
}
} catch (err) {
console.warn('âš loadProductSizes hata:', err)
}
const stockMapLocal = stockMap?.value || stockMap || {}
const bedenLabels = form.bedenLabels || []
const bedenValues = form.bedenler || []
const overLimit = []
for (let i = 0; i < bedenLabels.length; i++) {
const lbl = String(bedenLabels[i] ?? '').trim()
const stok = Number(stockMapLocal?.[lbl] ?? 0)
const girilen = Number(bedenValues?.[i] ?? 0)
if (stok > 0 && girilen > stok) {
overLimit.push({ beden: lbl, stok, girilen })
}
}
if (overLimit.length && $q) {
const msg = overLimit
.map(x => `• ${x.beden}: ${x.girilen} (Stok: ${x.stok})`)
.join('
')
const stokOK = await new Promise(resolve => {
$q.dialog({
title: 'Stok Uyarısı',
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:
${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:
`Min. Fiyat: ${minFiyat} ${form.pb}
` +
`GirdiÄŸiniz: ${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() === '')
? ' '
: 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
/* =======================================================
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 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
}
}
/* =======================================================
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
)
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,
}