Files
bssapp/ui/src/pages/OrderEntry.vue

3077 lines
91 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- ===========================================================
🧾 ORDER ENTRY PAGE (BSSApp)
v23 Sticky-stack + Drawer uyumlu yapı
============================================================ -->
<q-page
v-if="canReadOrder"
class="order-page"
>
<!-- 🔄 SAYFA LOADERI -->
<q-inner-loading :showing="loadingHeader || loadingCari || loadingModels" color="primary">
<q-spinner size="50px" />
</q-inner-loading>
<!-- =======================================================
🔹 STICKY STACK (Filter + Save + Header)
======================================================== -->
<div class="sticky-stack">
<!-- 🔸 1. Satır: Filtre Bar -->
<div class="filter-bar row q-col-gutter-md q-mb-sm">
<!-- 🧾 Cari Seçimi -->
<div class="col-5">
<q-select
v-model="form.CurrAccCode"
:options="filteredCariOptions"
label="Cari Seçimi"
filled
use-input
input-debounce="300"
emit-value
map-options
option-value="Cari_Kod"
:option-label="opt => `${opt.Cari_Kod} - ${opt.Cari_Ad}`"
@filter="filterCari"
@update:model-value="onCariChange"
:loading="loadingCari"
:disable="isEditMode || isClosedOrder || isViewOnly"
:readonly="isViewOnly"
clearable
>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>{{ scope.opt.Cari_Ad }}</q-item-label>
<q-item-label caption>{{ scope.opt.Cari_Kod }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<!-- 🔢 Sipariş No -->
<div class="col-2">
<q-input
v-model="form.OrderNumber"
label="Sipariş No"
filled
dense
:disable="isEditMode || isClosedOrder || isViewOnly"
:readonly="isViewOnly"
/>
</div>
<!-- 📅 Oluşturulma Tarihi -->
<div class="col-2">
<q-input
:model-value="formatDateInput(form.OrderDate)"
label="Oluşturulma Tarihi"
type="date"
filled
dense
@update:model-value="v => form.OrderDate = v"
:disable="isEditMode || isClosedOrder || isViewOnly"
:readonly="isViewOnly"
/>
</div>
<!-- 📅 Tahmini Termin Tarihi (AverageDueDate kilitlenmeyecek) -->
<div class="col-2">
<q-input
:model-value="formatDateInput(form.AverageDueDate)"
label="Tahmini Termin Tarihi"
type="date"
filled
dense
@update:model-value="v => form.AverageDueDate = v"
:readonly="isViewOnly"
:disable="isViewOnly"
/>
</div>
<!-- 💰 TOPLAM TUTAR + KDV -->
<div class="col-12 row q-col-gutter-sm q-mt-xs items-center">
<!-- 💰 Toplam Tutar -->
<div class="col-3">
<q-input
dense
filled
:model-value="Number(orderStore.totalAmount || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 })"
label="Toplam Tutar"
readonly
>
<template #append>{{ form.pb }}</template>
</q-input>
</div>
<!-- 🔘 KDV Checkbox -->
<div class="col-auto flex items-center">
<q-checkbox
v-model="form.includeVat"
label="KDV Dahil"
color="primary"
@update:model-value="onVatToggle"
:disable="isClosedRow||isViewOnly"
:readonly="isViewOnly"
/>
</div>
<!-- ⚙️ KDV ALANLARI: sadece tikliyken görünür -->
<template v-if="form.includeVat">
<!-- % oran sadece bilgi -->
<div class="col-1">
<q-input
dense
filled
:model-value="form.vatRate"
label="%"
readonly
>
<template #append>%</template>
</q-input>
</div>
<!-- 🧮 KDV Tutarı (manuel düzenlenebilir) -->
<div class="col-2">
<q-input
dense
filled
v-model="form.vatAmountInput"
label="KDV Tutarı"
@update:model-value="onVatAmountChange"
input-class="text-right"
:disable="isClosedRow || isViewOnly"
:readonly="isViewOnly"
>
<template #append>{{ form.pb }}</template>
</q-input>
</div>
<!-- 🧾 KDV Dahil Toplam -->
<div class="col-2">
<q-input
dense
filled
:model-value="Number(form.totalWithVat || 0).toLocaleString('tr-TR',{minimumFractionDigits:2})"
label="KDV Dahil Toplam"
readonly
>
<template #append>{{ form.pb }}</template>
</q-input>
</div>
</template>
</div>
</div>
<!-- 🔹 Cari Bilgi Barı -->
<q-slide-transition>
<div
v-if="cariInfo"
class="row q-col-gutter-md q-mt-xs cari-info-bar"
>
<div class="col-3">
<q-input
:model-value="cariInfo.Musteri_Temsilcisi || '-'"
label="Müşteri Temsilcisi"
filled dense readonly
/>
</div>
<div class="col-3">
<q-input
:model-value="cariInfo.Musteri_Ana_Grubu || '-'"
label="Ana Grup"
filled dense readonly
/>
</div>
<div class="col-3">
<q-input
:model-value="cariInfo.Piyasa || '-'"
label="Piyasa"
filled dense readonly
/>
</div>
<div class="col-3">
<q-input
:model-value="cariInfo.Ulke || '-'"
label="Ülke"
filled dense readonly
/>
</div>
</div>
</q-slide-transition>
<!-- 🔹 Save Toolbar -->
<div class="save-toolbar">
<div class="text-subtitle2 text-weight-bold">Sipariş Formu</div>
<div>
<q-btn
v-if="isViewOnly && canExportOrder"
label="🖨 SİPARİŞİ YAZDIR"
color="primary"
icon="print"
class="q-ml-sm"
@click="onPrintOrder"
/>
<q-btn
v-else-if="canSubmitOrder"
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
color="primary"
icon="save"
class="q-ml-sm"
:loading="orderStore.loading"
:disable="!canSubmitOrder"
@click="confirmAndSubmit"
/>
<q-btn
label="YENİ SİPARİŞ"
v-if="canWriteOrder"
color="secondary"
icon="add_circle"
class="q-ml-sm"
@click="onResetEditorClick"
:disable="isClosedRow || !canWriteOrder"
/>
</div>
</div>
<!-- 🔹 Grid Header -->
<div class="order-grid-header">
<div class="col-fixed model">MODEL</div>
<div class="col-fixed renk">RENK</div>
<div class="col-fixed ana">ÜRÜN ANA<br />GRUBU</div>
<div class="col-fixed alt">ÜRÜN ALT<br />GRUBU</div>
<div class="col-fixed aciklama-col">AÇIKLAMA</div>
<div class="beden-block">
<div
v-for="grp in (
Object.keys(orderStore?.schemaMap || {}).length
? Object.values(orderStore.schemaMap)
: Object.values(storeSchemaByKey)
)"
:key="grp.key"
class="grp-row"
:class="{ 'hl-pan': grp.key === 'pan' && highlightPantolon }"
>
<div class="grp-title">{{ grp.title }}</div>
<div class="grp-body">
<div
v-for="v in (grp.values || [])"
:key="'b-' + grp.key + '-' + v"
class="grp-cell hdr"
>
{{ v }}
</div>
</div>
</div>
</div>
<div class="total-row">
<div class="total-cell">ADET</div>
<div class="total-cell">FİYAT</div>
<div class="total-cell">PB</div>
<div class="total-cell">TUTAR</div>
<div class="total-cell">Tahmini Gönderim Tarihi</div>
</div>
</div>
</div>
<!-- =======================================================
🔹 GRID BODY (Final Stabil) + EDITOR aynı scrollda
======================================================== -->
<div class="order-scroll-y"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll -->
<div class="order-grid-body">
<template v-for="grp in groupedRows" :key="grp.name">
<div :class="['summary-group', grp.open ? 'open' : 'closed']">
<!-- 🟡 Sub-header -->
<div class="order-sub-header" @click="toggleGroup(grp.name)">
<div class="sub-left">{{ grp.name }}</div>
<div class="sub-center">
<div
v-for="v in (
orderStore.schemaMap?.[grp.grpKey]?.values
|| storeSchemaByKey?.[grp.grpKey]?.values
|| []
)"
:key="'hdr-' + grp.grpKey + '-' + v"
class="beden-cell"
>
{{ v }}
</div>
</div>
<div class="sub-right">
<div class="order-text-caption">
Toplam {{ grp.name }} Adet: {{ grp.toplamAdet }}
</div>
<div class="order-text-caption">
Toplam {{ grp.name }} Tutar:
{{ Number(grp.toplamTutar || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
{{ form.pb || aktifPB }}
</div>
<q-icon
:name="grp.open ? 'expand_less' : 'expand_more'"
size="20px"
class="cursor-pointer text-grey-8 q-ml-sm"
/>
</div>
</div>
<!-- 🧩 Grup satırları -->
<template v-if="grp.open">
<div
v-for="row in grp.rows"
:key="rowKey(row)"
class="summary-row"
:data-clientkey="row.clientKey"
:class="{
active: orderStore.editingKey === rowKey(row),
'is-editing': orderStore.editingKey === rowKey(row),
'row-closed': row.isClosed,
'row-error': row._error
}"
@click="!row.isClosed && !isViewOnly && editRow(row)"
>
<!-- 🔴 HATA İKONU (SADECE HATALI SATIRDA) -->
<q-icon
v-if="row._error"
name="error"
color="negative"
size="18px"
class="q-mr-sm row-error-icon"
>
<q-tooltip>
{{ row._error.message }}
</q-tooltip>
</q-icon>
<!-- Sol kolonlar -->
<div class="cell model">{{ row.model }}</div>
<div class="cell renk">
{{ row.renk }}{{ row.renk2 ? '-' + row.renk2 : '' }}
</div>
<div class="cell ana">{{ row.urunAnaGrubu }}</div>
<div class="cell alt">{{ row.urunAltGrubu }}</div>
<div class="cell aciklama">{{ row.aciklama }}</div>
<!-- Beden kolonları -->
<div class="grp-area">
<div class="grp-row">
<div
v-for="v in (
(orderStore.schemaMap?.[row.grpKey]?.values) ||
(storeSchemaByKey[row.grpKey]?.values) ||
(storeSchemaByKey.tak.values)
)"
:key="'val-' + v"
class="cell beden"
>
{{ resolveBedenValue(row.bedenMap, row.grpKey, v) }}
</div>
<div
v-for="i2 in (
16 -
(
(orderStore.schemaMap?.[row.grpKey]?.values?.length) ||
(storeSchemaByKey[row.grpKey]?.values?.length) ||
(storeSchemaByKey.tak.values.length)
)
)"
:key="'empty-' + i2"
class="cell beden ghost"
></div>
</div>
</div>
<!-- Sağ kolonlar -->
<div class="cell adet">{{ row.adet }}</div>
<div class="cell fiyat">{{ row.fiyat }}</div>
<div class="cell pb">{{ row.pb }}</div>
<div class="cell tutar">
{{ Number(row.tutar || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
</div>
<!-- 🗓 Termin Tarihi -->
<div class="cell termin">
<div class="termin-label text-center">
{{ formatDate(row.terminTarihi) }}
</div>
</div>
</div>
</template>
</div>
</template>
</div>
<!-- =======================================================
🔹 SATIR DÜZENLEYİCİ FORM (EDITOR)
======================================================== -->
<div class="editor q-mt-lg q-pa-sm">
<!-- 🔸 1. Satır: Model ve Ürün Bilgileri -->
<div class="row q-col-gutter-sm q-mb-sm">
<div class="col-3">
<!-- 🔹 Model Seçimi -->
<q-select
v-model="form.model"
:options="filteredModelOptions"
label="Model"
filled dense
use-input input-debounce="250"
emit-value map-options
option-value="value"
option-label="label"
clearable behavior="menu"
hint="Model kodu ile arayabilirsiniz"
:loading="loadingModels"
:disable=" isClosedRow||isViewOnly"
:readonly="isViewOnly"
@filter="filterModel"
@update:model-value="(val) => useComboWatcher('model', onModelChange)(val)"
/>
<!-- 🔹 1. Renk Seçimi -->
<div class="q-mt-sm">
<q-select
ref="renkSelect"
v-model="form.renk"
:options="renkOptions"
label="Renk"
filled dense clearable
emit-value map-options
option-value="value"
option-label="label"
:disable="isClosedRow||isViewOnly"
:readonly="isViewOnly"
@update:model-value="(val) => useComboWatcher('renk', onColorChange)(val)"
/>
</div>
<!-- 🔹 2. Renk Seçimi -->
<div class="q-mt-sm">
<q-select
ref="renk2Select"
v-model="form.renk2"
:options="renkOptions2"
label="2. Renk"
filled dense clearable
emit-value map-options
option-value="value"
option-label="label"
:disable="!renkOptions2.length || isEditing || isClosedRow"
@update:model-value="(val) => useComboWatcher('renk2', onColor2Change)(val)"
/>
</div>
</div>
<!-- Ürün teknik alanları -->
<div class="col-2">
<q-input v-model="form.urunAnaGrubu" label="Ürün Ana Grubu" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.urunAltGrubu" label="Alt Grup" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.fit" label="Fit" filled dense readonly />
</div>
<div class="col-2">
<q-input v-model="form.urunIcerik" label="İçerik" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.drop" label="Drop" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.askiliyan" label="ASKILI/YAN" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.kategori" label="Kategori" filled dense readonly />
</div>
</div>
<!-- 🔸 2. Satır: Seri Seçimi -->
<div class="row q-col-gutter-sm q-mt-xs">
<div class="col-3">
<q-select
ref="seriSelect"
v-show="Array.isArray(activeSeriesOptions) && activeSeriesOptions.length > 0"
v-model="selectedSeriSet"
:options="activeSeriesOptions"
label="Beden Seti Seç"
filled dense
emit-value map-options
option-value="value"
option-label="label"
:disable="isClosedRow"
/>
</div>
<div class="col-2 q-mt-sm">
<q-input
v-if="selectedSeriSet"
v-model.number="seriMultiplier"
type="number"
label="Çarpan"
min="1"
filled dense
:disable="isClosedRow"
/>
</div>
<div class="col-2 q-mt-sm">
<q-btn
v-if="selectedSeriSet && canMutateRows"
color="primary"
icon="add"
label="Seri Ekle"
@click="applySeriSet"
:disable="isClosedRow || isViewOnly || !canMutateRows"
:readonly="isViewOnly"
/>
</div>
</div>
<!-- =======================================================
🔹 BEDEN GİRİŞ ALANI + STOK ETİKETİ GÖRÜNÜMÜ
======================================================== -->
<div class="row q-mt-sm q-col-gutter-xs beden-grid">
<div
v-for="(lbl, i) in form.bedenLabels || []"
:key="'beden-'+i"
class="col-auto beden-wrap"
>
<div class="beden-label">{{ lbl }}</div>
<q-input
v-model.number="form.bedenler[i]"
dense outlined type="number" min="0"
style="width:60px"
@focus="activeBeden = i"
@blur="activeBeden = null"
@update:model-value="updateTotals(form)"
:class="{ 'beden-active': activeBeden === i }"
:disable="isClosedRow||isViewOnly"
:readonly="isViewOnly"
/>
<div
v-if="getStockFor(lbl) !== null"
class="stok-label text-caption text-center q-mt-xs"
:class="stockColorClass(getStockFor(lbl))"
>
Stok: {{ getStockFor(lbl) }}
</div>
</div>
</div>
<!-- 🔹 Aktif beden için küçük stok etiketi -->
<div
v-if="form.model && activeBeden !== null && getStockFor(form.bedenLabels[activeBeden]) !== null"
class="stok-label-sm"
:class="stockColorClass(getStockFor(form.bedenLabels[activeBeden]))"
>
Stok: {{ getStockFor(form.bedenLabels[activeBeden]) }}
</div>
<!-- =======================================================
🔹 ADET / FİYAT / PB / TUTAR
======================================================== -->
<div class="row q-mt-sm q-col-gutter-sm">
<div class="col-2">
<q-input
v-model.number="form.adet"
label="Adet"
dense
filled
readonly
:disable="isClosedRow"
/>
</div>
<div class="col-2">
<q-input
v-model.number="form.fiyat"
label="Fiyat"
dense
filled
type="number"
min="0"
@update:model-value="() => updateTotals(form)"
:disable="isClosedRow||isViewOnly"
:readonly="isViewOnly"
/>
</div>
<div class="col-2">
<q-select
v-model="form.pb"
:options="paraBirimOptions"
label="PB"
dense
filled
:disable="isClosedRow"
/>
</div>
<div class="col-3">
<q-input
v-model="form.tutar"
label="Tutar"
dense
filled
readonly
:disable="isClosedRow"
/>
</div>
</div>
<!-- =======================================================
🔹 SATIR BAZINDA TAHMİNİ TERMİN TARİHİ
======================================================== -->
<div class="row q-mt-sm">
<div class="col-4">
<q-input
v-model="form.terminTarihi"
type="date"
label="Tahmini Termin Tarihi"
filled
dense
:disable="isClosedRow"
/>
</div>
</div>
<!-- =======================================================
🔹 AÇIKLAMA ALANI
======================================================== -->
<div class="row q-mt-sm">
<div class="col-12">
<q-input
v-model="form.aciklama"
label="ıklama"
type="textarea"
filled
dense
autogrow
maxlength="1500"
counter
:disable="isClosedRow"
/>
</div>
</div>
<!-- =======================================================
🔹 BUTONLAR (Kaydet / Güncelle / Sil / Temizle)
======================================================== -->
<div class="row justify-between items-center q-mt-md">
<div class="row q-gutter-sm">
<q-btn
v-if="canMutateRows"
:color="isEditing ? 'positive' : 'primary'"
:label="isEditing ? 'Güncelle' : 'Kaydet'"
@click="onSaveOrUpdateRow"
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="isEditing && canMutateRows"
color="negative"
flat
label="Satırı Sil"
@click="removeSelected"
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="canMutateRows"
flat
color="grey-8"
label="Formu Temizle"
@click="onResetEditorClick"
:disable="isClosedRow||isViewOnly || !canMutateRows"
/>
</div>
</div>
<!-- =======================================================
🔹 ALT BİLGİLENDİRME ALANI
======================================================== -->
<div class="q-mt-md text-caption text-grey-7 text-center">
<q-icon name="info" size="16px" class="q-mr-xs" />
Bu sayfada yapılan siparişler henüz gönderilmemiştir.
<br />
<span class="text-negative">"Tümünü Kaydet (Toplu Gönder)"</span>
butonuna basarak işlemleri kaydedebilirsiniz.
</div>
<!-- =======================================================
🔹 SİPARİŞ GENEL AÇIKLAMASI
======================================================== -->
<div class="row q-mt-md">
<div class="col-12">
<q-input
v-model="form.Description"
type="textarea"
label="Sipariş Genel ıklaması"
filled
dense
autogrow
maxlength="1500"
counter
placeholder="Siparişe genel ıklama giriniz (örn. teslimat, üretim notu, müşteri isteği...)"
:disable="isClosedRow"
/>
</div>
</div>
</div> <!-- editor -->
</div> <!-- ✅ order-scroll-y -->
</q-page>
<q-page
v-else
class="order-page flex flex-center"
>
<div class="text-negative text-subtitle1">
Bu module erisim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
/* ===========================================================
🧩 ORDER ENTRY (v22Final) — Setup Başlangıcı
=========================================================== */
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick, toRaw } from 'vue'
import { useQuasar } from 'quasar'
import { useRoute, useRouter, onBeforeRouteLeave} from 'vue-router'
import { useOrderEntryStore,schemaByKey as storeSchemaByKey,detectBedenGroup} from 'src/stores/orderentryStore'
import dayjs from 'dayjs'
import api from 'src/services/api.js'
import { useAuthStore } from 'src/stores/authStore'
import { formatDateInput, formatDateDisplay } from 'src/utils/formatters'
import { usePermission } from 'src/composables/usePermission'
const { canRead, canWrite, canUpdate, canExport } = usePermission()
const canReadOrder = canRead('order')
const canWriteOrder = canWrite('order')
const canUpdateOrder = canUpdate('order')
const canExportOrder = canExport('order')
// script setup içinde
const formatDate = formatDateDisplay
/* ===========================================================
🔹 GLOBAL TANIMLAR VE ROUTE BİLGİLERİ
=========================================================== */
const $q = useQuasar()
const orderStore = useOrderEntryStore()
const orderentryStore = useOrderEntryStore()
orderStore.initSchemaMap()
const route = useRoute()
const router = useRouter()
const isClosedOrder = computed(() => !!orderStore.hasClosedLines)
// 🔹 Param: Artık sadece :orderHeaderID kullanıyoruz
const orderHeaderID = computed(() => route.params.orderHeaderID || null)
console.log('🧩 Route parametresi alındı (orderHeaderID):', orderHeaderID.value)
const routeMode = computed(() => resolveMode())
// ✅ Pinia store: siparişler, localStorage, API çağrıları
const auth = useAuthStore()
const isViewOnly = computed(() => orderStore.mode === 'view')
console.log('🧩 Route parametresi alındı (setup başında):', orderHeaderID.value)
// 🔹 Genel reaktif değişkenler
const aktifPB = ref('USD') // Varsayılan para birimi (Cari seçimiyle değişebilir)
// 🔹 Model detayları cache (product-detail API verilerini tutar)
const productCache = reactive({})
const confirmAndSubmit = async () => {
if (orderStore.loading) return
if (!hasSubmitPermission()) {
notifyNoPermission(
isEditMode.value
? 'Siparis guncelleme yetkiniz yok'
: 'Siparis kaydetme yetkiniz yok'
)
return
}
// Grid boşsa
if (!orderStore.summaryRows?.length) {
$q.notify({
type: 'warning',
message: 'Kaydedilecek satır yok'
})
return
}
try {
// NEW veya EDIT ayrımı store.mode üzerinden
await orderStore.submitAllReal(
$q,
router,
form,
summaryRows,
productCache
)
} catch (err) {
console.error('❌ confirmAndSubmit hata:', err)
}
}
/* ===========================================================
🗓️ SİPARİŞ TARİHLERİ — Varsayılan Değerler
Oluşturulma tarihi = bugünün tarihi
Tahmini termin tarihi = bugünden + 5 hafta (35 gün)
=========================================================== */
const today = new Date()
const Termindate = new Date(today)
Termindate.setDate(Termindate.getDate() + 35) // +5 hafta
const defaultOlusturmaTarihi = today.toISOString().substring(0, 10)
const defaultTerminTarihi = Termindate.toISOString().substring(0, 10)
const isEditMode = computed(() => orderStore.mode === 'edit')
const canSubmitOrder = computed(() => {
if (isViewOnly.value) return false
return isEditMode.value ? canUpdateOrder.value : canWriteOrder.value
})
const canMutateRows = computed(() => {
if (isViewOnly.value) return false
return isEditMode.value ? canUpdateOrder.value : canWriteOrder.value
})
function notifyNoPermission(message) {
$q.notify({
type: 'negative',
message
})
}
function hasSubmitPermission() {
if (isViewOnly.value) return false
return isEditMode.value ? canUpdateOrder.value : canWriteOrder.value
}
function hasRowMutationPermission() {
if (isViewOnly.value) return false
return isEditMode.value ? canUpdateOrder.value : canWriteOrder.value
}
function onPrintOrder() {
if (!canExportOrder.value) {
notifyNoPermission('Siparisi yazdirma yetkiniz yok')
return
}
orderStore.downloadOrderPdf()
}
async function onResetEditorClick() {
if (!canWriteOrder.value) {
notifyNoPermission('Yeni siparis baslatma yetkiniz yok')
return
}
await resetEditor()
}
/* ===========================================================
🔹 FORM NESNESİ — TEMEL ALANLAR
=========================================================== */
const form = reactive({
// ----------------------------------------------------------
// 🔸 TEMEL ALANLAR
// ----------------------------------------------------------
OrderHeaderID: '', // string (GUID)
OrderTypeCode: 1, // int8
ProcessCode: 'WS', // string
OrderNumber: '', // string
OrderTime: dayjs().format('HH:mm:ss'),
IsCancelOrder: false,
// ----------------------------------------------------------
// 🔸 ADRES / REFERANS
// ----------------------------------------------------------
BillingPostalAddressID: '',
GuarantorContactID: '',
ApplicationCode: '',
ApplicationID: '',
// ----------------------------------------------------------
// 🔸 TARİH / AÇIKLAMA
// ----------------------------------------------------------
OrderDate: dayjs().format('YYYY-MM-DD'),
AverageDueDate: dayjs().add(30, 'day').format('YYYY-MM-DD'),
Description: '',
InternalDescription: '',
// ----------------------------------------------------------
// 🔸 CARİ BİLGİLERİ
// ----------------------------------------------------------
CurrAccTypeCode: 1,
CurrAccCode: '',
CurrAccDescription: '',
// ----------------------------------------------------------
// 🔸 PARA BİRİMİ
// ----------------------------------------------------------
DocCurrencyCode: 'USD',
LocalCurrencyCode: 'TRY',
ExchangeRate: 1,
// ----------------------------------------------------------
// 🔸 DURUM ALANLARI
// ----------------------------------------------------------
IsCreditSale: true,
IsCreditableConfirmed: false,
IsSalesViaInternet: false,
IsSuspended: false,
IsCompleted: false,
IsPrinted: false,
IsLocked: false,
IsClosed: false,
// ----------------------------------------------------------
// 🔸 KULLANICI VE TARİH
// ----------------------------------------------------------
CreatedUserName: '',
CreatedDate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
LastUpdatedUserName: '',
LastUpdatedDate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
CreditableConfirmedUser: '',
CreditableConfirmedDate: '',
// ----------------------------------------------------------
// 🔸 SABİT / EK ALANLAR
// ----------------------------------------------------------
DocumentNumber: '',
PaymentTerm: '',
SubCurrAccID: '',
ShipmentMethodCode: '',
ContactID: '',
ShippingPostalAddressID: '',
GuarantorContactID2: '',
RoundsmanCode: '',
DeliveryCompanyCode: '',
TaxTypeCode: '',
WithHoldingTaxTypeCode: '',
DOVCode: '',
TaxExemptionCode: 0,
CompanyCode: 1,
OfficeCode: 101,
StoreTypeCode: 5,
StoreCode: 0,
POSTerminalID: 0,
WarehouseCode: '1-0-12',
ToWarehouseCode: '',
OrdererCompanyCode: 1,
OrdererOfficeCode: 101,
OrdererStoreCode: '',
GLTypeCode: '',
TDisRate1: 0,
TDisRate2: 0,
TDisRate3: 0,
TDisRate4: 0,
TDisRate5: 0,
DiscountReasonCode: 0,
SurplusOrderQtyToleranceRate: 0,
ImportFileNumber: '',
ExportFileNumber: '',
IncotermCode1: '',
IncotermCode2: '',
LettersOfCreditNumber: '',
PaymentMethodCode: '',
IsIncludedVat: 0,
UserLocked: 0,
IsProposalBased: 0,
// ----------------------------------------------------------
// 🔸 ÜRÜN / RENK / BEDEN
// ----------------------------------------------------------
model: '',
renk: '',
renk2: '',
urunAnaGrubu: '',
urunAltGrubu: '',
fit: '',
urunIcerik: '',
drop: '',
kategori: '',
askiliyan: '',
seri: '',
bedenLabels: [],
bedenler: [],
adet: 0,
fiyat: 0,
pb: aktifPB.value,
tutar: 0,
aciklama: '',
minFiyat: 0,
kur: 1,
minFiyatTRY: 0,
// ----------------------------------------------------------
// 🔸 TARİHLER
// ----------------------------------------------------------
olusturmaTarihi: defaultOlusturmaTarihi,
tahminiTerminTarihi: defaultTerminTarihi,
terminTarihi: defaultTerminTarihi,
includeVat: false, // 🔹 KDV dahil mi (q-toggle)
vatRate: 10, // 🔹 yüzde (gösterge + manuel değiştirilebilir)
subtotal: 0, // 🔹 KDV hariç tutar
vatAmount: 0, // 🔹 hesaplanan KDV tutarı
totalWithVat: 0 , // 🔹 toplam (KDV dahil)
vatAmountInput: '', // 🟢 KDV manuel giriş buffer
})
/* ===========================================================
🔹 REFS & COMPUTEDS (Grid + Edit Mode)
=========================================================== */
const summaryRows = computed(() => orderStore.summaryRows)
// ✅ Tek kaynak: editingKey
const isEditing = computed(() => !!orderStore.editingKey)
const rowKey = (row) => row?.clientKey || row?.id || row?.OrderLineID
const activeBeden = ref(null)
const isClosedRow = computed(() => {
const row = selectedRow.value
return row?.isClosed === true
})
const selectedCari = ref(null)
/* ===========================================================
🔹 EDITOR SEÇİMLERİ — Seri / Renk / Kategori setleri
=========================================================== */
const selectedSeriSet = ref(null)
const seriMultiplier = ref(1)
/** ----------------------------------------------------------
* REQUIRED REFS (Mutlaka Tanımlı Olmalı)
* ---------------------------------------------------------- */
/* ===========================================================
🔹 CARI / MODEL / RENK YÜKLEYİCİ REFLERİ — DOĞRU SIRA
=========================================================== */
const loadingHeader = ref(true)
const loadingCari = ref(true)
const loadingModels = ref(true)
/* ===========================================================
🔹 CARİ INFO STATE
=========================================================== */
const cariInfo = ref(null)
// Cari listeleri
const cariOptions = ref([])
const filteredCariOptions = ref([])
// Model listeleri
const modelOptions = ref([])
const filteredModelOptions = ref([])
// RENK SELECTLER — 🔥 MUTLAKA BURADA OLMALI
const renkSelect = ref(null) // <= BUNLAR EKSİKSE hata alırsın
const renk2Select = ref(null)
const renkOptions = ref([])
const renkOptions2 = ref([])
// Mode senkronu
orderStore.mode = routeMode.value
function resolveBedenValue(bedenMap, grpKey, v) {
if (!bedenMap || !grpKey) return ''
const map = bedenMap[grpKey]
if (!map) return ''
// 🔴 AKSBİR / boş beden KESİNLİKLE normalize edilmez
if (v === ' ') {
return map[' '] ?? ''
}
// 🔹 Diğer bedenler normal akış
return map[v] ?? ''
}
async function resetEditor(force = false) {
console.log('🧹 resetEditor', { force, editingKey: orderStore.editingKey })
// 🔒 Edit varken reset yok
if (!force && orderStore.editingKey) {
console.log('⛔ resetEditor iptal (edit mode)')
return
}
// ============================
// 🔓 EDIT STATE RESET
// ============================
orderStore.editingKey = null
orderStore.selected = null
// ============================
// 🧼 FORM — TAM TEMİZ
// ============================
Object.assign(form, {
model: '',
renk: '',
renk2: '',
urunAnaGrubu: '',
urunAltGrubu: '',
kategori: '',
aciklama: '',
fit: '',
urunIcerik: '',
drop: '',
askiliyan: '',
adet: 0,
fiyat: 0,
tutar: 0,
// ❌ BEDEN ŞEMASI TAMAMEN SIFIR
grpKey: null,
bedenLabels: [],
bedenler: []
})
// ============================
// 🧹 UI STATE TEMİZLİĞİ
// ============================
selectedSeriSet.value = null
seriMultiplier.value = 1
bedenStock.value = []
stockMap.value = {}
renkOptions.value = []
renkOptions2.value = []
await nextTick()
console.log('✅ resetEditor tamamlandı (BEDEN ŞEMASI YOK)')
}
/* ===========================================================
🔴 ROW ERROR — COMPONENT SCOPE (FIXED)
- summaryRows computed yazımı yok
- tek kaynak: store action
=========================================================== */
function applyRowError(err) {
const key = err?.clientKey
if (!key) return
// ✅ store üzerinden set et (tek kaynak)
if (typeof orderStore.setRowErrorByClientKey === 'function') {
orderStore.setRowErrorByClientKey(key, {
code: err?.code,
message: err?.message
})
} else {
// fallback (action yoksa) — yine de computed'a yazmıyoruz, store array mutate ediyoruz
const row = orderStore.summaryRows?.find(r => r?.clientKey === key)
if (row) {
row._error = { code: err?.code, message: err?.message }
}
}
scrollToRow(key)
}
/* ===========================================================
🔹 applyTerminToRows (FIXED)
- store.summaryRows reassign YOK
- tek kaynak: store action
=========================================================== */
function applyTerminToRows(dateStr) {
if (!dateStr) return
if (typeof orderStore.applyTerminToRowsIfEmpty === 'function') {
orderStore.applyTerminToRowsIfEmpty(dateStr)
return
}
// fallback (action yoksa) — reassign yok, sadece mutate
const rows = orderStore.summaryRows
if (!Array.isArray(rows)) return
for (const r of rows) {
if (!r?.terminTarihi || r.terminTarihi === '') {
r.terminTarihi = dateStr
}
}
}
/* ===========================================================
✅ selectedRow (FIXED)
- editingIndex yok
- tek kaynak: orderStore.editingKey
=========================================================== */
const selectedRow = computed(() => {
const key = orderStore.editingKey
if (!key) return null
const rows = orderStore.summaryRows
if (!Array.isArray(rows)) return null
// 🔑 store.getRowKey varsa onu kullan
if (typeof orderStore.getRowKey === 'function') {
return rows.find(r => orderStore.getRowKey(r) === key) || null
}
// fallback: clientKey || OrderLineID
return rows.find(r => (r?.clientKey || r?.OrderLineID) === key) || null
})
/* ===========================================================
🧩 GROUPED ROWS — FIXED (bedenValues bug fixed)
- schemaMap tek kaynak
- en geniş beden seti kazanır
- aksbir özel kural korunur
=========================================================== */
/* ===========================================================
🧩 GROUPED ROWS — FINAL & SAFE
-----------------------------------------------------------
✔ grpKey SADECE row.grpKey
✔ schemaMap tek kaynak
✔ detectBedenGroup YOK
=========================================================== */
const groupOpen = reactive({})
const groupedRows = computed(() => {
const rows = Array.isArray(summaryRows.value) ? summaryRows.value : []
const buckets = {}
const order = []
const schemaMap =
orderStore.schemaMap && typeof orderStore.schemaMap === 'object'
? orderStore.schemaMap
: storeSchemaByKey
for (const row of rows) {
const ana = (row?.urunAnaGrubu || 'GENEL')
.toUpperCase()
.trim()
if (!buckets[ana]) {
buckets[ana] = {
name: ana,
rows: [],
toplamAdet: 0,
toplamTutar: 0,
open: groupOpen[ana] ?? true,
// 🔑 TEK KAYNAK
grpKey: row.grpKey
}
order.push(ana)
}
const bucket = buckets[ana]
bucket.rows.push(row)
bucket.toplamAdet += Number(row.adet || 0)
bucket.toplamTutar += Number(row.tutar || 0)
}
return order.map(name => {
const grp = buckets[name]
const schema = schemaMap?.[grp.grpKey]
return {
...grp,
bedenValues: schema?.values || []
}
})
})
/* ===========================================================
✏️ GRID SATIR DÜZENLEME — editRow (Final v4 — IsClosed Safe)
-----------------------------------------------------------
- Kapalı satır (row.isClosed === true) → düzenlemeye izin yok
- UIda row.isClosed class ile gri görünür
- Kullanıcı tıklasa bile edit mode açılmaz
=========================================================== */
function toDateOnly(v) {
if (!v) return ''
// '2025-10-27 00:00:00' → '2025-10-27'
if (typeof v === 'string' && v.includes(' ')) {
return v.split(' ')[0]
}
return v
}
// 🧮 KDV Hesaplama Fonksiyonları
/* ===========================================================
🧮 KDV HESAPLAMA — FINAL (X3)
- Tek kaynak: orderStore.totalAmount
- Tek hesap fonksiyonu: recalcVat
=========================================================== */
const subtotal = computed(() => Number(orderStore.totalAmount || 0))
/* ===========================================================
🔹 KDV TOGGLE HANDLER (SAFE)
=========================================================== */
const onVatToggle = (val) => {
form.includeVat = !!val
recalcVat()
}
/* ===========================================================
🧮 recalcVat — FINAL
- Tek kaynak: orderStore.totalAmount
- Manuel KDV girişini destekler
=========================================================== */
function recalcVat() {
const baseTotal = Number(orderStore.totalAmount || 0)
const rate = Number(form.vatRate || 0) / 100
// 🔹 KDV dahil değilse
if (!form.includeVat) {
form.subtotal = baseTotal
form.vatAmount = 0
form.vatAmountInput = ''
form.totalWithVat = baseTotal
return
}
/* ---------------------------------------------------------
🔹 MANUEL KDV VAR MI?
--------------------------------------------------------- */
let vatAmt = 0
if (form.vatAmountInput !== '' && form.vatAmountInput != null) {
vatAmt = Number(
String(form.vatAmountInput).replace(',', '.')
)
vatAmt = isNaN(vatAmt) ? 0 : vatAmt
} else {
vatAmt = Number((baseTotal * rate).toFixed(2))
}
const totalWithVat = Number((baseTotal + vatAmt).toFixed(2))
form.subtotal = baseTotal
form.vatAmount = vatAmt
form.totalWithVat = totalWithVat
}
// Kullanıcı KDV tutarını manuel girdiyse true
const vatManualMode = ref(false)
/* -----------------------------------------------------------
Yardımcılar
----------------------------------------------------------- */
function toNumberTR(val) {
const cleaned = String(val ?? '').replace(',', '.').trim()
const n = parseFloat(cleaned)
return isNaN(n) ? 0 : n
}
function clampRate(val) {
const n = Number(val)
if (isNaN(n) || n < 0) return 0
if (n > 100) return 100
return n
}
/* ===========================================================
✅ ROUTE FLOW — SINGLE SOURCE OF TRUTH (FINAL)
=========================================================== */
/* -------------------- MODE HELPERS -------------------- */
function isInvalidId(id) {
return !id || ["new", "0", "null", "undefined"].includes(id)
}
function isGuid(id) {
return typeof id === "string" &&
/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(id)
}
function resolveMode() {
const qm = String(route.query.mode || "").toLowerCase()
const id = String(orderHeaderID.value || "")
if (["edit", "view", "new"].includes(qm)) return qm
if (!isInvalidId(id) && isGuid(id)) return "edit"
return "new"
}
/* -------------------- INTERNAL STATE -------------------- */
const routeBusy = ref(false)
const lastRouteSignature = ref("")
let autosaveTimer = null
let beforeUnloadHandler = null
let resizeHandler = null
/* ===========================================================
🚫 BEFOREUNLOAD (Browser Close / Refresh)
-----------------------------------------------------------
✔ NEW → taslak KORUNUR, uyarı var
✔ EDIT → snapshot KORUNUR, uyarı var
✔ VIEW → ASLA uyarı yok
✔ isControlledSubmit → ASLA uyarı yok
✔ allowRouteLeaveOnce → ASLA uyarı yok
❌ burada snapshot SİLİNMEZ (kritik)
=========================================================== */
function installBeforeUnloadGuard() {
clearBeforeUnload()
// 👁 VIEW MODE → hiç guard kurma
if (orderStore.mode === 'view') return
beforeUnloadHandler = (e) => {
/* -------------------------------------------------------
🔒 Kontrollü submit (kaydet / gönder)
→ browser close uyarısı YOK
-------------------------------------------------------- */
if (orderStore.isControlledSubmit) return
/* -------------------------------------------------------
✅ Programatik yönlendirme sonrası 1 kere bypass
(listeye dön, replace vs.)
-------------------------------------------------------- */
if (orderStore.allowRouteLeaveOnce) return
/* -------------------------------------------------------
💾 Değişiklik yok → sessiz çık
-------------------------------------------------------- */
if (!orderStore.hasUnsavedChanges) return
/* -------------------------------------------------------
⚠️ UYARI (NEW + EDIT için ortak)
❗ Snapshot SİLME YOK
❗ LocalStorage'a DOKUNMA YOK
-------------------------------------------------------- */
e.preventDefault()
e.returnValue = ''
}
window.addEventListener('beforeunload', beforeUnloadHandler)
}
/* -------------------- CLEANUP -------------------- */
function clearAutosave() {
if (autosaveTimer) {
clearInterval(autosaveTimer)
autosaveTimer = null
}
}
function clearBeforeUnload() {
if (beforeUnloadHandler) {
window.removeEventListener("beforeunload", beforeUnloadHandler)
beforeUnloadHandler = null
}
}
function installGuards() {
clearBeforeUnload()
clearAutosave()
// 👁 View ise hiçbir guard yok
if (orderStore.mode === 'view') return
/* ---------------- BEFOREUNLOAD ---------------- */
installBeforeUnloadGuard()
/* ---------------- AUTOSAVE ---------------- */
autosaveTimer = setInterval(() => {
orderStore.persistLocalStorage?.()
}, 30000)
}
/* -------------------- MAIN ROUTE ENGINE -------------------- */
/* -------------------- MAIN ROUTE ENGINE -------------------- */
async function runRouteFlow() {
const id = String(orderHeaderID.value || "")
const mode = resolveMode()
/* =======================================================
🔥 NEW MODE — signature BYPASS (KRİTİK)
======================================================= */
if (mode === 'new') {
lastRouteSignature.value = ''
}
const sig = `${mode}:${id}:${route.query.source || ""}`
if (routeBusy.value || lastRouteSignature.value === sig) return
lastRouteSignature.value = sig
routeBusy.value = true
loadingHeader.value = true
try {
orderStore.mode = mode
// Ortak lookuplar
if (!cariOptions.value.length) await loadCariList($q)
if (!modelOptions.value.length) await loadModels($q)
/* =======================================================
🟢 NEW MODE — FINAL & SAFE
-------------------------------------------------------
✔ route param boş / "new" ise → aktif NEW headera fix
✔ draft varsa → hydrate
✔ draft yoksa → startNewOrder (LOCAL UUID üretir)
✔ URL her zaman GERÇEK OrderHeaderID taşır
======================================================= */
if (mode === "new") {
const routeId = String(orderHeaderID.value || "")
const activeId = orderStore.getActiveNewHeaderId?.()
/* 1⃣ Route ID FIX (kritik) */
if (!routeId || routeId === "new") {
if (activeId) {
orderStore.allowRouteLeaveOnce = true
await router.replace({
name: "order-entry",
params: { orderHeaderID: activeId },
query: {
...route.query,
mode: "new",
source: route.query.source || "local"
}
})
return
}
}
/* 2⃣ EDIT snapshot temizle */
orderStore.clearEditSnapshotIfExists?.()
/* 3⃣ Draft hydrate → yoksa YENİ oluştur */
const resumed = orderStore.hydrateFromLocalStorageIfExists?.()
if (!resumed) {
const header = await orderStore.startNewOrder({ $q, form, productCache })
const newId = header?.OrderHeaderID
if (newId && newId !== routeId) {
orderStore.allowRouteLeaveOnce = true
await router.replace({
name: "order-entry",
params: { orderHeaderID: newId },
query: { mode: "new", source: "new" }
})
return
}
}
/* 4⃣ Form sync */
if (orderStore.header) {
Object.assign(form, orderStore.header)
syncCurrencyFromHeader()
}
return
}
/* =======================================================
🔵 EDIT / 👁 VIEW MODE
======================================================= */
if (isInvalidId(id)) {
await router.replace({ name: "order-list" })
return
}
let ok = false
try {
ok = await orderStore.openExistingForEdit(id, {
$q,
form,
productCache
})
} catch {}
if (!ok) {
$q.notify({ type: "negative", message: "Sipariş ılamadı" })
await router.replace({ name: "order-list" })
return
}
if (orderStore.header) {
Object.assign(form, orderStore.header)
syncCurrencyFromHeader()
}
} finally {
/* =======================================================
🔒 GUARDS — TEK MERKEZ
======================================================= */
installGuards()
loadingHeader.value = false
routeBusy.value = false
}
}
/* -------------------- ROUTE WATCH -------------------- */
watch(
() => [orderHeaderID.value, route.query.mode, route.query.source],
runRouteFlow,
{ immediate: true }
)
/* -------------------- SIGNAL WATCHERS -------------------- */
watch(() => orderStore.newOrderSignal, async (v) => {
if (!v) return
// NEW headerı üret (persistLocalStorage içinde draft + activeNewHeader yazılacak)
const header = await orderStore.startNewOrder({ $q, form, productCache })
const id = header?.OrderHeaderID || orderStore.getActiveNewHeaderId?.()
if (!id) return
await router.replace({
name: "order-entry",
params: { orderHeaderID: id },
query: { mode: "new", source: "new" }
})
})
watch(() => orderStore.replaceRouteSignal, async (id) => {
if (!id) return
await router.replace({
name: "order-entry",
params: { orderHeaderID: id },
query: { mode: "edit", source: "backend" }
})
})
/* -------------------- LIFECYCLE -------------------- */
onMounted(async () => {
await nextTick()
/* ---------------- UI ---------------- */
updateStickyVars()
measureHeaderGap()
resizeHandler = () => updateStickyVars()
window.addEventListener('resize', resizeHandler)
/* ------------- HYDRATE DECISION ------------- */
const mode = route.query.mode || 'new'
const source = route.query.source || ''
const id = orderHeaderID.value
console.log('🧩 hydrate decision', { mode, source, id })
if (mode === 'new' && source === 'draft' && id) {
await orderentryStore.hydrateFromLocalStorage(id)
return
}
if (mode === 'edit' && id) {
await orderentryStore.hydrateFromLocalStorage(id)
return
}
await orderentryStore.startNewOrder({ $q })
})
onUnmounted(() => {
if (resizeHandler) window.removeEventListener("resize", resizeHandler)
clearAutosave()
clearBeforeUnload()
})
/* ===========================================================
🚫 ROUTE LEAVE GUARD — FINAL (NEW DRAFT GUARANTEE)
-----------------------------------------------------------
✔ View mode → asla bloklama yok
✔ isControlledSubmit → sessiz geç
✔ allowRouteLeaveOnce → 1 kez bypass
✔ hasUnsavedChanges=false → sessiz geç
✔ EDIT mode → onayda edit snapshot temizlenir (+ optional reset)
✔ NEW mode → onayda hiçbir şey silinmez
✔ NEW mode → onayla çıkmadan önce DRAFT ZORLA PERSIST edilir
=========================================================== */
onBeforeRouteLeave((to, from, next) => {
// 1) Kontrollü submit (kaydet / gönder akışı) → guard devre dışı
if (orderStore.isControlledSubmit) {
next()
return
}
// 2) Programatik geçiş → 1 kere bypass
if (orderStore.allowRouteLeaveOnce) {
orderStore.allowRouteLeaveOnce = false
next()
return
}
// 3) VIEW → serbest
if (orderStore.mode === 'view') {
next()
return
}
// 4) Değişiklik yok → serbest
if (!orderStore.hasUnsavedChanges) {
next()
return
}
// 5) Kullanıcı onayı
$q.dialog({
title: 'Sayfadan ayrılıyorsunuz',
message:
orderStore.mode === 'edit'
? 'Değişiklikler kaybolacak. Devam edilsin mi?'
: 'Taslak korunacak. Sayfadan çıkmak istiyor musunuz?',
ok: { label: 'Evet', color: 'negative' },
cancel: { label: 'Hayır' },
persistent: true
})
.onOk(() => {
/* ===================================================
✅ NEW MODE — DRAFT GARANTİSİ
---------------------------------------------------
Amaç: gatewaye dönünce taslak kartı %100 görünsün.
Çözüm: çıkmadan hemen önce snapshotı tek kaynağa yaz.
=================================================== */
if (orderStore.mode === 'new') {
try {
// NEW taslak tek kaynağa yazılmalı: orderStore.getDraftKey
// persistLocalStorage bununla uyumlu olmalı.
orderStore.persistLocalStorage?.()
} catch (e) {
// persist başarısız olsa bile kullanıcı çıkmayı onayladıysa çıkışa engel olmayalım
console.warn('⚠️ NEW draft persist edilemedi (route leave):', e)
}
next()
return
}
/* ===================================================
🔥 EDIT MODE — TEMİZLİK
---------------------------------------------------
✔ local edit snapshot silinir
✔ (opsiyonel) state reset
=================================================== */
if (orderStore.mode === 'edit') {
try {
orderStore.clearEditSnapshotIfExists?.()
} catch (e) {
console.warn('⚠️ edit snapshot temizlenemedi:', e)
}
next()
return
}
// diğer modlar için default
next()
})
.onCancel(() => next(false))
})
// -----------------------------------------------------------
// 🔸 Cari Listesi
// -----------------------------------------------------------
// =======================================================
// 📦 Cari Listesini Yükle (Sadece new modda çağrılır)
// =======================================================
async function loadCariList($q) {
loadingCari.value = true
try {
const res = await api.get('/customer-list')
const data = res?.data
if (Array.isArray(data)) {
cariOptions.value = data
} else if (Array.isArray(data?.data)) {
cariOptions.value = data.data
} else {
cariOptions.value = []
}
filteredCariOptions.value = [...cariOptions.value]
console.log(`🧾 Cari listesi yüklendi: ${cariOptions.value.length} kayıt.`)
} catch (err) {
console.error('❌ Cari listesi alınamadı:', err)
$q.notify({
type: 'negative',
message: 'Cari listesi yüklenemedi ❌',
position: 'top'
})
} finally {
loadingCari.value = false
}
}
/* ===========================================================
🔍 Cari Arama Filtresi (QSelect @filter)
=========================================================== */
function filterCari(val, update) {
if (!val) {
update(() => {
filteredCariOptions.value = [...cariOptions.value]
})
return
}
const needle = val.toLowerCase()
update(() => {
filteredCariOptions.value = cariOptions.value.filter(opt => {
const kod = (opt.Cari_Kod || '').toLowerCase()
const ad = (opt.Cari_Ad || '').toLowerCase()
const unvan = (opt.Unvan || '').toLowerCase()
return `${kod} ${ad} ${unvan}`.includes(needle)
})
})
}
const highlightPantolon = computed(() =>
(summaryRows.value || []).some(r =>
(r.urunAnaGrubu || '').toLowerCase().includes('pantolon')
)
)
// -----------------------------------------------------------
// 🔸 Model Listesi
// -----------------------------------------------------------
async function loadModels($q) {
loadingModels.value = true
try {
const res = await api.get('/products')
const arr = res?.data || []
modelOptions.value = arr.map(x => ({
label: x.ProductCode,
value: x.ProductCode
}))
filteredModelOptions.value = modelOptions.value
console.log('✅ Model listesi yüklendi:', modelOptions.value.length)
} catch (err) {
console.error('❌ Model listesi alınamadı:', err)
$q.notify({
type: 'negative',
message: 'Model listesi alınamadı ❌',
position: 'top-right'
})
} finally {
loadingModels.value = false
}
}
function syncCurrencyFromHeader() {
const hdrPB = orderStore.header?.DocCurrencyCode || orderStore.header?.CurrencyCode
if (!hdrPB) return
form.pb = hdrPB
form.DocCurrencyCode = hdrPB
orderStore.setHeaderFields?.(
{ DocCurrencyCode: hdrPB, PriceCurrencyCode: hdrPB },
{ applyCurrencyToLines: true, immediatePersist: false }
)
}
/* ===========================================================
🔹 MODEL + PB Bazlı Minimum Fiyat
=========================================================== */
async function fetchMinPrice() {
if (!form.model || !form.pb) return
try {
const res = await api.get('/min-price', {
params: { model: form.model, currency: form.pb }
})
const data = res.data
form.minFiyat = Number(data.price || 0)
form.kur = Number(data.rateToTRY || 1)
form.minFiyatTRY = Number(data.priceTRY || 0)
console.log(`💰 Min Fiyat: ${form.minFiyat} ${form.pb} (${form.minFiyatTRY} TRY)`)
} catch (err) {
console.error('❌ Min fiyat alınamadı:', err)
form.minFiyat = 0
}
}
/* ===========================================================
🔽 SCROLL TO ROW — DOM SAFE
=========================================================== */
function scrollToRow(clientKey) {
requestAnimationFrame(() => {
const el = document.querySelector(
`[data-client-key="${clientKey}"]`
)
if (!el) return
el.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
el.classList.add('row-error-flash')
setTimeout(() => {
el.classList.remove('row-error-flash')
}, 1500)
})
}
/* ===========================================================
🔹 SERİ SETİ UYGULAMA (FINAL — grpKey SAFE)
=========================================================== */
function applySeriSet() {
if (!hasRowMutationPermission()) {
notifyNoPermission(
isEditMode.value
? 'Siparis satiri guncelleme yetkiniz yok'
: 'Siparis satiri ekleme yetkiniz yok'
)
return
}
if (!selectedSeriSet.value) return
/* 🔑 TEK KAYNAK */
const grpKey = activeGrpKey.value
if (!grpKey) {
console.warn('⚠️ applySeriSet: grpKey bulunamadı')
return
}
const setKey =
typeof selectedSeriSet.value === 'object'
? selectedSeriSet.value.value
: selectedSeriSet.value
const pattern = seriMatrix?.[grpKey]?.[setKey]
if (!pattern) {
console.warn(`⚠️ Seri seti bulunamadı → grpKey:${grpKey}, set:${setKey}`)
return
}
const mult = Number(seriMultiplier.value) || 1
/* =======================================================
🔹 BEDENLER — LABEL BAZLI
======================================================= */
form.bedenler = form.bedenLabels.map((lbl, i) => {
const base = Number(form.bedenler?.[i] || 0)
const inc = Number(pattern[lbl] || 0) * mult
return base + inc
})
updateTotals(form)
$q.notify({
type: 'positive',
message: `Seri "${setKey}" uygulandı (${grpKey})`,
position: 'top-right'
})
}
/* ===========================================================
🔹 TOPLAM HESAPLAMA
=========================================================== */
function updateTotals(f) {
f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
const fiyat = Number(f.fiyat) || 0
f.tutar = Number((f.adet * fiyat).toFixed(2))
}
function removeSelected() {
if (!hasRowMutationPermission()) {
notifyNoPermission('Siparis satiri silme/guncelleme yetkiniz yok')
return
}
const row = selectedRow.value
if (!row) {
$q.notify({ type: 'warning', message: 'Silmek için önce bir satır seçmelisiniz.' })
return
}
// 🔒 Kapalı satır koruması
if (row.isClosed === true) {
$q.notify({ type: 'warning', message: 'Kapalı satır silinemez.', position: 'top-right' })
return
}
$q.dialog({
title: 'Satırı Sil',
message: `<b>${row.model} / ${row.renk}</b> satırı silinsin mi?`,
html: true,
ok: { label: 'Sil', color: 'negative' },
cancel: { label: 'Vazgeç', flat: true }
}).onOk(() => {
orderStore.removeRowInternal(row)
// ✅ edit state temizle
orderStore.editingKey = null
orderStore.selected = null
resetEditor()
$q.notify({
type: 'positive',
message: 'Satır silindi (DELETE ops oluşturuldu)',
position: 'top-right'
})
})
}
// ===========================================================
// ✅ hydrateEditorFromRow — FINAL FIX (EDITOR BEDEN MAP)
// ===========================================================
async function hydrateEditorFromRow(row, opts = {}) {
const {
allowClosed = false,
notify = true,
message = 'Düzenleme moduna alındı',
loadSizes = true,
source = 'hydrate'
} = opts
if (!row) return false
/* -------------------------------------------------------
🔒 KAPALI SATIR KONTROLÜ
------------------------------------------------------- */
if (!allowClosed && row.isClosed === true) {
notify && $q.notify({
type: 'warning',
message: 'Bu satır kapalıdır ve düzenlenemez.',
position: 'top-right'
})
return false
}
/* -------------------------------------------------------
🔑 editingKey
------------------------------------------------------- */
const key =
typeof orderStore.getRowKey === 'function'
? orderStore.getRowKey(row)
: (row.clientKey || row.OrderLineID)
if (!key) return false
orderStore.editingKey = key
orderStore.selected = { ...row }
/* -------------------------------------------------------
🧩 FORM BASIC
------------------------------------------------------- */
Object.assign(form, {
model: row.model,
renk: row.renk,
renk2: row.renk2,
urunAnaGrubu: row.urunAnaGrubu,
urunAltGrubu: row.urunAltGrubu,
kategori: row.kategori,
aciklama: row.aciklama,
fiyat: Number(row.fiyat || 0),
pb: row.pb || aktifPB.value || 'USD',
terminTarihi: toDateOnly(row.terminTarihi || ''),
grpKey: row.grpKey
})
/* =======================================================
🔑 BEDEN EDITOR — TEK DOĞRU KAYNAK (GARANTİLİ)
-------------------------------------------------------
✔ schemaMap hazır değilse init edilir
✔ labels = schemaMap[grpKey].values
✔ values = row.bedenMap[grpKey] || 0
======================================================= */
const grpKey = form.grpKey
// 🔒 GARANTİ: schemaMap + grpKey
if (!orderStore.schemaMap || !orderStore.schemaMap[grpKey]) {
orderStore.initSchemaMap()
}
const schema = orderStore.schemaMap?.[grpKey]
if (schema?.values?.length) {
const rowMap = row?.bedenMap?.[grpKey] || {}
form.bedenLabels = [...schema.values]
form.bedenler = form.bedenLabels.map(lbl =>
Number(rowMap[lbl] || 0)
)
} else {
console.warn('⛔ schema bulunamadı:', grpKey)
form.bedenLabels = []
form.bedenler = []
}
/* -------------------------------------------------------
🧮 SATIR TOPLAM
------------------------------------------------------- */
updateTotals(form)
/* -------------------------------------------------------
⚙️ STOK / BEDEN ENVANTERİ (LABEL DOKUNMAZ)
------------------------------------------------------- */
if (loadSizes && form.model) {
await nextTick()
await orderStore.loadProductSizes(
form,
true,
$q
)
// backendden gelen snapshot stokları varsa
if (row.stokMap && typeof row.stokMap === 'object') {
stockMap.value = { ...row.stokMap }
}
await loadOrderInventory(true)
}
notify && $q.notify({
type: 'info',
message: `${message} → ${row.model}`,
position: 'top-right'
})
console.log('✅ hydrateEditorFromRow OK', {
source,
grpKey,
labels: form.bedenLabels,
values: form.bedenler
})
return true
}
/* ===========================================================
🔹 handleNewCombination (v6.3 — FINAL & STABLE)
- Model varsa ÇALIŞIR (renk opsiyonel)
- UI bedenleri → loadProductSizes
- Gerçek stok → loadOrderInventory
- Formu ASLA resetlemez
=========================================================== */
async function handleNewCombination() {
if (!form.model) {
console.warn('⚠️ handleNewCombination: model yok')
return
}
console.log('🆕 handleNewCombination', {
model: form.model,
renk: form.renk,
renk2: form.renk2
})
try {
/* -------------------------------------------------------
1⃣ Reaktivite sakinleşsin
------------------------------------------------------- */
await nextTick()
await new Promise(r => setTimeout(r, 200))
await nextTick()
/* -------------------------------------------------------
2⃣ UI için beden / grpKey hazırlığı
------------------------------------------------------- */
await orderStore.loadProductSizes(
form,
true,
$q,
productCache
)
/* -------------------------------------------------------
3⃣ GERÇEK STOK (MSSQL)
- merge=true → editor bozulmaz
------------------------------------------------------- */
await loadOrderInventory(true)
/* -------------------------------------------------------
4⃣ Stok bilgilendirme (opsiyonel)
------------------------------------------------------- */
const stoklar = Object.values(stockMap.value || {})
if (stoklar.length && stoklar.every(v => Number(v) === 0)) {
$q.notify({
type: 'warning',
message: '⚠️ Bu kombinasyon için stok bulunamadı (0)',
position: 'top-right'
})
} else {
console.log(`✅ Stok yüklendi (${stoklar.length} beden)`)
}
/* -------------------------------------------------------
5⃣ Gridde aynı kombinasyon varsa → edit moduna al
------------------------------------------------------- */
await openExistingCombination()
} catch (err) {
console.error('❌ handleNewCombination hata:', err)
$q.notify({
type: 'negative',
message: 'Stok bilgisi alınamadı ❌',
position: 'top-right'
})
}
}
/* ===========================================================
✅ openExistingCombination (X3 FINAL — helper kullanır)
- sadece eşleşen satırı bulur
- hydrate işini helper yapar
=========================================================== */
async function openExistingCombination() {
if (!form.model) return
const row = (orderStore.summaryRows || []).find(r =>
r.model === form.model &&
(r.renk || '') === (form.renk || '') &&
(r.renk2 || '') === (form.renk2 || '') &&
r.grpKey === form.grpKey
)
if (!row) {
return
}
// Kapalıysa uyar + çık
if (row.isClosed === true) {
$q.notify({
type: 'warning',
message: 'Bu satır kapalıdır.',
position: 'top-right'
})
return
}
await hydrateEditorFromRow(row, {
source: 'openExistingCombination',
message: 'Düzenleme moduna alındı',
notify: true,
loadSizes: true
})
}
const editRow = async (row) => {
try {
await hydrateEditorFromRow(row, {
source: 'editRow',
message: 'Düzenleme moduna geçildi',
notify: true,
loadSizes: true
})
} catch (err) {
console.error('❌ editRow hata:', err)
}
}
/* ===========================================================
🔹 STOK YARDIMCI FONKSİYONLARI
=========================================================== */
function getStockFor(lbl) {
if (!lbl || !stockMap.value) return 0
const val = stockMap.value[lbl]
const num = Number(val)
return isNaN(num) ? 0 : num
}
function getStockForRow(row, beden) {
if (!row || !beden) return 0
// 🔹 Eğer formda seçili model ve renk bu satıra aitse, bedenStock'tan getir
if (row.model === form.model && row.renk === form.renk) {
const f = bedenStock.value.find(x => x.beden === beden)
if (f) return Number(f.stok) || 0
}
// 🔹 Satırın kendi stokMapinde varsa oradan getir
if (row.stokMap && row.stokMap[beden] != null) {
const num = Number(row.stokMap[beden])
return isNaN(num) ? 0 : num
}
return 0
}
function stockColorClass(qty) {
const n = Number(qty)
if (isNaN(n)) return ''
if (n === 0) return 'stok-red'
if (n > 0 && n <= 2) return 'stok-yellow'
return 'stok-green'
}
const getKey =
typeof orderStore.getRowKey === 'function'
? orderStore.getRowKey
: (r => r?.clientKey || r?.id || r?.OrderLineID)
const activeGrpKey = computed(() => {
// 1⃣ Edit edilen satır varsa
if (orderStore.editingKey) {
const row = (summaryRows.value || []).find(
r => getKey(r) === orderStore.editingKey
)
if (row?.grpKey) return row.grpKey
}
// 2⃣ Editor formdan
if (form.grpKey) return form.grpKey
// 3⃣ Güvenli fallback
return 'tak'
})
const editingRow = computed(() => {
const key = orderStore.editingKey
if (!key) return null
const getKey = typeof orderStore.getRowKey === 'function'
? orderStore.getRowKey
: (r => r?.clientKey || r?.OrderLineID)
return (summaryRows.value || []).find(r => getKey(r) === key) || null
})
/* ===========================================================
🔹 Seri Matrix — Excel benzeri çarpan tabloları
Her ürün tipi için (takım, gömlek, pantolon vs.)
önceden tanımlanmış seri setlerini tutar.
Örneğin “4658 seri” seçilirse 46=1, 48=1, … şeklinde çarpanlar oluşur.
=========================================================== */
const seriMatrix = {
tak: {
'46-58 seri': { 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
'46-58 ara çift': { 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
'44-58 seri': { 44:1, 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
'44-58 ara çift': { 44:1, 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
'60-64 seri': { 60:1, 62:1, 64:1 },
'66-70 seri': { 66:1, 68:1, 70:1 },
'48-58 seri': { 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 }
},
gom: {
'XS-XXL': { XS:1, S:1, M:1, L:1, XL:1, XXL:1 },
'XS-XXL ara çift': { XS:1, S:1, M:2, L:2, XL:2, XXL:1 },
'3XL-5XL': { '3XL':1, '4XL':1, '5XL':1 }
},
ayk: {
'10\'lu seri': { 39:1, 40:2, 41:2, 42:2, 43:2, 44:1 },
'39-44': { 39:1, 40:1, 41:1, 42:1, 43:1, 44:1 },
'45-47': { 45:1, 46:1, 47:1 }
},
yas: {
'2-14Y': { 2:1, 4:1, 6:1, 8:1, 10:1, 12:1, 14:1 }
},
pan: {
'38-50 seri': { 38:1, 40:1, 42:1, 44:1, 46:1, 48:1, 50:1 },
'38-50 ara çift': { 38:1, 40:1, 42:2, 44:2, 46:2, 48:1, 50:1 },
'52-56 seri': { 52:1, 54:1, 56:1 },
'58-62 seri': { 58:1, 60:1, 62:1 }
}
}
const activeSeriesOptions = computed(() => {
const grpKey = activeGrpKey.value
const sets = seriMatrix[grpKey]
if (!sets) return []
return Object.keys(sets).map(k => ({
label: k,
value: k
}))
})
/* ===========================================================
🔹 Para Birimi ve Toplam Tutar Hesaplaması
Sipariş toplamları ve para birimi seçimi burada yönetilir.
=========================================================== */
const paraBirimOptions = ['USD', 'EUR', 'TRY','GBP'] // Kullanıcıya sunulacak döviz seçenekleri
// 🔸 3. Ana veri yüklemeleri (cari + modeller)
/* ===========================================================
🔹 Model Arama (QSelect içinde)
Kullanıcının yazdığı harflerle model kodlarını filtreler.
=========================================================== */
function filterModel(val, update) {
if (val === '') {
update(() => (filteredModelOptions.value = modelOptions.value))
return
}
update(() => {
const needle = val.toLowerCase()
filteredModelOptions.value = modelOptions.value.filter(v =>
(v.label || '').toLowerCase().includes(needle)
)
})
}
/* ===========================================================
🔹 MODEL SEÇİMİ (onModelChangeV2) — FINAL (grpKey FIXED)
-----------------------------------------------------------
✔ grpKey SADECE burada set edilir
✔ grpKey, urunAnaGrubu/kategoriye göre DETERMINISTIC
✔ Editor / loadProductSizes tahmin yapmaz (form.grpKey kesin)
✔ Pantolon gibi durumlarda 'tak' fallback engellenir
=========================================================== */
async function onModelChange(modelCode) {
// 🧹 önceki renkleri tamamen sıfırla
form.renk = ''
form.renk2 = ''
renkOptions.value = []
renkOptions2.value = []
if (renkSelect.value?.reset) renkSelect.value.reset()
if (renk2Select.value?.reset) renk2Select.value.reset()
if (!modelCode) {
console.warn('⚠️ Model kodu boş, sorgu yapılmadı.')
return
}
// 🧩 Önceki değerleri yedekle (korunacak alanlar)
const keep = {
aciklama: form.aciklama,
bedenler: Array.isArray(form.bedenler) ? [...form.bedenler] : [],
bedenLabels: Array.isArray(form.bedenLabels) ? [...form.bedenLabels] : [],
fiyat: form.fiyat,
adet: form.adet,
tutar: form.tutar
}
try {
/* -------------------------------------------------------
🎨 1⃣ Renk listesi
------------------------------------------------------- */
const resColors = await api.get('/product-colors', {
params: { code: modelCode }
})
renkOptions.value = (resColors?.data || []).map(x => ({
label: `${x.color_code || x.ColorCode} ${x.color_description || x.ColorDesc || ''}`,
value: x.color_code || x.ColorCode
}))
/* -------------------------------------------------------
🧱 2⃣ Ürün detayları
------------------------------------------------------- */
const resDetail = await api.get('/product-detail', {
params: { code: modelCode }
})
const d = resDetail?.data || {}
// ✅ Cache
if (modelCode && d) {
orderStore.productCache[modelCode] = productCache[modelCode]
productCache[modelCode] = {
...d,
ProductGroup: d.ProductGroup || d.UrunAnaGrubu || d.ProductAtt01Desc || '',
ProductSubGroup: d.ProductSubGroup || d.UrunAltGrubu || d.ProductAtt02Desc || '',
URUN_ANA_GRUBU: d.UrunAnaGrubu || d.ProductAtt01Desc || '',
URUN_ALT_GRUBU: d.UrunAltGrubu || d.ProductAtt02Desc || ''
}
console.log('🗂️ Cache eklendi:', modelCode, Object.keys(productCache[modelCode]))
}
/* -------------------------------------------------------
🧩 Form temel alanları
------------------------------------------------------- */
Object.assign(form, {
model: modelCode,
urunAnaGrubu: d.UrunAnaGrubu || d.ProductGroup || d.ProductAtt01Desc || '',
urunAltGrubu: d.UrunAltGrubu || d.ProductSubGroup || d.ProductAtt02Desc || '',
fit: d.Fit1 || d.Fit || '',
urunIcerik: d.UrunIcerik || d.Fabric || '',
drop: d.Drop || '',
kategori: d.Kategori || '',
askiliyan: d.AskiliYan || '',
aciklama: keep.aciklama,
fiyat: keep.fiyat,
adet: keep.adet,
tutar: keep.tutar,
})
/* =======================================================
🔑 BEDEN GRUBU — TEK VE KESİN KARAR (FIXED)
- detectBedenGroup içine "[]" verip 'tak' düşmesini engeller
- Önce urunAnaGrubu/kategori üzerinden hard-match
- Sonra detectBedenGroup (ürün bilgisiyle)
- En sonda güvenli fallback: 'tak'
======================================================= */
const ana = String(form.urunAnaGrubu || '').toLowerCase().trim()
const kat = String(form.kategori || '').toLowerCase().trim()
let bedenGrpKey = null
// ✅ Hard-match (senin ana gruplarına göre genişletebilirsin)
if (ana.includes('pantolon') || kat.includes('pantolon')) {
bedenGrpKey = 'pan'
} else if (ana.includes('gömlek') || ana.includes('gomlek') || kat.includes('gömlek') || kat.includes('gomlek')) {
bedenGrpKey = 'gom'
} else if (ana.includes('ayakkabı') || ana.includes('ayakkabi') || kat.includes('ayakkabı') || kat.includes('ayakkabi')) {
bedenGrpKey = 'ayk'
} else if (ana.includes('yaş') || ana.includes('yas') || kat.includes('yaş') || kat.includes('yas')) {
bedenGrpKey = 'yas'
}
// ✅ Hard-match bulamadıysa mevcut helper ile belirle
if (!bedenGrpKey) {
try {
// ⚠️ Boş array verme; ürün bilgisini kullanarak belirle
bedenGrpKey = detectBedenGroup(null, form.urunAnaGrubu, form.kategori)
} catch (e) {
console.warn('⚠️ detectBedenGroup hata:', e)
bedenGrpKey = null
}
}
// ✅ Son fallback
if (!bedenGrpKey) bedenGrpKey = 'tak'
form.grpKey = bedenGrpKey
console.log('🧭 Editor grpKey set edildi →', bedenGrpKey)
// ✅ Editor bedenleri hemen aç (UI seed) — schemaMap tek kaynak
const schema =
orderStore.schemaMap?.[form.grpKey] ||
storeSchemaByKey?.[form.grpKey]
if (Array.isArray(schema?.values) && schema.values.length) {
// önceki adetleri label bazlı koru
const prevMap = {}
;(keep.bedenLabels || []).forEach((lbl, i) => {
prevMap[lbl] = Number(keep.bedenler?.[i] || 0)
})
form.bedenLabels = [...schema.values]
form.bedenler = form.bedenLabels.map(lbl => Number(prevMap[lbl] || 0))
} else {
form.bedenLabels = []
form.bedenler = []
}
console.log('📦 Model detayları yüklendi:', form.urunAnaGrubu, form.fit)
/* -------------------------------------------------------
💰 3⃣ Min fiyat
------------------------------------------------------- */
await fetchMinPrice()
/* -------------------------------------------------------
⚙️ 4⃣ Renk yoksa direkt beden/stok
------------------------------------------------------- */
if (!renkOptions.value.length) {
await orderStore.loadProductSizes(form, true, $q, productCache)
await loadOrderInventory(true)
}
/* -------------------------------------------------------
🧮 5⃣ Gridde varsa → edit mod
------------------------------------------------------- */
await openExistingCombination()
$q.notify({
type: 'info',
message: `Model "${modelCode}" yüklendi ✅`,
position: 'top-right'
})
} catch (err) {
console.error('❌ Model verileri alınamadı:', err)
$q.notify({
type: 'negative',
message: 'Model bilgileri alınamadı ❌',
position: 'top-right'
})
}
}
/* ===========================================================
🔹 RENK SEÇİMİ (1. Renk Değişimi)
=========================================================== */
async function onColorChange(colorCode) {
form.renk = colorCode || ''
renkOptions2.value = []
form.renk2 = ''
// 2. renk QSelect bileşenini sıfırla
if (renk2Select.value) renk2Select.value.reset?.()
// ⚠️ Renk seçilmediyse işlemi iptal et
if (!form.renk) {
console.warn('⚠️ Renk seçilmedi, işlemler durduruldu.')
return
}
try {
// 🎨 2⃣ İkinci renk listesini yükle
const res = await api.get('/product-secondcolor', {
params: { code: form.model, color: colorCode }
})
const data = res?.data || []
if (Array.isArray(data) && data.length > 0) {
renkOptions2.value = data.map(x => ({
label: x.item_dim2_code,
value: x.item_dim2_code
}))
console.log('🎨 2. renk listesi yüklendi:', renkOptions2.value.length)
} else {
// 2. renk yoksa doğrudan beden/stok yükle
console.log('⚪ 2. renk yok, doğrudan beden/stok yükleniyor...')
}
await handleNewCombination()
} catch (err) {
console.error('❌ 1. renk sonrası hata:', err)
}
}
/* ===========================================================
🔹 2. RENK SEÇİMİ (onColor2Change)
=========================================================== */
async function onColor2Change(colorCode2) {
if (typeof colorCode2 === 'object' && colorCode2?.value) {
colorCode2 = colorCode2.value
}
form.renk2 = colorCode2 || ''
try {
await handleNewCombination()
} catch (err) {
console.error('❌ 2. renk sonrası hata:', err)
}
}
/* ===========================================================
🔹 Beden / Stok Yükleyici — Değişkenler
=========================================================== */
const bedenStock = ref([]) // Görsel tablo için stok listesi
const stockMap = ref({}) // { "48": 12, "50": 7, ... } şeklinde key-value map
const onSaveOrUpdateRow = async () => {
if (!hasRowMutationPermission()) {
notifyNoPermission(
isEditMode.value
? 'Siparis satiri guncelleme yetkiniz yok'
: 'Siparis satiri kaydetme yetkiniz yok'
)
return
}
await orderStore.saveOrUpdateRowUnified({
form,
recalcVat: typeof recalcVat === 'function' ? recalcVat : null,
resetEditor: typeof resetEditor === 'function' ? resetEditor : null,
// gerekiyorsa pass edebilirsin (store tarafında zaten optional)
stockMap,
$q
})
}
/* ===========================================================
🔹 loadOrderInventory (GÜNCELLENMİŞ)
MSSQL stok sorgusu — artık boş değerleri 0 yapmıyor.
merge=true ise sadece dolu değerleri günceller.
=========================================================== */
async function loadOrderInventory(merge = false) {
if (!form.model) {
console.warn('⚠️ Stok yüklenemedi: model seçilmemiş.')
return
}
try {
const params = { code: form.model }
if (form.renk?.trim()) params.color = form.renk.trim()
if (form.renk2?.trim()) params.color2 = form.renk2.trim()
console.log('📦 MSSQL stok sorgusu gönderiliyor:', params)
const res = await api.get('/order-inventory', { params })
const data = res?.data || []
console.log(`📦 MSSQL stok verisi geldi: ${data.length}`)
console.table(data)
// 1⃣ Normalize (gelen büyük harfli)
const invMap = {}
for (const x of data) {
const beden = String(x.Beden || '').trim() || ' '
const stokDeger = Number(x.KullanilabilirAdet ?? 0)
invMap[beden] = stokDeger
}
// 2⃣ Form bedenlerine göre map oluştur
const newMap = {}
for (const lbl of form.bedenLabels || []) {
const key = lbl?.trim() === '' ? ' ' : lbl.trim()
newMap[lbl] = invMap[key] ?? 0
}
// 3⃣ Merge veya replace
if (merge && stockMap.value) {
for (const lbl of Object.keys(newMap)) {
stockMap.value[lbl] = newMap[lbl]
}
} else {
stockMap.value = { ...newMap }
}
// 4⃣ Görsel listeyi güncelle
bedenStock.value = Object.entries(stockMap.value).map(([beden, stok]) => ({
beden,
stok
}))
console.log('✅ Stok haritası güncellendi:', stockMap.value)
} catch (err) {
console.error('❌ Order inventory yüklenemedi:', err)
$q.notify({
type: 'negative',
message: 'Stok verisi alınamadı ❌',
position: 'top-right'
})
}
}
// 🔹 Üst formdaki tahmini termin değişince:
watch(
() => form.AverageDueDate,
(yeni) => {
if (!yeni) return
applyTerminToRows(yeni)
}
)
/* ===========================================================
🔹 useComboWatcher (v6.3 — MUTATION AWARE & CLEAN)
- Edit modda combo değişirse:
DELETE → temiz edit state
- Guard KURMAZ
- Persist SADECE gerçek mutation varsa
=========================================================== */
function useComboWatcher(type, handler) {
return async (val) => {
const prevBusy = !!orderStore._uiBusy
const prevPrevent = !!orderStore.preventPersist
let mutated = false // 🔥 SADECE gerçekten değiştiyse true
try {
const currentRow = selectedRow.value
const isEditingNow = !!currentRow
/* =====================================================
1⃣ EDIT MODE → combo değiştiyse DELETE
====================================================== */
if (isEditingNow && currentRow) {
const nextCombo = {
model: type === 'model' ? val : form.model,
renk : type === 'renk' ? val : form.renk,
renk2: type === 'renk2' ? val : form.renk2
}
const comboChanged =
(currentRow.model || '') !== (nextCombo.model || '') ||
(currentRow.renk || '') !== (nextCombo.renk || '') ||
(currentRow.renk2 || '') !== (nextCombo.renk2 || '')
if (comboChanged) {
console.warn('🟥 Combo değişti → DELETE')
mutated = true // 🔥 GERÇEK DEĞİŞİKLİK
orderStore._uiBusy = true
orderStore.preventPersist = true
orderStore.removeRowInternal(currentRow)
// 🔑 Tek kaynak edit state
orderStore.editingKey = null
orderStore.selected = null
await nextTick()
}
}
/* =====================================================
2⃣ ASIL HANDLER
====================================================== */
if (typeof handler === 'function') {
await handler(val)
}
} catch (err) {
console.error('❌ useComboWatcher hata:', err)
} finally {
/* =====================================================
3⃣ STATE GERİ AL + ŞARTLI PERSIST
====================================================== */
orderStore._uiBusy = prevBusy
orderStore.preventPersist = prevPrevent
// ✅ SADECE mutation olduysa snapshot al
if (mutated) {
orderStore.persistLocalStorage?.()
}
}
}
}
// ======================================================
// 👁‍🗨 GROUPED ROWS WATCHER
// ======================================================
watch(groupedRows, (val) => {
if (!Array.isArray(val)) return
console.log(
'👀 groupedRows değişti:',
val.map(g => ({
name: g.name,
count: g.rows?.length || 0
}))
)
})
// =============================================
// ✅ onCariChange — %100 SAFE + CurrAccTypeCode Entegre
// =============================================
// =============================================
// ✅ onCariChange — FINAL (pb scope FIX)
// =============================================
async function onCariChange(kod) {
let pb = 'USD' // ✅ dış scope: try/catch/finally hepsinde erişilebilir
try {
if (!kod) return
// 🔹 Cari kaydını bul
const cari = cariOptions.value.find(c => c.Cari_Kod === kod)
if (!cari) {
console.warn('⚠️ Cari bulunamadı:', kod)
return
}
selectedCari.value = kod
cariInfo.value = cari
// 🔹 Para birimi (fallbackli)
pb =
cari.Doviz_Cinsi ||
cari.ParaBirimi ||
cari.DocCurrencyCode ||
'USD'
// 🔹 FORM sync (UI için)
form.CurrAccTypeCode = cari.CurrAccTypeCode || 1
form.CurrAccCode = kod
form.DocCurrencyCode = pb
form.pb = pb
aktifPB.value = pb
/* =====================================================
🔥 STORE HEADER SYNC
===================================================== */
orderStore.setHeaderFields(
{
CurrAccTypeCode: form.CurrAccTypeCode,
CurrAccCode: kod,
DocCurrencyCode: pb,
PriceCurrencyCode: pb
},
{
applyCurrencyToLines: true,
immediatePersist: true
}
)
/* =====================================================
💱 Kur (opsiyonel)
===================================================== */
if (orderStore.getTodayRate) {
try {
const rate = await orderStore.getTodayRate(pb, 'TRY')
if (!isNaN(rate)) {
orderStore.setHeaderFields({ ExchangeRate: Number(rate) })
}
} catch (e) {
console.warn('⚠️ Kur alınamadı:', e)
}
}
// 🔁 Toplamları yenile
recalcVat()
$q.notify({
type: 'positive',
message: `Cari değiştirildi → ${kod} (${pb})`,
position: 'top-right'
})
} catch (err) {
console.error('❌ onCariChange hata:', err)
$q.notify({
type: 'negative',
message: 'Cari değiştirilemedi',
position: 'top-right'
})
} finally {
// 🔥 X3: para birimini satırlara yay (varsa)
if (orderStore.applyCurrencyToLines) {
orderStore.applyCurrencyToLines(pb)
}
// 💾 tek persist
orderStore.persistLocalStorage?.()
}
}
// ===========================================================
// 🔹 STICKY VARIABLE GÜNCELLEYİCİ (Eksik Olan Fonksiyon)
// CSS değişkenlerini DOM üzerinden yeniden hesaplar
// ===========================================================
// 🔹 Sticky değişkenleri güncelle
function updateStickyVars() {
try {
const root = document.documentElement
const headerH = document.querySelector('.q-header')?.offsetHeight || 56
const filterH = document.querySelector('.filter-bar')?.offsetHeight || 72
const saveH = document.querySelector('.save-toolbar')?.offsetHeight || 52
const totalSticky = headerH + filterH + saveH
root.style.setProperty('--header-h', `${headerH}px`)
root.style.setProperty('--filter-h', `${filterH}px`)
root.style.setProperty('--save-h', `${saveH}px`)
root.style.setProperty('--sticky-total', `${totalSticky}px`)
console.log(`📐 Sticky vars → header:${headerH}, filter:${filterH}, save:${saveH}`)
} catch (err) {
console.warn('⚠️ updateStickyVars hata:', err)
}
}
// 🔹 Header ile grid arasındaki boşluğu ölç
function measureHeaderGap() {
try {
const hdr = document.querySelector('.order-grid-header')
if (!hdr) return
const height = hdr.getBoundingClientRect().height || 0
const gap = -height
document.documentElement.style.setProperty('--header-body-gap', `${gap}px`)
console.log('📏 Header boşluğu ölçüldü:', height, 'gap:', gap)
} catch (err) {
console.warn('⚠️ measureHeaderGap hata:', err)
}
}
</script>