/* =========================================================== 📦 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}` } const SIZE_GROUP_TITLES = { tak: 'TAKIM ELBISE', ayk: 'AYAKKABI', ayk_garson: 'AYAKKABI GARSON', yas: 'YAS', pan: 'PANTOLON', gom: 'GOMLEK', aksbir: 'AKSESUAR' } const FALLBACK_SCHEMA_MAP = { tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] } } export const schemaByKey = { ...FALLBACK_SCHEMA_MAP } const productSizeMatchCache = { loaded: false, rules: [], schemas: {} } function resetProductSizeMatchCache() { productSizeMatchCache.loaded = false productSizeMatchCache.rules = [] productSizeMatchCache.schemas = {} } function setProductSizeMatchCache(payload) { const rules = Array.isArray(payload?.rules) ? payload.rules : [] const schemasRaw = payload?.schemas && typeof payload.schemas === 'object' ? payload.schemas : {} const normalizedRules = rules .map(r => ({ productGroupID: Number(r?.product_group_id || r?.productGroupID || 0), kategori: normalizeTextForMatch(r?.kategori || ''), urunAnaGrubu: normalizeTextForMatch(r?.urun_ana_grubu || r?.urunAnaGrubu || ''), urunAltGrubu: normalizeTextForMatch(r?.urun_alt_grubu || r?.urunAltGrubu || ''), groupKeys: Array.isArray(r?.group_keys || r?.groupKeys) ? (r.group_keys || r.groupKeys).map(g => String(g || '').trim()).filter(Boolean) : [] })) .filter(r => r.groupKeys.length > 0) .sort((a, b) => { if (a.productGroupID && b.productGroupID) return a.productGroupID - b.productGroupID return 0 }) const normalizedSchemas = {} for (const [k, vals] of Object.entries(schemasRaw)) { const key = String(k || '').trim() if (!key) continue const arr = Array.isArray(vals) ? vals : String(vals || '').split(',') normalizedSchemas[key] = arr .map(v => { const s = String(v == null ? '' : v).trim() return s === '' ? ' ' : s }) .filter((v, idx, all) => all.indexOf(v) === idx) } productSizeMatchCache.loaded = true productSizeMatchCache.rules = normalizedRules productSizeMatchCache.schemas = normalizedSchemas } function buildSchemaMapFromCacheSchemas() { const out = {} const src = productSizeMatchCache.schemas || {} for (const [keyRaw, valuesRaw] of Object.entries(src)) { const key = String(keyRaw || '').trim() if (!key) continue const values = Array.isArray(valuesRaw) ? valuesRaw : [] out[key] = { key, title: SIZE_GROUP_TITLES[key] || key.toUpperCase(), values: values.map(v => String(v == null ? '' : v)) } } if (!out.tak) out.tak = { ...FALLBACK_SCHEMA_MAP.tak } return out } 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: SQL mk_size_group (cache) =========================================================== */ initSchemaMap() { if (this.schemaMap && Object.keys(this.schemaMap).length > 0) { return } this.schemaMap = buildSchemaMapFromCacheSchemas() if (!Object.keys(this.schemaMap).length) { this.schemaMap = { ...FALLBACK_SCHEMA_MAP } } console.log( '🧩 schemaMap INIT edildi:', Object.keys(this.schemaMap) ) }, async ensureProductSizeMatchRules($q = null, force = false) { if (!force && productSizeMatchCache.loaded && productSizeMatchCache.rules.length > 0) { this.schemaMap = buildSchemaMapFromCacheSchemas() return true } try { const res = await api.get('/product-size-match/rules') setProductSizeMatchCache(res?.data || {}) this.schemaMap = buildSchemaMapFromCacheSchemas() return true } catch (err) { if (force) { resetProductSizeMatchCache() } this.schemaMap = { ...FALLBACK_SCHEMA_MAP } console.warn('⚠ product-size-match rules alınamadı:', err) $q?.notify?.({ type: 'warning', message: 'Beden eşleme kuralları alınamadı.' }) return false } }, getRowKey(row) { if (!row) return null return row.OrderLineID || row.id || null } , updateHeaderTotals() { try { if (!Array.isArray(this.summaryRows)) return 0 const total = this.summaryRows.reduce( (sum, r) => sum + Number(r?.tutar || 0), 0 ) // Header sadece GÖSTERİM / BACKEND için if (this.header) { this.header.TotalAmount = Number(total.toFixed(2)) } return total } catch (err) { console.error('❌ updateHeaderTotals hata:', err) return 0 } } , /* =========================================================== 🚨 showInvalidVariantDialog — FINAL ----------------------------------------------------------- ✔ prItemVariant olmayan satırları listeler ✔ Satıra tıkla → scroll + highlight ✔ Kaydı BLOKLAYAN tek UI noktası =========================================================== */ async showInvalidVariantDialog($q, invalidList = []) { if (!Array.isArray(invalidList) || invalidList.length === 0) return return new Promise(resolve => { const dlg = $q.dialog({ title: '🚨 Tanımsız Ürün Kombinasyonları', message: `
${invalidList.map((v, i) => `
#${i + 1} | Item: ${v.itemCode}
Beden: ${v.dim1 || '(boş)'} | Renk: ${v.colorCode || '-'} | Qty: ${v.qty1}
Sebep: ${v.reason || 'Tanımsız ürün kombinasyonu'}
`).join('')}
`, html: true, ok: { label: 'Düzelt', color: 'negative' }, cancel: false, persistent: true }) .onOk(() => resolve()) .onDismiss(() => resolve()) // Quasar v2 chain API'de onShown yok; dialog DOM'u render olduktan sonra bağla. setTimeout(() => { if (!dlg) return const nodes = document.querySelectorAll('.invalid-row') nodes.forEach(n => { n.addEventListener('click', () => { const ck = n.getAttribute('data-clientkey') this.scrollToInvalidRow?.(ck) }) }) }, 0) }) } , /* =========================================================== 🎯 scrollToInvalidRow — FINAL ----------------------------------------------------------- ✔ ClientKey bazlı scroll ✔ Hem summaryRows hem orders destekli ✔ Highlight otomatik kalkar =========================================================== */ scrollToInvalidRow(clientKey) { if (!clientKey) return // 1️⃣ Store içindeki satırı bul const idx = this.summaryRows?.findIndex( r => r.clientKey === clientKey ) if (idx === -1) { console.warn('❌ Satır bulunamadı:', clientKey) return } // 2️⃣ DOM node const el = document.querySelector( `[data-clientkey="${clientKey}"]` ) if (!el) { console.warn('❌ DOM satırı bulunamadı:', clientKey) return } // 3️⃣ Scroll el.scrollIntoView({ behavior: 'smooth', block: 'center' }) // 4️⃣ Highlight el.classList.add('invalid-highlight') setTimeout(() => { el.classList.remove('invalid-highlight') }, 2500) } , async checkHeaderExists(orderHeaderID) { try { if (!orderHeaderID) return false const res = await api.get(`/orders/check/${orderHeaderID}`) // Backend “true/false” döner varsayımı return res?.data?.exists === true } catch (err) { console.warn("⚠ checkHeaderExists hata:", err) return false } } , async fetchOrderPdf(orderId) { try { const resp = await api.get(`/order/pdf/${orderId}`, { responseType: 'blob' }) return resp.data } catch (err) { const detail = await extractApiErrorDetail(err) const status = err?.status || err?.response?.status || '-' console.error(`? fetchOrderPdf hata [${status}] order=${orderId}: ${detail}`) throw new Error(detail) } } , buildMailLineLabel(line) { if (!line || typeof line !== 'object') return '' const item = String(line.ItemCode || '').trim() const color1 = String(line.ColorCode || '').trim() const color2 = String(line.ItemDim2Code || '').trim() const desc = String(line.LineDescription || '').trim() if (!item) return '' const colorPart = color2 ? `${color1}-${color2}` : color1 return [item, colorPart, desc].filter(Boolean).join(' ') } , buildOrderMailPayload(lines = [], isNew = false) { const uniq = (arr) => [...new Set((arr || []).map(v => String(v || '').trim()).filter(Boolean))] const normalized = Array.isArray(lines) ? lines : [] const mapLabel = (ln) => this.buildMailLineLabel(ln) if (isNew) { return { operation: 'create', deletedItems: [], updatedItems: [], addedItems: uniq( normalized .filter(ln => !ln?._deleteSignal) .map(mapLabel) ) } } const deletedItems = uniq( normalized .filter(ln => ln?._deleteSignal === true) .map(mapLabel) ) const updatedItems = uniq( normalized .filter(ln => !ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true) .map(mapLabel) ) const addedItems = uniq( normalized .filter(ln => !ln?._deleteSignal && !ln?.OrderLineID) .map(mapLabel) ) return { operation: 'update', deletedItems, updatedItems, addedItems } } , async sendOrderToMarketMails(orderId, payload = {}) { const id = String(orderId || this.header?.OrderHeaderID || '').trim() if (!id) { throw new Error('Sipariş ID bulunamadı') } try { const res = await api.post('/order/send-market-mail', { orderHeaderID: id, operation: payload?.operation || 'create', deletedItems: Array.isArray(payload?.deletedItems) ? payload.deletedItems : [], updatedItems: Array.isArray(payload?.updatedItems) ? payload.updatedItems : [], addedItems: Array.isArray(payload?.addedItems) ? payload.addedItems : [] }) return res?.data || {} } catch (err) { const detail = await extractApiErrorDetail(err) const status = err?.status || err?.response?.status || '-' console.error(`❌ sendOrderToMarketMails hata [${status}] order=${id}: ${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<61> [${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(this.getDraftKey) localStorage.removeItem('bss_active_new_header') localStorage.removeItem('bss_last_txn') this.activeNewHeaderId = null } , /* =========================================================== 🧹 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') const rulesReady = await this.ensureProductSizeMatchRules?.($q) if (!rulesReady) { $q?.notify?.({ type: 'negative', message: 'Beden eşleme kuralları yüklenemedi.' }) return false } /* ======================================================= 🔹 BACKEND — authoritative load ======================================================= */ const res = await api.get(`/order/get/${orderId}`) const backend = res?.data if (!backend?.header) { throw new Error('Backend header yok') } // Editor ile aynı grpKey kararını verebilmek için // eksik model metadata'sını (kategori/ana-alt grup) cache'e al. const backendLines = Array.isArray(backend?.lines) ? backend.lines : [] const modelCodes = [...new Set( backendLines .map(l => String(l?.ItemCode || l?.Model || '').trim()) .filter(Boolean) )] const missingCodes = modelCodes.filter(code => !pc?.[code]) if (missingCodes.length) { await Promise.all( missingCodes.map(async code => { try { const d = (await api.get('/product-detail', { params: { code } }))?.data || {} pc[code] = { ...(pc[code] || {}), ...d, UrunAnaGrubu: d.UrunAnaGrubu || d.ProductGroup || d.ProductAtt01Desc || '', UrunAltGrubu: d.UrunAltGrubu || d.ProductSubGroup || d.ProductAtt02Desc || '', Kategori: d.Kategori || '', YETISKIN_GARSON: d.YETISKIN_GARSON || d.YetiskinGarson || d.AskiliYan || '', YetiskinGarson: d.YetiskinGarson || d.YETISKIN_GARSON || d.AskiliYan || '', AskiliYan: d.AskiliYan || '' } } catch (e) { console.warn(`⚠ model detail alınamadı (${code})`, e) } }) ) } /* ======================================================= 🔹 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( backendLines, 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') localStorage.removeItem('bss_active_new_header') this.activeNewHeaderId = null 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 rawLbl = String(bedenLabels[i] ?? '') const lbl = rawLbl.trim() === '' ? ' ' : rawLbl const hasExactKey = Object.prototype.hasOwnProperty.call(stockMapLocal || {}, lbl) const hasTrimKey = Object.prototype.hasOwnProperty.call(stockMapLocal || {}, rawLbl.trim()) const stokRaw = hasExactKey ? stockMapLocal?.[lbl] : hasTrimKey ? stockMapLocal?.[rawLbl.trim()] : undefined const stok = Number(stokRaw ?? 0) const girilen = Number(bedenValues?.[i] ?? 0) // Stok 0 veya stok kaydı yokken giriş yapılırsa da uyarı ver. if (girilen > 0 && girilen > stok) { overLimit.push({ beden: lbl, stok, girilen, stokKaydiVar: hasExactKey || hasTrimKey }) } } if (overLimit.length && $q) { const msg = overLimit .map(x => `• ${x.beden}: ${x.girilen} (Stok: ${x.stokKaydiVar ? x.stok : 'kayıt yok'})`) .join('
') const stokOK = await new Promise(resolve => { $q.dialog({ title: 'Stok Uyarısı', message: `Bazı bedenlerde stoktan fazla giriş yaptınız:

${msg}`, html: true, ok: { label: 'Devam', color: 'primary' }, cancel: { label: 'İptal', color: 'negative' } }) .onOk(() => resolve(true)) .onCancel(() => resolve(false)) .onDismiss(() => resolve(false)) }) if (!stokOK) return false } /* ======================================================= 3️⃣ FİYAT (MIN) KONTROLÜ (FIXED) ======================================================= */ let fiyatOK = true try { let minFiyat = 0 if (typeof this.fetchMinPrice === 'function') { const p = await this.fetchMinPrice(form.model, form.pb, $q) minFiyat = Number(p?.price || 0) } else if (Number(form.minFiyat || 0) > 0) { minFiyat = Number(form.minFiyat) } const girilen = Number(form.fiyat || 0) if (minFiyat > 0 && girilen > 0 && girilen < minFiyat && $q) { fiyatOK = await new Promise(resolve => { $q.dialog({ title: 'Fiyat Uyarısı', message: `Min. Fiyat: ${minFiyat} ${form.pb}
` + `Girdiğiniz: ${girilen} ${form.pb}`, html: true, ok: { label: 'Devam', color: 'primary' }, cancel: { label: 'İptal', color: 'negative' } }) .onOk(() => resolve(true)) .onCancel(() => resolve(false)) .onDismiss(() => resolve(false)) }) } } catch (err) { console.warn('⚠ Min fiyat hata:', err) } if (!fiyatOK) return false /* ======================================================= 4️⃣ TOPLAM HESABI ======================================================= */ const adet = (form.bedenler || []).reduce((a, b) => a + Number(b || 0), 0) form.adet = adet form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2)) const newRow = toSummaryRowFromForm(form) if (!newRow) { $q?.notify?.({ type: 'negative', message: 'Beden grubu eşleşmesi bulunamadı.' }) return false } /* ======================================================= 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, _dirty: true, 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, _dirty: true, 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, _dirty: true, // 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, _dirty: true, 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', productCache = null) { if (!Array.isArray(lines)) return [] const merged = Object.create(null) const pc = (productCache && typeof productCache === 'object') ? productCache : {} 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() const meta = pc?.[model] || {} // ❗ BEDEN YOK → bu SADECE üst seviye grup anahtarı const modelKey = `${model}||${renk}||${renk2}` // grouped map: { [grpKey]: { beden: qty } } or flat map: { beden: qty } const groupedKey = raw.grpKey || (raw.bedenMap && typeof raw.bedenMap === 'object' ? Object.keys(raw.bedenMap).find(k => raw.bedenMap[k] && typeof raw.bedenMap[k] === 'object') : null) const srcMap = groupedKey && raw.bedenMap?.[groupedKey] && typeof raw.bedenMap[groupedKey] === 'object' ? raw.bedenMap[groupedKey] : raw.bedenMap const grpKey = groupedKey || detectBedenGroup( null, raw.urunAnaGrubu || raw.UrunAnaGrubu || meta.UrunAnaGrubu || meta.ProductGroup || '', raw.kategori || raw.Kategori || meta.Kategori || '', raw.yetiskinGarson || raw.YETISKIN_GARSON || raw.YetiskinGarson || raw.AskiliYan || raw.ASKILIYAN || raw.askiliyan || meta.YETISKIN_GARSON || meta.YetiskinGarson || meta.AskiliYan || '', raw.urunAltGrubu || raw.UrunAltGrubu || meta.UrunAltGrubu || meta.ProductSubGroup || '' ) || 'tak' 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() const meta = pc?.[model] || {} // ❗ 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 bedenRawUpper = safeTrimUpperJs(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 || meta.UrunAnaGrubu || meta.ProductGroup || 'GENEL', urunAltGrubu: raw.UrunAltGrubu || meta.UrunAltGrubu || meta.ProductSubGroup || '', kategori: raw.Kategori || raw.YETISKIN_GARSON || raw.YetiskinGarson || raw.yetiskinGarson || meta.Kategori || meta.YETISKIN_GARSON || meta.YetiskinGarson || '', yetiskinGarson: raw.YETISKIN_GARSON || raw.YetiskinGarson || raw.yetiskinGarson || raw.AskiliYan || raw.ASKILIYAN || raw.askiliyan || meta.YETISKIN_GARSON || meta.YetiskinGarson || meta.AskiliYan || '', aciklama: raw.LineDescription || '', fiyat: Number(raw.Price || 0), pb: raw.DocCurrencyCode || pbFallback, __tmpMap: {}, // beden → qty lineIdMap: {}, // beden → OrderLineID yasPayloadMap: {}, 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 (bedenRawUpper && /^(\d+)\s*(Y|YAS|YAŞ)$/.test(bedenRawUpper)) { const canonical = normalizeBedenLabel(bedenRawUpper) entry.yasPayloadMap[canonical] = pickPreferredYasPayloadLabel( entry.yasPayloadMap[canonical], bedenRawUpper ) } 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 } // 🔒 TEK VE KESİN KARAR const grpKey = detectBedenGroup( null, row.urunAnaGrubu, row.kategori || '', row.yetiskinGarson, row.urunAltGrubu || '' ) 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) } if (grpKey === 'yas') { row.yasPayloadMap = row.yasPayloadMap || {} for (const b of Object.keys(cleanedMap || {})) { const s = String(b || '').trim() if (/^\d+$/.test(s) && !row.yasPayloadMap[s]) { row.yasPayloadMap[s] = `${s}Y` } } } 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 } form.yasPayloadMap = { ...(cached.yasPayloadMap || {}) } 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 = {} const apiYasPayloadMap = {} for (const x of data) { const rawDim1 = x.item_dim1_code == null ? ' ' : String(x.item_dim1_code) const key = rawDim1 === ' ' ? ' ' : normalizeBedenLabel(rawDim1) apiStockMap[key] = Number(x.kullanilabilir_envanter ?? 0) const rawUpper = safeTrimUpperJs(rawDim1) if (grpKey === 'yas' && /^(\d+)\s*(Y|YAS|YAŞ)$/.test(rawUpper)) { const canonical = normalizeBedenLabel(rawUpper) apiYasPayloadMap[canonical] = pickPreferredYasPayloadLabel( apiYasPayloadMap[canonical], rawUpper ) } } const finalStockMap = {} for (const lbl of (form.bedenLabels || [])) { const normalizedLbl = lbl == null || String(lbl).trim() === '' ? ' ' : normalizeBedenLabel(String(lbl)) finalStockMap[lbl] = apiStockMap[normalizedLbl] ?? apiStockMap[lbl] ?? 0 } stockMap.value = { ...finalStockMap } bedenStock.value = Object.entries(stockMap.value).map( ([beden, stok]) => ({ beden, stok }) ) form.yasPayloadMap = { ...(form.yasPayloadMap || {}), ...apiYasPayloadMap } /* ======================================================= 💾 CACHE ======================================================= */ sizeCache.value[cacheKey] = { labels: [...form.bedenLabels], stockArray: [...bedenStock.value], stockMap: { ...stockMap.value }, yasPayloadMap: { ...(form.yasPayloadMap || {}) } } 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 bedenSet = new Set() if (row?.bedenMap && typeof row.bedenMap === 'object') { const grp = row?.grpKey && row.bedenMap[row.grpKey] && typeof row.bedenMap[row.grpKey] === 'object' ? row.bedenMap[row.grpKey] : null if (grp) { Object.keys(grp).forEach(k => bedenSet.add(String(k || ''))) } else { Object.values(row.bedenMap).forEach(m => { if (m && typeof m === 'object') { Object.keys(m).forEach(k => bedenSet.add(String(k || ''))) } }) } } if (bedenSet.size === 0 && Array.isArray(row?.bedenLabels)) { row.bedenLabels.forEach(lbl => { bedenSet.add(String(lbl == null ? '' : lbl)) }) } return detectBedenGroup( Array.from(bedenSet), row?.urunAnaGrubu || '', row?.kategori || '', row?.YETISKIN_GARSON || row?.yetiskinGarson || '', row?.urunAltGrubu || '' ) }, /* ======================================================= 🔹 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}`) } , // Sync only header-related fields from the form before submit. syncHeaderFromForm(form) { if (!form || typeof form !== 'object') return const keys = [ 'OrderHeaderID', 'OrderTypeCode', 'ProcessCode', 'OrderNumber', 'OrderDate', 'AverageDueDate', 'Description', 'InternalDescription', 'CurrAccTypeCode', 'CurrAccCode', 'ShippingPostalAddressID', 'BillingPostalAddressID', 'CurrAccDescription', 'DocCurrencyCode', 'LocalCurrencyCode', 'ExchangeRate', 'OfficeCode', 'CreatedUserName', 'CreatedDate', 'LastUpdatedUserName', 'LastUpdatedDate', 'PaymentTerm', 'WarehouseCode', 'StoreCode' ] const patch = {} for (const k of keys) { if (Object.prototype.hasOwnProperty.call(form, k)) { patch[k] = form[k] } } if (Object.keys(patch).length > 0) { this.header = { ...(this.header || {}), ...patch } } } , /* =========================================================== 🟦 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 let purgeNewDraftOnExit = false try { this.loading = true // 🔒 Kontrollü submit → route leave guard susar this.isControlledSubmit = true // ✅ Formdaki header alanlarını store'a taşı this.syncHeaderFromForm?.(form) 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' ) // Ensure backend string fields are sent as strings. if (header) { header.OfficeCode = header.OfficeCode != null ? String(header.OfficeCode) : header.OfficeCode header.StoreCode = header.StoreCode != null ? String(header.StoreCode) : header.StoreCode header.OrdererOfficeCode = header.OrdererOfficeCode != null ? String(header.OrdererOfficeCode) : header.OrdererOfficeCode header.OrdererStoreCode = header.OrdererStoreCode != null ? String(header.OrdererStoreCode) : header.OrdererStoreCode // PaymentTerm must be numeric (int16) or null for backend. if (header.PaymentTerm === '' || header.PaymentTerm == null) { header.PaymentTerm = null } else if (typeof header.PaymentTerm === 'string') { const n = Number(header.PaymentTerm) header.PaymentTerm = Number.isNaN(n) ? null : n } } console.log('HEADER:', JSON.parse(JSON.stringify(header))) console.log('HEADER KEYS:', { OrderHeaderID: header?.OrderHeaderID, CurrAccTypeCode: header?.CurrAccTypeCode, CurrAccCode: header?.CurrAccCode, OfficeCode: header?.OfficeCode, StoreTypeCode: header?.StoreTypeCode, StoreCode: header?.StoreCode, OrdererOfficeCode: header?.OrdererOfficeCode, OrdererStoreCode: header?.OrdererStoreCode, PaymentTerm: header?.PaymentTerm, DocCurrencyCode: header?.DocCurrencyCode }) 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 // ======================================================= if (!isNew) { const linesToValidate = lines.filter( l => l._deleteSignal === true || l._dirty === true || !l.OrderLineID ) const v = await api.post('/order/validate', { header, lines: linesToValidate }) const invalid = v?.data?.invalid || [] if (invalid.length > 0) { await this.showInvalidVariantDialog?.($q, invalid) return // ❌ 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') } purgeNewDraftOnExit = isNew /* ======================================================= 🔁 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 + NEW DRAFT temizlenir ======================================================= */ this.clearAllOrderSnapshots() if (isNew) { this.clearNewDraft() } $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 }) // 📧 Piyasa eşleşen alıcılara sipariş PDF gönderimi (kayıt başarılı olduktan sonra) try { const mailPayload = this.buildOrderMailPayload(lines, isNew) const mailRes = await this.sendOrderToMarketMails(serverOrderId, mailPayload) const sentCount = Number(mailRes?.sentCount || 0) $q.notify({ type: 'positive', message: sentCount > 0 ? `Sipariş PDF mail gönderildi (${sentCount} alıcı)` : 'Sipariş PDF mail gönderimi tamamlandı' }) } catch (mailErr) { $q.notify({ type: 'warning', message: `Sipariş kaydedildi, mail gönderilemedi: ${mailErr?.message || 'Bilinmeyen hata'}` }) } /* ======================================================= ❓ 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 { // ✅ NEW kaydı başarılıysa taslağı exit noktasında da zorla temizle if (purgeNewDraftOnExit) { this.clearNewDraft() } // 🔓 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 } // UI'de yas grubu 2/4/6... gösterilir; payload'a 2Y/4Y/6... yazılır. const toPayloadBeden = (row, grpKey, v) => { const base = normBeden(v) if (!base) return '' if (grpKey === 'yas' && /^\d+$/.test(base)) { const map = row?.yasPayloadMap && typeof row.yasPayloadMap === 'object' ? row.yasPayloadMap : {} const mapped = safeStr(map[base]).toUpperCase() if (mapped) return mapped return `${base}Y` } return base } /* ========================= 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, _dirty: row?._dirty === true, _deleteSignal: isDeleteSignal === true, 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._deleteSignal = true existing.Qty1 = 0 return } /* MERGE */ existing.Qty1 += qty if (row?._dirty === true) { existing._dirty = true } 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) // ✅ UI/combokey için kanonik beden (yas'ta 2/4/6...) const bedenCanonical = normBeden(bedenRaw) // ✅ payload beden: yas grubunda 2Y/4Y/6..., diğerlerinde normal const bedenPayload = toPayloadBeden(row, grpKey, bedenRaw) // ✅ combokey beden: boşsa '_' ile stabil kalsın const bedenKey = bedenCanonical || '_' let orderLineId = '' if (this.mode === 'edit') { // lineIdMap anahtarı sizde hangi bedenle tutuluyorsa ikisini de dene orderLineId = safeStr(lineIdMap?.[bedenKey]) || safeStr(lineIdMap?.[bedenCanonical]) || 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 } function pickPreferredYasPayloadLabel(currentRaw, nextRaw) { const cur = safeTrimUpperJs(currentRaw) const nxt = safeTrimUpperJs(nextRaw) if (!nxt) return cur if (!cur) return nxt const curYas = /YAS$|YAŞ$/.test(cur) const nxtYas = /YAS$|YAŞ$/.test(nxt) if (!curYas && nxtYas) return nxt return cur } export function normalizeBedenLabel(v) { let s = (v == null ? '' : String(v)).trim() if (s === '') return ' ' s = s.toUpperCase() // Yas bedenleri backendte 2Y/4Y/6Y veya 2YAS/2YAŞ gibi gelebilir. // UI şeması 2/4/6/8/10/12/14 ile çalıştığı için numeric'e indir. const yasMatch = s.match(/^(\d+)\s*(Y|YAS|YAŞ)$/) if (yasMatch && yasMatch[1]) { return yasMatch[1] } // 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 } // Backward-compatible alias kept for older call sites/bundles. export function normalizeBeden(v) { return normalizeBedenLabel(v) } function deriveKategoriToken(urunKategori = '', yetiskinGarson = '') { const kat = normalizeTextForMatch(urunKategori || '') if (kat.includes('GARSON')) return 'GARSON' if (kat.includes('YETISKIN')) return 'YETISKIN' return '' } function normalizeRuleAltGroup(urunAltGrubu = '') { return normalizeTextForMatch(urunAltGrubu || '') } function pickBestGroupFromCandidates(groupKeys = [], bedenList = []) { if (!Array.isArray(groupKeys) || groupKeys.length === 0) return '' if (groupKeys.length === 1) return groupKeys[0] const normalizedBeden = (Array.isArray(bedenList) ? bedenList : []) .map(v => normalizeBedenLabel(v)) .filter(Boolean) if (!normalizedBeden.length) return groupKeys[0] let bestKey = groupKeys[0] let bestScore = -1 for (const key of groupKeys) { const schema = Array.isArray(productSizeMatchCache.schemas?.[key]) ? productSizeMatchCache.schemas[key] : [] const normalizedSchema = new Set(schema.map(v => normalizeBedenLabel(v))) let score = 0 for (const b of normalizedBeden) { if (normalizedSchema.has(b)) score += 1 } if (score > bestScore) { bestScore = score bestKey = key } } return bestKey || groupKeys[0] } function resolveGroupFromProductSizeMatchRules( bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '', urunAltGrubu = '' ) { if (!productSizeMatchCache.loaded || !Array.isArray(productSizeMatchCache.rules) || !productSizeMatchCache.rules.length) { return '' } const kategoriToken = deriveKategoriToken(urunKategori, yetiskinGarson) const kategoriRaw = normalizeTextForMatch(urunKategori || '') const ana = normalizeTextForMatch(urunAnaGrubu || '') const alt = normalizeRuleAltGroup(urunAltGrubu) if (!ana) return '' const bestScoreGroupKeys = [] let bestScore = -1 for (const rule of productSizeMatchCache.rules) { if (!rule?.urunAnaGrubu || rule.urunAnaGrubu !== ana) continue const ruleKategori = normalizeTextForMatch(rule.kategori || '') const catExact = (kategoriToken && ruleKategori === kategoriToken) || (kategoriRaw && ruleKategori === kategoriRaw) const catWildcard = ruleKategori === '' if (!catExact && !catWildcard) continue const ruleAlt = normalizeTextForMatch(rule.urunAltGrubu || '') const altExact = !!alt && ruleAlt === alt const altWildcard = ruleAlt === '' if (!altExact && !altWildcard) continue const score = (catExact ? 2 : 0) + (altExact ? 1 : 0) if (score < bestScore) continue if (score > bestScore) { bestScore = score bestScoreGroupKeys.length = 0 } for (const g of (rule.groupKeys || [])) { const key = String(g || '').trim() if (key && !bestScoreGroupKeys.includes(key)) { bestScoreGroupKeys.push(key) } } } if (!bestScoreGroupKeys.length) return '' return pickBestGroupFromCandidates(bestScoreGroupKeys, bedenList) } /* =========================================================== Size Group Detection - Core logic aligned with backend detectBedenGroupGo - Keeps frontend aksbir bucket for accessory lines =========================================================== */ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '', urunAltGrubu = '') { const list = Array.isArray(bedenList) && bedenList.length > 0 ? bedenList.map(v => (v || '').toString().trim().toUpperCase()) : [' '] const ruleBased = resolveGroupFromProductSizeMatchRules( list, urunAnaGrubu, urunKategori, yetiskinGarson, urunAltGrubu ) if (productSizeMatchCache.loaded) { if (!ruleBased) { console.warn('⚠ product-size-match eşleşme bulunamadı', { kategori: deriveKategoriToken(urunKategori, yetiskinGarson), urunAnaGrubu: normalizeTextForMatch(urunAnaGrubu || ''), urunAltGrubu: normalizeRuleAltGroup(urunAltGrubu), bedenList: list }) } return ruleBased || 'tak' } console.warn('⚠ product-size-match cache hazir degil, fallback=tak', { kategori: deriveKategoriToken(urunKategori, yetiskinGarson), urunAnaGrubu: normalizeTextForMatch(urunAnaGrubu || ''), urunAltGrubu: normalizeRuleAltGroup(urunAltGrubu), bedenList: list }) return 'tak' const rawAna = normalizeTextForMatch(urunAnaGrubu || '') const rawKat = normalizeTextForMatch(urunKategori || '') const rawYetiskinGarson = normalizeTextForMatch(yetiskinGarson || '') const isYetiskin = rawKat.includes('YETISKIN') const isGomlekKlasikOrAtayaka = rawAna.includes('GOMLEK KLASIK') || rawAna.includes('GOMLEK ATA YAKA') || rawAna.includes('GOMLEK ATAYAKA') // Özel kural: // Kategorisi YETISKIN ve ana grubu GOMLEK KLASIK/ATA YAKA olanlar her zaman "gom" grubundadır. if (isYetiskin && isGomlekKlasikOrAtayaka) { return 'gom' } // Beden seti çocuk yaş formatındaysa metadata beklemeden "yas" aç. // Örn: 2,4,6,8,10,12,14 veya 2Y,4Y,6Y... const yasNums = new Set(['2', '4', '6', '8', '10', '12', '14']) const yasParsed = list .map(v => v.replace(/\s+/g, '').replace(/YAS$/i, '').replace(/Y$/i, '')) if (yasParsed.length > 0 && yasParsed.every(v => yasNums.has(v))) { return 'yas' } const isYetiskinGomlekKlasik = isYetiskin && rawAna.includes('GOMLEK KLASIK') const mappedRawAna = isYetiskinGomlekKlasik ? rawAna.replace('GOMLEK KLASIK', 'GOMLEK ATA YAKA') : rawAna // Ozel kural: // YETISKIN/GARSON = GARSON ve URUN ANA GRUBU "GOMLEK ATA YAKA" veya "GOMLEK KLASIK" ise // sonuc "yas" olmalidir. const isGarsonGomlekAnaGrubu = mappedRawAna.includes('GOMLEK ATAYAKA') || mappedRawAna.includes('GOMLEK ATA YAKA') || mappedRawAna.includes('GOMLEK KLASIK') const hasGarsonSignal = mappedRawAna.includes('GARSON') || rawKat.includes('GARSON') || rawYetiskinGarson.includes('GARSON') if (isGarsonGomlekAnaGrubu && (rawKat.includes('GARSON') || rawYetiskinGarson.includes('GARSON'))) { return 'yas' } // Ayakkabi kurali garsondan once uygulanmali: // GARSON + AYAKKABI => ayk_garson, digerleri => ayk if (mappedRawAna.includes('AYAKKABI') || rawKat.includes('AYAKKABI')) { return hasGarsonSignal ? 'ayk_garson' : 'ayk' } const hasGarson = hasGarsonSignal if (hasGarson) return 'yas' // 🔸 Harfli beden varsa doğrudan "gom" (gömlek/üst giyim) // STD/ONE SIZE aksbir için saklı kalsın. const harfliBedenler = ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] if (list.some(b => harfliBedenler.includes(b))) return 'gom' const ana = mappedRawAna .trim() .replace(/\(.*?\)/g, '') .replace(/[^A-Z0-9\s]/g, '') .replace(/\s+/g, ' ') const kat = normalizeTextForMatch(urunKategori || '').trim() // 🔸 Aksesuar ise "aksbir" const aksesuarGruplari = [ 'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP', 'FULAR','MENDIL','KASKOL','ASKI', 'YAKA','KOL DUGMESI' ] const giyimGruplari = ['GOMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TISORT'] // 🔸 Pantolon özel durumu if ( aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) && !giyimGruplari.some(g => ana.includes(g)) ) return 'aksbir' if (ana.includes('PANTOLON') && kat.includes('YETISKIN')) return 'pan' // 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı const allNumeric = list.every(v => /^\d+$/.test(v)) if (allNumeric) { const nums = list.map(v => parseInt(v, 10)).filter(Boolean) const diffs = nums.slice(1).map((v, i) => v - nums[i]) if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk' } // 🔸 Yaş grubu (çocuk/garson) if (kat.includes('GARSON') || kat.includes('COCUK')) return 'yas' // 🔸 Varsayılan: takım elbise return 'tak' } export function toSummaryRowFromForm(form) { if (!form) return null const grpKey = form.grpKey if (!grpKey) return null 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 || '', kategori: form.kategori || '', yetiskinGarson: form.yetiskinGarson || form.askiliyan || '', yasPayloadMap: { ...(form.yasPayloadMap || {}) }, 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, }