3077 lines
91 KiB
Vue
3077 lines
91 KiB
Vue
<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ı scroll’da
|
||
======================================================== -->
|
||
<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="Açı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 Açıklaması"
|
||
filled
|
||
dense
|
||
autogrow
|
||
maxlength="1500"
|
||
counter
|
||
placeholder="Siparişe genel açı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 (v22–Final) — 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
|
||
- UI’da 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 lookup’lar
|
||
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 header’a 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ş açı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ç: gateway’e 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
|
||
)
|
||
|
||
// backend’den 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 stokMap’inde 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 “46–58 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/kategori’ye 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 (fallback’li)
|
||
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>
|