Files
bssapp/ui/src/stores/deneme
2026-02-11 17:46:22 +03:00

3024 lines
102 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- ===========================================================
🧾 ORDER ENTRY PAGE (BSSApp)
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: '',
// 🔹 Frontende ö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 indexi
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 refleri 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 indexini bulur
const findExistingIndexByForm = () =>
summaryRows.value.findIndex(r => isSameCombo(r, form))
/* ===========================================================
🔹 Ürün Ana Grubu Bazında Gruplanmış Satırlar
groupedRows computed fonksiyonu, satırları urunAnaGrubuna 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 summaryRowsa 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 3944)
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 “4658 seri” seçilirse 46=1, 48=1, … şeklinde çarpanlar oluşur.
=========================================================== */
const seriMatrix = {
tak: {
'46-58 seri': { 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
'46-58 ara çift': { 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
'44-58 seri': { 44:1, 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
'44-58 ara çift': { 44:1, 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
'60-64 seri': { 60:1, 62:1, 64:1 },
'66-70 seri': { 66:1, 68:1, 70:1 },
'48-58 seri': { 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 }
},
gom: {
'XS-XXL': { XS:1, S:1, M:1, L:1, XL:1, XXL:1 },
'XS-XXL ara çift': { XS:1, S:1, M:2, L:2, XL:2, XXL:1 },
'3XL-5XL': { '3XL':1, '4XL':1, '5XL':1 }
},
ayk: {
'10\'lu seri': { 39:1, 40:2, 41:2, 42:2, 43:2, 44:1 },
'39-44': { 39:1, 40:1, 41:1, 42:1, 43:1, 44:1 },
'45-47': { 45:1, 46:1, 47:1 }
},
yas: {
'2-14Y': { 2:1, 4:1, 6:1, 8:1, 10:1, 12:1, 14:1 }
},
pan: {
'38-50 seri': { 38:1, 40:1, 42:1, 44:1, 46:1, 48:1, 50:1 },
'38-50 ara çift': { 38:1, 40:1, 42:2, 44:2, 46:2, 48:1, 50:1 },
'52-56 seri': { 52:1, 54:1, 56:1 },
'58-62 seri': { 58:1, 60:1, 62:1 }
}
}
// 🔹 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ı LocalStoragedan yüklendi:', summaryRows.value.length)
} else {
console.log(' LocalStorage boş, grid başlatılmadı.')
}
})
// 🔄 Store değişiklikleri anlık olarak gride 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 endpointinden 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
Backendde 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 {
// Storedaki fetchMinPrice fonksiyonu backendden 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 ERPden renk+model bazlı beden ve stok bilgisini çeker.
Ayrıca MSSQL stoklarıyla merge eder ve cacheler.
=========================================================== */
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)
// 💾 Cacheden veri varsa ve forceRefresh=false ise cache kullan
if (!forceRefresh && sizeCache.value[key]) {
console.log(`💾 Cacheden 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 patternini 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 summaryRowsa 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, // formdaki 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 editingi 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)
Griddeki 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 maplerini
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
storedan 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 idye göre, yoksa indexe 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 formun 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
LocalStoragea 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 → forma 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 UIda 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 stokMapinde 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, backendden 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 (12)
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>