3024 lines
102 KiB
Plaintext
3024 lines
102 KiB
Plaintext
<template>
|
||
<!-- ===========================================================
|
||
🧾 ORDER ENTRY PAGE (BSSApp)
|
||
v22 — Model + Renk + 2. Renk + Beden/Stok Otomatik Eşleme
|
||
============================================================ -->
|
||
<q-page class="q-pa-md order-page">
|
||
<div class="order-scroll-x">
|
||
<div class="order-width">
|
||
<div class="row items-center q-mb-xs">
|
||
<q-chip
|
||
:color="isEditMode ? 'blue-7' : 'green-7'"
|
||
text-color="white"
|
||
icon="edit"
|
||
v-if="isEditMode"
|
||
>
|
||
Düzenleme Modu
|
||
</q-chip>
|
||
<q-chip
|
||
text-color="white"
|
||
icon="add_circle"
|
||
v-else
|
||
>
|
||
Yeni Sipariş
|
||
</q-chip>
|
||
</div>
|
||
|
||
<!-- =======================================================
|
||
🔹 FILTER BAR — OrderHeader Bilgileri
|
||
SQL eşleşmeli alanlar
|
||
======================================================= -->
|
||
<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"
|
||
behavior="menu"
|
||
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
|
||
:readonly="isEditMode"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 📅 Oluşturulma Tarihi -->
|
||
<div class="col-2">
|
||
<q-input
|
||
v-model="form.OrderDate"
|
||
label="Oluşturulma Tarihi"
|
||
type="date"
|
||
filled
|
||
dense
|
||
/>
|
||
</div>
|
||
|
||
<!-- 📅 Tahmini Termin Tarihi -->
|
||
<div class="col-2">
|
||
<q-input
|
||
v-model="form.AverageDueDate"
|
||
label="Tahmini Termin Tarihi"
|
||
type="date"
|
||
filled
|
||
dense
|
||
/>
|
||
</div>
|
||
|
||
<!-- 💰 TOPLAM TUTAR + KDV -->
|
||
<div class="col-12 row items-center q-gutter-sm q-mt-sm">
|
||
|
||
<div class="col-3">
|
||
<q-input
|
||
dense
|
||
filled
|
||
v-model.number="toplamTutar"
|
||
label="Toplam Tutar"
|
||
readonly
|
||
:suffix="form.pb"
|
||
/>
|
||
</div>
|
||
|
||
<div class="col-auto">
|
||
<q-checkbox
|
||
v-model="includeKDV"
|
||
label="KDV Dahil"
|
||
/>
|
||
</div>
|
||
|
||
<div class="col-2">
|
||
<q-input
|
||
dense
|
||
filled
|
||
v-model.number="manualKDV"
|
||
label="KDV"
|
||
readonly
|
||
suffix="%"
|
||
/>
|
||
</div>
|
||
|
||
<div class="col-3">
|
||
<q-input
|
||
dense
|
||
filled
|
||
:model-value="toplamKDVli.toFixed(2)"
|
||
label="KDV Dahil Toplam"
|
||
readonly
|
||
:suffix="form.pb"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 🔸 Cari Bilgi Kutuları -->
|
||
<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>
|
||
<!-- =======================================================
|
||
🔹 KAYDET / GÜNCELLE BUTONU
|
||
======================================================= -->
|
||
<q-btn
|
||
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
|
||
color="primary"
|
||
icon="save"
|
||
class="q-ml-sm"
|
||
:loading="orderStore.loading"
|
||
@click="submitAll"
|
||
/>
|
||
|
||
<!-- =======================================================
|
||
🔹 YENİ SİPARİŞ / FORM SIFIRLA BUTONU
|
||
======================================================= -->
|
||
<q-btn
|
||
label="YENİ SİPARİŞ"
|
||
color="secondary"
|
||
icon="add_circle"
|
||
class="q-ml-sm"
|
||
@click="resetForm"
|
||
/>
|
||
|
||
</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 schema" :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
|
||
======================================================== -->
|
||
<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 (grp.bedenValues || [])"
|
||
:key="'hdr-' + v"
|
||
class="beden-cell"
|
||
>
|
||
{{ v }}
|
||
</div>
|
||
</div>
|
||
<div class="sub-right">
|
||
<div class="text-caption">
|
||
Toplam {{ grp.name }} Adet: {{ grp.toplamAdet }}
|
||
</div>
|
||
<div class="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 }, i) in grp.rows"
|
||
:key="i"
|
||
class="summary-row"
|
||
:class="{
|
||
active: editingIndex === summaryRows.findIndex(r => r === row),
|
||
'is-editing': editingIndex === summaryRows.findIndex(r => r === row)
|
||
}"
|
||
@click="editRow(row, i)"
|
||
>
|
||
<!-- 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 (schemaByKey[row.grpKey]?.values || [])" :key="'val-' + v" class="cell beden">
|
||
{{ row.bedenMap?.[row.grpKey]?.[v] ?? '' }}
|
||
</div>
|
||
<div v-for="i in (16 - (schemaByKey[row.grpKey]?.values?.length || 0))" :key="'empty-' + i" 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>
|
||
<!-- ESKİ (inline düzenlenebilir) -->
|
||
<!-- 🔹 Termin Tarihi — sadece gösterge -->
|
||
<!-- 🔹 Termin Tarihi — sadece gösterge -->
|
||
<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="isEditing"
|
||
@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="isEditing"
|
||
@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"
|
||
@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
|
||
v-if="activeSeriesOptions && activeSeriesOptions.length"
|
||
v-model="selectedSeriSet"
|
||
:options="activeSeriesOptions"
|
||
label="Beden Seti Seç"
|
||
filled dense
|
||
emit-value map-options
|
||
option-value="value"
|
||
option-label="label"
|
||
/>
|
||
</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
|
||
/>
|
||
</div>
|
||
|
||
<div class="col-2 q-mt-sm">
|
||
<q-btn
|
||
v-if="selectedSeriSet"
|
||
color="primary"
|
||
icon="add"
|
||
label="Seri Ekle"
|
||
@click="applySeriSet"
|
||
/>
|
||
</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 }"
|
||
/>
|
||
|
||
<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 />
|
||
</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)"
|
||
/>
|
||
</div>
|
||
<div class="col-2">
|
||
<q-select v-model="form.pb" :options="paraBirimOptions" label="PB" dense filled />
|
||
</div>
|
||
<div class="col-3">
|
||
<q-input v-model="form.tutar" label="Tutar" dense filled readonly />
|
||
</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
|
||
/>
|
||
</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
|
||
/>
|
||
</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
|
||
:color="editingIndex === -1 ? 'primary' : 'positive'"
|
||
:label="editingIndex === -1 ? 'Kaydet' : 'Güncelle'"
|
||
@click="saveOrUpdate"
|
||
/>
|
||
<q-btn
|
||
v-if="editingIndex !== -1"
|
||
color="negative" flat label="Satırı Sil"
|
||
@click="removeSelected"
|
||
/>
|
||
<q-btn
|
||
flat color="grey-8"
|
||
label="Formu Temizle"
|
||
@click="resetForm"
|
||
/>
|
||
</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...)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
</div> <!-- editor -->
|
||
</div> <!-- order-width -->
|
||
</div> <!-- order-scroll-x -->
|
||
</q-page>
|
||
</template>
|
||
|
||
<script setup>
|
||
/* ===========================================================
|
||
🧩 ORDER ENTRY (v22-final)
|
||
Tüm fonksiyonları kapsayan gelişmiş setup bloğu.
|
||
Bu dosya backend ile sıkı entegredir, axios ve Pinia store
|
||
ile veri alışverişi yapar.
|
||
=========================================================== */
|
||
|
||
// Vue çekirdek importları
|
||
import { ref, reactive, computed, onMounted, watch, onBeforeUnmount, nextTick } from 'vue'
|
||
import { api } from 'boot/axios'
|
||
import { useRoute, useRouter } from 'vue-router' // ✅ Buradan olmalı
|
||
import { useQuasar } from 'quasar'
|
||
import axios from 'axios'
|
||
import { useOrderentryStore } from 'src/stores/orderentryStore'
|
||
import dayjs from 'dayjs'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const $q = useQuasar()
|
||
const orderId =
|
||
route.params.id ||
|
||
route.query.id ||
|
||
route.query.orderId ||
|
||
null
|
||
|
||
console.log('🧩 Route parametresi alındı (setup başında):', orderId)
|
||
const isEditMode = ref(false)
|
||
// ===========================================================
|
||
// 🔹 Mode & Transaction State
|
||
// Yeni sipariş mi, düzenleme mi kontrolü için
|
||
// ===========================================================
|
||
const mode = ref('new') // 'new' | 'edit'
|
||
const txId = ref('')
|
||
const headerId = ref('') // Quasar bileşenleri ve $q.notify kullanımı için
|
||
const orderStore = useOrderentryStore() // Pinia store: siparişler, localStorage, API çağrıları
|
||
|
||
// 🔹 Genel reaktif değişkenler
|
||
const aktifPB = ref('USD') // Varsayılan para birimi (Cari seçimiyle değişebilir)
|
||
const siparisNo = ref('SP-2025-001') // Geçici sipariş numarası örneği
|
||
const allSeriesSets = ref([]) // Seri listeleri (seriMatrix'ten dinamik doldurulur)
|
||
/* ===========================================================
|
||
🗓️ 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(today.getDate() + 35) // 5 hafta sonrası
|
||
|
||
const defaultOlusturmaTarihi = today.toISOString().substring(0, 10)
|
||
const defaultTerminTarihi = terminDate.toISOString().substring(0, 10)
|
||
// 💰 KDV Hesaplama Alanları
|
||
const includeKDV = ref(false) // KDV kutusu işaretli mi?
|
||
const manualKDV = ref(0) // Kullanıcının elle girdiği KDV tutarı
|
||
const kdvOrani = 0.10 // Varsayılan %10 oran
|
||
|
||
/* ===========================================================
|
||
🔹 FORM TANIMI (reactive form)
|
||
Tüm giriş alanları ve hesaplanan değerler tek reactive obje içinde.
|
||
=========================================================== */
|
||
|
||
const form = reactive({
|
||
/* =========================================================
|
||
🔹 TEMEL ALANLAR
|
||
========================================================= */
|
||
OrderHeaderID: '', // string (GUID)
|
||
OrderTypeCode: 1, // int8
|
||
ProcessCode: 'WS', // string
|
||
OrderNumber: '', // string
|
||
OrderTime: dayjs().format('HH:mm:ss'), // saat formatı (örn. "14:35:22")
|
||
IsCancelOrder: false,
|
||
|
||
/* =========================================================
|
||
🔹 ADRES / REFERANS ALANLARI
|
||
========================================================= */
|
||
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: 0,
|
||
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: '', // backend dolduracak (auth user)
|
||
CreatedDate: dayjs().toISOString(),
|
||
LastUpdatedUserName: '',
|
||
LastUpdatedDate: dayjs().toISOString(),
|
||
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,
|
||
model: '', // Ürün kodu
|
||
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,
|
||
|
||
// 🗓️ Tarihler
|
||
olusturmaTarihi: defaultOlusturmaTarihi,
|
||
tahminiTerminTarihi: defaultTerminTarihi,
|
||
terminTarihi: defaultTerminTarihi
|
||
})
|
||
const orderLine = reactive({
|
||
// 🔹 Temel Bilgiler
|
||
OrderLineID: '', // GUID
|
||
OrderHeaderID: '', // foreign key
|
||
SortOrder: 1,
|
||
ItemTypeCode: 0,
|
||
ItemCode: '',
|
||
ColorCode: '',
|
||
ItemDim1Code: '',
|
||
ItemDim2Code: '',
|
||
ItemDim3Code: '',
|
||
|
||
// 🔹 Miktarlar
|
||
Qty1: 0,
|
||
Qty2: 0,
|
||
CancelQty1: 0,
|
||
CancelQty2: 0,
|
||
|
||
// 🔹 Tarihler
|
||
CancelDate: null, // null ya da ISO string
|
||
ClosedDate: null,
|
||
DeliveryDate: null,
|
||
PlannedDateOfLading: '',
|
||
|
||
// 🔹 Durum & Neden
|
||
OrderCancelReasonCode: '',
|
||
IsClosed: false,
|
||
|
||
// 🔹 Satış Planı
|
||
SalespersonCode: '',
|
||
PaymentPlanCode: '',
|
||
PurchasePlanCode: '',
|
||
|
||
// 🔹 Ürün Bilgisi
|
||
LineDescription: '',
|
||
UsedBarcode: '',
|
||
CostCenterCode: '',
|
||
VatCode: '',
|
||
VatRate: 0,
|
||
PCTCode: '',
|
||
PCTRate: 0,
|
||
|
||
// 🔹 Satır Bazlı İndirimler
|
||
LDisRate1: 0,
|
||
LDisRate2: 0,
|
||
LDisRate3: 0,
|
||
LDisRate4: 0,
|
||
LDisRate5: 0,
|
||
|
||
// 🔹 Para / Fiyat Bilgileri
|
||
DocCurrencyCode: 'USD',
|
||
PriceCurrencyCode: 'USD',
|
||
PriceExchangeRate: 1,
|
||
Price: 0,
|
||
|
||
// 🔹 Referans Bilgiler
|
||
PriceListLineID: '',
|
||
BaseProcessCode: '',
|
||
BaseOrderNumber: '',
|
||
BaseCustomerTypeCode: 0,
|
||
BaseCustomerCode: '',
|
||
BaseSubCurrAccID: '',
|
||
BaseStoreCode: '',
|
||
SupportRequestHeaderID: '',
|
||
|
||
// 🔹 Takip / Sistem Bilgileri
|
||
OrderLineSumID: 0,
|
||
OrderLineBOMID: 0,
|
||
|
||
// 🔹 Kullanıcı / Tarih
|
||
CreatedUserName: '',
|
||
CreatedDate: dayjs().toISOString(),
|
||
LastUpdatedUserName: '',
|
||
LastUpdatedDate: dayjs().toISOString(),
|
||
|
||
// 🔹 Vergi & Ek Kodlar
|
||
SurplusOrderQtyToleranceRate: 0,
|
||
PurchaseRequisitionLineID: '',
|
||
WithHoldingTaxTypeCode: '',
|
||
DOVCode: '',
|
||
OrderLineLinkedProductID: '',
|
||
|
||
// 🔹 Frontend’e özel ek alanlar (UI binding için)
|
||
selectedModel: '', // model (ürün kodu)
|
||
selectedColor: '', // renk
|
||
selectedColor2: '', // ikinci renk
|
||
bedenLabels: [], // beden başlıkları
|
||
bedenValues: {}, // {38:2, 40:1, 42:0} gibi
|
||
unitPrice: 0,
|
||
totalAmount: 0,
|
||
note: '',
|
||
})
|
||
const editingIndex = ref(-1) // aktif düzenlenen satırın index’i
|
||
const summaryRows = ref([]) // tüm satırların listesi (grid kaynağı)
|
||
// 🔹 Düzenleme durumunu hesaplayan computed
|
||
const isEditing = computed(() => editingIndex.value >= 0)
|
||
/* ===========================================================
|
||
🔹 1. ve 2. Renk Select Referansları
|
||
QSelect bileşenlerine erişmek için template ref’leri tutulur.
|
||
=========================================================== */
|
||
const renkSelect = ref(null)
|
||
const renk2Select = ref(null)
|
||
|
||
const renkOptions = ref([]) // 1. renk seçenekleri
|
||
const renkOptions2 = ref([]) // 2. renk seçenekleri
|
||
|
||
/* ===========================================================
|
||
🔹 Kombinasyon Anahtarı Fonksiyonları
|
||
Aynı model + renk + 2. renk kombinasyonunun gridde olup olmadığını bulmak için
|
||
bu yardımcı fonksiyonlar kullanılır.
|
||
=========================================================== */
|
||
const getComboKey = (o) => [o.model || '', o.renk || '', o.renk2 || ''].join('||')
|
||
|
||
// 99999 veya boş renkleri toleranslı eşleştirme
|
||
const isSameCombo = (row, form) => {
|
||
const sameModel = row.model === form.model
|
||
const renkOk =
|
||
(row.renk || '') === (form.renk || '') ||
|
||
(row.renk || '') === '99999' ||
|
||
(form.renk || '') === '99999'
|
||
const renk2Ok =
|
||
(row.renk2 || '') === (form.renk2 || '') ||
|
||
(row.renk2 || '') === '99999' ||
|
||
(form.renk2 || '') === '99999'
|
||
return sameModel && renkOk && renk2Ok
|
||
}
|
||
|
||
// Grid içinde aynı kombinasyonun index’ini bulur
|
||
const findExistingIndexByForm = () =>
|
||
summaryRows.value.findIndex(r => isSameCombo(r, form))
|
||
|
||
/* ===========================================================
|
||
🔹 Ürün Ana Grubu Bazında Gruplanmış Satırlar
|
||
groupedRows computed fonksiyonu, satırları urunAnaGrubu’na göre gruplar.
|
||
Her grup alt toplamları, açık/kapalı durumu ve beden setlerini içerir.
|
||
=========================================================== */
|
||
const groupOpen = reactive({}) // {"TAKIM ELBİSE": true, "GÖMLEK": false, ...}
|
||
|
||
const groupedRows = computed(() => {
|
||
const buckets = {}
|
||
const order = []
|
||
|
||
// Null veya hatalı satırları filtrele
|
||
const safeRows = (summaryRows.value || []).filter(r => r && r.urunAnaGrubu)
|
||
|
||
for (const [idx, row] of safeRows.entries()) {
|
||
const anaGrup = row.urunAnaGrubu.trim().toUpperCase()
|
||
if (!anaGrup) continue
|
||
|
||
// 🔹 Beden grubu anahtarını tespit et (schemaByKey üzerinden)
|
||
const bedenList = Object.keys(row.bedenMap?.[row.grpKey] || {})
|
||
const grpKey = detectBedenGroup(bedenList, row.urunAnaGrubu, row.kategori)
|
||
const grpSchema = schemaByKey.value[grpKey]
|
||
const bedenValues = grpSchema ? grpSchema.values : []
|
||
|
||
// 🔹 Eğer grup ilk kez görülüyorsa, yeni obje oluştur
|
||
if (!buckets[anaGrup]) {
|
||
buckets[anaGrup] = {
|
||
name: anaGrup,
|
||
key: grpKey,
|
||
rows: [],
|
||
toplamAdet: 0,
|
||
toplamTutar: 0,
|
||
bedenValues
|
||
}
|
||
order.push(anaGrup)
|
||
// ilk kez eklendiğinde default açık bırak
|
||
if (groupOpen[anaGrup] === undefined) groupOpen[anaGrup] = true
|
||
}
|
||
|
||
// 🔹 Satırları grup altına ekle
|
||
const adet = Number(row.adet || 0)
|
||
const tutar = Number(row.tutar || 0)
|
||
buckets[anaGrup].rows.push({ row, idx })
|
||
buckets[anaGrup].toplamAdet += adet
|
||
buckets[anaGrup].toplamTutar += tutar
|
||
|
||
// 🔹 Daha geniş beden seti varsa grup seviyesinde güncelle
|
||
if (buckets[anaGrup].bedenValues.length < bedenValues.length)
|
||
buckets[anaGrup].bedenValues = bedenValues
|
||
}
|
||
|
||
// 🔹 Sonuç sıralı dizi olarak döner (UI grid için)
|
||
return order.map(name => ({
|
||
...buckets[name],
|
||
open: groupOpen[name]
|
||
}))
|
||
})
|
||
/* ===========================================================
|
||
🔹 Grup Aç/Kapa Fonksiyonu
|
||
groupedRows içindeki grupOpen reactive objesini günceller.
|
||
Kullanıcı bir ürün grubunu kapattığında alt satırlar gizlenir.
|
||
=========================================================== */
|
||
function toggleGroup(groupName) {
|
||
if (!groupName) return
|
||
groupOpen[groupName] = !groupOpen[groupName]
|
||
console.log(`📂 Grup "${groupName}" artık ${groupOpen[groupName] ? 'açık' : 'kapalı'}`)
|
||
}
|
||
// ===========================================================
|
||
// 🔹 Grup Açık/Kapalı Durumunu summaryRows’a göre otomatik güncelle
|
||
// Eski sipariş çağrıldığında tüm gruplar açık gelsin
|
||
// ===========================================================
|
||
watch(summaryRows, rows => {
|
||
if (!Array.isArray(rows)) return
|
||
rows.forEach(r => {
|
||
if (r.urunAnaGrubu && groupOpen[r.urunAnaGrubu] === undefined) {
|
||
groupOpen[r.urunAnaGrubu] = true
|
||
}
|
||
})
|
||
})
|
||
|
||
|
||
// 🔹 Sipariş genel açıklaması (ör. “Yaz sezonu toplu sipariş”)
|
||
const siparisGenelAciklama = ref('')
|
||
const DRAFT_KEY = computed(() =>
|
||
mode.value === 'edit'
|
||
? `bssapp:order:draft:edit:${headerId.value}`
|
||
: `bssapp:order:draft:new:${txId.value || 'noguid'}`
|
||
)
|
||
|
||
// ===========================================================
|
||
// ✅ AXIOS INSTANCE TANIMI
|
||
// Tüm backend çağrıları bu instance üzerinden geçer.
|
||
// Token otomatik eklenir, 401 durumunda login sayfasına yönlendirir.
|
||
// ===========================================================
|
||
const API_BASE = 'http://localhost:8080'
|
||
|
||
|
||
// İstek öncesi interceptor — token ekleme
|
||
api.interceptors.request.use(cfg => {
|
||
const token = localStorage.getItem('token')
|
||
if (token) cfg.headers.Authorization = `Bearer ${token}`
|
||
return cfg
|
||
})
|
||
|
||
// Yanıt interceptor — 401 durumunda login'e yönlendir
|
||
api.interceptors.response.use(
|
||
r => r,
|
||
err => {
|
||
if (err?.response?.status === 401) {
|
||
localStorage.removeItem('token')
|
||
if (typeof window !== 'undefined') window.location.href = '/login'
|
||
}
|
||
return Promise.reject(err)
|
||
}
|
||
)
|
||
|
||
/* ===========================================================
|
||
🔹 detectBedenGroup — Beden Grubunu Otomatik Tespit Et
|
||
Bu fonksiyon, ürünün "ana grup" ve "kategori" bilgilerine
|
||
göre hangi beden setine (takım, gömlek, pantolon, ayakkabı vs.)
|
||
ait olduğunu belirler. Bu bilgi grid yapısını belirler.
|
||
=========================================================== */
|
||
function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
|
||
// Beden listesi normalize edilir (trim, uppercase, boşsa default ' ')
|
||
const list = Array.isArray(bedenList) && bedenList.length > 0
|
||
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
|
||
: [' ']
|
||
|
||
// Ana grup temizleme: parantez içlerini ve özel karakterleri kaldırır
|
||
const ana = (urunAnaGrubu || '')
|
||
.toUpperCase()
|
||
.trim()
|
||
.replace(/\(.*?\)/g, '') // (Regular Fit) gibi notları siler
|
||
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '') // özel karakterleri temizler
|
||
.replace(/\s+/g, ' ') // fazla boşlukları tek boşluk yapar
|
||
|
||
// Kategori de temizlenir
|
||
const kat = (urunKategori || '').toUpperCase().trim()
|
||
|
||
// ✅ Aksesuar gruplarının listesi
|
||
const aksesuarGruplari = [
|
||
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER',
|
||
'CORAP', 'ÇORAP', 'FULAR', 'MENDIL', 'MENDİL',
|
||
'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI', 'KOL DÜĞMESİ'
|
||
]
|
||
|
||
// ✅ Giyim grupları
|
||
const giyimGruplari = ['GÖMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT', 'TİŞÖRT']
|
||
|
||
// ✅ Harfli beden tespiti: XS, S, M, L, XL, XXL gibi
|
||
const harfliBedenler = ['XS', 'S', 'M', 'L', 'XL', 'XXL', '3XL', '4XL']
|
||
if (list.some(b => harfliBedenler.includes(b))) {
|
||
return 'gom' // gömlek / tişört tarzı gruplar
|
||
}
|
||
|
||
// ⚙️ Eğer aksesuar kelimesi geçiyor ama giyim grubu değilse
|
||
if (
|
||
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
|
||
!giyimGruplari.some(g => ana.includes(g))
|
||
) {
|
||
return 'aksbir'
|
||
}
|
||
|
||
// 🔢 Pantolon + Garson (yaş grubu) özel kuralları
|
||
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
|
||
if (kat.includes('GARSON')) return 'yas'
|
||
|
||
// 🔢 Tamamen sayısal bedenler (örneğin 39–44)
|
||
const allNumeric = list.every(v => /^\d+$/.test(v))
|
||
if (allNumeric) {
|
||
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
|
||
const diffs = nums.slice(1).map((v, i) => v - nums[i])
|
||
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
|
||
}
|
||
|
||
// 🧩 Eğer hiçbiri değilse:
|
||
// Harf içeriyorsa 'gom', değilse 'tak' (takım elbise)
|
||
const sample = list[0]
|
||
if (/[A-Z]/.test(sample)) return 'gom'
|
||
return 'tak'
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 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 }
|
||
}
|
||
}
|
||
|
||
|
||
// 🔹 Aktif ürün grubuna göre uygun seri setlerini dinamik hesaplar
|
||
const activeSeriesOptions = computed(() => {
|
||
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
|
||
const sets = seriMatrix[grpKey] || {}
|
||
return Object.keys(sets).map(k => ({ label: k, value: k, isActive: true }))
|
||
})
|
||
|
||
/* ===========================================================
|
||
🔹 Para Birimi ve Toplam Tutar Hesaplaması
|
||
Sipariş toplamları ve para birimi seçimi burada yönetilir.
|
||
=========================================================== */
|
||
const paraBirimOptions = ['USD', 'EUR', 'TRY'] // Kullanıcıya sunulacak döviz seçenekleri
|
||
|
||
// Grid altındaki “Toplam Tutar” alanı dinamik hesaplanır.
|
||
const toplamTutar = computed(() => {
|
||
const sum = orderStore.totalAmount ? Number(orderStore.totalAmount) : 0
|
||
return isNaN(sum) ? 0 : sum
|
||
})
|
||
|
||
/* ===========================================================
|
||
🔹 Cari Bilgileri
|
||
Cari (müşteri) listesi, arama filtresi ve seçim sonrası
|
||
para birimi kontrolü burada yapılır.
|
||
=========================================================== */
|
||
const selectedCari = ref(null) // Kullanıcının seçtiği cari kodu
|
||
const cariOptions = ref([]) // Tüm cari listesi (backend'den gelir)
|
||
const filteredCariOptions = ref([]) // Arama filtrelenmiş hali
|
||
const loadingCari = ref(false) // Yükleniyor göstergesi
|
||
const cariInfo = ref(null) // Seçilen carinin tüm bilgisi
|
||
|
||
/* ===========================================================
|
||
🔹 Cari Listesini Yükleme Fonksiyonu
|
||
Uygulama açıldığında veya cari seçimi değiştiğinde çağrılır.
|
||
Backend'den /api/customer-list endpoint'ini çağırır.
|
||
=========================================================== */
|
||
async function loadCariList() {
|
||
loadingCari.value = true
|
||
try {
|
||
ensureAuthOrRedirect() // Token kontrolü, gerekirse login yönlendirmesi
|
||
const res = await api.get('/customer-list')
|
||
const data = res?.data
|
||
|
||
// Gelen data farklı formatlarda olabilir, esnek parse yapılır
|
||
if (!data) {
|
||
cariOptions.value = []
|
||
} else if (Array.isArray(data)) {
|
||
cariOptions.value = data
|
||
} else if (Array.isArray(data?.data)) {
|
||
cariOptions.value = data.data
|
||
} else {
|
||
cariOptions.value = []
|
||
}
|
||
|
||
// Filtre listesi de aynı anda güncellenir
|
||
filteredCariOptions.value = cariOptions.value
|
||
console.log('🧾 Cari listesi yüklendi:', cariOptions.value.length)
|
||
} catch (err) {
|
||
console.error('❌ Cari listesi alınamadı:', err)
|
||
$q.notify({ type: 'negative', message: 'Cari listesi yüklenemedi ❌' })
|
||
} finally {
|
||
loadingCari.value = false
|
||
}
|
||
}
|
||
// ===========================================================
|
||
// 🔹 Local Draft Yönetimi
|
||
// ===========================================================
|
||
function saveDraft() {
|
||
const draft = {
|
||
header: {
|
||
OrderDate: form.olusturmaTarihi,
|
||
AverageDueDate: form.tahminiTerminTarihi,
|
||
CurrAccCode: selectedCari.value,
|
||
DocCurrencyCode: form.pb,
|
||
Description: siparisGenelAciklama.value
|
||
},
|
||
lines: summaryRows.value
|
||
}
|
||
localStorage.setItem(DRAFT_KEY.value, JSON.stringify(draft))
|
||
}
|
||
|
||
function loadDraft() {
|
||
const raw = localStorage.getItem(DRAFT_KEY.value)
|
||
if (!raw) return false
|
||
try {
|
||
const { header, lines } = JSON.parse(raw)
|
||
if (header) {
|
||
form.olusturmaTarihi = header.OrderDate || form.olusturmaTarihi
|
||
form.tahminiTerminTarihi = header.AverageDueDate || form.tahminiTerminTarihi
|
||
form.pb = header.DocCurrencyCode || form.pb
|
||
selectedCari.value = header.CurrAccCode || ''
|
||
siparisGenelAciklama.value = header.Description || ''
|
||
}
|
||
if (Array.isArray(lines)) summaryRows.value = [...lines]
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
function clearDraft() {
|
||
localStorage.removeItem(DRAFT_KEY.value)
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 onMounted — İlk Yüklemeler
|
||
Uygulama ilk açıldığında auth kontrolü, store restore,
|
||
cari ve model listesi yükleme işlemleri yapılır.
|
||
=========================================================== */
|
||
/* ===========================================================
|
||
🔹 onMounted — İlk Yüklemeler
|
||
Uygulama ilk açıldığında auth kontrolü, store restore,
|
||
cari ve model listesi yükleme işlemleri yapılır.
|
||
=========================================================== */
|
||
onMounted(async () => {
|
||
ensureAuthOrRedirect()
|
||
|
||
if (orderId) {
|
||
console.log('✏️ Düzenleme modu başlatılıyor:', orderId)
|
||
mode.value = 'edit'
|
||
isEditMode.value = true
|
||
headerId.value = orderId
|
||
await loadOrderById(orderId)
|
||
} else {
|
||
console.log('🆕 Yeni sipariş modu başlatılıyor')
|
||
mode.value = 'new'
|
||
isEditMode.value = false
|
||
form.OrderHeaderID = crypto.randomUUID()
|
||
txId.value = form.OrderHeaderID
|
||
resetForm()
|
||
}
|
||
|
||
await Promise.all([loadCariList(), loadModels()])
|
||
console.log('✅ OrderEntry ekranı hazır — mode:', mode.value)
|
||
})
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 Cari Filtreleme (Arama Kutusu)
|
||
QSelect bileşeninde “use-input” aktif olduğunda çalışır.
|
||
Kullanıcının yazdığı değeri cari listesinde arar (kod + ad bazlı).
|
||
=========================================================== */
|
||
function filterCari(val, update) {
|
||
if (val === '') {
|
||
// Boş arama → tüm cari listesi geri yüklenir
|
||
update(() => (filteredCariOptions.value = cariOptions.value))
|
||
return
|
||
}
|
||
|
||
// Küçük harfe çevirip hem ad hem kod üzerinden arama yap
|
||
update(() => {
|
||
const needle = val.toLowerCase()
|
||
filteredCariOptions.value = cariOptions.value.filter(v =>
|
||
(v.Cari_Ad || '').toLowerCase().includes(needle) ||
|
||
(v.Cari_Kod || '').toLowerCase().includes(needle)
|
||
)
|
||
})
|
||
}
|
||
onMounted(() => {
|
||
// ♻️ Daha önce kaydedilmiş siparişler varsa geri yükle
|
||
orderStore.loadFromStorage()
|
||
|
||
// 💾 Order değişikliklerini izleyip her değişiklikte kaydet
|
||
orderStore.watchOrders()
|
||
|
||
// 💾 LocalStorage geri yüklendikten sonra grid senkronizasyonu
|
||
onMounted(async () => {
|
||
await nextTick()
|
||
if (orderStore.orders && orderStore.orders.length > 0) {
|
||
summaryRows.value = [...orderStore.orders]
|
||
console.log('💾 Grid satırları LocalStorage’dan yüklendi:', summaryRows.value.length)
|
||
} else {
|
||
console.log('ℹ️ LocalStorage boş, grid başlatılmadı.')
|
||
}
|
||
})
|
||
|
||
// 🔄 Store değişiklikleri anlık olarak grid’e yansıt
|
||
watch(
|
||
() => orderStore.orders,
|
||
newOrders => {
|
||
summaryRows.value = [...newOrders]
|
||
},
|
||
{ deep: true, immediate: true }
|
||
)
|
||
|
||
console.log(
|
||
'♻️ LocalStorage geri yükleme tamamlandı. Aktif transaction:',
|
||
orderStore.activeTransactionId || '—'
|
||
)
|
||
})
|
||
|
||
// 🧹 Sayfa kapanmadan önce tekrar yedekle (fail-safe)
|
||
onBeforeUnmount(() => {
|
||
orderStore.saveToStorage()
|
||
console.log('💾 Sayfa kapatılırken veriler son kez kaydedildi.')
|
||
})
|
||
|
||
// 🔄 Reaktif izleme: orderStore.orders değiştiğinde kalıcı yaz
|
||
watch(
|
||
() => orderStore.orders,
|
||
() => orderStore.saveToStorage(),
|
||
{ deep: true }
|
||
)
|
||
/* ===========================================================
|
||
🔹 onMounted: Header Gap Güncelleme
|
||
order-grid-header yüksekliğini ölçüp CSS değişkeni olarak kaydeder.
|
||
Böylece sticky header ile grid gövdesi arasında tam hizalama sağlanır.
|
||
=========================================================== */
|
||
onMounted(() => {
|
||
nextTick(() => { // DOM tamamen yüklensin
|
||
const hdr = document.querySelector('.order-grid-header')
|
||
if (!hdr) {
|
||
console.warn('⚠️ .order-grid-header bulunamadı, ölçüm atlandı.')
|
||
return
|
||
}
|
||
|
||
const updateHeaderGap = () => {
|
||
if (!hdr || !hdr.parentNode) return // ⚠️ güvenlik kontrolü eklendi
|
||
const rect = hdr.getBoundingClientRect()
|
||
const height = rect.height || 0
|
||
const fineAdjust = -height
|
||
document.documentElement.style.setProperty('--header-body-gap', `${fineAdjust}px`)
|
||
console.log('📏 Header yüksekliği ölçüldü:', height, 'gap:', fineAdjust)
|
||
}
|
||
|
||
updateHeaderGap()
|
||
|
||
const resizeObs = new ResizeObserver(() => {
|
||
if (hdr?.parentNode) updateHeaderGap()
|
||
})
|
||
resizeObs.observe(hdr)
|
||
|
||
const onResize = () => {
|
||
if (hdr?.parentNode) updateHeaderGap()
|
||
}
|
||
window.addEventListener('resize', onResize)
|
||
|
||
onBeforeUnmount(() => {
|
||
resizeObs.disconnect()
|
||
window.removeEventListener('resize', onResize)
|
||
})
|
||
})
|
||
})
|
||
|
||
/* ===========================================================
|
||
🔹 Cari Seçimi (onCariChange)
|
||
Kullanıcı cari seçtiğinde hem cari bilgisi yüklenir,
|
||
hem de ilgili para birimi (PB) otomatik olarak set edilir.
|
||
=========================================================== */
|
||
async function onCariChange(kod) {
|
||
// 1️⃣ Cari bilgiyi lokal listeden bul
|
||
cariInfo.value = cariOptions.value.find(c => c.Cari_Kod === kod) || null
|
||
|
||
// 2️⃣ Varsayılan PB USD (fallback)
|
||
let pb = cariInfo.value?.Doviz_Cinsi || 'USD'
|
||
|
||
try {
|
||
// 3️⃣ Eğer local veride Doviz_Cinsi yoksa backend'den çağır
|
||
if (!cariInfo.value?.Doviz_Cinsi && kod) {
|
||
const res = await api.get('/customer-detail', { params: { code: kod } })
|
||
const data = res?.data || {}
|
||
|
||
// Backend farklı property isimleri döndürebileceği için esnek kontrol
|
||
if (data.Doviz_Cinsi || data.ParaBirimi || data.currency) {
|
||
pb = data.Doviz_Cinsi || data.ParaBirimi || data.currency
|
||
console.log(`💱 Cari (${kod}) para birimi backend'den alındı: ${pb}`)
|
||
} else {
|
||
console.log(`💵 Cari (${kod}) için PB bulunamadı, USD olarak atanıyor.`)
|
||
pb = 'USD'
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('⚠️ Cari detay alınamadı, USD olarak devam ediliyor.', err)
|
||
pb = 'USD'
|
||
}
|
||
|
||
// 4️⃣ Global ve form seviyesinde PB'yi güncelle
|
||
aktifPB.value = pb
|
||
form.pb = pb
|
||
|
||
// 5️⃣ Eğer model seçiliyse, PB değiştiği için min fiyatı yeniden çek
|
||
if (form.model) {
|
||
try {
|
||
await fetchMinPrice()
|
||
} catch (e) {
|
||
console.warn('⚠️ Min fiyat yenilenemedi:', e)
|
||
}
|
||
}
|
||
|
||
// 6️⃣ Bilgi logu
|
||
console.log(`💱 Aktif PB setlendi: ${pb}`)
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Model Listesi (Ürün Seçimi)
|
||
Backend'den /api/products endpoint’inden tüm ürün kodları çekilir.
|
||
=========================================================== */
|
||
const modelOptions = ref([]) // Model seçenekleri
|
||
const filteredModelOptions = ref([]) // Arama ile filtrelenmiş hali
|
||
const loadingModels = ref(false) // Spinner için flag
|
||
|
||
async function loadModels() {
|
||
loadingModels.value = true
|
||
try {
|
||
ensureAuthOrRedirect()
|
||
const res = await api.get('/products')
|
||
const arr = res?.data || []
|
||
|
||
// Backend'den ProductCode alanı çekilir
|
||
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ı ❌' })
|
||
} finally {
|
||
loadingModels.value = false
|
||
}
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 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)
|
||
Yeni model seçildiğinde renkler, ürün bilgileri, min fiyat,
|
||
stok ve beden grubu eksiksiz yenilenir; açıklama ve adet korunur.
|
||
=========================================================== */
|
||
/* ===========================================================
|
||
🔹 MODEL SEÇİMİ (onModelChange)
|
||
Yeni model seçildiğinde renkler, ürün bilgileri, min fiyat,
|
||
stok ve beden grubu eksiksiz yenilenir; açıklama ve adet korunur.
|
||
=========================================================== */
|
||
async function onModelChange(modelCode) {
|
||
if (!modelCode) {
|
||
console.warn('⚠️ Model kodu boş, sorgu yapılmadı.')
|
||
return
|
||
}
|
||
|
||
// 🧩 Önceki değerleri yedekle (korunacak alanlar)
|
||
const keep = {
|
||
aciklama: form.aciklama,
|
||
bedenler: [...form.bedenler],
|
||
bedenLabels: [...form.bedenLabels],
|
||
fiyat: form.fiyat,
|
||
adet: form.adet,
|
||
tutar: form.tutar
|
||
}
|
||
|
||
try {
|
||
ensureAuthOrRedirect()
|
||
|
||
// 🎨 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 || {}
|
||
|
||
Object.assign(form, {
|
||
model: modelCode,
|
||
urunAnaGrubu: d.UrunAnaGrubu || d.ProductGroup || '',
|
||
urunAltGrubu: d.UrunAltGrubu || d.ProductSubGroup || '',
|
||
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,
|
||
bedenLabels: keep.bedenLabels,
|
||
bedenler: keep.bedenler
|
||
})
|
||
|
||
console.log('📦 Model detayları yüklendi:', d.UrunAnaGrubu, d.Fit1)
|
||
|
||
// 💰 3️⃣ Min fiyatı yükle
|
||
await fetchMinPrice()
|
||
|
||
// ⚙️ 4️⃣ Renk bulunmazsa doğrudan bedenleri yükle
|
||
if (!renkOptions.value.length) {
|
||
await loadProductSizes(true)
|
||
}
|
||
|
||
// 🧮 5️⃣ Gridde mevcut kombinasyon varsa düzenleme moduna al
|
||
await openExistingCombination()
|
||
|
||
// ✅ 6️⃣ Yeni model sonrası otomatik stok/beden yükle (forceRefresh)
|
||
await handleNewCombination()
|
||
|
||
$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)
|
||
Kullanıcı model seçtikten sonra 1. rengi seçtiğinde:
|
||
- 2. renk seçenekleri sıfırlanır
|
||
- 2. renk listesi backend'den yüklenir
|
||
- Eğer 2. renk tanımı yoksa doğrudan bedenler yüklenir
|
||
=========================================================== */
|
||
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 {
|
||
ensureAuthOrRedirect()
|
||
|
||
// 🎨 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 loadProductSizes(true)
|
||
}
|
||
|
||
await handleNewCombination()
|
||
} catch (err) {
|
||
console.error('❌ 1. renk sonrası hata:', err)
|
||
}
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 2. RENK SEÇİMİ (onColor2Change)
|
||
Kullanıcı 2. renk seçtiğinde beden/stok sorgusu yeniden yapılır.
|
||
Ayrıca kombinasyon gridde varsa form otomatik doldurulur.
|
||
=========================================================== */
|
||
async function onColor2Change(colorCode2) {
|
||
if (typeof colorCode2 === 'object' && colorCode2?.value) {
|
||
colorCode2 = colorCode2.value
|
||
}
|
||
|
||
form.renk2 = colorCode2 || ''
|
||
|
||
try {
|
||
ensureAuthOrRedirect()
|
||
|
||
// 2. renk seçildikten sonra stok/beden yükle
|
||
await loadProductSizes(true)
|
||
|
||
// Aynı kombinasyon varsa düzenleme moduna al
|
||
await openExistingCombination()
|
||
|
||
await handleNewCombination()
|
||
} catch (err) {
|
||
console.error('❌ 2. renk sonrası hata:', err)
|
||
}
|
||
}
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 MODEL + PB Bazlı Minimum Fiyat
|
||
Backend’de her model + para birimi kombinasyonu için
|
||
minimum satış fiyatı tutulur. Kullanıcı fiyat girdiğinde
|
||
bu alt limitin altına inmemesi sağlanır.
|
||
=========================================================== */
|
||
async function fetchMinPrice() {
|
||
if (!form.model || !form.pb) {
|
||
console.warn('⚠️ Fiyat sorgusu için model veya PB eksik.')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// Store’daki fetchMinPrice fonksiyonu backend’den veri çeker
|
||
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
|
||
if (priceData) {
|
||
// Döviz bazlı fiyatlar + TL karşılığı (rateToTRY) setlenir
|
||
form.minFiyat = Number(priceData.price || 0)
|
||
form.kur = Number(priceData.rateToTRY || 1)
|
||
form.minFiyatTRY = Number(priceData.priceTRY || 0)
|
||
console.log(
|
||
`💰 Fiyatlar yüklendi: ${form.minFiyat} ${form.pb} (${form.minFiyatTRY.toFixed(2)} TRY)`
|
||
)
|
||
} else {
|
||
// Backend boş döndüyse default sıfırla
|
||
form.minFiyat = 0
|
||
form.kur = 1
|
||
form.minFiyatTRY = 0
|
||
}
|
||
} catch (err) {
|
||
console.error('❌ Min fiyat alınamadı:', err)
|
||
form.minFiyat = 0
|
||
}
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Beden / Stok Yükleyici (loadProductSizes)
|
||
Bu fonksiyon ERP’den renk+model bazlı beden ve stok bilgisini çeker.
|
||
Ayrıca MSSQL stoklarıyla merge eder ve cache’ler.
|
||
=========================================================== */
|
||
const sizeCache = ref({}) // Tekrar sorguları engellemek için cache
|
||
const bedenStock = ref([]) // Görsel tablo için stok listesi
|
||
const stockMap = ref({}) // { "48": 12, "50": 7, ... } şeklinde key-value map
|
||
|
||
/* ===========================================================
|
||
🔹 Beden / Stok Yükleme Fonksiyonu
|
||
forceRefresh = true → cache'i yok say, API'den güncel çek
|
||
=========================================================== */
|
||
async function loadProductSizes(forceRefresh = false) {
|
||
if (!form.model) {
|
||
console.warn('⚠️ Beden yüklenemedi: model seçilmemiş.')
|
||
return
|
||
}
|
||
|
||
const colorKey = (form.renk && form.renk.trim() !== '') ? form.renk.trim() : 'nocolor'
|
||
const color2Key = (form.renk2 && form.renk2.trim() !== '') ? form.renk2.trim() : 'no2color'
|
||
const key = `${form.model}_${colorKey}_${color2Key}`
|
||
|
||
console.log('🧩 loadProductSizes → key:', key, '| forceRefresh:', forceRefresh)
|
||
|
||
// 💾 Cache’den veri varsa ve forceRefresh=false ise cache kullan
|
||
if (!forceRefresh && sizeCache.value[key]) {
|
||
console.log(`💾 Cache’den yüklendi: ${key}`)
|
||
const cached = sizeCache.value[key]
|
||
|
||
// ✅ Mevcut adetleri koru
|
||
const previousMap = {}
|
||
form.bedenLabels?.forEach((lbl, i) => {
|
||
previousMap[lbl] = Number(form.bedenler?.[i] || 0)
|
||
})
|
||
|
||
form.bedenLabels = cached.labels
|
||
form.bedenler = form.bedenLabels.map(lbl => Number(previousMap[lbl] || 0))
|
||
bedenStock.value = cached.stockArray
|
||
stockMap.value = { ...cached.stockMap }
|
||
|
||
// ⚡ Cache yüklenmiş olsa bile MSSQL stoklarını güncelle (edit mode için)
|
||
console.log('🔄 Cache sonrası MSSQL stokları tazeleniyor...')
|
||
await loadOrderInventory(true)
|
||
|
||
return
|
||
}
|
||
|
||
try {
|
||
ensureAuthOrRedirect()
|
||
|
||
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('📦 Beden/stok sorgusu gönderiliyor:', params)
|
||
const res = await api.get('/product-colorsize', { params })
|
||
const data = res?.data || []
|
||
|
||
console.log(`📦 Gelen beden/stok kayıt sayısı: ${data.length}`)
|
||
if (!Array.isArray(data) || data.length === 0) {
|
||
console.warn('⚪ Bu kombinasyon için tanımlı beden bulunamadı.')
|
||
form.bedenLabels = []
|
||
form.bedenler = []
|
||
bedenStock.value = []
|
||
stockMap.value = {}
|
||
return
|
||
}
|
||
|
||
// 🔹 Gelen beden kodlarını normalize et
|
||
const bedenList = Array.from(
|
||
new Set(
|
||
data.map(x => x.item_dim1_code?.trim() || x.ItemDim1Code?.trim()).filter(Boolean)
|
||
)
|
||
)
|
||
|
||
// 🔹 Önceki adetleri koru
|
||
const previousMap = {}
|
||
form.bedenLabels?.forEach((lbl, i) => {
|
||
previousMap[lbl] = Number(form.bedenler?.[i] || 0)
|
||
})
|
||
|
||
// 🔹 Aktif beden grubu belirle
|
||
const grpKey = detectBedenGroup(bedenList, form.urunAnaGrubu, form.kategori)
|
||
const grp = schemaByKey.value[grpKey] || schemaByKey.value.tak
|
||
form.bedenLabels = grp.values
|
||
form.bedenler = form.bedenLabels.map(lbl => Number(previousMap[lbl] || 0))
|
||
|
||
console.log(`✅ Aktif beden grubu: ${grp.title} (${grpKey})`)
|
||
|
||
// 🔹 ERP stoklarını işle
|
||
const stockArray = data.map(x => ({
|
||
beden: x.item_dim1_code?.trim() || x.ItemDim1Code?.trim(),
|
||
stok: Number(
|
||
x.kullanilabilir_envanter ||
|
||
x.Kullanilabilir_Envanter ||
|
||
x.stock ||
|
||
0
|
||
)
|
||
}))
|
||
const stockMapLocal = {}
|
||
for (const s of stockArray) stockMapLocal[s.beden] = s.stok
|
||
stockMap.value = { ...stockMapLocal }
|
||
bedenStock.value = [...stockArray]
|
||
|
||
console.log(`🧮 İlk stok verisi işlendi (${stockArray.length} beden)`)
|
||
|
||
// 🔹 MSSQL stoklarını merge et
|
||
await loadOrderInventory(true)
|
||
|
||
// 💾 Cache güncelle
|
||
sizeCache.value[key] = {
|
||
labels: [...form.bedenLabels],
|
||
stockArray: [...bedenStock.value],
|
||
stockMap: { ...stockMap.value }
|
||
}
|
||
|
||
console.log(`✅ Cache güncellendi: ${key}`)
|
||
} catch (err) {
|
||
console.error('❌ Beden/stok verisi yüklenirken hata oluştu:', err)
|
||
$q.notify({ type: 'negative', message: 'Beden/stok verisi alınamadı ❌' })
|
||
}
|
||
}
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 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 {
|
||
ensureAuthOrRedirect()
|
||
|
||
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)
|
||
|
||
// 1️⃣ Normalize et
|
||
const invMap = {}
|
||
for (const x of data) {
|
||
let beden = (x.Beden || x.beden || '').trim()
|
||
if (beden === '') beden = ' '
|
||
const stokDeger =
|
||
x.KullanilabilirEnvanter ??
|
||
x.kullanilabilir_envanter ??
|
||
x.Kullanilabilir_Envanter ??
|
||
null
|
||
if (stokDeger != null) invMap[beden] = Number(stokDeger)
|
||
}
|
||
|
||
// 2️⃣ Form bedenlerine göre map oluştur
|
||
const newMap = {}
|
||
for (const lbl of form.bedenLabels || []) {
|
||
const key = lbl?.trim() === '' ? ' ' : lbl.trim()
|
||
if (invMap[key] != null) newMap[lbl] = invMap[key]
|
||
}
|
||
|
||
// 3️⃣ Merge veya replace
|
||
if (merge && stockMap.value) {
|
||
for (const lbl of Object.keys(newMap)) {
|
||
// sadece yeni stok bilgisi varsa güncelle
|
||
if (newMap[lbl] != null && !isNaN(newMap[lbl])) {
|
||
stockMap.value[lbl] = newMap[lbl]
|
||
}
|
||
}
|
||
} else {
|
||
stockMap.value = { ...newMap }
|
||
}
|
||
|
||
// 4️⃣ bedenStock listesini senkronize et
|
||
bedenStock.value = Object.keys(stockMap.value).map(k => ({
|
||
beden: k,
|
||
stok: stockMap.value[k]
|
||
}))
|
||
|
||
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'
|
||
})
|
||
}
|
||
}
|
||
function formatDate(v) {
|
||
if (!v) return '—'
|
||
try {
|
||
// ISO tarih ise parçala
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
||
const [y, m, d] = v.split('-')
|
||
return `${d}.${m}.${y}`
|
||
}
|
||
// diğer olası formatlar
|
||
const date = new Date(v)
|
||
if (isNaN(date.getTime())) return '—'
|
||
const dd = String(date.getDate()).padStart(2, '0')
|
||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||
const yyyy = date.getFullYear()
|
||
return `${dd}.${mm}.${yyyy}`
|
||
} catch {
|
||
return '—'
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 applyTerminToRows — Tahmini Termin Tarihini Grid Satırlarına Aktar
|
||
Formdaki tahmini termin tarihi değiştiğinde,
|
||
griddeki tüm satırlara aynı tarih işlenir (boş olanlara veya yeni eklenenlere)
|
||
=========================================================== */
|
||
function applyTerminToRows(dateStr) {
|
||
if (!dateStr || !Array.isArray(summaryRows.value)) return
|
||
summaryRows.value = summaryRows.value.map(r => {
|
||
if (!r.terminTarihi || r.terminTarihi === '') {
|
||
return { ...r, terminTarihi: dateStr }
|
||
}
|
||
return r
|
||
})
|
||
}
|
||
|
||
// 🔹 Watcher: Formdaki tahmini termin tarihi değiştiğinde tüm satırlara uygula
|
||
// ÜST formdaki tahmini termin değişince:
|
||
watch(
|
||
() => form.tahminiTerminTarihi,
|
||
(yeni, eski) => {
|
||
// 1) Önce editördeki tarih alanını güncelle
|
||
// Eğer kullanıcı editörde manuel farklı bir tarih tutmuyorsa, senkronla.
|
||
if (!form.terminTarihi || form.terminTarihi === eski) {
|
||
form.terminTarihi = yeni || ''
|
||
}
|
||
|
||
// 2) İstersen griddeki BOŞ termin alanlarına da uygula
|
||
// (dolu satırlara dokunmaz)
|
||
applyTerminToRows(yeni)
|
||
}
|
||
)
|
||
|
||
|
||
/*=========================================================== */
|
||
const selectedSeriSet = ref(null) // Seçili seri seti (ör: “46-58 seri”)
|
||
const seriMultiplier = ref(1) // Çarpan (ör: 2 → her bedene 2 ekler)
|
||
|
||
/* ===========================================================
|
||
🔹 Seri Uygulama (Tüm bedenlere aynı değeri atar)
|
||
Kullanıcı “ALL1” veya “ALL2” gibi genel seri modunu seçerse
|
||
tüm bedenlere aynı miktar yazılır.
|
||
=========================================================== */
|
||
function applySeri(f) {
|
||
f.bedenler = (f.bedenLabels || []).map(() =>
|
||
f.seri === 'ALL2' ? 2 : f.seri === 'ALL1' ? 1 : 0
|
||
)
|
||
updateTotals(f)
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Toplam Güncelleme Fonksiyonu
|
||
Beden girişlerinde adet veya fiyat değiştiğinde toplam tutarı hesaplar.
|
||
=========================================================== */
|
||
function updateTotals(f) {
|
||
f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
|
||
const fiyat = Number(f.fiyat) || 0
|
||
f.tutar = (f.adet * fiyat).toFixed(2)
|
||
}
|
||
|
||
// dışarıya alınmalı:
|
||
watch([includeKDV, toplamTutar], ([aktif, toplam]) => {
|
||
manualKDV.value = aktif ? Number(toplam || 0) * kdvOrani : 0
|
||
})
|
||
|
||
const toplamKDVli = computed(() => {
|
||
return Number(toplamTutar.value || 0) + (includeKDV.value ? Number(manualKDV.value || 0) : 0)
|
||
})
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 Seri Seti Uygulama (applySeriSet)
|
||
Kullanıcı bir seri seti seçtiğinde, o setin beden-adet oranlarını
|
||
ilgili gruptan alır ve formdaki bedenlere ekler.
|
||
=========================================================== */
|
||
function applySeriSet() {
|
||
if (!selectedSeriSet.value) return
|
||
|
||
// 🔹 Aktif beden grubu anahtarını tespit et
|
||
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
|
||
|
||
// 🔹 Seçilen seri setinin anahtarını al (object veya string olabilir)
|
||
const setKey =
|
||
typeof selectedSeriSet.value === 'object'
|
||
? (selectedSeriSet.value.value || selectedSeriSet.value.label)
|
||
: selectedSeriSet.value
|
||
|
||
// 🔹 Seri pattern’ini bul
|
||
const pattern = seriMatrix[grpKey]?.[setKey] || {}
|
||
const mult = Number(seriMultiplier.value) || 1
|
||
|
||
// 🔸 EKLEYEREK uygula — mevcut değerlere çarpanlı ekleme yapar
|
||
form.bedenler = form.bedenLabels.map((lbl, idx) => {
|
||
const current = Number(form.bedenler[idx] || 0)
|
||
const inc = Number(pattern[lbl] || 0) * mult
|
||
return current + inc
|
||
})
|
||
|
||
// 🔹 Toplam adet ve tutarı güncelle
|
||
updateTotals(form)
|
||
|
||
// 🔔 Kullanıcıya bilgi bildirimi
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: `Seri seti "${setKey}" başarıyla eklendi ✅`
|
||
})
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Editing ve Satır İşlemleri
|
||
summaryRows, ekranda gridde görülen satırların tam listesidir.
|
||
editingIndex aktif olarak düzenlenen satırı tutar.
|
||
=========================================================== */
|
||
|
||
/* ===========================================================
|
||
🔹 Form → Grid Satırı Dönüştürücü (v2)
|
||
Kullanıcının formda yaptığı girişleri griddeki summaryRows
|
||
formatına dönüştürür. Ayrıca formdaki tahmini termin tarihini
|
||
grid satırına "terminTarihi" olarak işler.
|
||
=========================================================== */
|
||
function toSummaryRowFromForm() {
|
||
// 🔸 Beden grubu anahtarını tespit et
|
||
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
|
||
|
||
// 🔸 Beden map oluştur
|
||
const bedenMap = {}
|
||
for (const lbl of form.bedenLabels) {
|
||
const index = form.bedenLabels.indexOf(lbl)
|
||
bedenMap[lbl] = Number(form.bedenler[index] || 0)
|
||
}
|
||
|
||
// 🔸 Grid satır objesi
|
||
const row = {
|
||
id: Date.now(), // geçici ID
|
||
model: form.model,
|
||
renk: form.renk,
|
||
renk2: form.renk2,
|
||
urunAnaGrubu: form.urunAnaGrubu,
|
||
urunAltGrubu: form.urunAltGrubu,
|
||
aciklama: form.aciklama,
|
||
fiyat: Number(form.fiyat || 0),
|
||
pb: form.pb || aktifPB.value || 'USD',
|
||
adet: Number(form.adet || 0),
|
||
tutar: Number(form.tutar || 0),
|
||
|
||
// 🔹 Grup yapısı
|
||
grpKey,
|
||
bedenMap: { [grpKey]: { ...bedenMap } },
|
||
|
||
// 🔹 Yeni: Tahmini termin tarihi formdan alınır
|
||
terminTarihi: form.terminTarihi || form.tahminiTerminTarihi || ''
|
||
|
||
}
|
||
|
||
console.log('📄 Grid satırı oluşturuldu:', row)
|
||
return row
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 updateRow (Grid satırını güncelle veya ekle)
|
||
- Edit mode aktifse mevcut satırı günceller
|
||
- Yeni kayıt ise summaryRows’a ekler
|
||
- Adet, fiyat, tutar, aciklama, pb, bedenMap alanlarını senkronize eder
|
||
=========================================================== */
|
||
async function updateRow() {
|
||
try {
|
||
ensureAuthOrRedirect()
|
||
|
||
// 1️⃣ Temel doğrulama
|
||
if (!form.model) {
|
||
$q.notify({ type: 'warning', message: 'Lütfen model seçin ⚠️' })
|
||
return
|
||
}
|
||
|
||
// 2️⃣ Toplamları güncelle
|
||
updateTotals(form)
|
||
|
||
// 3️⃣ Formu summaryRow formatına çevir
|
||
const row = toSummaryRowFromForm()
|
||
|
||
// 4️⃣ Edit mode aktifse → mevcut satırı güncelle
|
||
if (editingIndex.value !== -1 && summaryRows.value[editingIndex.value]) {
|
||
const idx = editingIndex.value
|
||
const existing = summaryRows.value[idx]
|
||
|
||
console.log(`✏️ updateRow çalıştı → ID: ${existing.id || '(geçici)'}`)
|
||
|
||
// 🔹 Mevcut satır verilerini koruyarak güncelle
|
||
summaryRows.value[idx] = {
|
||
...existing,
|
||
...row, // form’daki yeni değerleri uygula
|
||
id: existing.id || row.id, // ID aynı kalmalı
|
||
grpKey: existing.grpKey || row.grpKey,
|
||
bedenMap: row.bedenMap, // beden adetleri
|
||
fiyat: form.fiyat,
|
||
pb: form.pb,
|
||
tutar: form.tutar,
|
||
aciklama: form.aciklama,
|
||
adet: form.adet
|
||
}
|
||
|
||
// 🧮 Grup ve toplamlar yeniden hesaplanır (recomputed)
|
||
updateTotals(form)
|
||
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: 'Satır başarıyla güncellendi ✅',
|
||
position: 'top-right'
|
||
})
|
||
|
||
console.log('✅ Satır güncellendi:', summaryRows.value[idx])
|
||
}
|
||
else {
|
||
// 5️⃣ Yeni satır ekleme
|
||
summaryRows.value.push(row)
|
||
console.log('🆕 Yeni satır eklendi:', row)
|
||
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: 'Yeni kombinasyon eklendi 🧩',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
|
||
// 6️⃣ Formu temizle (sadece yeni ekleme sonrası)
|
||
if (editingIndex.value === -1) {
|
||
resetForm(true)
|
||
} else {
|
||
// Edit sonrası formu bırak ama editing’i kapat
|
||
editingIndex.value = -1
|
||
orderStore.selected = null
|
||
}
|
||
|
||
console.log('🧾 Toplam satır sayısı:', summaryRows.value.length)
|
||
} catch (err) {
|
||
console.error('❌ Satır güncellenirken hata oluştu:', err)
|
||
$q.notify({ type: 'negative', message: 'Satır güncellenemedi ❌' })
|
||
}
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 openExistingCombination
|
||
Formda seçilen model + renk + renk2 kombinasyonu zaten gridde varsa,
|
||
o satırı düzenleme moduna alır ve formu otomatik doldurur.
|
||
Yoksa formu temizleyip yeni girişe hazırlar.
|
||
=========================================================== */
|
||
/* ===========================================================
|
||
🔹 openExistingCombination (Zero-Stock Safe v3)
|
||
Edit modunda stokların 0 görünme hatası çözülmüş versiyon.
|
||
Reactive flush + parametre doğrulaması içerir.
|
||
=========================================================== */
|
||
async function openExistingCombination() {
|
||
if (!form.model) return
|
||
|
||
// 🔍 1️⃣ Aynı kombinasyon gridde var mı?
|
||
const idx = findExistingIndexByForm()
|
||
if (idx === -1) {
|
||
// 🆕 Yeni kombinasyon
|
||
editingIndex.value = -1
|
||
orderStore.selected = null
|
||
|
||
Object.assign(form, {
|
||
adet: 0,
|
||
fiyat: 0,
|
||
tutar: 0,
|
||
pb: aktifPB.value || 'USD'
|
||
})
|
||
console.log('🆕 Yeni kombinasyon → Form temizlendi')
|
||
return
|
||
}
|
||
|
||
// 🔹 2️⃣ Mevcut satırı bul
|
||
const r = summaryRows.value[idx]
|
||
editingIndex.value = idx
|
||
orderStore.selected = { ...r }
|
||
|
||
// 🔒 Edit modda aktif model ve renk seçeneklerini sabitle
|
||
renkOptions.value = [{ label: r.renk, value: r.renk }]
|
||
renkOptions2.value = r.renk2
|
||
? [{ label: r.renk2, value: r.renk2 }]
|
||
: []
|
||
|
||
// 🔹 3️⃣ Formu doldur
|
||
Object.assign(form, {
|
||
model: r.model,
|
||
renk: r.renk,
|
||
renk2: r.renk2,
|
||
urunAnaGrubu: r.urunAnaGrubu,
|
||
urunAltGrubu: r.urunAltGrubu,
|
||
aciklama: r.aciklama,
|
||
fiyat: Number(r.fiyat || 0),
|
||
pb: r.pb || aktifPB.value || 'USD',
|
||
adet: Number(r.adet || 0),
|
||
tutar: Number(r.tutar || 0)
|
||
})
|
||
|
||
// ✅ 3B️⃣ GRIDDEKİ TERMİN → EDİTÖR
|
||
form.terminTarihi = r.terminTarihi || form.terminTarihi || form.tahminiTerminTarihi || ''
|
||
|
||
// 🔹 4️⃣ Beden grubunu geri yükle
|
||
const key = r.grpKey || activeGroupKeyForRow(r)
|
||
const grp = schemaByKey.value[key]
|
||
form.bedenLabels = grp?.values || []
|
||
|
||
const savedMap = r.bedenMap?.[key] || {}
|
||
form.bedenler = form.bedenLabels.map(lbl => Number(savedMap[lbl] || 0))
|
||
|
||
// ✅ 5️⃣ Stokları yeniden getir (forceRefresh = true)
|
||
try {
|
||
console.log('🔄 Stoklar tazeleniyor (edit mode)...')
|
||
await nextTick()
|
||
await new Promise(resolve => setTimeout(resolve, 250))
|
||
await nextTick()
|
||
|
||
console.log('🧩 loadProductSizes çağrısı öncesi:', {
|
||
model: form.model,
|
||
renk: form.renk,
|
||
renk2: form.renk2
|
||
})
|
||
|
||
if (!form.model || !form.renk) {
|
||
console.warn('⚠️ Model veya renk eksik, stok çağrısı atlanıyor.', form)
|
||
} else {
|
||
await loadProductSizes(true)
|
||
const stoklar = Object.values(stockMap.value || {})
|
||
console.log(`📦 Güncel stoklar: ${stoklar.join(', ')}`)
|
||
if (stoklar.length && stoklar.every(v => Number(v) === 0)) {
|
||
console.warn('⚠️ Backend 0 stok döndürdü → form:', form)
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: '⚠️ Bu kombinasyon için stok bulunamadı (0).',
|
||
position: 'top-right'
|
||
})
|
||
} else {
|
||
console.log('✅ Stoklar başarıyla yüklendi.')
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('❌ Stok bilgisi yenilenemedi:', err)
|
||
}
|
||
|
||
// 💱 Para birimi doğrulaması
|
||
if (!form.pb || form.pb === '') {
|
||
form.pb = aktifPB.value || 'USD'
|
||
console.log('💱 Para birimi formda eksikti, otomatik setlendi:', form.pb)
|
||
}
|
||
|
||
// 💰 Minimum fiyat kontrolü
|
||
try {
|
||
if (form.model && form.pb) {
|
||
console.log('💰 Min fiyat kontrolü başlatılıyor...')
|
||
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
|
||
const minFiyat = Number(priceData?.price || 0)
|
||
if (minFiyat > 0 && form.fiyat < minFiyat) {
|
||
form.fiyat = minFiyat
|
||
form.tutar = (form.adet || 0) * (form.fiyat || 0)
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: `Fiyat min. seviyeye güncellendi (${minFiyat.toLocaleString('tr-TR')} ${form.pb})`,
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('⚠️ Fiyat bilgisi yenilenemedi:', e)
|
||
}
|
||
|
||
// 🔢 Toplam güncelle
|
||
updateTotals(form)
|
||
|
||
// 💬 Kullanıcıya bilgi ver
|
||
$q.notify({
|
||
type: 'info',
|
||
message: 'Mevcut kombinasyon düzenleme moduna alındı ✏️',
|
||
position: 'top-right'
|
||
})
|
||
|
||
console.log(
|
||
`✏️ Editör aktif → model=${r.model}, renk=${r.renk || '-'}, renk2=${r.renk2 || '-'}, pb=${form.pb}, termin=${form.terminTarihi}`
|
||
)
|
||
}
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 Yeni Kombinasyon Seçimi (Model / Renk / 2. Renk)
|
||
Kullanıcı yeni model veya renk seçtiğinde stokların 0 görünmesini önler.
|
||
Vue flush tamamlandıktan sonra loadProductSizes(true) çağrılır.
|
||
=========================================================== */
|
||
async function handleNewCombination() {
|
||
if (!form.model) {
|
||
console.warn('⚠️ Model seçilmeden stok yüklenemez.')
|
||
return
|
||
}
|
||
|
||
console.log('🆕 Yeni kombinasyon seçildi:', {
|
||
model: form.model,
|
||
renk: form.renk,
|
||
renk2: form.renk2
|
||
})
|
||
|
||
try {
|
||
// 🧠 Reaktivite flush bitene kadar bekle
|
||
await nextTick()
|
||
await new Promise(resolve => setTimeout(resolve, 250))
|
||
await nextTick()
|
||
|
||
// ⚙️ Gereksiz çağrıları önle
|
||
if (!form.model || !form.renk) {
|
||
console.warn('⚠️ Model veya renk eksik, loadProductSizes atlandı.')
|
||
return
|
||
}
|
||
|
||
console.log('🧩 loadProductSizes çağrılıyor (handleNewCombination)...')
|
||
await loadProductSizes(true)
|
||
|
||
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('✅ Stoklar başarıyla yüklendi (yeni kombinasyon).')
|
||
}
|
||
|
||
// 🔹 Gridde varsa düzenleme moduna al
|
||
await openExistingCombination()
|
||
} catch (err) {
|
||
console.error('❌ handleNewCombination hatası:', err)
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: 'Stok bilgisi alınamadı ❌',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
/* ===========================================================
|
||
🔹 useComboWatcher — Model / Renk / Renk2 değişimlerini izler
|
||
Tek bir yardımcı fonksiyonla handleNewCombination çağrısını yönetir.
|
||
Parametre: hangi alan değişti (model, renk, renk2)
|
||
=========================================================== */
|
||
/* ===========================================================
|
||
🔹 useComboWatcher v3 (Yeni Satır Güvenli)
|
||
Model / Renk / Renk2 değişimlerinde reaktif flush sonrası
|
||
otomatik stok yükleme + mevcut satır koruma.
|
||
=========================================================== */
|
||
function useComboWatcher(type, handler) {
|
||
return async (val) => {
|
||
try {
|
||
console.log(`🎯 useComboWatcher tetiklendi → ${type}:`, val)
|
||
const isNewRow = editingIndex.value === -1
|
||
const prevModel = form.model
|
||
|
||
// 1️⃣ İlgili handler'ı (ör. onModelChange) çalıştır
|
||
if (typeof handler === 'function') {
|
||
await handler(val)
|
||
}
|
||
|
||
// 2️⃣ Vue flush tamamlanmasını bekle
|
||
await nextTick()
|
||
await new Promise(resolve => setTimeout(resolve, 200))
|
||
await nextTick()
|
||
|
||
// 3️⃣ Türüne göre zinciri yürüt
|
||
if (type === 'model') {
|
||
if (isNewRow || form.model !== prevModel) {
|
||
// 🗓️ Yeni satır açılıyorsa, editörde termin boşsa default terminle doldur
|
||
if (!form.terminTarihi || form.terminTarihi === '') {
|
||
form.terminTarihi = form.tahminiTerminTarihi || ''
|
||
}
|
||
console.log('🆕 Yeni satır veya model değişimi → stok yükleniyor...')
|
||
await handleNewCombination()
|
||
}
|
||
} else if (type === 'renk' || type === 'renk2') {
|
||
// 🗓️ Editörde termin boşsa default terminle doldur
|
||
if (!form.terminTarihi || form.terminTarihi === '') {
|
||
form.terminTarihi = form.tahminiTerminTarihi || ''
|
||
}
|
||
await handleNewCombination()
|
||
}
|
||
|
||
console.log(`✅ useComboWatcher(${type}) tamamlandı.`)
|
||
} catch (err) {
|
||
console.error(`❌ useComboWatcher(${type}) hatası:`, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Satır Düzenleme (Manuel Edit – stok güncelleme dahil)
|
||
Grid’deki bir satırı tıklayınca formu o satırla doldurur.
|
||
Termin: GRID → EDİTÖR senkronu eklendi.
|
||
=========================================================== */
|
||
const editRow = async (row, localIndex) => {
|
||
const globalIndex = summaryRows.value.findIndex(r =>
|
||
r.model === row.model &&
|
||
r.renk === row.renk &&
|
||
r.renk2 === row.renk2 &&
|
||
r.aciklama === row.aciklama
|
||
)
|
||
|
||
if (globalIndex === -1) {
|
||
console.warn('⚠️ Editlenecek satır bulunamadı.')
|
||
return
|
||
}
|
||
|
||
editingIndex.value = globalIndex
|
||
orderStore.selected = { ...row }
|
||
|
||
// 🔒 Edit modda seçili renkleri kilitle (seçenek olarak satırdaki değerleri koy)
|
||
renkOptions.value = row.renk ? [{ label: row.renk, value: row.renk }] : []
|
||
renkOptions2.value = row.renk2 ? [{ label: row.renk2, value: row.renk2 }] : []
|
||
|
||
// 🔄 Formu satır bilgileriyle doldur
|
||
Object.assign(form, {
|
||
model: row.model,
|
||
renk: row.renk,
|
||
renk2: row.renk2,
|
||
urunAnaGrubu: row.urunAnaGrubu,
|
||
urunAltGrubu: row.urunAltGrubu,
|
||
aciklama: row.aciklama,
|
||
fiyat: Number(row.fiyat || 0),
|
||
pb: row.pb,
|
||
adet: Number(row.adet || 0),
|
||
tutar: Number(row.tutar || 0)
|
||
})
|
||
|
||
// 🗓️ GRID → EDİTÖR termin senkronu
|
||
form.terminTarihi = row.terminTarihi || form.terminTarihi || form.tahminiTerminTarihi || ''
|
||
|
||
// 🔹 Beden şemasını geri yükle
|
||
const key = row.grpKey || activeGroupKeyForRow(row)
|
||
const grp = schemaByKey.value[key]
|
||
form.bedenLabels = grp?.values || []
|
||
const savedMap = row.bedenMap?.[key] || {}
|
||
form.bedenler = form.bedenLabels.map(lbl => Number(savedMap[lbl] || 0))
|
||
|
||
// 🔢 Toplamları yeniden hesapla
|
||
updateTotals(form)
|
||
form.tutar = (form.adet || 0) * (form.fiyat || 0)
|
||
|
||
// 🧩 Stokları forceRefresh ile yükle
|
||
if (form.model && form.renk) {
|
||
try {
|
||
await nextTick()
|
||
await loadProductSizes(true) // 💾 forceRefresh
|
||
console.log('📦 Edit mode stoklar yenilendi:', stockMap.value)
|
||
} catch (err) {
|
||
console.warn('⚠️ Edit modda stok yenileme başarısız:', err)
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Stok verisi yenilenemedi ⚠️',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
} else {
|
||
console.log('⚪ Model veya renk eksik, stok çağrısı atlandı.')
|
||
}
|
||
|
||
console.log(`✏️ Edit mode aktif → index: ${globalIndex}, model: ${row.model}, termin: ${form.terminTarihi}`)
|
||
}
|
||
|
||
// Bu fonksiyonu parent bileşenlerden çağırabilmek için export et
|
||
defineExpose({ editRow })
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 Kaydet / Güncelle (saveOrUpdate)
|
||
- Aynı kombinasyon varsa güncelleme moduna geçer,
|
||
yoksa yeni satır ekler.
|
||
- Kaydetmeden hemen önce stokları forceRefresh ile tazeler.
|
||
- Stok ve Min Fiyat validasyonları içerir.
|
||
=========================================================== */
|
||
async function saveOrUpdate() {
|
||
// 0️⃣ Kombinasyon kontrolü (99999 toleranslı)
|
||
const existingIndex = summaryRows.value.findIndex(r =>
|
||
r.model === form.model &&
|
||
((r.renk || '') === (form.renk || '') || (r.renk || '') === '99999' || (form.renk || '') === '99999') &&
|
||
((r.renk2 || '') === (form.renk2 || '') || (r.renk2 || '') === '99999' || (form.renk2 || '') === '99999')
|
||
)
|
||
|
||
if (existingIndex !== -1 && editingIndex.value === -1) {
|
||
console.log(`⚙️ Kombinasyon zaten var → index ${existingIndex} güncellenecek`)
|
||
editingIndex.value = existingIndex
|
||
}
|
||
|
||
// 1️⃣ Model zorunlu alan
|
||
if (!form.model) {
|
||
$q.notify({ type: 'warning', message: '⚠️ Model seçimi gerekli!' })
|
||
return
|
||
}
|
||
|
||
// 2️⃣ Para birimi doğrulaması (cari yoksa USD)
|
||
if (!form.pb || form.pb === '') {
|
||
form.pb = aktifPB.value || 'USD'
|
||
console.log('💱 Para birimi otomatik setlendi:', form.pb)
|
||
}
|
||
|
||
// 3️⃣ Kaydetmeden önce stok verisini tazele (forceRefresh)
|
||
try {
|
||
await loadProductSizes(true)
|
||
console.log('📦 Stok bilgisi kaydetmeden önce yenilendi (forceRefresh).')
|
||
} catch (err) {
|
||
console.warn('⚠️ Stok bilgisi kaydetmeden önce yenilenemedi:', err)
|
||
}
|
||
|
||
// 4️⃣ Stok kontrolü (beden bazında)
|
||
let stokOK = true
|
||
const overLimit = []
|
||
for (let i = 0; i < form.bedenLabels.length; i++) {
|
||
const lbl = form.bedenLabels[i]
|
||
const stok = Number(stockMap.value?.[lbl] || 0)
|
||
const girilen = Number(form.bedenler[i] || 0)
|
||
if (stok > 0 && girilen > stok) {
|
||
overLimit.push({ beden: lbl, stok, girilen })
|
||
}
|
||
}
|
||
|
||
if (overLimit.length > 0) {
|
||
const msgLines = overLimit
|
||
.map(x => `🟡 ${x.beden}: ${x.girilen} (Stok: ${x.stok})`)
|
||
.join('<br>')
|
||
|
||
stokOK = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Stok Uyarısı',
|
||
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:<br><br>${msgLines}`,
|
||
html: true,
|
||
ok: { label: 'Devam Et', color: 'primary' },
|
||
cancel: { label: 'İptal', color: 'negative' }
|
||
})
|
||
.onOk(() => resolve(true))
|
||
.onCancel(() => resolve(false))
|
||
})
|
||
}
|
||
if (!stokOK) return
|
||
|
||
// 5️⃣ Min fiyat kontrolü (stok OK sonrası)
|
||
let fiyatOK = true
|
||
try {
|
||
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
|
||
const minFiyat = Number(priceData?.price || 0)
|
||
if (priceData && form.fiyat < minFiyat) {
|
||
fiyatOK = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Fiyat Uyarısı',
|
||
message: `
|
||
<b>Min. Fiyat:</b> ${minFiyat.toLocaleString('tr-TR')} ${form.pb}<br>
|
||
<b>Girdiğin:</b> ${form.fiyat.toLocaleString('tr-TR')} ${form.pb}`,
|
||
html: true,
|
||
ok: { label: 'Devam Et', color: 'primary' },
|
||
cancel: { label: 'İptal', color: 'negative' }
|
||
})
|
||
.onOk(() => resolve(true))
|
||
.onCancel(() => resolve(false))
|
||
})
|
||
}
|
||
} catch (err) {
|
||
console.warn('⚠️ Fiyat kontrolü yapılamadı:', err)
|
||
}
|
||
if (!fiyatOK) return
|
||
// 6️⃣ Satır objesi oluştur (form → grid row)
|
||
const row = toSummaryRowFromForm()
|
||
if (!row || !row.model) return
|
||
|
||
// 7️⃣ Tutarlılık kontrolleri
|
||
row.adet = Number(row.adet || form.adet || 0)
|
||
row.fiyat = Number(row.fiyat || form.fiyat || 0)
|
||
row.tutar = Number(row.tutar || (row.adet * row.fiyat) || 0)
|
||
row.pb = form.pb || aktifPB.value || 'USD'
|
||
|
||
// 8️⃣ Güncelle veya yeni satır ekle
|
||
if (editingIndex.value !== -1) {
|
||
const currentRow = summaryRows.value[editingIndex.value]
|
||
if (currentRow?.id) {
|
||
// 🔹 ID varsa doğrudan güncelle
|
||
row.id = currentRow.id
|
||
summaryRows.value.splice(editingIndex.value, 1, row)
|
||
orderStore.updateRow(currentRow.id, { ...row })
|
||
} else {
|
||
// 🔹 ID yoksa index bazlı güncelle
|
||
summaryRows.value.splice(editingIndex.value, 1, row)
|
||
if (orderStore.orders[editingIndex.value])
|
||
orderStore.orders[editingIndex.value] = { ...row }
|
||
}
|
||
|
||
orderStore.saveToStorage?.()
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: 'Mevcut kombinasyon güncellendi ✏️',
|
||
position: 'top-right'
|
||
})
|
||
} else {
|
||
// 🆕 Yeni satır ekleme akışı
|
||
orderStore.addRow({ ...row })
|
||
summaryRows.value = [...orderStore.orders]
|
||
orderStore.saveToStorage?.()
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: 'Yeni kombinasyon eklendi ✅',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
|
||
// 9️⃣ Düzenleme durumunu sıfırla
|
||
editingIndex.value = -1
|
||
orderStore.selected = null
|
||
resetForm()
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Form Sıfırlama (Tam Temizleme)
|
||
Tüm form alanlarını, renk seçeneklerini, stok map’lerini
|
||
ve select bileşenlerini sıfırlar.
|
||
=========================================================== */
|
||
/* ===========================================================
|
||
🔹 resetForm (Güvenli Sıfırlama)
|
||
Yeni sipariş başlatılırken formu temizler.
|
||
Edit modunda ise aktif satırı korur.
|
||
=========================================================== */
|
||
/* ===========================================================
|
||
🔹 Reset Form (Yeni Sipariş Başlatma)
|
||
=========================================================== */
|
||
function resetForm() {
|
||
form.olusturmaTarihi = dayjs().format('YYYY-MM-DD')
|
||
form.tahminiTerminTarihi = dayjs().add(30, 'day').format('YYYY-MM-DD')
|
||
form.pb = 'USD'
|
||
aktifPB.value = 'USD'
|
||
selectedCari.value = ''
|
||
siparisGenelAciklama.value = ''
|
||
summaryRows.value = []
|
||
orderStore.newOrderTemplate()
|
||
isEditMode.value = false
|
||
console.log('🧹 Form sıfırlandı.')
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Satır Silme (removeSelected)
|
||
Kullanıcı bir satırı düzenleme modundayken silmek isterse
|
||
güvenlik diyaloğu açılır. Onay verilirse hem gridden hem
|
||
store’dan kaldırılır.
|
||
=========================================================== */
|
||
function removeSelected() {
|
||
if (editingIndex.value === -1) {
|
||
$q.notify({
|
||
type: 'info',
|
||
message: 'Silinecek satır seçili değil ⚠️'
|
||
})
|
||
return
|
||
}
|
||
|
||
const idx = editingIndex.value
|
||
const selectedRow = summaryRows.value[idx]
|
||
if (!selectedRow) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Geçersiz satır, silinemedi ⚠️'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 🔹 Onay penceresi
|
||
$q.dialog({
|
||
title: 'Satırı Sil',
|
||
message: `
|
||
Bu satırı silmek istediğinizden emin misiniz?<br><br>
|
||
<b>Model:</b> ${selectedRow.model || '-'}<br>
|
||
<b>Renk:</b> ${selectedRow.renk || '-'}<br>
|
||
<b>PB:</b> ${selectedRow.pb || '-'}<br>
|
||
<b>Tutar:</b> ${Number(selectedRow.tutar || 0).toLocaleString(
|
||
'tr-TR',
|
||
{ minimumFractionDigits: 2 }
|
||
)}`,
|
||
html: true,
|
||
ok: { label: 'Evet, Sil', color: 'negative' },
|
||
cancel: { label: 'Vazgeç', flat: true }
|
||
}).onOk(() => {
|
||
// 🔹 Grid'den kaldır
|
||
summaryRows.value.splice(idx, 1)
|
||
|
||
// 🔹 Store'dan kaldır (id varsa id’ye göre, yoksa index’e göre)
|
||
if (selectedRow.id != null) {
|
||
orderStore.removeRow(selectedRow.id)
|
||
} else {
|
||
orderStore.orders.splice(idx, 1)
|
||
orderStore.saveToStorage?.()
|
||
}
|
||
|
||
// 🔹 Formu sıfırla
|
||
editingIndex.value = -1
|
||
orderStore.selected = null
|
||
resetForm()
|
||
|
||
// 🔹 Kullanıcıya bilgi
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: 'Satır silindi ✅',
|
||
position: 'top-right'
|
||
})
|
||
})
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Grid Şeması (Beden Haritası)
|
||
Uygulamada hangi ürün grubunda hangi beden seti kullanılacak
|
||
burada tanımlanır. Key → kod, title → görünür ad, values → bedenler.
|
||
=========================================================== */
|
||
const schema = ref([
|
||
{
|
||
key: 'ayk',
|
||
title: 'AYAKKABI',
|
||
values: ['39','40','41','42','43','44','45']
|
||
},
|
||
{
|
||
key: 'yas',
|
||
title: 'YAŞ',
|
||
values: ['2','4','6','8','10','12','14']
|
||
},
|
||
{
|
||
key: 'pan',
|
||
title: 'PANTOLON',
|
||
values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68']
|
||
},
|
||
{
|
||
key: 'gom',
|
||
title: 'GÖMLEK',
|
||
values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL']
|
||
},
|
||
{
|
||
key: 'tak',
|
||
title: 'TAKIM ELBİSE',
|
||
values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74']
|
||
},
|
||
{
|
||
key: 'aksbir',
|
||
title: 'AKSESUAR',
|
||
values: [' ', '44', 'STD', '110CM', '115CM', '120CM', '125CM', '130CM', '135CM']
|
||
}
|
||
])
|
||
|
||
/* ===========================================================
|
||
🔹 Aktif Grup Anahtarı Tespiti
|
||
Ürün grubunun adından (ör. “TAKIM”, “PANTOLON”) hareketle
|
||
uygun schema.key değerini döndürür. Default: 'tak'
|
||
=========================================================== */
|
||
function activeGroupKeyForRow(row) {
|
||
const g = row.urunAnaGrubu?.toUpperCase() || ''
|
||
if (g.includes('TAKIM')) return 'tak'
|
||
if (g.includes('PANTOLON')) return 'pan'
|
||
if (g.includes('GÖMLEK')) return 'gom'
|
||
if (g.includes('AYAKKABI')) return 'ayk'
|
||
if (g.includes('YAŞ')) return 'yas'
|
||
return 'tak' // default fallback
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 schemaByKey (Computed)
|
||
Reaktif olarak schema listesinden key → object map üretir.
|
||
Böylece örn. schemaByKey.value['pan'] diyerek doğrudan erişim sağlanır.
|
||
=========================================================== */
|
||
const schemaByKey = computed(() => {
|
||
const m = {}
|
||
schema.value.forEach(grp => (m[grp.key] = grp))
|
||
return m
|
||
})
|
||
|
||
/* ===========================================================
|
||
🔹 highlightPantolon (Computed)
|
||
Eğer form’un aktif ürünü PANTOLON ise,
|
||
grid veya formda özel vurgu yapılabilir.
|
||
=========================================================== */
|
||
const highlightPantolon = computed(() =>
|
||
form.urunAnaGrubu?.toUpperCase()?.includes('PANTOLON')
|
||
)
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 Sayfa Kapanırken Verileri Kaydet
|
||
onBeforeUnmount: sayfa kapatıldığında sipariş verileri
|
||
LocalStorage’a otomatik yazılır.
|
||
=========================================================== */
|
||
onBeforeUnmount(() => {
|
||
orderStore.saveToStorage()
|
||
console.log('💾 Sayfa kapatılırken veriler kaydedildi.')
|
||
})
|
||
|
||
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 Tümünü Kaydet (Toplu Gönder) — Store Entegrasyonlu Versiyon
|
||
=========================================================== */
|
||
async function submitAll() {
|
||
try {
|
||
ensureAuthOrRedirect()
|
||
|
||
const headerPayload = {
|
||
OrderDate: form.olusturmaTarihi,
|
||
AverageDueDate: form.tahminiTerminTarihi,
|
||
CurrAccCode: selectedCari.value,
|
||
DocCurrencyCode: form.pb,
|
||
Description: siparisGenelAciklama.value
|
||
}
|
||
const linesPayload = summaryRows.value
|
||
|
||
let res
|
||
if (mode.value === 'new') {
|
||
res = await api.post('/api/orders', { header: headerPayload, lines: linesPayload })
|
||
$q.notify({ type: 'positive', message: 'Yeni sipariş oluşturuldu ✅' })
|
||
mode.value = 'edit'
|
||
headerId.value = res?.data?.OrderHeaderID || headerId.value
|
||
form.OrderHeaderID = headerId.value
|
||
} else {
|
||
res = await api.put(`/api/orders/${form.OrderHeaderID}`, { header: headerPayload, lines: linesPayload })
|
||
$q.notify({ type: 'positive', message: 'Sipariş güncellendi ✅' })
|
||
}
|
||
|
||
saveDraft()
|
||
} catch (err) {
|
||
console.error('❌ submitAll hata:', err)
|
||
$q.notify({ type: 'negative', message: 'Kaydedilemedi ❌' })
|
||
}
|
||
}
|
||
|
||
|
||
// ===========================================================
|
||
// 🔹 Sipariş Yükleme Fonksiyonu (Güvenli Sürüm)
|
||
// ===========================================================
|
||
async function loadOrderById(orderId) {
|
||
console.log('📦 loadOrderById çağrıldı →', orderId)
|
||
|
||
const hasLoading = !!$q.loading
|
||
const hasNotify = !!$q.notify
|
||
|
||
try {
|
||
// 🔹 Loading başlat
|
||
if (hasLoading) {
|
||
$q.loading.show({
|
||
message: 'Sipariş yükleniyor...',
|
||
spinnerSize: 50,
|
||
spinnerColor: 'gold',
|
||
backgroundColor: 'rgba(255,255,255,0.6)',
|
||
})
|
||
} else {
|
||
console.warn('⚠️ $q.loading tanımlı değil (plugin aktif mi?).')
|
||
}
|
||
|
||
// 🔹 API çağrısı
|
||
const { data } = await api.get(`/order/get/${orderId}`)
|
||
console.log('📡 API yanıtı:', data)
|
||
|
||
if (!data || Object.keys(data).length === 0) {
|
||
if (hasNotify) {
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: 'Sipariş verisi bulunamadı.',
|
||
position: 'top',
|
||
})
|
||
}
|
||
console.error('❌ Boş yanıt geldi.')
|
||
return
|
||
}
|
||
|
||
// 🔹 Header → form’a aktar
|
||
if (data.header) {
|
||
Object.assign(form, data.header)
|
||
console.log('✅ Header yüklendi:', form.OrderNumber || '(numara yok)')
|
||
}
|
||
|
||
// 🔹 Detay satırlarını frontend modeline çevir
|
||
if (Array.isArray(data.lines)) {
|
||
summaryRows.value = data.lines.map(l => {
|
||
const grpKey = detectBedenGroup([l.ItemDim1Code], l.ProductGroup, '')
|
||
return {
|
||
model: l.ItemCode || '',
|
||
renk: l.ColorCode || '',
|
||
renk2: l.ItemDim2Code || '',
|
||
urunAnaGrubu: l.ProductGroup || l.UrunAnaGrubu || '',
|
||
urunAltGrubu: l.ProductSubGroup || l.UrunAltGrubu || '',
|
||
aciklama: l.LineDescription || l.Aciklama || '',
|
||
fiyat: Number(l.Price || 0),
|
||
pb: l.PriceCurrencyCode || l.DocCurrencyCode || 'USD',
|
||
adet: Number(l.Qty1 || 0),
|
||
tutar: Number((l.Qty1 || 0) * (l.Price || 0)),
|
||
grpKey,
|
||
bedenMap: {
|
||
[grpKey]: { [l.ItemDim1Code]: Number(l.Qty1 || 0) },
|
||
},
|
||
terminTarihi: l.DeliveryDate
|
||
? dayjs(l.DeliveryDate).format('YYYY-MM-DD')
|
||
: form.tahminiTerminTarihi,
|
||
}
|
||
})
|
||
console.log('📋 Grid satırları hazır:', summaryRows.value.length)
|
||
} else {
|
||
summaryRows.value = []
|
||
}
|
||
} catch (err) {
|
||
console.error('❌ loadOrderById hata:', err)
|
||
if (hasNotify) {
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: 'Sipariş yüklenirken hata oluştu.',
|
||
position: 'top',
|
||
})
|
||
}
|
||
} finally {
|
||
// 🔹 Loading gizle
|
||
if (hasLoading) {
|
||
try {
|
||
$q.loading.hide()
|
||
} catch (hideErr) {
|
||
console.warn('⚠️ Loading kapatılamadı:', hideErr)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
/* ===========================================================
|
||
🔹 Stok Bilgisi Görüntüleme Fonksiyonları
|
||
Her beden veya satır için stok bilgisini UI’da göstermek için kullanılır.
|
||
=========================================================== */
|
||
function getStockFor(lbl) {
|
||
if (!lbl || !stockMap.value) return 0
|
||
const val = stockMap.value[lbl]
|
||
const num = Number(val)
|
||
return isNaN(num) ? 0 : num // 🎯 string "433" bile olsa 433 olarak döner
|
||
}
|
||
|
||
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 // 🎯 string → number dönüşümü eklendi
|
||
}
|
||
|
||
// 🔹 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
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Watchers — Model & Renk Değişimlerinde Otomatik Temizlik
|
||
Kullanıcı model veya renk değiştirdiğinde önceki form verileri
|
||
sıfırlanır, gereksiz stok veya beden bilgileri temizlenir.
|
||
=========================================================== */
|
||
watch(
|
||
() => form.model,
|
||
async (newVal, oldVal) => {
|
||
// MODEL TEMİZLENDİYSE: her şey sıfırlanır
|
||
if (!newVal) {
|
||
console.log('🧹 Model kaldırıldı, beden seti sıfırlanıyor...')
|
||
resetForm()
|
||
selectedSeriSet.value = null
|
||
seriMultiplier.value = 1
|
||
return
|
||
}
|
||
|
||
// MODEL DEĞİŞTİYSE: form yeniden başlatılır ve yeni model yüklenir
|
||
if (oldVal && newVal !== oldVal) {
|
||
console.log('🌀 Model değişti, form temizleniyor...')
|
||
resetForm()
|
||
await nextTick()
|
||
form.model = newVal
|
||
onModelChange(newVal)
|
||
}
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => form.renk,
|
||
(newVal, oldVal) => {
|
||
// Renk değiştiğinde ikinci renk ve ilgili stok bilgileri temizlenir
|
||
if (oldVal && newVal && newVal !== oldVal) {
|
||
console.log('🎨 Renk değişti, alt veriler sıfırlanıyor...')
|
||
form.renk2 = ''
|
||
renkOptions2.value = []
|
||
if (renk2Select.value) renk2Select.value.reset()
|
||
}
|
||
}
|
||
)
|
||
|
||
/* ===========================================================
|
||
🔹 Beden + Stok Etiketli Görünüm (Frontend Haritalama)
|
||
Grid veya tablo üzerinde bedenlerin yanında stok göstermek için
|
||
kullanılacak computed alan.
|
||
=========================================================== */
|
||
const bedenWithStock = computed(() => {
|
||
if (!form.bedenLabels.length) return []
|
||
return form.bedenLabels.map(lbl => ({
|
||
label: lbl,
|
||
stok: stockMap.value[lbl] ?? null,
|
||
value: form.bedenler[form.bedenLabels.indexOf(lbl)]
|
||
}))
|
||
})
|
||
|
||
/* ===========================================================
|
||
🔹 PB (Para Birimi) değişiminde min fiyat yenileme
|
||
Eğer kullanıcı para birimini değiştirirse, backend’den yeniden
|
||
min fiyat sorgusu yapılır (örneğin USD → EUR dönüşüm).
|
||
=========================================================== */
|
||
watch(
|
||
() => form.pb,
|
||
async (newVal, oldVal) => {
|
||
if (newVal && newVal !== oldVal) {
|
||
console.log('💱 PB değişti:', newVal)
|
||
await fetchMinPrice()
|
||
}
|
||
}
|
||
)
|
||
/* ===========================================================
|
||
🔹 Aktif Beden Alanı ve Renkli Stok Etiketleri
|
||
Stok miktarına göre hücre arka planı veya yazı rengini belirler.
|
||
Kullanıcı stok durumunu görsel olarak hemen fark eder.
|
||
=========================================================== */
|
||
const activeBeden = ref(null)
|
||
|
||
function stockColorClass(qty) {
|
||
const n = Number(qty)
|
||
if (isNaN(n)) return ''
|
||
if (n === 0) return 'stok-red' // 🔴 Stok yok
|
||
if (n > 0 && n <= 2) return 'stok-yellow' // 🟡 Kritik stok (1–2)
|
||
return 'stok-green' // 🟢 Yeterli stok (3+)
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Yardımcı Fonksiyonlar — Auth ve Token Kontrolü
|
||
Kullanıcının token’ı yoksa login sayfasına yönlendirir.
|
||
Tüm backend çağrıları öncesi güvenlik katmanı sağlar.
|
||
=========================================================== */
|
||
function getToken() {
|
||
return localStorage.getItem('token')
|
||
}
|
||
|
||
function ensureAuthOrRedirect() {
|
||
const token = getToken()
|
||
if (!token) {
|
||
if (typeof window !== 'undefined') window.location.href = '/login'
|
||
throw new Error('🚫 Yetkilendirme gerekli.')
|
||
}
|
||
}
|
||
|
||
/* ===========================================================
|
||
🔹 Son Bilgilendirme ve Debug Log
|
||
Geliştirme aşamasında konsolda bileşenin başarıyla
|
||
yüklendiği bilgisini verir.
|
||
=========================================================== */
|
||
console.log('🧩 OrderEntry (v22-final) bileşeni başarıyla yüklendi.')
|
||
</script>
|