/* =========================================================== 📦 orderentryStore.js (v3.4 CLEAN — AUTH + LOCAL PERSIST + AUTO RESUME) =========================================================== */ import { defineStore } from 'pinia' import api, { extractApiErrorDetail } from 'src/services/api' import dayjs from 'src/boot/dayjs' import { ref, toRaw, nextTick } from 'vue' // ✅ düzeltildi import { useAuthStore } from 'src/stores/authStore' // =========================================================== // 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler) // =========================================================== /* =========================================================== 🔹 BEDEN ŞEMALARI — STORE SOURCE OF TRUTH =========================================================== */ // ⬆️ orderentryStore.js EN ÜSTÜNE // =========================================================== // 🔑 COMBO KEY CONTRACT (Frontend ↔ Backend) — v1 // - trim + UPPER // - dim1 boşsa " " // - dim2 boşsa "" // =========================================================== const BEDEN_EMPTY = '_' const norm = (v) => (v == null ? '' : String(v)).trim() const normUpper = (v) => norm(v).toUpperCase() export function buildComboKey(row, beden) { const model = normUpper(row?.model || row?.ItemCode) const renk = normUpper(row?.renk || row?.ColorCode) const renk2 = normUpper(row?.renk2 || row?.ItemDim2Code) const bdn = normUpper(beden) const bedenFinal = bdn === '' ? BEDEN_EMPTY : bdn // 🔒 KANONİK SIRA return `${model}||${renk}||${renk2}||${bedenFinal}` } export const BEDEN_SCHEMA = [ { key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] }, { key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] }, { key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] }, { key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] }, { key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] }, { key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] } ] export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => { m[g.key] = g return m }, {}) export const stockMap = ref({}) export const bedenStock = ref([]) export const sizeCache = ref({}) // =========================================================== // 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler) // =========================================================== // ======================== // 🧰 GLOBAL DATE NORMALIZER // ======================== function newGuid() { return crypto.randomUUID() } // 🔑 Her beden satırı için deterministik clientKey üretimi function makeLineClientKey(row, grpKey, beden) { const base = row.clientRowKey || row.clientKey || row.id || row._id || row.tmpId || `${row.model || ''}|${row.renk || ''}|${row.renk2 || ''}` return `${base}::${grpKey}::${beden}` } // =========================================================== // 🧩 Pinia Store — ORDER ENTRY STORE (REV 2025-11-03.2) // =========================================================== export const useOrderEntryStore = defineStore('orderentry', { state: () => ({ isControlledSubmit: false, allowRouteLeaveOnce: false, schemaMap: {}, productCache: {}, _lastSavedFingerprint: null, activeNewHeaderId: localStorage.getItem("bss_active_new_header") || null, loading: false, selected: null, error: null, customers: [], selectedCustomer: null, products: [], colors: [], secondColors: [], inventory: [], selectedProduct: null, selectedColor: null, selectedColor2: null, OrderHeaderID: null, // Persist config persistKey: 'bss_orderentry_data', lastSnapshotKey: 'bss_orderentry_snapshot', // Editor state editingKey: null, currentOrderId: null, mode: 'new', // Grid state orders: [], header: {}, summaryRows: [], lastSavedAt: null, // Guards preventPersist: false, _uiBusy: false, _unsavedChanges: false, }), getters: { getDraftKey() { // NEW taslak → GLOBAL ama tekil return 'bss_orderentry_new_draft' }, getEditKey() { // EDIT → OrderHeaderID’ye bağlı const id = this.header?.OrderHeaderID return id ? `bss_orderentry_edit:${id}` : null } , hasUnsavedChanges(state) { try { return ( state._lastSavedFingerprint !== state._persistFingerprint?.() ) } catch { return false } }, getPersistKey: (state) => state.header?.OrderHeaderID ? `${state.persistKey}:${state.header.OrderHeaderID}` : state.persistKey, getSnapshotKey: (state) => state.header?.OrderHeaderID ? `${state.lastSnapshotKey}:${state.header.OrderHeaderID}` : state.lastSnapshotKey, totalQty: (state) => (state.orders || []).reduce((sum, r) => sum + (Number(r?.adet) || 0), 0), hasAnyClosedLine(state) { return Array.isArray(state.summaryRows) && state.summaryRows.some(r => r?.isClosed === true) }, totalAmount(state) { if (!Array.isArray(state.summaryRows)) return 0 return state.summaryRows.reduce( (sum, r) => sum + Number(r?.tutar || 0), 0 ) } }, actions: { normalizeComboUI(row) { return buildComboKey(row, BEDEN_EMPTY) } , /* =========================================================== 🧩 initSchemaMap — BEDEN ŞEMA İNİT - TEK SOURCE OF TRUTH: BEDEN_SCHEMA =========================================================== */ initSchemaMap() { if (this.schemaMap && Object.keys(this.schemaMap).length > 0) { return } const map = {} for (const g of BEDEN_SCHEMA) { map[g.key] = { key: g.key, title: g.title, values: [...g.values] } } this.schemaMap = map console.log( '🧩 schemaMap INIT edildi:', Object.keys(this.schemaMap) ) }, getRowKey(row) { if (!row) return null return row.OrderLineID || row.id || null } , updateHeaderTotals() { try { if (!Array.isArray(this.summaryRows)) return 0 const total = this.summaryRows.reduce( (sum, r) => sum + Number(r?.tutar || 0), 0 ) // Header sadece GÖSTERİM / BACKEND için if (this.header) { this.header.TotalAmount = Number(total.toFixed(2)) } return total } catch (err) { console.error('❌ updateHeaderTotals hata:', err) return 0 } } , /* =========================================================== 🚨 showInvalidVariantDialog — FINAL ----------------------------------------------------------- ✔ prItemVariant olmayan satırları listeler ✔ Satıra tıkla → scroll + highlight ✔ Kaydı BLOKLAYAN tek UI noktası =========================================================== */ async showInvalidVariantDialog($q, invalidList = []) { if (!Array.isArray(invalidList) || invalidList.length === 0) return return new Promise(resolve => { const dlg = $q.dialog({ title: '🚨 Tanımsız Ürün Kombinasyonları', message: `
${invalidList.map((v, i) => `
#${i + 1} | Item: ${v.itemCode}
Beden: ${v.dim1 || '(boÅŸ)'} | Renk: ${v.colorCode || '-'} | Qty: ${v.qty1}
Sebep: ${v.reason || 'Tanımsız ürün kombinasyonu'}
`).join('')}
`, html: true, ok: { label: 'Düzelt', color: 'negative' }, cancel: false, persistent: true }) .onOk(() => resolve()) .onDismiss(() => resolve()) // Quasar v2 chain API'de onShown yok; dialog DOM'u render olduktan sonra baÄŸla. setTimeout(() => { if (!dlg) return const nodes = document.querySelectorAll('.invalid-row') nodes.forEach(n => { n.addEventListener('click', () => { const ck = n.getAttribute('data-clientkey') this.scrollToInvalidRow?.(ck) }) }) }, 0) }) } , /* =========================================================== 🎯 scrollToInvalidRow — FINAL ----------------------------------------------------------- ✔ ClientKey bazlı scroll ✔ Hem summaryRows hem orders destekli ✔ Highlight otomatik kalkar =========================================================== */ scrollToInvalidRow(clientKey) { if (!clientKey) return // 1️⃣ Store içindeki satırı bul const idx = this.summaryRows?.findIndex( r => r.clientKey === clientKey ) if (idx === -1) { console.warn('❌ Satır bulunamadı:', clientKey) return } // 2️⃣ DOM node const el = document.querySelector( `[data-clientkey="${clientKey}"]` ) if (!el) { console.warn('❌ DOM satırı bulunamadı:', clientKey) return } // 3️⃣ Scroll el.scrollIntoView({ behavior: 'smooth', block: 'center' }) // 4️⃣ Highlight el.classList.add('invalid-highlight') setTimeout(() => { el.classList.remove('invalid-highlight') }, 2500) } , async checkHeaderExists(orderHeaderID) { try { if (!orderHeaderID) return false const res = await api.get(`/orders/check/${orderHeaderID}`) // Backend “true/false” döner varsayımı return res?.data?.exists === true } catch (err) { console.warn("âš  checkHeaderExists hata:", err) return false } } , async fetchOrderPdf(orderId) { try { const resp = await api.get(`/order/pdf/${orderId}`, { responseType: 'blob' }) return resp.data } catch (err) { const detail = await extractApiErrorDetail(err) const status = err?.status || err?.response?.status || '-' console.error(`❌ fetchOrderPdf hata [${status}] order=${orderId}: ${detail}`) throw new Error(detail) } } , async downloadOrderPdf(id = null) { try { const orderId = id || this.header?.OrderHeaderID if (!orderId) { console.error('❌ PDF ID bulunamadı') return } const res = await api.get(`/order/pdf/${orderId}`, { responseType: 'blob' }) const blob = new Blob([res.data], { type: 'application/pdf' }) const url = URL.createObjectURL(blob) window.open(url, '_blank') setTimeout(() => URL.revokeObjectURL(url), 60_000) } catch (err) { const detail = await extractApiErrorDetail(err) const orderId = id || this.header?.OrderHeaderID || '-' const status = err?.status || err?.response?.status || '-' console.error(`❌ PDF açma hatası [${status}] order=${orderId}: ${detail}`) throw new Error(detail) } } , setActiveNewHeader(id) { this.activeNewHeaderId = id || null if (id) localStorage.setItem("bss_active_new_header", id) else localStorage.removeItem("bss_active_new_header") }, getActiveNewHeaderId() { return this.activeNewHeaderId || localStorage.getItem("bss_active_new_header") }, /* =========================================================== 🧩 initFromRoute (v5.6 — groupedRows TOUCH YOK) ----------------------------------------------------------- - Route ID ve bss_last_txn arasında en dolu snapshot'ı seçer - header + orders + summaryRows restore edilir - groupedRows hydrate edilmez / resetlenmez / dokunulmaz - Route ID farklıysa router.replace ile URL düzeltilir =========================================================== */ async initFromRoute(orderId, router = null) { // ✅ NEW MODE → SADECE global draft if (this.mode === 'new') { const raw = localStorage.getItem(this.getDraftKey) if (raw) { try { const payload = JSON.parse(raw) this.header = payload.header || {} this.orders = payload.orders || [] this.summaryRows = payload.summaryRows || this.orders console.log('♻️ NEW draft restore edildi (global)') return } catch {} } console.log('⚪ NEW draft yok, boÅŸ baÅŸlatılıyor') return } if (!this.schemaMap || !Object.keys(this.schemaMap).length) { this.initSchemaMap() } try { console.log('🧩 [initFromRoute] orderId:', orderId) const lastTxn = localStorage.getItem('bss_last_txn') || null const readPayload = (id) => { if (!id) return null const raw = localStorage.getItem(`bss_orderentry_data:${id}`) if (!raw) return null try { return JSON.parse(raw) } catch { return null } } const fromRoute = readPayload(orderId) const fromLast = readPayload(lastTxn) const hasData = (p) => !!p && ( (Array.isArray(p.orders) && p.orders.length > 0) || (Array.isArray(p.summaryRows) && p.summaryRows.length > 0) ) let chosenId = null let chosenPayload = null if (hasData(fromRoute)) { chosenId = orderId chosenPayload = fromRoute console.log('✅ [initFromRoute] Route ID snapshot seçildi:', chosenId) } else if (hasData(fromLast)) { chosenId = lastTxn chosenPayload = fromLast console.log('✅ [initFromRoute] lastTxn snapshot seçildi:', chosenId) } /* ------------------------------------------------------- 🚫 SNAPSHOT YOK → BOŞ BAŞLA -------------------------------------------------------- */ if (!chosenId || !chosenPayload) { console.log('⚪ [initFromRoute] Snapshot yok, boÅŸ baÅŸlatılıyor') this.header = { ...(this.header || {}), OrderHeaderID: orderId || lastTxn || crypto.randomUUID() } this.orders = [] this.summaryRows = [] // ❗ groupedRows'a DOKUNMA return } /* ------------------------------------------------------- ✅ SNAPSHOT RESTORE (SAFE CLONE) -------------------------------------------------------- */ this.header = { ...(chosenPayload.header || {}), OrderHeaderID: chosenId } const orders = Array.isArray(chosenPayload.orders) ? [...chosenPayload.orders] : [] const summaryRows = Array.isArray(chosenPayload.summaryRows) ? [...chosenPayload.summaryRows] : orders this.orders = orders this.summaryRows = summaryRows // ❗ groupedRows hydrate edilmez, resetlenmez /* ------------------------------------------------------- 🔁 lastTxn SENKRON -------------------------------------------------------- */ try { localStorage.setItem('bss_last_txn', chosenId) } catch (e) { console.warn('⚠️ bss_last_txn yazılamadı:', e) } /* ------------------------------------------------------- 🔁 ROUTE DÜZELTME (GEREKİRSE) -------------------------------------------------------- */ if (router && orderId && orderId !== chosenId) { console.log('🔁 [initFromRoute] Route ID düzeltiliyor →', chosenId) await router.replace({ name: 'order-entry', params: { orderHeaderID: chosenId } }) } console.log( '✅ [initFromRoute] Restore tamam. Satır sayısı:', this.summaryRows.length ) } catch (err) { console.error('❌ [initFromRoute] hata:', err) } } , /* =========================================================== 🆕 startNewOrder (v8.3 — FINAL & STABLE) =========================================================== */ async startNewOrder({ $q }) { if (!this.schemaMap || !Object.keys(this.schemaMap).length) { this.initSchemaMap() } const headerId = crypto.randomUUID() let orderNumber = `LOCAL-${dayjs().format("YYMMDD-HHmmss")}` try { const res = await api.get("/order/new-number") if (res?.data?.OrderNumber) { orderNumber = res.data.OrderNumber } } catch { console.info('ℹ️ Backend order number yok, LOCAL kullanıldı') } this.mode = 'new' this.isControlledSubmit = false this.allowRouteLeaveOnce = false this.header = { OrderHeaderID: headerId, OrderNumber: orderNumber, OrderDate: new Date().toISOString().slice(0, 10), CurrAccCode: null, DocCurrencyCode: 'USD', PriceCurrencyCode: 'USD', PriceExchangeRate: 1 } this.orders = [] this.summaryRows = [] // ✅ fingerprint bazlı sistem için reset this._lastSavedFingerprint = null // ✅ NEW draft hemen yazılır this.persistLocalStorage?.() return this.header } , dedupeActiveLinesByCombo(lines) { const map = new Map() for (const ln of lines) { const key = buildComboKey({ model: ln.ItemCode, renk: ln.ColorCode, renk2: ln.ItemDim2Code }, ln.ItemDim1Code) if (!map.has(key)) { ln.ComboKey = key map.set(key, ln) continue } const ex = map.get(key) ex.Qty1 = (Number(ex.Qty1) || 0) + (Number(ln.Qty1) || 0) // OrderLineID boÅŸsa doldur (editte önemli) if (!ex.OrderLineID && ln.OrderLineID) ex.OrderLineID = ln.OrderLineID } return Array.from(map.values()) } , /* =========================================================== 🧹 Core reset helper — sadece state'i sıfırlar =========================================================== */ resetCoreState() { this.orders = [] this.summaryRows = [] this.groupedRows = [] this.header = {} this.editingKey = null this.currentOrderId = null },resetForNewOrder() { // mevcut her ÅŸeyi temizle this.header = { OrderHeaderID: this.header?.OrderHeaderID || null, OrderDate: new Date().toISOString().slice(0,10), CurrAccCode: null, DocCurrencyCode: 'TRY', PriceCurrencyCode: 'TRY', // ihtiyaç duyduÄŸun diÄŸer default header alanları } this.orders = [] this.summaryRows = [] this.productCache = {} this.stockMap = {} this.setMode('new') } , resetForEdit() { // EDIT modda grid temizlenmez — sadece UI state resetlenir this.editingKey = null this.groupedRows = [] this.mode = 'edit' } ,markAsSaved() { try { this._lastSavedFingerprint = this._persistFingerprint() console.log('✅ markAsSaved → fingerprint senkron') } catch (e) { console.warn('⚠️ markAsSaved hata:', e) } } ,clearLocalSnapshot() { try { const id = this.header?.OrderHeaderID if (!id) return localStorage.removeItem(`bss_orderentry_data:${id}`) console.log('🧹 Local snapshot temizlendi:', id) } catch (e) { console.warn('⚠️ clearLocalSnapshot hata:', e) } },/* =========================================================== 🧹 HARD CLEAN — ALL ORDERENTRY SNAPSHOTS =========================================================== */ clearAllOrderSnapshots () { Object.keys(localStorage) .filter(k => k.startsWith('bss_orderentry_data:') || k.startsWith('bss_orderentry_edit:') ) .forEach(k => { console.log('🧹 snapshot silindi:', k) localStorage.removeItem(k) }) localStorage.removeItem('bss_last_txn') } , /* =========================================================== 🧹 Store Hard Reset — Submit Sonrası Temizlik (FIXED) - Grid, header, toplamlar, local state'ler sıfırlanır - persistKey / lastSnapshotKey NULL yapılmaz (config sabit kalır) - localStorage txn/snapshot temizliÄŸi güvenli yapılır =========================================================== */ hardResetAfterSubmit() { try { // 🔑 mevcut id’yi yakala (local temizliÄŸi için) const id = this.header?.OrderHeaderID || null /* ------------------------------------------------------- 1) Grid ve satırlar -------------------------------------------------------- */ this.orders = [] this.summaryRows = [] this.groupedRows = [] /* ------------------------------------------------------- 2) Header & meta -------------------------------------------------------- */ this.header = {} /* ------------------------------------------------------- 3) Mode & edit state -------------------------------------------------------- */ this.mode = 'new' this.editingKey = null this.currentOrderId = null /* ------------------------------------------------------- 4) Snapshot / transaction meta ⚠️ persistKey / lastSnapshotKey store config → NULL YAPMA -------------------------------------------------------- */ this.activeTransactionId = null this.submitted = false // fingerprint / debounce meta varsa sıfırla this._lastSavedFingerprint = null this._lastPersistFingerprint = null if (this._persistTimeout) { clearTimeout(this._persistTimeout) this._persistTimeout = null } /* ------------------------------------------------------- 5) LocalStorage temizlik (opsiyonel ama submit sonrası doÄŸru) -------------------------------------------------------- */ try { if (id) { localStorage.removeItem(`bss_orderentry_data:${id}`) localStorage.removeItem(`bss_orderentry_snapshot:${id}`) } localStorage.removeItem('bss_last_txn') localStorage.removeItem('bss_active_new_header') } catch (e) { console.warn('⚠️ hardResetAfterSubmit localStorage temizliÄŸi hata:', e) } console.log('🧹 Store resetlendi (submit sonrası).') } catch (err) { console.error('❌ hardResetAfterSubmit hata:', err) } } , /* =========================================================== ✏️ openExistingForEdit (v12 — FINAL & CLEAN) ----------------------------------------------------------- ✔ Backend authoritative (orderlist açılışı local'i dikkate almaz) ✔ mode=new → backend çaÄŸrısı YOK ✔ normalizeOrderLines → grpKey + bedenMap garanti ✔ isClosed varsa → view, yoksa → edit ✔ Form sync opsiyonel ✔ İlk açılışta snapshot yazılır (edit boyunca persist ile güncellenir) =========================================================== */ async openExistingForEdit( orderId, { $q = null, form = null, productCache = null } = {} ) { // 🔑 schemaMap garanti if (!this.schemaMap || !Object.keys(this.schemaMap).length) { this.initSchemaMap?.() } if (!orderId) return false /* ======================================================= 🟦 NEW MODE — ASLA backend çaÄŸrısı yok ======================================================= */ if (this.mode === 'new') { console.log('⚪ openExistingForEdit skip (mode=new)') return false } // productCache hem ref hem reactive olabilir → güvenli oku const pc = productCache?.value ? productCache.value : (productCache && typeof productCache === 'object' ? productCache : {}) try { // geçici varsayım (sonra isClosed durumuna göre set edilecek) this.setMode?.('edit') /* ======================================================= 🔹 BACKEND — authoritative load ======================================================= */ const res = await api.get(`/order/get/${orderId}`) const backend = res?.data if (!backend?.header) { throw new Error('Backend header yok') } /* ======================================================= 🔹 HEADER — SADECE BACKEND (orderlist açılışında local merge YOK) ======================================================= */ this.header = { ...backend.header, OrderHeaderID: backend.header.OrderHeaderID || orderId } /* ======================================================= 🔹 NORMALIZE LINES (TEK KAYNAK) normalizeOrderLines ÅŸu alanları üretmeli: ✔ row.grpKey ✔ row.bedenMap[grpKey] ✔ row.isClosed (boolean) ======================================================= */ const normalized = this.normalizeOrderLines( backend.lines || [], this.header.DocCurrencyCode || 'USD', pc ) this.orders = Array.isArray(normalized) ? normalized : [] this.summaryRows = [...this.orders] /* ======================================================= 🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN) - herhangi bir isClosed=true → view - deÄŸilse → edit ======================================================= */ const hasClosedLine = (this.summaryRows || []).some(r => r?.isClosed === true) this.setMode?.(hasClosedLine ? 'view' : 'edit') /* ======================================================= 🔹 FORM SYNC (opsiyonel) ======================================================= */ if (form) { Object.assign(form, this.header) } /* ======================================================= 🔹 LOCAL SNAPSHOT (edit boyunca tutulacak temel) - Açılışta snapshot yaz - Sonraki deÄŸiÅŸikliklerde zaten persistLocalStorage çaÄŸrıları var ======================================================= */ this.persistLocalStorage?.() try { localStorage.setItem('bss_last_txn', String(orderId)) } catch {} console.log('✅ openExistingForEdit OK:', { id: orderId, rows: this.summaryRows.length, mode: this.mode, hasClosedLine }) return true } catch (err) { console.error('❌ openExistingForEdit hata:', err) // new deÄŸilse uyar if (this.mode !== 'new') { $q?.notify?.({ type: 'negative', message: 'SipariÅŸ yüklenemedi' }) } return false } } , /* =========================================================== ♻️ hydrateFromLocalStorage (v5.5 — FIXED & CLEAN) ----------------------------------------------------------- - Tek assign (double overwrite YOK) - groupedRows hydrate edilmez - mode ASLA set edilmez - header + rows güvenli restore =========================================================== */ async hydrateFromLocalStorage(orderId, log = false) {if (this.mode === 'new') { return this.hydrateFromLocalStorageIfExists() } try { const key = `bss_orderentry_data:${orderId}` const payload = JSON.parse(localStorage.getItem(key) || 'null') if (!payload) { log && console.log('ℹ️ hydrate → snapshot yok:', orderId) return null } // 🔑 source bilgisi (mode set edilmez) this.source = payload.source || 'local' /* ------------------------------------------------------- MSSQL tarih helper’ları -------------------------------------------------------- */ const safeDateTime = v => { if (!v) return null const d = dayjs(v) return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : null } const safeDateOnly = v => { if (!v) return null const d = dayjs(v) return d.isValid() ? d.format('YYYY-MM-DD') : null } const safeTimeOnly = v => { if (!v) return null const d = dayjs(v) return d.isValid() ? d.format('HH:mm:ss') : null } /* ------------------------------------------------------- HEADER -------------------------------------------------------- */ this.header = { ...(payload.header || {}), OrderHeaderID: payload.header?.OrderHeaderID ?? orderId, OrderNumber : payload.header?.OrderNumber ?? null } const h = this.header h.CreatedDate = safeDateTime(h.CreatedDate) h.LastUpdatedDate = safeDateTime(h.LastUpdatedDate) h.CreditableConfirmedDate = safeDateTime(h.CreditableConfirmedDate) h.OrderDate = safeDateOnly(h.OrderDate) h.OrderTime = safeTimeOnly(h.OrderTime) this.header = h /* ------------------------------------------------------- ROWS (TEK KAYNAK) -------------------------------------------------------- */ const orders = Array.isArray(payload.orders) ? payload.orders : [] this.orders = orders this.summaryRows = Array.isArray(payload.summaryRows) ? payload.summaryRows : orders // ❌ groupedRows hydrate edilmez (computed olmalı) this.groupedRows = [] /* ------------------------------------------------------- SNAPSHOT ÖZET -------------------------------------------------------- */ const output = { type : payload.submitted === true ? 'submitted' : 'draft', source : this.source, headerId : orderId, orderNumber: this.header?.OrderNumber ?? null, rows : this.summaryRows.length, submitted : payload.submitted === true || payload.header?.IsSubmitted === true } log && console.log('♻️ hydrate sonuc (FIXED):', output) return output } catch (err) { console.warn('⚠️ hydrateFromLocalStorage hata:', err) return null } } , hydrateFromLocalStorageIfExists() { try { let raw = null if (this.mode === 'new') { raw = localStorage.getItem(this.getDraftKey) // ✅ } if (this.mode === 'edit') { const key = this.getEditKey // ✅ if (key) raw = localStorage.getItem(key) } if (!raw) return false const payload = JSON.parse(raw) this.header = payload.header || {} this.orders = payload.orders || [] this.summaryRows = payload.summaryRows || this.orders console.log('♻️ hydrate OK:', this.mode) return true } catch (err) { console.warn('hydrateFromLocalStorageIfExists hata:', err) return false } } , /* =========================================================== 🔀 mergeOrders (local + backend) normalizeISO → kaldırıldı safe MSSQL helpers eklendi =========================================================== */ mergeOrders(local, backend, preferLocal = true) { if (!backend && !local) return { header: {}, orders: [] } const safeMerge = (base = {}, override = {}) => { const out = { ...base } for (const [k, v] of Object.entries(override || {})) { if (v === undefined || v === null) continue if (typeof v === 'string' && v.trim() === '') continue out[k] = v } return out } // Header merge const header = safeMerge(backend?.header || {}, local?.header || {}) header.OrderHeaderID = backend?.header?.OrderHeaderID || local?.header?.OrderHeaderID || header.OrderHeaderID || null const getKey = (r) => (r.OrderLineID || `${r.model || r.ItemCode}_${r.renk || r.ColorCode}_${r.renk2 || r.ColorCode2}` ).toString().toUpperCase() const map = new Map() // Backend satırları for (const b of (backend?.lines || backend?.orders || [])) { map.set(getKey(b), { ...b, _src: 'backend' }) } // Local satırları merge et for (const l of (local?.orders || [])) { const k = getKey(l) if (map.has(k)) { const merged = safeMerge(map.get(k), l) merged._src = preferLocal ? 'local' : 'backend' map.set(k, merged) } else { map.set(k, { ...l, _src: 'local-only' }) } } const mergedOrders = Array.from(map.values()) console.log(`🧩 mergeOrders → ${mergedOrders.length} satır birleÅŸti (ID:${header.OrderHeaderID})`) // ==================================================== // 🕒 HEADER TARİHLERİNİ MSSQL FORMATINA NORMALİZE ET // ==================================================== const safeDateTime = v => { if (!v) return null const d = dayjs(v) return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : null } const safeDateOnly = v => { if (!v) return null const d = dayjs(v) return d.isValid() ? d.format("YYYY-MM-DD") : null } const safeTimeOnly = v => { if (!v) return null const d = dayjs(v) return d.isValid() ? d.format("HH:mm:ss") : null } header.CreatedDate = safeDateTime(header.CreatedDate) header.LastUpdatedDate = safeDateTime(header.LastUpdatedDate) header.CreditableConfirmedDate = safeDateTime(header.CreditableConfirmedDate) header.OrderDate = safeDateOnly(header.OrderDate) header.OrderTime = safeTimeOnly(header.OrderTime) return { header, orders: mergedOrders } } , markRowSource(row) { if (row._src === 'local-only') return '🟠 Offline' if (row._src === 'local') return '🔵 Local' return '⚪ Backend' } , /* =========================================================== 🔄 mergeAndPersistBackendOrder (edit mode) =========================================================== */ mergeAndPersistBackendOrder(orderId, backendPayload) { const key = `bss_orderentry_data:${orderId}` const localPayload = JSON.parse(localStorage.getItem(key) || 'null') const merged = this.mergeOrders(localPayload, backendPayload, true) localStorage.setItem(key, JSON.stringify({ ...merged, source: 'db', mode: 'edit', updatedAt: new Date().toISOString() })) console.log(`💾 mergeAndPersistBackendOrder → ${orderId} localStorage’a yazıldı`) } , persistLocalStorage() { try { if (this.preventPersist || this._uiBusy) return const payload = { mode: this.mode, header: toRaw(this.header || {}), orders: toRaw(this.orders || []), summaryRows: toRaw(this.summaryRows || []), updatedAt: new Date().toISOString() } /* =============================== 🟢 NEW MODE — GLOBAL TEK TASLAK =============================== */ if (this.mode === 'new') { localStorage.setItem(this.getDraftKey, JSON.stringify(payload)) // 🔒 sadece aktif new header bilgisi this.setActiveNewHeader?.(this.header?.OrderHeaderID) return } /* =============================== 🔵 EDIT MODE — ID BAZLI =============================== */ if (this.mode === 'edit') { const key = this.getEditKey if (!key) return localStorage.setItem(key, JSON.stringify(payload)) } } catch (e) { console.warn('persistLocalStorage error:', e) } } , clearEditSnapshotIfExists() { if (this.mode !== 'edit') return const key = this.getEditKey // ✅ if (!key) return localStorage.removeItem(key) console.log('🧹 EDIT snapshot silindi:', key) } ,/* =========================================================== 🧠 _persistFingerprint — kritik state’leri tek stringe indirger - X3: orders+header yetmez → mode, summaryRows, id/no, map’ler dahil =========================================================== */ _persistFingerprint() { // 🔹 orders: çok büyürse pahalı olabilir ama snapshot tutarlılığı için önemli // (istersen burada sadece length + rowKey listesi gibi optimize ederiz) const ordersSnap = JSON.stringify(this.orders || []) // 🔹 header: sadece kritik alanları al (tam header yerine daha stabil) const h = this.header || {} const headerSnap = JSON.stringify({ OrderHeaderID: h.OrderHeaderID || '', OrderNumber: h.OrderNumber || '', CurrAccCode: h.CurrAccCode || '', DocCurrencyCode: h.DocCurrencyCode || '', ExchangeRate: h.ExchangeRate ?? null }) // 🔹 summaryRows: hash yerine ÅŸimdilik “length + rowKey listesi” (hafif + etkili) const sr = Array.isArray(this.summaryRows) ? this.summaryRows : [] const summaryMeta = JSON.stringify({ len: sr.length, keys: sr.map(r => this.getRowKey?.(r) || r?.key || r?.id || '').filter(Boolean) }) // 🔹 comboLineIds / lineIdMap gibi kritik map’ler // (sende hangisi varsa onu otomatik topluyoruz) const mapSnap = JSON.stringify({ lineIdMap: this.lineIdMap || null, comboLineIds: this.comboLineIds || null, comboLineIdMap: this.comboLineIdMap || null, comboLineIdSet: this.comboLineIdSet ? Array.from(this.comboLineIdSet) : null }) // 🔹 mode const modeSnap = String(this.mode || 'new') // ✅ Tek fingerprint return `${modeSnap}|${headerSnap}|${summaryMeta}|${mapSnap}|${ordersSnap}` } , /* =========================================================== 🕒 _safePersistDebounced — snapshot deÄŸiÅŸmediÄŸi sürece yazmaz (X3) - fingerprint: mode + header(id/no) + summaryRows meta + lineIdMap/combo + orders =========================================================== */ _safePersistDebounced(delay = 1200) { clearTimeout(this._persistTimeout) this._persistTimeout = setTimeout(() => { try { // ✅ Persist guard’ları (varsa) if (this.preventPersist) return if (this._uiBusy) return const fp = this._persistFingerprint() if (fp === this._lastPersistFingerprint) { return } this._lastPersistFingerprint = fp this.persistLocalStorage() console.log(`🕒 Otomatik LocalStorage senkron (${this.orders?.length || 0} satır).`) } catch (err) { console.warn('⚠️ Debounce persist hata:', err) } }, delay) } , /* =========================================================== 💰 fetchMinPrice — model/pb için min fiyat =========================================================== */ async fetchMinPrice(model, currency, $q) { try { const res = await api.get('/min-price', { params: { model, currency } }) const data = res?.data || {} console.log('💰 [store.fetchMinPrice] yanıt:', data) return { price: Number(data.price || 0), rateToTRY: Number(data.rateToTRY || 1), priceTRY: Number(data.priceTRY || 0) } } catch (err) { console.error('❌ [store.fetchMinPrice] Min fiyat alınamadı:', err) $q?.notify?.({ type: 'warning', message: 'Min. fiyat bilgisi alınamadı, kontrol atlandı ⚠️', position: 'top-right' }) return { price: 0, rateToTRY: 1, priceTRY: 0 } } } , applyCurrencyToLines(newPB) { if (!newPB) return // 🔹 Header if (this.header) { this.header.DocCurrencyCode = newPB this.header.PriceCurrencyCode = newPB } // 🔹 Lines if (Array.isArray(this.orders)) { this.orders = this.orders.map(r => ({ ...r, pb: newPB, DocCurrencyCode: newPB, PriceCurrencyCode: newPB })) } // 🔹 Summary if (Array.isArray(this.summaryRows)) { this.summaryRows = this.summaryRows.map(r => ({ ...r, pb: newPB, DocCurrencyCode: newPB, PriceCurrencyCode: newPB })) } // ❗ totalAmount SET ETME // ✔️ TEK MERKEZ this.updateHeaderTotals?.() } , /* =========================================================== 💠 HEADER SET & CURRENCY PROPAGATION =========================================================== */ setHeaderFields(fields, opts = {}) { const { applyCurrencyToLines = false, immediatePersist = false } = opts // 1️⃣ HEADER this.header = { ...(this.header || {}), ...fields } // 2️⃣ SATIRLARA GERÇEKTEN YAY if (applyCurrencyToLines && Array.isArray(this.summaryRows)) { this.summaryRows = this.summaryRows.map(r => ({ ...r, pb: fields.DocCurrencyCode ?? r.pb, DocCurrencyCode: fields.DocCurrencyCode ?? r.DocCurrencyCode, PriceCurrencyCode: fields.PriceCurrencyCode ?? fields.DocCurrencyCode ?? r.PriceCurrencyCode })) } // 3️⃣ STORE ORDERS REFERANSI this.orders = [...this.summaryRows] // 4️⃣ PERSIST if (immediatePersist) { this.persistLocalStorage('header-change') } } , applyHeaderCurrencyToOrders() { if (!Array.isArray(this.orders)) return const doc = this.header?.DocCurrencyCode ?? null const prc = this.header?.PriceCurrencyCode ?? null const rate = this.header?.PriceExchangeRate ?? null let cnt = 0 for (const r of this.orders) { if (doc) r.DocCurrencyCode = doc if (prc) r.PriceCurrencyCode = prc if (rate != null) r.PriceExchangeRate = rate cnt++ } console.log(`💱 ${cnt} satırda PB güncellendi → Doc:${doc} Price:${prc} Rate:${rate}`) } ,/* =========================================================== 📸 saveSnapshot — küçük debug snapshot =========================================================== */ saveSnapshot(tag = 'snapshot') { try { const id = this.header?.OrderHeaderID if (!id) return const key = `bss_orderentry_snapshot:${id}` const snap = { tag, mode: this.mode, orders: toRaw(this.orders || []), header: toRaw(this.header || {}), savedAt: dayjs().toISOString() } localStorage.setItem(key, JSON.stringify(snap)) console.log(`📸 Snapshot kaydedildi [${key}]`) } catch (err) { console.warn('⚠️ saveSnapshot hata:', err) } } , /* =========================================================== ♻️ loadFromStorage — eski generic persist için =========================================================== */ loadFromStorage(force = false) { try { const raw = localStorage.getItem(this.getPersistKey) if (!raw) { console.info('ℹ️ LocalStorage boÅŸ, grid baÅŸlatılmadı.') return false } if (!force && this.mode === 'edit') { console.info('⚠️ Edit modda local restore atlandı (force=false).') return false } const data = JSON.parse(raw) this.orders = Array.isArray(data.orders) ? data.orders : [] this.header = data.header || {} this.currentOrderId = data.currentOrderId || null this.selectedCustomer = data.selectedCustomer || null // 🔧 Temiz ID this.header.OrderHeaderID = data.header?.OrderHeaderID || null this.mode = data.mode || 'new' this.lastSavedAt = data.savedAt || null console.log(`♻️ Storage yüklendi • txn:${this.header.OrderHeaderID} (${this.orders.length} satır)`) // Header PB -> satırlara this.applyHeaderCurrencyToOrders() this._safePersistDebounced(200) return data } catch (err) { console.warn('⚠️ localStorage okuma hatası:', err) return false } } , clearStorage() { try { localStorage.removeItem(this.getPersistKey) console.log(`🗑️ LocalStorage temizlendi [${this.getPersistKey}]`) } catch (err) { console.warn('⚠️ clearStorage hatası:', err) } } , clearNewDraft() { localStorage.removeItem(this.getDraftKey) // ✅ localStorage.removeItem('bss_last_txn') console.log('🧹 NEW taslak temizlendi') } , // =========================================================== // 🔹 isSameCombo — STORE LEVEL (TEK KAYNAK) // - model ZORUNLU eÅŸleÅŸir // - renk / renk2 boÅŸsa → joker // =========================================================== isSameCombo(a, b) { if (!a || !b) return false const n = v => (v == null ? '' : String(v).trim().toUpperCase()) const A = { model: n(a.model), renk: n(a.renk), renk2: n(a.renk2) } const B = { model: n(b.model), renk: n(b.renk), renk2: n(b.renk2) } if (!A.model || !B.model) return false const renkOk = (A.renk === B.renk) || !A.renk || !B.renk const renk2Ok = (A.renk2 === B.renk2) || !A.renk2 || !B.renk2 return A.model === B.model && renkOk && renk2Ok }, // =========================================================== // 🔹 saveOrUpdateRowUnified (v6.6 — COMBO SAFE + FIXED STOCK+PRICE + UI) // - v6.5 korunur (stok+min fiyat + this.loadProductSizes) // - ✅ NEW MODE: dupIdx artık _deleteSignal satırlarını BAŞTAN hariç tutar // - EDIT MODE: sameCombo → update, combo deÄŸiÅŸti → delete + insert (korundu) // - lineIdMap koruması korunur // =========================================================== async saveOrUpdateRowUnified({ form, recalcVat = null, resetEditor = null, stockMap = null, loadProductSizes = null, $q = null }) { try { console.log('🔥 saveOrUpdateRowUnified v6.6', { model: form?.model, mode: this.mode, editingKey: this.editingKey }) const getKey = typeof this.getRowKey === 'function' ? this.getRowKey : (r => r?.clientKey || r?.id || r?.OrderLineID) const rows = Array.isArray(this.summaryRows) ? [...this.summaryRows] : [] /* ======================================================= 1️⃣ ZORUNLU KONTROLLER ======================================================= */ if (!form?.model) { $q?.notify?.({ type: 'warning', message: 'Model seçiniz' }) return false } if (!form.pb) { form.pb = this.header?.DocCurrencyCode || 'USD' } /* ======================================================= 2️⃣ STOK KONTROLÜ (FIXED) - stok guard’dan önce this.loadProductSizes(form,true,$q) - opsiyonel callback loadProductSizes(true) - tek dialog + doÄŸru await ======================================================= */ // ✅ store fonksiyonu try { if (typeof this.loadProductSizes === 'function') { await this.loadProductSizes(form, true, $q) } } catch (err) { console.warn('âš  this.loadProductSizes hata:', err) } // ✅ dışarıdan callback geldiyse try { if (typeof loadProductSizes === 'function') { await loadProductSizes(true) } } catch (err) { console.warn('âš  loadProductSizes hata:', err) } const stockMapLocal = stockMap?.value || stockMap || {} const bedenLabels = form.bedenLabels || [] const bedenValues = form.bedenler || [] const overLimit = [] for (let i = 0; i < bedenLabels.length; i++) { const lbl = String(bedenLabels[i] ?? '').trim() const stok = Number(stockMapLocal?.[lbl] ?? 0) const girilen = Number(bedenValues?.[i] ?? 0) if (stok > 0 && girilen > stok) { overLimit.push({ beden: lbl, stok, girilen }) } } if (overLimit.length && $q) { const msg = overLimit .map(x => `• ${x.beden}: ${x.girilen} (Stok: ${x.stok})`) .join('
') const stokOK = await new Promise(resolve => { $q.dialog({ title: 'Stok Uyarısı', message: `Bazı bedenlerde stoktan fazla giriş yaptınız:

${msg}`, html: true, ok: { label: 'Devam', color: 'primary' }, cancel: { label: 'İptal', color: 'negative' } }) .onOk(() => resolve(true)) .onCancel(() => resolve(false)) .onDismiss(() => resolve(false)) }) if (!stokOK) return false } /* ======================================================= 3️⃣ FİYAT (MIN) KONTROLÜ (FIXED) ======================================================= */ let fiyatOK = true try { let minFiyat = 0 if (typeof this.fetchMinPrice === 'function') { const p = await this.fetchMinPrice(form.model, form.pb, $q) minFiyat = Number(p?.price || 0) } else if (Number(form.minFiyat || 0) > 0) { minFiyat = Number(form.minFiyat) } const girilen = Number(form.fiyat || 0) if (minFiyat > 0 && girilen > 0 && girilen < minFiyat && $q) { fiyatOK = await new Promise(resolve => { $q.dialog({ title: 'Fiyat Uyarısı', message: `Min. Fiyat: ${minFiyat} ${form.pb}
` + `Girdiğiniz: ${girilen} ${form.pb}`, html: true, ok: { label: 'Devam', color: 'primary' }, cancel: { label: 'İptal', color: 'negative' } }) .onOk(() => resolve(true)) .onCancel(() => resolve(false)) .onDismiss(() => resolve(false)) }) } } catch (err) { console.warn('⚠ Min fiyat hata:', err) } if (!fiyatOK) return false /* ======================================================= 4️⃣ TOPLAM HESABI ======================================================= */ const adet = (form.bedenler || []).reduce((a, b) => a + Number(b || 0), 0) form.adet = adet form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2)) const newRow = toSummaryRowFromForm(form) /* ======================================================= 5️⃣ EDIT MODE (editingKey ZORUNLU) ======================================================= */ if (this.editingKey) { const idx = rows.findIndex(r => getKey(r) === this.editingKey) if (idx === -1) { this.editingKey = null resetEditor?.(true) return false } const prev = rows[idx] if (this.isRowLocked?.(prev)) { $q?.notify?.({ type: 'warning', message: 'Satır kapalı' }) this.editingKey = null resetEditor?.(true) return false } // ✅ kritik: store-level const sameCombo = this.isSameCombo(prev, newRow) const preservedLineIdMap = (prev?.lineIdMap && typeof prev.lineIdMap === 'object') ? { ...prev.lineIdMap } : (newRow?.lineIdMap && typeof newRow.lineIdMap === 'object') ? { ...newRow.lineIdMap } : {} /* ===== SAME COMBO → UPDATE ===== */ if (sameCombo) { rows[idx] = { ...prev, ...newRow, id: prev.id, OrderLineID: prev.OrderLineID || null, lineIdMap: preservedLineIdMap } this.summaryRows = rows this.orders = rows this.updateHeaderTotals?.() this.persistLocalStorage?.() this.editingKey = null resetEditor?.(true) recalcVat?.() $q?.notify?.({ type: 'positive', message: 'Satır güncellendi' }) return true } /* ===== COMBO CHANGED → DELETE + INSERT ===== */ const grpKey = prev?.grpKey || Object.keys(prev?.bedenMap || {})[0] || 'tak' const emptyMap = {} const srcMap = (prev?.bedenMap?.[grpKey] && typeof prev.bedenMap[grpKey] === 'object') ? prev.bedenMap[grpKey] : (preservedLineIdMap && typeof preservedLineIdMap === 'object') ? preservedLineIdMap : null if (srcMap) { for (const beden of Object.keys(srcMap)) emptyMap[beden] = 0 } else { emptyMap['STD'] = 0 } const deleteRow = { ...prev, id: `DEL::${prev.id || prev.OrderLineID || crypto.randomUUID()}`, _deleteSignal: true, adet: 0, Qty1: 0, tutar: 0, ComboKey: '', OrderLineID: prev.OrderLineID || null, grpKey, bedenMap: { [grpKey]: emptyMap }, lineIdMap: preservedLineIdMap, comboLineIds: { ...(prev.comboLineIds || {}) } } const insertedRow = { ...newRow, id: crypto.randomUUID(), OrderLineID: null, lineIdMap: {} } rows.splice(idx, 1, insertedRow) this.summaryRows = rows this.orders = [...rows, deleteRow] this.updateHeaderTotals?.() this.persistLocalStorage?.() this.editingKey = null resetEditor?.(true) recalcVat?.() $q?.notify?.({ type: 'positive', message: 'Kombinasyon değişti' }) return true } /* ======================================================= 6️⃣ NEW MODE (MERGE / INSERT) — COMBO SAFE - aynı combo → bedenMap merge (satır sayısı artmaz) - farklı combo → yeni satır - ✅ FIX: _deleteSignal satırlarını dup aramasında hariç tut ======================================================= */ const dupIdx = rows.findIndex(r => !r?._deleteSignal && this.isSameCombo(r, newRow) ) // helper: bedenMap çıkar (gruplu ya da düz) const extractMap = (row) => { const grpKey = row?.grpKey || Object.keys(row?.bedenMap || {})[0] || 'GENEL' const grouped = row?.bedenMap?.[grpKey] const flat = (row?.bedenMap && typeof row.bedenMap === 'object' && !grouped) ? row.bedenMap : null return { grpKey, map: (grouped || flat || {}) } } if (dupIdx !== -1) { const prev = rows[dupIdx] // delete satırına merge yapma (ek güvenlik) if (prev?._deleteSignal !== true) { const { grpKey: prevGrp, map: prevMap } = extractMap(prev) const { grpKey: newGrp, map: newMap } = extractMap(newRow) // hangi grpKey kullanılacak? const grpKey = newRow?.grpKey || prevGrp || newGrp || 'GENEL' // MERGE: bedenleri topluyoruz (override değil) const merged = { ...(prevMap || {}) } for (const [k, v] of Object.entries(newMap || {})) { const beden = (k == null || String(k).trim() === '') ? ' ' : normalizeBedenLabel(String(k)) merged[beden] = Number(merged[beden] || 0) + Number(v || 0) } // toplam adet/tutar recalc const totalAdet = Object.values(merged).reduce((a, b) => a + Number(b || 0), 0) const price = Number(newRow?.fiyat ?? prev?.fiyat ?? 0) const totalTutar = Number((totalAdet * price).toFixed(2)) rows[dupIdx] = { ...prev, ...newRow, // kritik korumalar id: prev.id, OrderLineID: prev.OrderLineID || null, lineIdMap: { ...(prev.lineIdMap || {}) }, // MERGED bedenMap grpKey, bedenMap: { [grpKey]: merged }, // adet/tutar adet: totalAdet, tutar: totalTutar, updatedAt: dayjs().toISOString() } this.summaryRows = rows this.orders = rows this.updateHeaderTotals?.() this.persistLocalStorage?.() resetEditor?.(true) recalcVat?.() $q?.notify?.({ type: 'positive', message: 'Aynı kombinasyon bulundu, bedenler birleştirildi' }) return true } } // dup yoksa (veya dup delete satırıydı) → yeni satır rows.push({ ...newRow, id: newRow.id || crypto.randomUUID(), OrderLineID: null, lineIdMap: { ...(newRow.lineIdMap || {}) } }) this.summaryRows = rows this.orders = rows this.updateHeaderTotals?.() this.persistLocalStorage?.() resetEditor?.(true) recalcVat?.() $q?.notify?.({ type: 'positive', message: 'Yeni satır eklendi' }) return true } catch (err) { console.error('❌ saveOrUpdateRowUnified:', err) $q?.notify?.({ type: 'negative', message: 'Satır kaydı başarısız' }) return false } } , /* =========================================================== 🔄 setTransaction — yeni transaction ID set et =========================================================== */ setTransaction(id, autoResume = true) { if (!id) return // 🔧 temiz ID this.header.OrderHeaderID = id localStorage.setItem('bss_last_txn', id) console.log('🔄 Transaction değiştirildi:', id) if (autoResume) { const hasData = Array.isArray(this.orders) && this.orders.length > 0 if (!hasData) { const ok = this.hydrateFromLocalStorage(id,true) if (ok) console.info('📦 Local kayıt geri yüklendi (boş grid için).') } else { console.log('🚫 Grid dolu, auto-resume atlandı (mevcut satırlar korundu).') } } } , /* =========================================================== 🧹 clearTransaction — sadece NEW MODE taslaklarını temizler =========================================================== */ clearTransaction() { try { const id = this.header?.OrderHeaderID if (id) { localStorage.removeItem(`bss_orderentry_data:${id}`) } this.orders = [] this.summaryRows = [] this.groupedRows = [] this.header = {} this.mode = 'new' localStorage.removeItem('bss_last_txn') console.log('🧹 Transaction temizlendi') } catch (err) { console.warn('⚠️ clearTransaction hata:', err) } } , // ======================================================= // 🔒 KİLİT KONTROLÜ — Sadece EDIT modunda, backend satırı // ======================================================= isRowLocked(row) { if (!row) return false // Sadece edit modunda, // ve backend'den gelen gerçek OrderLineID varsa, // ve IsClosed=1 ise satır kilitli return ( this.mode === 'edit' && !!row.OrderLineID && row.isClosed === true ) }, findExistingIndexByForm(form) { return this.orders.findIndex(r => this.isSameCombo(r, form)) }, addRow(row) { if (!row) return const existingIndex = this.orders.findIndex(r => { const sameId = r.id && row.id && r.id === row.id const sameCombo = this.isSameCombo(r, row) return sameId || sameCombo }) if (existingIndex !== -1) { const old = this.orders[existingIndex] this.orders[existingIndex] = { ...old, adet: Number(row.adet ?? old.adet ?? 0), fiyat: Number(row.fiyat ?? old.fiyat ?? 0), tutar: Number(row.fiyat ?? old.fiyat ?? 0) * Number(row.adet ?? old.adet ?? 0), ItemDim1Code: row.ItemDim1Code || old.ItemDim1Code, aciklama: row.aciklama || old.aciklama, updatedAt: dayjs().toISOString() } console.log(`⚠️ Aynı kombinasyon bulundu, satır güncellendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`) } else { this.orders.push(toRaw(row)) console.log(`➕ Yeni kombinasyon eklendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`) } this.persistLocalStorage() this.saveSnapshot('after-add') }, updateRow(index, patch) { if (index < 0 || index >= this.orders.length) return this.orders[index] = { ...this.orders[index], ...toRaw(patch), updatedAt: dayjs().toISOString() } this.persistLocalStorage() this.saveSnapshot('after-update') console.log(`✏️ Satır güncellendi (store): #${index}`) }, removeRow(index) { if (index < 0 || index >= this.orders.length) return const removed = this.orders.splice(index, 1) if (Array.isArray(this.summaryRows)) { this.summaryRows.splice(index, 1) } this.persistLocalStorage() this.saveSnapshot('after-remove') console.log(`🗑️ Satır silindi: ${removed[0]?.model || '(model yok)'}`) }, removeSelectedRow(row, $q = null) { if (!row) return // 1) Kilitli satır silinemez if (this.isRowLocked(row)) { $q?.notify?.({ type: 'warning', message: '🔒 Bu satır (IsClosed=1) kapatılmış. Silinemez.' }) return false } // 2) Kullanıcıya onay sor return new Promise(resolve => { $q?.dialog({ title: 'Satır Sil', message: `${row.model} / ${row.renk} / ${row.renk2} kombinasyonu silinsin mi?`, ok: { label: 'Evet', color: 'negative' }, cancel: { label: 'Vazgeç' } }) .onOk(() => { this.removeRowInternal(row) resolve(true) }) .onCancel(() => resolve(false)) }) } , removeRowInternal(row) { if (!row) return false // 1️⃣ Kilit kontrolü if (this.isRowLocked(row)) { console.warn('🔒 Kilitli satır silinemez.') return false } const getKey = typeof this.getRowKey === 'function' ? this.getRowKey : (r => r?.clientKey || r?.id || r?.OrderLineID) const rowKey = getKey(row) if (!rowKey) return false const idx = this.summaryRows.findIndex(r => getKey(r) === rowKey) if (idx === -1) return false console.log('🗑️ X2 removeRowInternal →', row) // 🔐 UI BUSY this._uiBusy = true this.preventPersist = true try { // 2️⃣ UI’dan kaldır this.summaryRows.splice(idx, 1) // orders = UI satırları (temiz kopya) this.orders = [...this.summaryRows] // 3️⃣ EDIT MODE → DELETE SİNYALİ if (this.mode === 'edit') { const grpKey = row.grpKey || Object.keys(row.bedenMap || {})[0] || 'tak' // ✅ lineIdMap referansı (varsa) const lineIdMap = (row.lineIdMap && typeof row.lineIdMap === 'object') ? { ...row.lineIdMap } : {} const emptyMap = {} // Öncelik: bedenMap[grpKey] → lineIdMap → fallback if (row.bedenMap && row.bedenMap[grpKey]) { for (const beden of Object.keys(row.bedenMap[grpKey] || {})) { emptyMap[beden] = 0 } } else if (Object.keys(lineIdMap).length) { for (const beden of Object.keys(lineIdMap)) { emptyMap[beden] = 0 } } else { emptyMap['STD'] = 0 } const deleteSignalRow = { ...row, // 🔴 UI KEY id: `DEL::${row.id || row.OrderLineID || crypto.randomUUID()}`, // 🔴 BACKEND DELETE SIGNAL adet: 0, Qty1: 0, tutar: 0, // 🔴 CRITICAL: duplicate guard'a girmesin ComboKey: '', // 🔴 legacy tekil alan (varsa kalsın) OrderLineID: row.OrderLineID || null, // ✅ CRITICAL grpKey, bedenMap: { [grpKey]: emptyMap }, lineIdMap, comboLineIds: { ...(row.comboLineIds || {}) }, _deleteSignal: true } console.log('📡 DELETE sinyali üretildi:', deleteSignalRow) this.orders.push(deleteSignalRow) } // 4️⃣ Totals (persist YOK) this.updateHeaderTotals?.() } finally { // 🔓 GUARD KAPAT this.preventPersist = false this._uiBusy = false } // 5️⃣ TEK VE KONTROLLÜ persist this.persistLocalStorage() return true } , /* =========================================================== 📦 normalizeOrderLines (v9 — lineIdMap FIXED + AKSBİR SAFE) ----------------------------------------------------------- ✔ grpKey SADECE burada set edilir ✔ detectBedenGroup SADECE store’da kullanılır ✔ aksbir → ' ' bedeni = GERÇEK adet ✔ backend satırlarında BEDEN → OrderLineID map’i üretilir =========================================================== */ normalizeOrderLines(lines, pbFallback = 'USD') { if (!Array.isArray(lines)) return [] const merged = Object.create(null) const makeBaseKey = (model, renk, renk2) => `${model || ''}||${renk || ''}||${renk2 || ''}` for (const raw of lines) { if (!raw) continue const isClosed = raw.IsClosed === true || raw.isClosed === true || raw.IsClosed?.Bool === true /* ======================================================= 1️⃣ UI / SNAPSHOT KAYNAKLI SATIR ------------------------------------------------------- ✔ ComboKey YOK ✔ Sadece model / renk / renk2 bazında gruplanır ======================================================= */ if (raw.bedenMap && Object.keys(raw.bedenMap).length) { const model = (raw.model || raw.ItemCode || '').trim() const renk = (raw.renk || raw.ColorCode || '').trim() const renk2 = (raw.renk2 || raw.ItemDim2Code || '').trim() // ❗ BEDEN YOK → bu SADECE üst seviye grup anahtarı const modelKey = `${model}||${renk}||${renk2}` const grpKey = raw.grpKey || 'tak' const srcMap = raw.bedenMap[grpKey] || {} const adet = Object.values(srcMap).reduce((a, b) => a + (Number(b) || 0), 0) const fiyat = Number(raw.fiyat || 0) const pb = raw.pb || raw.DocCurrencyCode || pbFallback const tutar = Number(raw.tutar ?? adet * fiyat) merged[modelKey] ??= [] merged[modelKey].push({ ...raw, grpKey, bedenMap: { [grpKey]: { ...srcMap } }, adet, fiyat, pb, tutar, isClosed }) continue } /* ======================================================= 2️⃣ BACKEND / LEGACY SATIR (FIXED) ------------------------------------------------------- ✔ ComboKey YOK ✔ Sadece model / renk / renk2 bazlı gruplanır ✔ BEDEN sadece bedenMap + lineIdMap için kullanılır ======================================================= */ const model = (raw.Model || raw.ItemCode || '').trim() const renk = (raw.ColorCode || '').trim() const renk2 = (raw.ItemDim2Code || '').trim() // ❗ BEDEN HARİÇ — üst seviye grup anahtarı const modelKey = `${model}||${renk}||${renk2}` merged[modelKey] ??= [] const bedenRaw = raw.ItemDim1Code == null ? '' : String(raw.ItemDim1Code).trim() const beden = bedenRaw === '' ? ' ' : normalizeBedenLabel(bedenRaw) const qty = Number(raw.Qty1 || raw.Qty || 0) let entry = merged[modelKey][0] if (!entry) { entry = { id: raw.OrderLineID || crypto.randomUUID(), model, renk, renk2, urunAnaGrubu: raw.UrunAnaGrubu || 'GENEL', urunAltGrubu: raw.UrunAltGrubu || '', kategori: raw.Kategori || '', aciklama: raw.LineDescription || '', fiyat: Number(raw.Price || 0), pb: raw.DocCurrencyCode || pbFallback, __tmpMap: {}, // beden → qty lineIdMap: {}, // beden → OrderLineID adet: 0, tutar: 0, terminTarihi: raw.DeliveryDate || null, isClosed } merged[modelKey].push(entry) } /* ------------------------------------------------------- 🔑 BEDEN → OrderLineID (DETERMINISTIC & SAFE) -------------------------------------------------------- */ const rawLineId = raw.OrderLineID || raw.OrderLineId || raw.orderLineID || null if (rawLineId) { entry.lineIdMap[beden] = String(rawLineId) } if (qty > 0) { entry.__tmpMap[beden] = (entry.__tmpMap[beden] || 0) + qty entry.adet += qty entry.tutar += qty * entry.fiyat } } /* ======================================================= 3️⃣ FINAL — grpKey KESİN + AKSBİR FIX ======================================================= */ const out = [] for (const rows of Object.values(merged)) { for (const row of rows) { if (!row.__tmpMap) { out.push(row) continue } const bedenList = Object.keys(row.__tmpMap) // 🔒 TEK VE KESİN KARAR const grpKey = detectBedenGroup( bedenList, row.urunAnaGrubu, row.kategori ) const cleanedMap = { ...row.__tmpMap } const hasNonBlankBeden = Object.keys(cleanedMap) .some(k => String(k).trim() !== '') // Gomlek/takim/pantolon gibi gruplarda bos beden sadece legacy kirli veri olabilir. // Gecerli bedenler varsa bos bedeni payload/UI'dan temizliyoruz. if (grpKey !== 'aksbir' && hasNonBlankBeden) { delete cleanedMap[' '] delete cleanedMap[''] if (row.lineIdMap && typeof row.lineIdMap === 'object') { delete row.lineIdMap[' '] delete row.lineIdMap[''] } } row.grpKey = grpKey row.bedenMap = { [grpKey]: { ...cleanedMap } } row.adet = Object.values(cleanedMap).reduce((a, b) => a + (Number(b) || 0), 0) row.tutar = Number((row.adet * Number(row.fiyat || 0)).toFixed(2)) /* =================================================== 🔒 AKSBİR — BOŞLUK BEDEN GERÇEK ADETİ ALIR ❗ STD’ye dönme YOK ❗ 0 yazma YOK =================================================== */ if (grpKey === 'aksbir') { row.bedenMap[grpKey] ??= {} row.bedenMap[grpKey][' '] = Number(row.adet || 0) } delete row.__tmpMap out.push(row) } } console.log( `📦 normalizeOrderLines (v9 + lineIdMap) → ${out.length} satır` ) return out } , /** * =========================================================== * loadProductSizes — FINAL v4.2 (EDITOR SAFE) * ----------------------------------------------------------- * ✔ grpKey SADECE form.grpKey * ✔ schemaMap TEK OTORİTE * ✔ edit modda BEDEN LABEL DOKUNULMAZ * ✔ ' ' (boş beden) korunur * =========================================================== */ async loadProductSizes(form, forceRefresh = false, $q = null) { if (!form?.model) return const store = this const prevBusy = !!store._uiBusy const prevPrevent = !!store.preventPersist store._uiBusy = true store.preventPersist = true try { const grpKey = form.grpKey if (!grpKey) { console.warn('⛔ loadProductSizes iptal → grpKey yok') return } const colorKey = form.renk || 'nocolor' const color2Key = form.renk2 || 'no2color' const cacheKey = `${form.model}_${colorKey}_${color2Key}_${grpKey}` /* ======================================================= ♻️ CACHE (LABEL DOKUNMADAN) ======================================================= */ if (!forceRefresh && sizeCache.value?.[cacheKey]) { const cached = sizeCache.value[cacheKey] bedenStock.value = [...cached.stockArray] stockMap.value = { ...cached.stockMap } console.log(`♻️ loadProductSizes CACHE → ${grpKey}`) return } /* ======================================================= 📡 API ======================================================= */ const params = { code: form.model } if (form.renk) params.color = form.renk if (form.renk2) params.color2 = form.renk2 const res = await api.get('/product-colorsize', { params }) const data = Array.isArray(res?.data) ? res.data : [] if (!data.length) { bedenStock.value = [] stockMap.value = {} return } /* ======================================================= 📦 STOK MAP (' ' KORUNUR) ======================================================= */ const apiStockMap = {} for (const x of data) { const key = x.item_dim1_code === null || x.item_dim1_code === '' ? ' ' : String(x.item_dim1_code) apiStockMap[key] = Number(x.kullanilabilir_envanter ?? 0) } const finalStockMap = {} for (const lbl of form.bedenLabels) { finalStockMap[lbl] = apiStockMap[lbl] ?? 0 } stockMap.value = { ...finalStockMap } bedenStock.value = Object.entries(stockMap.value).map( ([beden, stok]) => ({ beden, stok }) ) /* ======================================================= 💾 CACHE ======================================================= */ sizeCache.value[cacheKey] = { labels: [...form.bedenLabels], stockArray: [...bedenStock.value], stockMap: { ...stockMap.value } } console.log(`✅ loadProductSizes FINAL v4.2 → ${grpKey}`) } catch (err) { console.error('❌ loadProductSizes hata:', err) $q?.notify?.({ type: 'negative', message: 'Beden / stok alınamadı' }) } finally { store._uiBusy = prevBusy store.preventPersist = prevPrevent console.log('🧩 Editor beden hydrate', { grpKey: form.grpKey, labels: form.bedenLabels, values: form.bedenler }) } } , // ======================================================= // 🔸 TOPLAM HESAPLAMA (store içi) — X3 SAFE // ------------------------------------------------------- // ✔ f.adet / f.tutar hesaplanır // ✔ store.totalAmount ASLA set edilmez // ✔ gerçek toplam → header.TotalAmount // ======================================================= updateTotals(f) { // 1️⃣ Satır adet f.adet = (f.bedenler || []).reduce( (a, b) => a + Number(b || 0), 0 ) // 2️⃣ Satır tutar const fiyat = Number(f.fiyat) || 0 f.tutar = Number((f.adet * fiyat).toFixed(2)) // 3️⃣ Header toplam (tek gerçek state) if (this.header) { const total = (this.summaryRows || []).reduce( (sum, r) => sum + Number(r?.tutar || 0), 0 ) this.header.TotalAmount = Number(total.toFixed(2)) } return f } , // ======================================================= // 🔸 GRUP ANAHTARI TESPİTİ // ======================================================= activeGroupKeyForRow(row) { const g = (row?.urunAnaGrubu || '').toUpperCase() if (g.includes('TAKIM')) return 'tak' if (g.includes('PANTOLON')) return 'pan' if (g.includes('GOMLEK')) return 'gom' if (g.includes('AYAKKABI')) return 'ayk' if (g.includes('YAS')) return 'yas' return 'tak' }, /* ======================================================= 🔹 MODE YÖNETİMİ — new / edit arası geçiş ======================================================= */ setMode(mode) { if (!['new', 'edit', 'view'].includes(mode)) { console.warn('⚠️ Geçersiz mode:', mode) return } this.mode = mode console.log(`🧭 Order mode set edildi → ${mode}`) } , /* =========================================================== 🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE) ----------------------------------------------------------- ✔ NEW → INSERT, EDIT → UPDATE (tek karar noktası) ✔ Controlled submit → route guard SUSAR ✔ Snapshot temizliği route öncesi ✔ Kaydet → edit replace → backend reload ✔ Listeye giderken guard popup 1 kez bypass ✔ ✅ PRE-VALIDATE → prItemVariant olmayan kombinasyonlar kaydı DURDURUR =========================================================== */ async submitAllReal($q, router, form, summaryRows, productCache) { let serverOrderId = null let serverOrderNo = null try { this.loading = true // 🔒 Kontrollü submit → route leave guard susar this.isControlledSubmit = true const isNew = this.mode === 'new' const { header, lines } = this.buildFinalOrderJson() // ======================================================= // 🧾 DEBUG — FRONTEND → BACKEND GİDEN PAYLOAD // ======================================================= console.groupCollapsed( `%c📤 ORDER PAYLOAD (${this.mode})`, 'color:#c9a873;font-weight:bold' ) console.log('HEADER:', JSON.parse(JSON.stringify(header))) lines.forEach((l, i) => { console.log(`LINE[${i}]`, { OrderLineID: l.OrderLineID, ClientKey: l.ClientKey, ItemCode: l.ItemCode, ColorCode: l.ColorCode, ItemDim1Code: l.ItemDim1Code, ItemDim2Code: l.ItemDim2Code, ItemDim3Code: l.ItemDim3Code, Qty1: l.Qty1, ComboKey: l.ComboKey }) }) console.groupEnd() // ======================================================= // 🧾 DEBUG (opsiyonel helper) // ======================================================= this.debugOrderPayload?.(header, lines, 'PRE-VALIDATE') // ======================================================= // 🧩 DUMMY CURRENCY PAYLOAD (model genişletmeden) // - trOrderLineCurrency için gerekli alanları satıra basar // - örnek satırdaki gibi: PriceVI/AmountVI = KDV dahil, Price/Amount = KDV hariç // ======================================================= const r2 = (n) => Number((Number(n) || 0).toFixed(2)) const r4 = (n) => Number((Number(n) || 0).toFixed(4)) for (const ln of lines) { const qty = Number(ln?.Qty1 || 0) const unitBase = Number(ln?.Price || 0) // KDV hariç birim const vatRate = Number(ln?.VatRate || 0) const exRate = Number(ln?.PriceExchangeRate || header?.ExchangeRate || 1) || 1 const taxBase = r2(unitBase * qty) // Amount const vat = r2((taxBase * vatRate) / 100) // Vat const net = r2(taxBase + vat) // AmountVI / NetAmount const unitWithVat = qty > 0 ? r4(net / qty) : r4(unitBase * (1 + vatRate / 100)) const docCurrency = String(ln?.DocCurrencyCode || header?.DocCurrencyCode || 'TRY').trim() || 'TRY' // Backend model alanları ln.RelationCurrencyCode = docCurrency ln.DocPrice = unitWithVat ln.DocAmount = net ln.LocalPrice = unitBase ln.LocalAmount = taxBase ln.LineDiscount = Number(ln?.LineDiscount || 0) ln.TotalDiscount = Number(ln?.TotalDiscount || 0) ln.TaxBase = taxBase ln.Pct = Number(ln?.Pct || 0) ln.VatAmount = vat ln.VatDeducation = 0 ln.NetAmount = net // SQL kolonu isimleriyle dummy alias (decoder ignore etse de payload'da görünür) ln.CurrencyCode = docCurrency ln.ExchangeRate = exRate ln.PriceVI = unitWithVat ln.AmountVI = net ln.Amount = taxBase ln.LDiscount1 = Number(ln?.LDiscount1 || 0) ln.TDiscount1 = Number(ln?.TDiscount1 || 0) ln.Vat = vat } // ======================================================= // 🧪 PRE-VALIDATE — prItemVariant ön kontrol // - invalid varsa CREATE/UPDATE ÇALIŞMAZ // ======================================================= const v = await api.post('/order/validate', { header, lines }) const invalid = v?.data?.invalid || [] if (invalid.length > 0) { await this.showInvalidVariantDialog?.($q, invalid) return // ❌ create / update ÇALIŞMAZ } console.log('📤 submitAllReal payload', { mode: this.mode, lines: lines.length, deletes: lines.filter(l => l._deleteSignal).length }) /* ======================================================= 🚀 API CALL — TEK NOKTA ======================================================= */ const resp = await api.post( isNew ? '/order/create' : '/order/update', { header, lines } ) const data = resp?.data || {} serverOrderId = data.orderID || data.orderHeaderID || data.id || header?.OrderHeaderID serverOrderNo = data.orderNumber || data.orderNo || header?.OrderNumber if (!serverOrderId) { throw new Error('OrderHeaderID backend’den dönmedi') } /* ======================================================= 🔁 MODE SWITCH → EDIT ======================================================= */ this.setMode('edit') // Header patch (ID / No) this.header = { ...this.header, OrderHeaderID: serverOrderId, OrderNumber: serverOrderNo } /* ======================================================= 🧹 KRİTİK: Snapshot + Dirty temizliği ❗ ROUTE değişmeden ÖNCE ======================================================= */ this.updateHeaderTotals?.() this.markAsSaved?.() /* ======================================================= 🧹 KRİTİK: NEW → EDIT geçişinde TÜM SNAPSHOT TEMİZLENİR ======================================================= */ this.clearAllOrderSnapshots() $q.notify({ type: 'positive', message: `Sipariş kaydedildi: ${serverOrderNo || ''}`.trim() }) /* ======================================================= 🔀 ROUTE REPLACE (EDIT MODE) - aynı sayfa → param değişti - guard 1 kez bypass ======================================================= */ this.allowRouteLeaveOnce = true await router.replace({ name: 'order-entry', params: { orderHeaderID: serverOrderId }, query: { mode: 'edit', source: 'backend' } }) /* ======================================================= 🔄 BACKEND RELOAD (TEK GERÇEK KAYNAK) ======================================================= */ await this.openExistingForEdit(serverOrderId, { $q, form, summaryRowsRef: summaryRows, productCache }) /* ======================================================= ❓ USER NEXT STEP ======================================================= */ const choice = await new Promise(resolve => { $q.dialog({ title: 'Sipariş Kaydedildi', options: { type: 'radio', model: 'continue', items: [ { label: '✏️ Düzenlemeye Devam', value: 'continue' }, { label: '🖨 Yazdır', value: 'print' }, { label: '📋 Listeye Dön', value: 'list' } ] }, ok: { label: 'Seç' }, cancel: { label: 'Kapat' } }) .onOk(v => resolve(v)) .onCancel(() => resolve('continue')) }) /* ======================================================= 🧭 USER ROUTING ======================================================= */ if (choice === 'print') { const id = this.header?.OrderHeaderID || serverOrderId if (id) { try { await this.downloadOrderPdf(id) } catch (pdfErr) { console.error('⚠️ PDF açılamadı, kayıt başarılı:', pdfErr) $q.notify({ type: 'warning', message: pdfErr?.message || 'Sipariş kaydedildi fakat PDF açılamadı.' }) } } return } if (choice === 'list') { this.allowRouteLeaveOnce = true await router.push({ name: 'order-list' }) return } // continue → sayfada kal (hiçbir şey yapma) } catch (err) { console.error('❌ submitAllReal:', err) $q.notify({ type: 'negative', message: err?.response?.data?.detail || err?.response?.data?.message || err?.message || 'Kayıt sırasında hata' }) } finally { // 🔓 Guard’lar normale dönsün this.isControlledSubmit = false this.loading = false } } , /* ======================================================= 🧪 SUBMIT ALL TEST ======================================================= */ async submitAllTest($q = null) { try { const { header, lines } = this.buildFinalOrderJson() console.log('🧾 TEST HEADER', Object.keys(header).length, 'alan') console.log(JSON.stringify(header, null, 2)) console.log('🧾 TEST LINES', lines.length, 'satır') console.log(JSON.stringify(lines, null, 2)) $q?.notify?.({ type: 'info', message: `Header (${Object.keys(header).length}) + Lines (${lines.length}) gösterildi`, position: 'top' }) } catch (err) { console.error('❌ submitAllTest hata:', err) $q?.notify?.({ type: 'negative', message: 'Gösterimde hata oluştu ❌', position: 'top' }) } }, /* ======================================================= 🧹 KAYIT SONRASI TEMİZLİK ======================================================= */ afterSubmit(opts = { keepLocalStorage: true, backendPayload: null, resetMode: true // 🔑 yeni }) { try { console.log('🧹 afterSubmit başlatıldı', opts) if (opts?.backendPayload?.header?.OrderHeaderID) { this.mergeAndPersistBackendOrder( opts.backendPayload.header.OrderHeaderID, opts.backendPayload ) } if (!opts?.keepLocalStorage) { this.clearStorage() this.clearTransaction() } else { this.saveSnapshot() } this.orders = [] this.header = {} this.editingKey = null this.currentOrderId = null // 🔐 MODE RESET OPSİYONEL if (opts.resetMode === true) { this.mode = 'new' } console.log('✅ afterSubmit tamamlandı.') } catch (err) { console.error('❌ afterSubmit hata:', err) } } , /* =========================================================== 🟦 BUILD FINAL ORDER JSON — SAFE v26.1 (FINAL) ----------------------------------------------------------- ✔ ComboKey TEK OTORİTE → buildComboKey (bedenKey ile) ✔ UI/Map placeholder: '_' (bedenKey) ✔ DB/payload: '' (bedenPayload) → "_" ASLA GİTMEZ ✔ payload içinde aynı ComboKey TEK satır ✔ backend duplicate guard %100 uyumlu (ComboKey stabil) ✔ Final assert: payload’da "_" yakalanırsa patlatır =========================================================== */ buildFinalOrderJson () { const auth = useAuthStore() const u = auth?.user || {} const now = dayjs() /* ========================= HELPERS ========================== */ const toNum = v => Number(v) || 0 const safeStr = v => (v == null ? '' : String(v).trim()) const formatDateOnly = v => (v ? dayjs(v).format('YYYY-MM-DD') : null) const formatTimeOnly = v => dayjs(v).format('HH:mm:ss') const formatDateTime = v => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : null) // ✅ Payload beden normalize: "_" / "-" / "" => '' const normBeden = (v) => { const s = safeStr(v) if (s === '' || s === '_' || s === '-') return '' // payload empty return s } /* ========================= USER META ========================== */ const group = safeStr(u?.v3usergroup) const v3name = safeStr(u?.v3_username) const who = (group && v3name) ? `${group} ${v3name}` : (v3name || 'BSS') const PCT_CODE_ZERO = '%0' const VAT_CODE_ZERO = '%0' /* ========================= HEADER ========================== */ const headerId = this.header?.OrderHeaderID || crypto.randomUUID() const docCurrency = safeStr(this.header?.DocCurrencyCode) || 'TRY' const exRate = toNum(this.header?.ExchangeRate) || 1 const avgDueSource = this.header?.AverageDueDate || dayjs(this.header?.OrderDate || now).add(14, 'day') const header = { ...this.header, OrderHeaderID: headerId, OrderDate: formatDateOnly(this.header?.OrderDate || now), OrderTime: formatTimeOnly(now), AverageDueDate: formatDateOnly(avgDueSource), DocCurrencyCode: docCurrency, LocalCurrencyCode: safeStr(this.header?.LocalCurrencyCode) || 'TRY', ExchangeRate: exRate, CreatedUserName: this.mode === 'edit' ? (this.header?.CreatedUserName || who) : who, CreatedDate: this.mode === 'edit' ? formatDateTime(this.header?.CreatedDate || now) : formatDateTime(now), LastUpdatedUserName: who, LastUpdatedDate: formatDateTime(now) } /* ======================================================= LINES — COMBOKEY AGGREGATE (TEK MAP) ======================================================= */ const lines = [] const lineByCombo = new Map() // 🔒 KEY = ComboKey const pushOrMerge = (row, ctx) => { const { grpKey, bedenKey, // ✅ sadece ComboKey / Map için ('_' olabilir) bedenPayload, // ✅ DB için ('' / 'S' / 'M' ...) qty, orderLineId, isDeleteSignal } = ctx if (qty <= 0 && !isDeleteSignal) return // ComboKey stabil kalsın diye bedenKey kullan const comboKey = buildComboKey(row, bedenKey) const makeLine = () => ({ OrderLineID: orderLineId || '', ClientKey: makeLineClientKey(row, grpKey, bedenKey), ComboKey: comboKey, SortOrder: 0, ItemTypeCode: 1, ItemCode: safeStr(row.model), ColorCode: safeStr(row.renk), // ✅ PAYLOAD: "_" ASLA YOK ItemDim1Code: bedenPayload, ItemDim2Code: safeStr(row.renk2), ItemDim3Code: '', Qty1: isDeleteSignal ? 0 : qty, Qty2: 0, CancelQty1: 0, CancelQty2: 0, DeliveryDate: row.terminTarihi ? formatDateTime(row.terminTarihi) : null, PlannedDateOfLading: row.terminTarihi ? formatDateOnly(row.terminTarihi) : null, LineDescription: safeStr(row.aciklama), UsedBarcode: '', CostCenterCode: '', VatCode: VAT_CODE_ZERO, VatRate: toNum(row.vatRate ?? row.VatRate ?? 0), PCTCode: PCT_CODE_ZERO, PCTRate: 0, LDisRate1: 0, LDisRate2: 0, LDisRate3: 0, LDisRate4: 0, LDisRate5: 0, DocCurrencyCode: header.DocCurrencyCode, PriceCurrencyCode: header.DocCurrencyCode, PriceExchangeRate: toNum(header.ExchangeRate), Price: toNum(row.fiyat), BaseProcessCode: 'WS', BaseOrderNumber: header.OrderNumber, BaseCustomerTypeCode: 0, BaseCustomerCode: header.CurrAccCode, BaseSubCurrAccID: null, BaseStoreCode: '', OrderHeaderID: headerId, CreatedUserName: who, CreatedDate: formatDateTime(row.CreatedDate || now), LastUpdatedUserName: who, LastUpdatedDate: formatDateTime(now), SurplusOrderQtyToleranceRate: 0, WithHoldingTaxTypeCode: '', DOVCode: '' }) const existing = lineByCombo.get(comboKey) if (!existing) { const ln = makeLine() lineByCombo.set(comboKey, ln) lines.push(ln) return } /* DELETE */ if (isDeleteSignal) { if (orderLineId && !existing.OrderLineID) { existing.OrderLineID = orderLineId } existing.Qty1 = 0 return } /* MERGE */ existing.Qty1 += qty if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) { existing.OrderLineID = orderLineId } existing.Price = toNum(row.fiyat) } /* ======================================================= ORDER ROW LOOP ======================================================= */ for (const row of this.orders || []) { if (row?.isClosed === true) continue const grpKey = row.grpKey || Object.keys(row.bedenMap || {})[0] || 'GENEL' const lineIdMap = row.lineIdMap || {} const grouped = row.bedenMap?.[grpKey] const flat = (row.bedenMap && typeof row.bedenMap === 'object' && !grouped) ? row.bedenMap : null const map = grouped || flat const hasAnyBeden = map && typeof map === 'object' && Object.keys(map).length > 0 /* 🔹 BEDENSİZ / AKSBİR */ if (!hasAnyBeden) { const allowBlankPayload = grpKey === 'aksbir' || row._deleteSignal === true if (!allowBlankPayload) { continue } const qty = toNum(row.qty ?? row.Qty1 ?? row.miktar ?? 0) // ✅ ComboKey stabil: bedenKey = '_' const bedenKey = '_' // ✅ Payload: boş string const bedenPayload = '' let orderLineId = '' if (this.mode === 'edit') { // lineIdMap burada '_' ile tutuluyorsa onu da oku orderLineId = safeStr(lineIdMap?.[bedenKey]) || safeStr(lineIdMap?.[bedenPayload]) || safeStr(lineIdMap?.[' ']) || safeStr(row.OrderLineID) } pushOrMerge(row, { grpKey, bedenKey, bedenPayload, qty, orderLineId, isDeleteSignal: row._deleteSignal === true && !!orderLineId }) continue } /* 🔹 BEDENLİ */ for (const [bedenRaw, qtyRaw] of Object.entries(map)) { const isBlankBeden = safeStr(bedenRaw) === '' if ( isBlankBeden && grpKey !== 'aksbir' && row._deleteSignal !== true ) { continue } const qty = toNum(qtyRaw) // ✅ payload beden: '' / 'S' / 'M' ... const bedenPayload = normBeden(bedenRaw) // ✅ combokey beden: boşsa '_' ile stabil kalsın const bedenKey = bedenPayload || '_' let orderLineId = '' if (this.mode === 'edit') { // lineIdMap anahtarı sizde hangi bedenle tutuluyorsa ikisini de dene orderLineId = safeStr(lineIdMap?.[bedenKey]) || safeStr(lineIdMap?.[bedenPayload]) || safeStr(lineIdMap?.[' ']) || (Object.keys(map).length === 1 ? safeStr(row.OrderLineID) : '') } pushOrMerge(row, { grpKey, bedenKey, bedenPayload, qty, orderLineId, isDeleteSignal: row._deleteSignal === true && !!orderLineId }) } } /* ======================================================= FINAL SORT ======================================================= */ lines.forEach((ln, i) => { ln.SortOrder = i + 1 }) /* ======================================================= ASSERT — payload’da "_" OLAMAZ ======================================================= */ if (lines.some(l => (l.ItemDim1Code || '') === '_' )) { console.error('❌ Payload’da "_" yakalandı', lines.filter(l => l.ItemDim1Code === '_')) throw new Error('Payload ItemDim1Code "_" olamaz') } /* ======================================================= 🔍 DEBUG — BUILD FINAL ORDER JSON OUTPUT ======================================================= */ console.groupCollapsed('%c📦 BUILD FINAL ORDER JSON', 'color:#c9a873;font-weight:bold') console.log('🧾 HEADER:', header) console.table( lines.map((l, i) => ({ i: i + 1, OrderLineID: l.OrderLineID, ClientKey: l.ClientKey, ComboKey: l.ComboKey, ItemCode: l.ItemCode, ColorCode: l.ColorCode, ItemDim1Code: JSON.stringify(l.ItemDim1Code), // <-- kritik ItemDim2Code: l.ItemDim2Code, Qty1: l.Qty1, Price: l.Price })) ) console.groupEnd() return { header, lines } } ,/* =========================================================== ✅ STORE ACTIONS — FIXED HELPERS - setRowErrorByClientKey - clearRowErrorByClientKey - applyTerminToRowsIfEmpty =========================================================== */ setRowErrorByClientKey(clientKey, payload) { if (!clientKey) return if (!Array.isArray(this.summaryRows)) return const row = this.summaryRows.find(r => r?.clientKey === clientKey) if (!row) return row._error = { code: payload?.code, message: payload?.message } }, clearRowErrorByClientKey(clientKey) { if (!clientKey) return if (!Array.isArray(this.summaryRows)) return const row = this.summaryRows.find(r => r?.clientKey === clientKey) if (!row) return if (row._error) { delete row._error } }, applyTerminToRowsIfEmpty(dateStr) { if (!dateStr) return if (!Array.isArray(this.summaryRows)) return // ❗ reassign YOK — patch/mutate for (const r of this.summaryRows) { if (!r?.terminTarihi || r.terminTarihi === '') { r.terminTarihi = dateStr } } // opsiyonel ama genelde doğru: this.persistLocalStorage?.() } } // actions sonu }) // defineStore sonu /* =========================================================== Size Label Normalization (frontend helper) =========================================================== */ function safeTrimUpperJs(v) { return (v == null ? '' : String(v)).trim().toUpperCase() } function normalizeTextForMatch(v) { return safeTrimUpperJs(v) .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') } function parseNumericSizeJs(v) { const s = safeTrimUpperJs(v) if (s === '' || !/^\d+$/.test(s)) return null const n = Number.parseInt(s, 10) return Number.isNaN(n) ? null : n } export function normalizeBedenLabel(v) { let s = (v == null ? '' : String(v)).trim() if (s === '') return ' ' s = s.toUpperCase() // Backend parity: normalize common "standard size" aliases. switch (s) { case 'STD': case 'STANDART': case 'STANDARD': case 'ONE SIZE': case 'ONESIZE': return 'STD' } // Backend parity: only values ending with CM are converted to numeric part. if (s.endsWith('CM')) { const num = s.slice(0, -2).trim() if (num !== '') return num } switch (s) { case 'XS': case 'S': case 'M': case 'L': case 'XL': case '2XL': case '3XL': case '4XL': case '5XL': case '6XL': case '7XL': return s } return s } /* =========================================================== Size Group Detection - Core logic aligned with backend detectBedenGroupGo - Keeps frontend aksbir bucket for accessory lines =========================================================== */ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') { const list = Array.isArray(bedenList) ? bedenList : [] const ana = normalizeTextForMatch(urunAnaGrubu) const alt = normalizeTextForMatch(urunKategori) // Frontend compatibility: accessory-only products should stay in aksbir. const accessoryGroups = [ 'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER', 'CORAP', 'FULAR', 'MENDIL', 'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI' ] const clothingGroups = ['GOMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT'] if ( accessoryGroups.some(g => ana.includes(g) || alt.includes(g)) && !clothingGroups.some(g => ana.includes(g)) ) { return 'aksbir' } if (ana.includes('AYAKKABI') || alt.includes('AYAKKABI')) { return 'ayk' } let hasYasNumeric = false let hasAykNumeric = false let hasPanNumeric = false for (const raw of list) { const b = safeTrimUpperJs(raw) switch (b) { case 'XS': case 'S': case 'M': case 'L': case 'XL': case '2XL': case '3XL': case '4XL': case '5XL': case '6XL': case '7XL': return 'gom' } const n = parseNumericSizeJs(b) if (n == null) continue if (n >= 2 && n <= 14) hasYasNumeric = true if (n >= 39 && n <= 45) hasAykNumeric = true if (n >= 38 && n <= 68) hasPanNumeric = true } if (hasAykNumeric) return 'ayk' if (ana.includes('PANTOLON')) return 'pan' if (hasPanNumeric) return 'pan' if (alt.includes('COCUK') || alt.includes('GARSON')) return 'yas' if (hasYasNumeric) return 'yas' return 'tak' } export function toSummaryRowFromForm(form) { if (!form) return null const grpKey = form.grpKey || 'tak' const bedenMap = {} const labels = Array.isArray(form.bedenLabels) ? form.bedenLabels : [] const values = Array.isArray(form.bedenler) ? form.bedenler : [] for (let i = 0; i < labels.length; i++) { const rawLbl = labels[i] const lbl = rawLbl == null || String(rawLbl).trim() === '' ? ' ' : normalizeBedenLabel(String(rawLbl)) const val = Number(values[i] || 0) if (val > 0) { bedenMap[lbl] = val } } return { id: form.id || newGuid(), OrderLineID: form.OrderLineID || null, model: form.model || '', renk: form.renk || '', renk2: form.renk2 || '', urunAnaGrubu: form.urunAnaGrubu || '', urunAltGrubu: form.urunAltGrubu || '', aciklama: form.aciklama || '', fiyat: Number(form.fiyat || 0), pb: form.pb || 'USD', adet: Number(form.adet || 0), tutar: Number(form.tutar || 0), grpKey, bedenMap: { [grpKey]: { ...bedenMap } }, terminTarihi: (form.terminTarihi || '').substring(0, 10) } } /* =========================================================== 🔹 TOPLAM HESAPLAMA (EXPORT) ----------------------------------------------------------- Hem store içinde hem de component tarafında kullanılabilir. =========================================================== */ export function updateTotals(f) { f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0) const fiyat = Number(f.fiyat) || 0 f.tutar = (f.adet * fiyat).toFixed(2) return f } /* =========================================================== 🔹 EXPORT SET — Tek Merkezli Dışa Aktarımlar =========================================================== */ /** * 🧩 Shared Reactive Refs * ----------------------------------------------------------- * import { sharedOrderEntryRefs } from 'src/stores/orderentryStore' * const { stockMap, bedenStock, sizeCache } = sharedOrderEntryRefs */ export const sharedOrderEntryRefs = { stockMap, bedenStock, sizeCache, }