3485 lines
110 KiB
JavaScript
3485 lines
110 KiB
JavaScript
/* ===========================================================
|
||
📦 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]
|
||
: []
|
||
|
||
/* =======================================================
|
||
1ï¸âƒ£ ZORUNLU KONTROLLER
|
||
======================================================= */
|
||
if (!form?.model) {
|
||
$q?.notify?.({ type: 'warning', message: 'Model seçiniz' })
|
||
return false
|
||
}
|
||
|
||
if (!form.pb) {
|
||
form.pb = this.header?.DocCurrencyCode || 'USD'
|
||
}
|
||
|
||
/* =======================================================
|
||
2ï¸âƒ£ STOK KONTROLÜ (FIXED)
|
||
- stok guard’dan önce this.loadProductSizes(form,true,$q)
|
||
- opsiyonel callback loadProductSizes(true)
|
||
- tek dialog + doÄŸru await
|
||
======================================================= */
|
||
|
||
// ✅ store fonksiyonu
|
||
try {
|
||
if (typeof this.loadProductSizes === 'function') {
|
||
await this.loadProductSizes(form, true, $q)
|
||
}
|
||
} catch (err) {
|
||
console.warn('âš this.loadProductSizes hata:', err)
|
||
}
|
||
|
||
// ✅ dışarıdan callback geldiyse
|
||
try {
|
||
if (typeof loadProductSizes === 'function') {
|
||
await loadProductSizes(true)
|
||
}
|
||
} catch (err) {
|
||
console.warn('âš loadProductSizes hata:', err)
|
||
}
|
||
|
||
const stockMapLocal = stockMap?.value || stockMap || {}
|
||
const bedenLabels = form.bedenLabels || []
|
||
const bedenValues = form.bedenler || []
|
||
|
||
const overLimit = []
|
||
for (let i = 0; i < bedenLabels.length; i++) {
|
||
const lbl = String(bedenLabels[i] ?? '').trim()
|
||
const stok = Number(stockMapLocal?.[lbl] ?? 0)
|
||
const girilen = Number(bedenValues?.[i] ?? 0)
|
||
|
||
if (stok > 0 && girilen > stok) {
|
||
overLimit.push({ beden: lbl, stok, girilen })
|
||
}
|
||
}
|
||
|
||
if (overLimit.length && $q) {
|
||
const msg = overLimit
|
||
.map(x => `• <b>${x.beden}</b>: ${x.girilen} (Stok: ${x.stok})`)
|
||
.join('<br>')
|
||
|
||
const stokOK = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Stok Uyarısı',
|
||
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:<br><br>${msg}`,
|
||
html: true,
|
||
ok: { label: 'Devam', color: 'primary' },
|
||
cancel: { label: 'İptal', color: 'negative' }
|
||
})
|
||
.onOk(() => resolve(true))
|
||
.onCancel(() => resolve(false))
|
||
.onDismiss(() => resolve(false))
|
||
})
|
||
|
||
if (!stokOK) return false
|
||
}
|
||
|
||
/* =======================================================
|
||
3ï¸âƒ£ FİYAT (MIN) KONTROLÜ (FIXED)
|
||
======================================================= */
|
||
let fiyatOK = true
|
||
try {
|
||
let minFiyat = 0
|
||
|
||
if (typeof this.fetchMinPrice === 'function') {
|
||
const p = await this.fetchMinPrice(form.model, form.pb, $q)
|
||
minFiyat = Number(p?.price || 0)
|
||
} else if (Number(form.minFiyat || 0) > 0) {
|
||
minFiyat = Number(form.minFiyat)
|
||
}
|
||
|
||
const girilen = Number(form.fiyat || 0)
|
||
|
||
if (minFiyat > 0 && girilen > 0 && girilen < minFiyat && $q) {
|
||
fiyatOK = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Fiyat Uyarısı',
|
||
message:
|
||
`<b>Min. Fiyat:</b> ${minFiyat} ${form.pb}<br>` +
|
||
`<b>GirdiÄŸiniz:</b> ${girilen} ${form.pb}`,
|
||
html: true,
|
||
ok: { label: 'Devam', color: 'primary' },
|
||
cancel: { label: 'İptal', color: 'negative' }
|
||
})
|
||
.onOk(() => resolve(true))
|
||
.onCancel(() => resolve(false))
|
||
.onDismiss(() => resolve(false))
|
||
})
|
||
}
|
||
} catch (err) {
|
||
console.warn('âš Min fiyat hata:', err)
|
||
}
|
||
if (!fiyatOK) return false
|
||
|
||
/* =======================================================
|
||
4ï¸âƒ£ TOPLAM HESABI
|
||
======================================================= */
|
||
const adet = (form.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
|
||
form.adet = adet
|
||
form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2))
|
||
|
||
const newRow = toSummaryRowFromForm(form)
|
||
|
||
/* =======================================================
|
||
5ï¸âƒ£ EDIT MODE (editingKey ZORUNLU)
|
||
======================================================= */
|
||
if (this.editingKey) {
|
||
const idx = rows.findIndex(r => getKey(r) === this.editingKey)
|
||
if (idx === -1) {
|
||
this.editingKey = null
|
||
resetEditor?.(true)
|
||
return false
|
||
}
|
||
|
||
const prev = rows[idx]
|
||
|
||
if (this.isRowLocked?.(prev)) {
|
||
$q?.notify?.({ type: 'warning', message: 'Satır kapalı' })
|
||
this.editingKey = null
|
||
resetEditor?.(true)
|
||
return false
|
||
}
|
||
|
||
// ✅ kritik: store-level
|
||
const sameCombo = this.isSameCombo(prev, newRow)
|
||
|
||
const preservedLineIdMap =
|
||
(prev?.lineIdMap && typeof prev.lineIdMap === 'object')
|
||
? { ...prev.lineIdMap }
|
||
: (newRow?.lineIdMap && typeof newRow.lineIdMap === 'object')
|
||
? { ...newRow.lineIdMap }
|
||
: {}
|
||
|
||
/* ===== SAME COMBO → UPDATE ===== */
|
||
if (sameCombo) {
|
||
rows[idx] = {
|
||
...prev,
|
||
...newRow,
|
||
id: prev.id,
|
||
OrderLineID: prev.OrderLineID || null,
|
||
lineIdMap: preservedLineIdMap
|
||
}
|
||
|
||
this.summaryRows = rows
|
||
this.orders = rows
|
||
|
||
this.updateHeaderTotals?.()
|
||
this.persistLocalStorage?.()
|
||
|
||
this.editingKey = null
|
||
resetEditor?.(true)
|
||
recalcVat?.()
|
||
|
||
$q?.notify?.({ type: 'positive', message: 'Satır güncellendi' })
|
||
return true
|
||
}
|
||
|
||
/* ===== COMBO CHANGED → DELETE + INSERT ===== */
|
||
const grpKey =
|
||
prev?.grpKey ||
|
||
Object.keys(prev?.bedenMap || {})[0] ||
|
||
'tak'
|
||
|
||
const emptyMap = {}
|
||
const srcMap =
|
||
(prev?.bedenMap?.[grpKey] && typeof prev.bedenMap[grpKey] === 'object')
|
||
? prev.bedenMap[grpKey]
|
||
: (preservedLineIdMap && typeof preservedLineIdMap === 'object')
|
||
? preservedLineIdMap
|
||
: null
|
||
|
||
if (srcMap) {
|
||
for (const beden of Object.keys(srcMap)) emptyMap[beden] = 0
|
||
} else {
|
||
emptyMap['STD'] = 0
|
||
}
|
||
|
||
const deleteRow = {
|
||
...prev,
|
||
id: `DEL::${prev.id || prev.OrderLineID || crypto.randomUUID()}`,
|
||
_deleteSignal: true,
|
||
adet: 0,
|
||
Qty1: 0,
|
||
tutar: 0,
|
||
ComboKey: '',
|
||
|
||
OrderLineID: prev.OrderLineID || null,
|
||
|
||
grpKey,
|
||
bedenMap: { [grpKey]: emptyMap },
|
||
lineIdMap: preservedLineIdMap,
|
||
comboLineIds: { ...(prev.comboLineIds || {}) }
|
||
}
|
||
|
||
const insertedRow = {
|
||
...newRow,
|
||
id: crypto.randomUUID(),
|
||
OrderLineID: null,
|
||
lineIdMap: {}
|
||
}
|
||
|
||
rows.splice(idx, 1, insertedRow)
|
||
|
||
this.summaryRows = rows
|
||
this.orders = [...rows, deleteRow]
|
||
|
||
this.updateHeaderTotals?.()
|
||
this.persistLocalStorage?.()
|
||
|
||
this.editingKey = null
|
||
resetEditor?.(true)
|
||
recalcVat?.()
|
||
|
||
$q?.notify?.({ type: 'positive', message: 'Kombinasyon deÄŸiÅŸti' })
|
||
return true
|
||
}
|
||
|
||
/* =======================================================
|
||
6ï¸âƒ£ NEW MODE (MERGE / INSERT) — COMBO SAFE
|
||
- aynı combo → bedenMap merge (satır sayısı artmaz)
|
||
- farklı combo → yeni satır
|
||
- ✅ FIX: _deleteSignal satırlarını dup aramasında hariç tut
|
||
======================================================= */
|
||
const dupIdx = rows.findIndex(r =>
|
||
!r?._deleteSignal &&
|
||
this.isSameCombo(r, newRow)
|
||
)
|
||
|
||
// helper: bedenMap çıkar (gruplu ya da düz)
|
||
const extractMap = (row) => {
|
||
const grpKey =
|
||
row?.grpKey ||
|
||
Object.keys(row?.bedenMap || {})[0] ||
|
||
'GENEL'
|
||
|
||
const grouped = row?.bedenMap?.[grpKey]
|
||
const flat = (row?.bedenMap && typeof row.bedenMap === 'object' && !grouped)
|
||
? row.bedenMap
|
||
: null
|
||
|
||
return { grpKey, map: (grouped || flat || {}) }
|
||
}
|
||
|
||
if (dupIdx !== -1) {
|
||
const prev = rows[dupIdx]
|
||
|
||
// delete satırına merge yapma (ek güvenlik)
|
||
if (prev?._deleteSignal !== true) {
|
||
const { grpKey: prevGrp, map: prevMap } = extractMap(prev)
|
||
const { grpKey: newGrp, map: newMap } = extractMap(newRow)
|
||
|
||
// hangi grpKey kullanılacak?
|
||
const grpKey = newRow?.grpKey || prevGrp || newGrp || 'GENEL'
|
||
|
||
// MERGE: bedenleri topluyoruz (override deÄŸil)
|
||
const merged = { ...(prevMap || {}) }
|
||
for (const [k, v] of Object.entries(newMap || {})) {
|
||
const beden = (k == null || String(k).trim() === '')
|
||
? ' '
|
||
: 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,
|
||
|
||
|
||
}
|