6042 lines
214 KiB
Vue
6042 lines
214 KiB
Vue
<template>
|
||
<q-page v-if="canReadOrder" class="pcd-page" :style="pageVars">
|
||
<div ref="stickyStackRef" class="sticky-stack pcd-sticky-stack">
|
||
<div ref="saveToolbarRef" class="save-toolbar pcd-save-toolbar q-px-md">
|
||
<div class="pcd-toolbar-row">
|
||
<div class="pcd-toolbar-left">
|
||
<div class="pcd-toolbar-title">Maliyet Detay Sayfasi</div>
|
||
<div v-if="detailHeader && !detailLoading" class="pcd-toolbar-summary">
|
||
<!-- tbStok kontrolu (exists-bulk) manuel giris kapandigi icin devre disi -->
|
||
<div class="pcd-toolbar-summary-row">
|
||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||
<span class="pcd-toolbar-pill-label">USD</span>
|
||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.usdTotal) }}</span>
|
||
</div>
|
||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||
<span class="pcd-toolbar-pill-label">TRY</span>
|
||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.tryTotal) }}</span>
|
||
</div>
|
||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||
<span class="pcd-toolbar-pill-label">EUR</span>
|
||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.eurTotal) }}</span>
|
||
</div>
|
||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||
<span class="pcd-toolbar-pill-label">GBP</span>
|
||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.gbpTotal) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="pcd-toolbar-summary-row">
|
||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||
<span class="pcd-toolbar-pill-label">USD Kur</span>
|
||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.usdRate) }}</span>
|
||
</div>
|
||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||
<span class="pcd-toolbar-pill-label">EUR Kur</span>
|
||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.eurRate) }}</span>
|
||
</div>
|
||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||
<span class="pcd-toolbar-pill-label">GBP Kur</span>
|
||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.gbpRate) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pcd-toolbar-actions">
|
||
<div
|
||
v-if="last10WarningCount > 0"
|
||
class="pcd-toolbar-pill pcd-toolbar-pill-warn"
|
||
style="cursor:pointer;"
|
||
title="Son 10 ort. fiyata gore %10+ sapma var"
|
||
@click="last10WarningDialogOpen = true"
|
||
>
|
||
<span class="pcd-toolbar-pill-label">Fiyat Uyari</span>
|
||
<span class="pcd-toolbar-pill-value">{{ last10WarningCount }}</span>
|
||
</div>
|
||
<q-btn
|
||
flat
|
||
dense
|
||
color="grey-7"
|
||
class="pcd-toolbar-btn"
|
||
:label="summaryOpen ? 'OZET GIZLE' : 'OZET GOSTER'"
|
||
:icon="summaryOpen ? 'visibility_off' : 'visibility'"
|
||
@click="summaryOpen = !summaryOpen"
|
||
/>
|
||
<q-btn
|
||
flat
|
||
dense
|
||
color="grey-7"
|
||
class="pcd-toolbar-btn"
|
||
:label="headerInfoCollapsed ? 'HEADER GOSTER' : 'HEADER DARALT'"
|
||
:icon="headerInfoCollapsed ? 'expand_more' : 'expand_less'"
|
||
@click="toggleHeaderInfo"
|
||
/>
|
||
<q-btn icon="arrow_back" label="Geri" dense flat color="grey-8" class="pcd-toolbar-btn" @click="goBack" />
|
||
<q-btn
|
||
label="Yenile"
|
||
icon="refresh"
|
||
dense
|
||
color="primary"
|
||
class="pcd-toolbar-btn"
|
||
:loading="detailLoading"
|
||
@click="confirmRefresh"
|
||
/>
|
||
<q-btn
|
||
label="Toplu Fiyat Cagir"
|
||
icon="playlist_add_check"
|
||
dense
|
||
color="secondary"
|
||
outline
|
||
class="pcd-toolbar-btn"
|
||
:loading="bulkPriceLoading"
|
||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||
@click="fetchBulkItemPrices"
|
||
/>
|
||
<q-btn
|
||
label="SATIR EKLE"
|
||
dense
|
||
color="secondary"
|
||
icon="add"
|
||
class="pcd-toolbar-btn"
|
||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||
@click="openNewRowDialog"
|
||
/>
|
||
<q-btn
|
||
label="Kaydet"
|
||
icon="save"
|
||
dense
|
||
color="primary"
|
||
outline
|
||
class="pcd-toolbar-btn"
|
||
:loading="saveLoading"
|
||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||
@click="saveChanges"
|
||
/>
|
||
<q-btn
|
||
label="PDF"
|
||
icon="picture_as_pdf"
|
||
dense
|
||
color="grey-9"
|
||
outline
|
||
class="pcd-toolbar-btn"
|
||
:disable="!detailHeader || detailLoading || saveLoading"
|
||
@click="exportCostingPDF"
|
||
/>
|
||
<q-btn
|
||
label="Kaydi Sil"
|
||
icon="delete"
|
||
dense
|
||
color="negative"
|
||
outline
|
||
class="pcd-toolbar-btn"
|
||
:disable="!canDeleteCosting || detailLoading || saveLoading || bulkPriceLoading"
|
||
@click="deleteCosting"
|
||
>
|
||
<q-tooltip v-if="!canDeleteCosting">Once maliyet olusturulunca aktif olur</q-tooltip>
|
||
</q-btn>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<q-banner v-if="detailError" class="bg-red text-white q-mb-md">
|
||
Hata: {{ detailError }}
|
||
</q-banner>
|
||
|
||
<q-expansion-item
|
||
v-if="detailHeader && !detailLoading"
|
||
v-model="summaryOpen"
|
||
dense
|
||
expand-separator
|
||
class="pcd-summary-expansion q-mx-md q-mb-md"
|
||
icon="summarize"
|
||
label="Ozet"
|
||
>
|
||
<div class="row q-col-gutter-md q-pa-sm">
|
||
<div class="col-12 col-lg-6">
|
||
<div class="pcd-summary-title">Maliyetlere Islenen Toplam Tutar</div>
|
||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||
<tbody>
|
||
<tr>
|
||
<td class="pcd-summary-k">USD</td>
|
||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.usd) }}</td>
|
||
<td class="pcd-summary-k">TRY</td>
|
||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.try) }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="pcd-summary-k">EUR</td>
|
||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.eur) }}</td>
|
||
<td class="pcd-summary-k">GBP</td>
|
||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.gbp) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</q-markup-table>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-6">
|
||
<div class="pcd-summary-title">Iscilik Fiyatlari (CM2)</div>
|
||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">Parca</th>
|
||
<th class="text-right">Giris</th>
|
||
<th class="text-left">Pr.Br.</th>
|
||
<th class="text-right">USD Tutar</th>
|
||
<th class="text-right">TRY Tutar</th>
|
||
<th class="text-center">CMT/Malzemeli</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="r in mailSummary.laborRows" :key="'labor-'+r.part">
|
||
<td class="text-left">{{ r.part }}</td>
|
||
<td class="text-right">{{ r.inputAmountLabel }}</td>
|
||
<td class="text-left">{{ r.inputCurrencyLabel }}</td>
|
||
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||
<td class="text-center">{{ r.hasCmtOrMalzemeli ? '✓' : '' }}</td>
|
||
</tr>
|
||
<tr class="pcd-summary-total-row">
|
||
<td class="text-left text-weight-bold">TOPLAM</td>
|
||
<td></td>
|
||
<td></td>
|
||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.laborRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.laborRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||
<td></td>
|
||
</tr>
|
||
</tbody>
|
||
</q-markup-table>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-6">
|
||
<div class="pcd-summary-title">Malzeme Fiyatlari (DT/TP, maliyete dahil)</div>
|
||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">Parca</th>
|
||
<th class="text-right">USD Tutar</th>
|
||
<th class="text-right">TRY Tutar</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="r in mailSummary.materialRows" :key="'mat-'+r.part">
|
||
<td class="text-left">{{ r.part }}</td>
|
||
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||
</tr>
|
||
<tr class="pcd-summary-total-row">
|
||
<td class="text-left text-weight-bold">TOPLAM</td>
|
||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.materialRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.materialRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</q-markup-table>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-6">
|
||
<div class="pcd-summary-title">Kumas Fiyatlari (FABRIC, maliyete dahil)</div>
|
||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">Parca</th>
|
||
<th class="text-right">Metraj</th>
|
||
<th class="text-right">MT Giris Fiyat</th>
|
||
<th class="text-left">Pr.Br.</th>
|
||
<th class="text-right">USD Tutar</th>
|
||
<th class="text-right">TRY Tutar</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="r in mailSummary.fabricRows" :key="'fab-'+r.part">
|
||
<td class="text-left">{{ r.part }}</td>
|
||
<td class="text-right">{{ r.meterLabel }}</td>
|
||
<td class="text-right">{{ r.inputUnitLabel }}</td>
|
||
<td class="text-left">{{ r.inputCurrencyLabel }}</td>
|
||
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||
</tr>
|
||
<tr class="pcd-summary-total-row">
|
||
<td class="text-left text-weight-bold">TOPLAM</td>
|
||
<td class="text-right text-weight-bold">
|
||
{{
|
||
(() => {
|
||
const total = mailSummary.fabricRows.reduce((a, x) => a + (Number(x.meterQty || 0) || 0), 0)
|
||
return total > 0 ? `${formatBarQuantity(total)} MT` : '-'
|
||
})()
|
||
}}
|
||
</td>
|
||
<td></td>
|
||
<td></td>
|
||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.fabricRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.fabricRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</q-markup-table>
|
||
</div>
|
||
</div>
|
||
</q-expansion-item>
|
||
|
||
<q-dialog v-model="last10WarningDialogOpen" persistent>
|
||
<q-card style="min-width: 860px; max-width: 92vw;">
|
||
<q-card-section class="row items-center">
|
||
<div class="text-h6">Son 10 Ort. Fiyat Sapmalari</div>
|
||
<q-space />
|
||
<q-btn
|
||
dense
|
||
outline
|
||
color="secondary"
|
||
icon="download"
|
||
label="Excel"
|
||
class="q-mr-sm"
|
||
:disable="!last10Warnings || last10Warnings.length === 0"
|
||
@click="downloadLast10WarningsExcel"
|
||
/>
|
||
<q-btn icon="close" flat round dense v-close-popup />
|
||
</q-card-section>
|
||
<q-separator />
|
||
<q-card-section style="max-height: 70vh; overflow: auto;">
|
||
<q-markup-table dense flat bordered>
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">Kod</th>
|
||
<th class="text-left">Aciklama</th>
|
||
<th class="text-left">Doviz</th>
|
||
<th class="text-right">Giris</th>
|
||
<th class="text-right">Ort10</th>
|
||
<th class="text-right">Sapma %</th>
|
||
<th class="text-right">Sample</th>
|
||
<th class="text-left">Tarih Araligi</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="w in last10Warnings" :key="w.item_code + '|' + w.currency_code">
|
||
<td class="text-left">{{ w.item_code }}</td>
|
||
<td class="text-left">{{ w.item_description || '' }}</td>
|
||
<td class="text-left">{{ w.currency_code }}</td>
|
||
<td class="text-right">{{ formatMoney(w.input_price) }}</td>
|
||
<td class="text-right">{{ formatMoney(w.avg_doc_price) }}</td>
|
||
<td class="text-right">{{ formatPercent(w.diff_ratio) }}</td>
|
||
<td class="text-right">{{ w.sample_count }}</td>
|
||
<td class="text-left">{{ (w.min_invoice_date || '-') + ' / ' + (w.max_invoice_date || '-') }}</td>
|
||
</tr>
|
||
<tr v-if="last10Warnings.length === 0">
|
||
<td colspan="8" class="text-center text-grey-7">Kayit yok</td>
|
||
</tr>
|
||
</tbody>
|
||
</q-markup-table>
|
||
</q-card-section>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<div v-if="detailHeader && !detailLoading && !headerInfoCollapsed" class="filter-bar pcd-detail-header-bar q-mx-md q-mb-md">
|
||
<div class="row q-col-gutter-sm">
|
||
<div class="col-12 col-md-3">
|
||
<q-input
|
||
dense
|
||
filled
|
||
readonly
|
||
label="Maliyet Tarihi"
|
||
:model-value="formatDateTR(costDate)"
|
||
class="pcd-emphasis-field-alt"
|
||
>
|
||
<template #append>
|
||
<q-icon name="event" class="cursor-pointer">
|
||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||
<q-date v-model="costDate" mask="YYYY-MM-DD" locale="tr-TR" />
|
||
</q-popup-proxy>
|
||
</q-icon>
|
||
</template>
|
||
</q-input>
|
||
</div>
|
||
|
||
<div class="col-12 col-md-3">
|
||
<q-select
|
||
v-model="detailHeader.UretimSekliID"
|
||
:options="productionTypes"
|
||
option-value="id"
|
||
option-label="aciklama"
|
||
emit-value
|
||
map-options
|
||
dense
|
||
filled
|
||
label="Uretim Sekli"
|
||
class="pcd-emphasis-field-alt"
|
||
@update:model-value="onUretimSekliChange"
|
||
/>
|
||
</div>
|
||
|
||
<div class="col-12 col-md-4">
|
||
<q-input dense filled readonly label="Uretimi Yapan Firma" :model-value="detailHeader.UretimiYapanFirma || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-4">
|
||
<q-input dense filled readonly label="2.Firma" :model-value="detailHeader.SonIsEmriVeren || '-'" />
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<q-input dense filled readonly label="nOnMLNo" :model-value="detailHeader.nOnMLNo || '-'" />
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<q-input dense filled readonly label="UrunKodu" :model-value="detailHeader.UrunKodu || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="UrunAdi" :model-value="detailHeader.UrunAdi || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Urun Ilk Grubu" :model-value="detailHeader.UrunIlkGrubu || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Urun Ana Grubu" :model-value="detailHeader.UrunAnaGrubu || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Urun Alt Grubu" :model-value="detailHeader.UrunAltGrubu || '-'" />
|
||
</div>
|
||
|
||
<div class="col-6 col-md-2">
|
||
<q-input dense filled readonly label="sKullaniciAdi" :model-value="detailHeader.sKullaniciAdi || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Son Guncelleme Tarihi" :model-value="formatDateTR(detailHeader.dteGuncellemeTarihi)" />
|
||
</div>
|
||
<div class="col-12 col-md-2">
|
||
<q-input dense filled readonly label="sGuncellemeKullaniciAdi" :model-value="detailHeader.sGuncellemeKullaniciAdi || '-'" />
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
|
||
</div>
|
||
|
||
<div v-if="false && partSummary && partSummary.length > 0" class="col-12">
|
||
<div class="pcd-part-summary-card">
|
||
<div class="pcd-part-summary-title">Parça Bazlı Maliyet Özellikleri</div>
|
||
<q-markup-table dense flat bordered separator="cell" class="pcd-part-summary-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">Parça</th>
|
||
<th class="text-right">TRY</th>
|
||
<th class="text-right">USD</th>
|
||
<th class="text-right">EUR</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="ps in partSummary" :key="ps.name">
|
||
<td class="text-left text-weight-bold">{{ ps.name }}</td>
|
||
<td class="text-right">{{ formatMoney(ps.try) }}</td>
|
||
<td class="text-right">{{ formatMoney(ps.usd) }}</td>
|
||
<td class="text-right">{{ formatMoney(ps.eur) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</q-markup-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="detailHeader && !detailLoading && headerInfoCollapsed" class="filter-bar pcd-detail-header-bar q-mx-md q-mb-md">
|
||
<div class="row q-col-gutter-sm">
|
||
<div class="col-6 col-md-2">
|
||
<q-input dense filled readonly label="nOnMLNo" :model-value="detailHeader.nOnMLNo || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Uretimi Yapan Firma" :model-value="detailHeader.UretimiYapanFirma || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="2.Firma" :model-value="detailHeader.SonIsEmriVeren || '-'" />
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<q-input dense filled readonly label="UrunKodu" :model-value="detailHeader.UrunKodu || '-'" />
|
||
</div>
|
||
<div class="col-12 col-md-2">
|
||
<q-input dense filled readonly label="Uretim Sekli" :model-value="formatUretimSekli(detailHeader)" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="detailLoading" class="row justify-center q-pa-lg">
|
||
<q-spinner color="primary" size="36px" />
|
||
</div>
|
||
|
||
<div v-else-if="detailGroups.length === 0" class="text-grey-7 q-pa-md">
|
||
Kayit bulunamadi.
|
||
</div>
|
||
|
||
<div v-else class="column q-gutter-md pcd-content-body">
|
||
<div
|
||
v-for="(grp, gi) in detailGroups"
|
||
:key="groupKey(grp, gi)"
|
||
class="pcd-group-card"
|
||
>
|
||
<div class="order-sub-header pcd-sub-header">
|
||
<div class="sub-left">
|
||
<span class="pcd-sub-title">{{ grp.sAciklama3 || 'TANIMSIZ' }}</span>
|
||
<span v-if="normalizeGroupName(grp.sAciklama3) === 'FABRIC'" class="pcd-sub-mt-qty">
|
||
Toplam Miktar: {{ formatBarQuantity(resolveGroupQuantity(grp)) }} MT
|
||
</span>
|
||
</div>
|
||
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
|
||
<span class="pcd-group-total-text">
|
||
Grup Toplamı TRY: {{ formatBarMoney(resolveGroupTRYTutar(gi, groupTotalsRevision)) }}
|
||
<span class="pcd-group-total-divider">|</span>
|
||
USD: {{ formatBarMoney(resolveGroupUSDTutar(gi, groupTotalsRevision)) }}
|
||
</span>
|
||
<span class="pcd-group-debug">
|
||
Tanı: {{ resolveRenderedGroupIncludedCount(gi) }}/{{ resolveRenderedGroupRowCount(gi) }} kalem
|
||
<span>|</span>
|
||
Ham TRY: {{ formatBarMoney(resolveGroupTRYTutar(gi, groupTotalsRevision)) }}
|
||
</span>
|
||
<q-icon
|
||
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
|
||
size="18px"
|
||
class="q-ml-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<q-table
|
||
v-if="isGroupOpen(grp, gi)"
|
||
class="pcd-detail-table"
|
||
dense
|
||
flat
|
||
bordered
|
||
separator="cell"
|
||
row-key="__rowKey"
|
||
:rows="grp.items"
|
||
:columns="detailColumns"
|
||
:table-row-class-fn="resolveDetailRowClass"
|
||
@row-click="onDetailRowClick"
|
||
hide-bottom
|
||
:rows-per-page-options="[0]"
|
||
>
|
||
<template #header-cell-lMiktar="props">
|
||
<q-th :props="props" class="pcd-entry-header">
|
||
{{ props.col.label }}
|
||
</q-th>
|
||
</template>
|
||
|
||
<template #header-cell-inputPrice="props">
|
||
<q-th :props="props" class="pcd-entry-header">
|
||
{{ props.col.label }}
|
||
</q-th>
|
||
</template>
|
||
|
||
<template #header-cell-inputPricePrBr="props">
|
||
<q-th :props="props" class="pcd-entry-header">
|
||
{{ props.col.label }}
|
||
</q-th>
|
||
</template>
|
||
|
||
<template #header-cell-maliyeteDahil="props">
|
||
<q-th :props="props" class="pcd-secondary-header">
|
||
{{ props.col.label }}
|
||
</q-th>
|
||
</template>
|
||
|
||
<template #header-cell-cmPriceType="props">
|
||
<q-th :props="props" class="pcd-secondary-header">
|
||
{{ props.col.label }}
|
||
</q-th>
|
||
</template>
|
||
|
||
<template #body-cell-actions="props">
|
||
<q-td :props="props" class="q-gutter-xs no-wrap">
|
||
<q-btn
|
||
flat
|
||
round
|
||
dense
|
||
size="sm"
|
||
color="primary"
|
||
icon="history"
|
||
@click.stop="openLineHistory(props.row)"
|
||
>
|
||
<q-tooltip>Eski Fiyatlar / Geçmiş</q-tooltip>
|
||
</q-btn>
|
||
<q-icon
|
||
name="edit"
|
||
size="xs"
|
||
color="grey-7"
|
||
class="cursor-pointer"
|
||
@click.stop="openRowEditorForEdit(props.row)"
|
||
>
|
||
<q-tooltip>Satırı Düzenle</q-tooltip>
|
||
</q-icon>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-maliyeteDahil="props">
|
||
<q-td :props="props" class="text-center pcd-secondary-cell">
|
||
<q-checkbox
|
||
v-model="props.row.maliyeteDahil"
|
||
color="primary"
|
||
keep-color
|
||
dense
|
||
@update:model-value="value => onRowMaliyeteDahilChange(props.row, value)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-cmPriceType="props">
|
||
<q-td :props="props" class="text-center pcd-secondary-cell">
|
||
<q-checkbox
|
||
v-if="isCMGroupName(props.row.sAciklama3)"
|
||
:model-value="resolveCMPriceTypeChecked(props.row)"
|
||
color="secondary"
|
||
keep-color
|
||
dense
|
||
@update:model-value="value => onRowCMPriceTypeChange(props.row, value)"
|
||
/>
|
||
<span v-else class="text-grey-6">-</span>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-nOnMLDetNo="props">
|
||
<q-td :props="props">
|
||
<div v-if="props.row.isNew" class="text-primary text-weight-bold">YENI</div>
|
||
<div>{{ props.value }}</div>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-sParcaAdi="props">
|
||
<q-td :props="props">
|
||
{{ props.value || '-' }}
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-nHammaddeTuruNo="props">
|
||
<q-td :props="props">
|
||
<div class="text-weight-medium" style="font-size: 1.1em;">
|
||
{{ props.value }}<span v-if="props.row.sHammaddeTuruAdi"> - {{ props.row.sHammaddeTuruAdi }}</span>
|
||
</div>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-sKodu="props">
|
||
<q-td
|
||
:props="props"
|
||
:class="[resolveAutoOrICodeHighlightClass(props.row), resolveMissingStockCodeClass(props.row)]"
|
||
>
|
||
<span>{{ props.value }}</span>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-sAciklama="props">
|
||
<q-td
|
||
:props="props"
|
||
:class="[resolveAutoOrICodeHighlightClass(props.row), resolveMissingStockCodeClass(props.row)]"
|
||
>
|
||
<span>{{ props.value }}</span>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-sRenk="props">
|
||
<q-td :props="props">
|
||
<span>{{ props.value }}</span>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-lMiktar="props">
|
||
<q-td :props="props">
|
||
<q-input
|
||
v-model="props.row.miktarInput"
|
||
class="pcd-inline-input pcd-entry-input"
|
||
color="primary"
|
||
dense
|
||
filled
|
||
@update:model-value="value => onRowQuantityInput(props.row, value)"
|
||
@blur="normalizeRowQuantityDisplay(props.row)"
|
||
input-class="text-right"
|
||
inputmode="decimal"
|
||
placeholder="0,0000"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-inputPrice="props">
|
||
<q-td :props="props">
|
||
<q-input
|
||
v-model="props.row.inputPrice"
|
||
class="pcd-inline-input pcd-entry-input"
|
||
color="primary"
|
||
dense
|
||
filled
|
||
@update:model-value="value => onRowInputPriceChange(props.row, value)"
|
||
@blur="normalizeRowPriceDisplay(props.row)"
|
||
input-class="text-right"
|
||
inputmode="decimal"
|
||
placeholder="0,00"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-inputPricePrBr="props">
|
||
<q-td :props="props">
|
||
<q-select
|
||
v-model="props.row.inputPricePrBr"
|
||
class="pcd-inline-input pcd-entry-input"
|
||
:options="priceCurrencyOptions"
|
||
color="primary"
|
||
dense
|
||
filled
|
||
@update:model-value="value => onRowInputPriceCurrencyChange(props.row, value)"
|
||
emit-value
|
||
map-options
|
||
options-dense
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-sBirim="props">
|
||
<q-td :props="props">
|
||
<span>{{ props.value }}</span>
|
||
</q-td>
|
||
</template>
|
||
</q-table>
|
||
</div>
|
||
</div>
|
||
</q-page>
|
||
|
||
<q-page v-else class="q-pa-md flex flex-center">
|
||
<div class="text-negative text-subtitle1">
|
||
Bu module erisim yetkiniz yok.
|
||
</div>
|
||
</q-page>
|
||
|
||
<q-dialog v-model="rowEditorDialogOpen" persistent @show="resetRowEditorDialogPosition" @hide="stopRowEditorDialogDrag">
|
||
<q-card class="pcd-row-editor-dialog" :style="rowEditorDialogStyle">
|
||
<q-card-section
|
||
class="row items-center justify-between q-pb-sm pcd-row-editor-drag-handle"
|
||
@mousedown.prevent.stop="startRowEditorDialogDrag"
|
||
>
|
||
<div class="text-subtitle1 text-weight-bold">
|
||
{{ rowEditorMode === 'edit' ? 'Satir Duzenle' : 'Yeni Satir Ekle' }}
|
||
</div>
|
||
<q-btn flat round dense icon="close" v-close-popup />
|
||
</q-card-section>
|
||
|
||
<q-separator />
|
||
|
||
<q-card-section class="q-pa-md">
|
||
<div class="row q-col-gutter-md">
|
||
<div class="col-12 col-md-2">
|
||
<q-input dense filled readonly label="No" v-model="rowEditorForm.nOnMLDetNo" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Parca Adi" v-model="rowEditorForm.sParcaAdi" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Parca Grubu" v-model="rowEditorForm.sAciklama3" />
|
||
</div>
|
||
<div class="col-12 col-md-4">
|
||
<q-select
|
||
v-model="rowEditorForm.nHammaddeTuruNo"
|
||
class="pcd-row-editor-entry"
|
||
:options="rowEditorHammaddeOptions"
|
||
:loading="rowEditorHammaddeLoading"
|
||
option-value="value"
|
||
option-label="label"
|
||
emit-value
|
||
map-options
|
||
use-input
|
||
fill-input
|
||
hide-selected
|
||
input-debounce="250"
|
||
dense
|
||
filled
|
||
label="Hammadde Turu"
|
||
placeholder="Aramak icin yazin"
|
||
@filter="filterRowEditorHammaddeOptions"
|
||
@update:model-value="onRowEditorHammaddeChange"
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<q-select
|
||
v-model="rowEditorForm.sKodu"
|
||
class="pcd-row-editor-entry"
|
||
:options="rowEditorItemOptions"
|
||
:loading="rowEditorItemLoading"
|
||
option-value="value"
|
||
option-label="label"
|
||
emit-value
|
||
map-options
|
||
use-input
|
||
fill-input
|
||
hide-selected
|
||
input-debounce="350"
|
||
dense
|
||
filled
|
||
label="Kod / Aciklama"
|
||
placeholder="En az 2 karakter yazin"
|
||
@filter="filterRowEditorItemOptions"
|
||
@update:model-value="onRowEditorItemChange"
|
||
/>
|
||
<div v-if="showFabricCopyBtn" class="q-mt-xs row items-center q-gutter-xs">
|
||
<q-btn
|
||
dense
|
||
outline
|
||
color="primary"
|
||
icon="content_copy"
|
||
label="Kumastan Kopyala"
|
||
@click="openFabricCopyDialog"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<q-select
|
||
v-model="rowEditorForm.ColorCode"
|
||
class="pcd-row-editor-entry"
|
||
:options="rowEditorColorOptions"
|
||
:loading="rowEditorColorLoading"
|
||
option-value="value"
|
||
option-label="label"
|
||
emit-value
|
||
map-options
|
||
use-input
|
||
fill-input
|
||
hide-selected
|
||
input-debounce="250"
|
||
dense
|
||
filled
|
||
label="Renk"
|
||
placeholder="Aramak icin yazin"
|
||
@filter="filterRowEditorColorOptions"
|
||
@update:model-value="onRowEditorColorChange"
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-2">
|
||
<q-input v-model="rowEditorForm.miktarInput" class="pcd-row-editor-entry" dense filled label="Miktar" input-class="text-right" inputmode="decimal" />
|
||
</div>
|
||
<div class="col-12 col-md-2">
|
||
<q-input v-model="rowEditorForm.inputPrice" class="pcd-row-editor-entry" dense filled label="Fiyat Giris" input-class="text-right" inputmode="decimal" />
|
||
</div>
|
||
<div class="col-12 col-md-2">
|
||
<q-select
|
||
v-model="rowEditorForm.inputPricePrBr"
|
||
class="pcd-row-editor-entry"
|
||
:options="priceCurrencyOptions"
|
||
emit-value
|
||
map-options
|
||
options-dense
|
||
dense
|
||
filled
|
||
label="Pr.Br."
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<q-input dense filled readonly label="Birim" v-model="rowEditorForm.sBirim" />
|
||
</div>
|
||
<div class="col-12 col-md-2 pcd-row-editor-flag" style="display:flex;align-items:center;">
|
||
<q-checkbox v-model="rowEditorForm.maliyeteDahil" label="Maliyete Dahil" color="primary" keep-color />
|
||
</div>
|
||
<div v-if="isCMGroupName(rowEditorForm.sAciklama3)" class="col-12 col-md-3 pcd-row-editor-flag" style="display:flex;align-items:center;">
|
||
<q-checkbox v-model="rowEditorForm.cmPriceTypeChecked" label="CMT Malzeme Dahil" color="secondary" keep-color />
|
||
</div>
|
||
</div>
|
||
</q-card-section>
|
||
|
||
<q-card-actions align="right" class="q-pa-md">
|
||
<q-btn
|
||
v-if="rowEditorMode === 'edit'"
|
||
flat
|
||
color="negative"
|
||
icon="delete"
|
||
label="Satiri Sil"
|
||
class="q-mr-auto"
|
||
@click="deleteRowEditor"
|
||
/>
|
||
<q-btn flat label="Vazgec" color="grey-7" v-close-popup />
|
||
<q-btn color="secondary" icon="save" label="Satira Uygula" @click="saveRowEditor" />
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<q-dialog v-model="priceDeviationDialogOpen" persistent>
|
||
<q-card class="pcd-price-deviation-dialog">
|
||
<q-card-section class="row items-center justify-between q-pb-sm">
|
||
<div>
|
||
<div class="text-subtitle1 text-weight-bold">Fiyat Kontrolu (Satinalma Ortalama)</div>
|
||
<div class="text-caption text-grey-7">
|
||
NEBIM_V3 satinalma gecmisindeki son 10 USD ortalamasindan %10'dan fazla sapan tum satirlar.
|
||
</div>
|
||
</div>
|
||
<q-badge color="primary" outline>{{ priceDeviationRows.length }} satir</q-badge>
|
||
</q-card-section>
|
||
<q-separator />
|
||
<q-card-section class="q-pa-sm">
|
||
<div class="row items-center q-gutter-sm q-mb-sm">
|
||
<q-checkbox
|
||
:model-value="priceDeviationAllSelected"
|
||
color="primary"
|
||
label="Tumunu sec"
|
||
@update:model-value="setAllPriceDeviationRowsSelected"
|
||
/>
|
||
<q-btn
|
||
dense
|
||
outline
|
||
color="primary"
|
||
icon="price_change"
|
||
label="Secilileri Ortalama ile Degistir"
|
||
:disable="!priceDeviationRows.some(r => r.selected)"
|
||
@click="applySelectedPriceDeviationAverages"
|
||
/>
|
||
</div>
|
||
<div class="pcd-price-deviation-table-wrap">
|
||
<table class="pcd-price-deviation-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Sec</th>
|
||
<th>Kod</th>
|
||
<th>Ort USD</th>
|
||
<th>Girilen USD</th>
|
||
<th>Fark %</th>
|
||
<th>Yeni Fiyat</th>
|
||
<th>Pr.Br.</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in priceDeviationRows" :key="row.key">
|
||
<td class="text-center">
|
||
<q-checkbox v-model="row.selected" dense color="primary" />
|
||
</td>
|
||
<td class="text-weight-bold text-no-wrap">{{ row.code }}</td>
|
||
<td class="text-right">{{ formatMoney(row.avgUSD) }}</td>
|
||
<td class="text-right">{{ formatMoney(row.enteredUSD) }}</td>
|
||
<td class="text-right text-weight-bold" :class="row.pct >= 0 ? 'text-negative' : 'text-positive'">
|
||
{{ formatSignedPercent(row.pct) }}
|
||
</td>
|
||
<td>
|
||
<q-input
|
||
v-model="row.newPrice"
|
||
dense
|
||
filled
|
||
input-class="text-right"
|
||
inputmode="decimal"
|
||
:disable="!row.selected"
|
||
/>
|
||
</td>
|
||
<td>
|
||
<q-select
|
||
v-model="row.newCurrency"
|
||
dense
|
||
filled
|
||
emit-value
|
||
map-options
|
||
options-dense
|
||
:options="priceCurrencyOptions"
|
||
:disable="!row.selected"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</q-card-section>
|
||
<q-separator />
|
||
<q-card-actions align="right" class="q-pa-md">
|
||
<q-btn flat color="grey-7" label="Geri Don" @click="cancelPriceDeviationDialog" />
|
||
<q-btn color="primary" icon="save" label="Onayla ve Kaydet" @click="confirmPriceDeviationDialog" />
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- FABRIC copy helper: pick another fabric row and copy Code + Price + Pr.Br into editor -->
|
||
<q-dialog v-model="fabricCopyDialogOpen">
|
||
<q-card style="min-width: min(720px, 92vw);">
|
||
<q-card-section class="row items-center justify-between q-pb-sm">
|
||
<div class="text-subtitle1 text-weight-bold">Kumastan Kopyala</div>
|
||
<q-btn flat round dense icon="close" v-close-popup />
|
||
</q-card-section>
|
||
<q-separator />
|
||
<q-card-section class="q-pa-md">
|
||
<q-select
|
||
v-model="fabricCopySelectedKey"
|
||
dense
|
||
filled
|
||
label="Kaynak Kumaş"
|
||
:options="fabricCopyOptions"
|
||
option-value="value"
|
||
option-label="label"
|
||
emit-value
|
||
map-options
|
||
/>
|
||
</q-card-section>
|
||
<q-separator />
|
||
<q-card-actions align="right" class="q-pa-sm">
|
||
<q-btn flat label="Iptal" color="grey-7" v-close-popup />
|
||
<q-btn
|
||
unelevated
|
||
color="primary"
|
||
label="Kopyala"
|
||
:disable="!fabricCopySelectedKey"
|
||
@click="applyFabricCopySelection"
|
||
/>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<q-dialog v-model="lineHistoryDialogOpen" maximized>
|
||
<q-card class="pcd-history-dialog">
|
||
<q-card-section class="row items-center justify-between q-gutter-sm">
|
||
<div>
|
||
<div class="text-subtitle1 text-weight-bold">Eski Satinalma ve Recete Fiyatlari</div>
|
||
<div class="text-caption text-grey-7">
|
||
{{ lineHistoryTargetSummary }}
|
||
</div>
|
||
</div>
|
||
<q-btn flat round dense icon="close" v-close-popup />
|
||
</q-card-section>
|
||
|
||
<q-separator />
|
||
|
||
<q-card-section class="q-pa-md">
|
||
<q-banner v-if="lineHistoryError" class="bg-red text-white q-mb-md">
|
||
Hata: {{ lineHistoryError }}
|
||
</q-banner>
|
||
|
||
<q-banner v-if="lineHistoryShowingFallback" class="bg-amber-1 text-amber-9 q-mb-md rounded-borders border-amber">
|
||
<template v-slot:avatar>
|
||
<q-icon name="info" color="amber-9" />
|
||
</template>
|
||
Tam eşleşme bulunamadı. Benzer kodlara (prefix) ait geçmiş fiyatlar gösteriliyor.
|
||
</q-banner>
|
||
|
||
<div v-if="lineHistoryCanFetchSimilar || lineHistoryCanFetchAlternative" class="row q-col-gutter-sm q-mb-md">
|
||
<div v-if="lineHistorySearchMode !== 'exact'" class="col-auto">
|
||
<q-btn
|
||
label="Geri (Tam Eslesme)"
|
||
icon="arrow_back"
|
||
color="grey-7"
|
||
outline
|
||
no-caps
|
||
@click="fetchSimilarItemHistory('exact')"
|
||
/>
|
||
</div>
|
||
<div v-if="lineHistoryCanFetchSimilar" class="col-auto">
|
||
<q-btn
|
||
label="Benzer Kodları Göster"
|
||
icon="travel_explore"
|
||
color="secondary"
|
||
outline
|
||
no-caps
|
||
:loading="lineHistoryLoading && lineHistorySearchMode === 'prefix'"
|
||
@click="fetchSimilarItemHistory('prefix')"
|
||
/>
|
||
</div>
|
||
<div v-if="lineHistoryCanFetchAlternative" class="col-auto">
|
||
<q-btn
|
||
label="Diğer Alternatifleri Göster"
|
||
icon="alt_route"
|
||
color="primary"
|
||
outline
|
||
no-caps
|
||
:loading="lineHistoryLoading && lineHistorySearchMode === 'alternative'"
|
||
@click="fetchSimilarItemHistory('alternative')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="lineHistoryLoading" class="row justify-center q-pa-lg">
|
||
<q-spinner color="primary" size="32px" />
|
||
</div>
|
||
|
||
<div v-else-if="lineHistoryRows.length === 0" class="column items-center q-pa-xl">
|
||
<q-icon name="history" size="64px" color="grey-4" />
|
||
<div class="text-grey-7 q-mt-md text-center">
|
||
Bu kalem için geçmiş hareket bulunamadı.
|
||
<div v-if="lineHistoryTargetHammaddeTuruNo" class="q-mt-sm">
|
||
<q-btn
|
||
label="Hammadde Türü Bazında Benzer Ürünleri Göster"
|
||
icon="travel_explore"
|
||
color="secondary"
|
||
outline
|
||
no-caps
|
||
:loading="lineHistoryLoading"
|
||
@click="fetchSimilarItemHistory('prefix')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else>
|
||
<q-table
|
||
class="pcd-history-table"
|
||
dense
|
||
flat
|
||
bordered
|
||
row-key="__historyKey"
|
||
:rows="lineHistoryRows"
|
||
:columns="lineHistoryColumns"
|
||
:table-row-class-fn="resolveLineHistoryRowClass"
|
||
:rows-per-page-options="[0]"
|
||
hide-bottom
|
||
>
|
||
<template v-slot:body-cell-sourceLabel="props">
|
||
<q-td :props="props">
|
||
<span class="pcd-history-source-chip" :class="`pcd-history-source-chip--${String(props.row.sourceType || '').toLowerCase()}`">
|
||
{{ props.row.sourceLabel }}
|
||
</span>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template v-slot:body-cell-price="props">
|
||
<q-td :props="props" class="text-right">
|
||
{{ formatMoney(props.row.price) }}
|
||
</q-td>
|
||
</template>
|
||
|
||
<template v-slot:body-cell-quantity="props">
|
||
<q-td :props="props" class="text-right">
|
||
{{ formatMoney(props.row.quantity) }}
|
||
</q-td>
|
||
</template>
|
||
|
||
<template v-slot:body-cell-amount="props">
|
||
<q-td :props="props" class="text-right">
|
||
{{ formatMoney(props.row.amount) }}
|
||
</q-td>
|
||
</template>
|
||
|
||
<template v-slot:body-cell-select="props">
|
||
<q-td :props="props" class="text-right">
|
||
<q-btn
|
||
label="Sec"
|
||
color="primary"
|
||
dense
|
||
unelevated
|
||
@click="applyLineHistorySelection(props.row)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
</q-table>
|
||
</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
</q-dialog>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import { useQuasar } from 'quasar'
|
||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||
import { usePermission } from 'src/composables/usePermission'
|
||
import { get, post, download, extractApiErrorDetail } from 'src/services/api'
|
||
import { createTraceId, slog } from 'src/utils/slog'
|
||
import { formatDataSourceLabel } from 'src/utils/formatters'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const $q = useQuasar()
|
||
const { canRead } = usePermission()
|
||
const canReadOrder = canRead('order')
|
||
|
||
const detailLoading = ref(false)
|
||
const detailError = ref('')
|
||
const detailGroups = ref([])
|
||
const groupTotalsRevision = ref(0)
|
||
const renderedGroupAmounts = ref({})
|
||
const detailHeader = ref(null)
|
||
const costDate = ref('')
|
||
const exchangeRates = ref(createEmptyExchangeRates())
|
||
const deletedDetailRows = ref([])
|
||
const localDraftTimer = ref(0)
|
||
const initialHeaderSnapshot = ref('')
|
||
const saveLoading = ref(false)
|
||
const bulkPriceLoading = ref(false)
|
||
const newRowSequence = ref(0)
|
||
const ROW_EDITOR_OPTIONS_LIMIT = 100
|
||
const ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH = 2
|
||
const LINE_HISTORY_ROW_LIMIT = 500
|
||
const LINE_HISTORY_COMBINED_ROW_LIMIT = LINE_HISTORY_ROW_LIMIT * 2
|
||
const rowEditorDialogOpen = ref(false)
|
||
const rowEditorMode = ref('new')
|
||
const rowEditorTargetRowKey = ref('')
|
||
const rowEditorForm = ref(createRowEditorForm())
|
||
const priceDeviationDialogOpen = ref(false)
|
||
const priceDeviationRows = ref([])
|
||
const priceDeviationResolver = ref(null)
|
||
const fabricCopyDialogOpen = ref(false)
|
||
const fabricCopySelectedKey = ref('')
|
||
|
||
// Draggable "Satir Duzenle" dialog (mouse drag by header).
|
||
const rowEditorDialogPos = ref({ x: 0, y: 0 })
|
||
const rowEditorDialogPanBase = ref({ x: 0, y: 0 })
|
||
const rowEditorDialogDragging = ref(false)
|
||
const rowEditorDialogDragMouseStart = ref({ x: 0, y: 0 })
|
||
const rowEditorDialogDragPosStart = ref({ x: 0, y: 0 })
|
||
const rowEditorDialogStyle = computed(() => ({
|
||
transform: `translate(${Number(rowEditorDialogPos.value?.x || 0)}px, ${Number(rowEditorDialogPos.value?.y || 0)}px)`
|
||
}))
|
||
|
||
function resetRowEditorDialogPosition () {
|
||
rowEditorDialogPos.value = { x: 0, y: 0 }
|
||
rowEditorDialogPanBase.value = { x: 0, y: 0 }
|
||
}
|
||
|
||
function startRowEditorDialogDrag (e) {
|
||
if (!e) return
|
||
rowEditorDialogDragging.value = true
|
||
rowEditorDialogDragMouseStart.value = { x: Number(e.clientX || 0), y: Number(e.clientY || 0) }
|
||
rowEditorDialogDragPosStart.value = { x: Number(rowEditorDialogPos.value?.x || 0), y: Number(rowEditorDialogPos.value?.y || 0) }
|
||
window.addEventListener('mousemove', onRowEditorDialogDragMove, true)
|
||
window.addEventListener('mouseup', stopRowEditorDialogDrag, true)
|
||
}
|
||
|
||
function onRowEditorDialogDragMove (e) {
|
||
if (!rowEditorDialogDragging.value) return
|
||
const startMouse = rowEditorDialogDragMouseStart.value || { x: 0, y: 0 }
|
||
const startPos = rowEditorDialogDragPosStart.value || { x: 0, y: 0 }
|
||
const dx = Number(e?.clientX || 0) - Number(startMouse.x || 0)
|
||
const dy = Number(e?.clientY || 0) - Number(startMouse.y || 0)
|
||
rowEditorDialogPos.value = { x: Number(startPos.x || 0) + dx, y: Number(startPos.y || 0) + dy }
|
||
}
|
||
|
||
function stopRowEditorDialogDrag () {
|
||
rowEditorDialogDragging.value = false
|
||
try {
|
||
window.removeEventListener('mousemove', onRowEditorDialogDragMove, true)
|
||
window.removeEventListener('mouseup', stopRowEditorDialogDrag, true)
|
||
} catch {}
|
||
}
|
||
const rowEditorHammaddeOptions = ref([])
|
||
const rowEditorHammaddeAllOptions = ref([])
|
||
const rowEditorHammaddeLoading = ref(false)
|
||
const rowEditorItemOptions = ref([])
|
||
const rowEditorItemAllOptions = ref([])
|
||
const rowEditorItemLoading = ref(false)
|
||
const rowEditorLastValidItemValue = ref('')
|
||
const rowEditorColorOptions = ref([])
|
||
const rowEditorColorAllOptions = ref([])
|
||
const rowEditorColorLoading = ref(false)
|
||
const lineHistoryDialogOpen = ref(false)
|
||
const lineHistoryLoading = ref(false)
|
||
const lineHistoryError = ref('')
|
||
const lineHistoryRows = ref([])
|
||
const lineHistoryTargetRowKey = ref('')
|
||
const lineHistoryTargetHammaddeTuruNo = ref('')
|
||
const lineHistoryTargetItemCode = ref('')
|
||
const lineHistoryTargetSummary = ref('')
|
||
const lineHistorySearchMode = ref('exact')
|
||
const lineHistoryLastPurchaseMatchStage = ref('')
|
||
const lineHistoryLastRecipeMatchStage = ref('')
|
||
|
||
// tbStok validation (missing stock cards)
|
||
// Manuel giris kapandigi icin tbStok exists-bulk kontrolunu devre disi biraktik.
|
||
// Eski UI/CSS hook'lari kalsin diye state'i tutuyoruz ama request atmayacagiz.
|
||
const missingTbStokCodesMap = ref({})
|
||
const tbStokValidationLastError = ref('')
|
||
const tbStokMissingDialogShown = ref(false)
|
||
let tbStokValidationTimer = null
|
||
|
||
function normalizeStockCodeKey (code) {
|
||
return String(code || '').trim().toUpperCase()
|
||
}
|
||
|
||
function openMissingTbStokDialog () {
|
||
const missing = Object.keys(missingTbStokCodesMap.value || {})
|
||
if (missing.length === 0) return
|
||
$q.dialog({
|
||
title: 'tbStok Eksik Kodlar',
|
||
message: `Asagidaki kodlar tbStok'ta yok. Stok kartlarini duzeltmeden maliyet kaydetmeyin:\n\n${missing.slice(0, 80).join(', ')}${missing.length > 80 ? `\n(+${missing.length - 80} daha)` : ''}`,
|
||
ok: { label: 'Tamam' }
|
||
})
|
||
}
|
||
|
||
function isTbStokMissingCode (code) {
|
||
code = normalizeStockCodeKey(code)
|
||
if (!code) return false
|
||
return Boolean(missingTbStokCodesMap.value?.[code])
|
||
}
|
||
|
||
function resolveMissingStockCodeClass (row) {
|
||
const code = normalizeStockCodeKey(row?.sKodu)
|
||
if (!code) return ''
|
||
if (isTbStokMissingCode(code)) return 'pcd-missing-stock-code'
|
||
return ''
|
||
}
|
||
|
||
function collectUniqueCodesFromRows () {
|
||
const out = []
|
||
const seen = new Set()
|
||
for (const r of flatDetailRows.value || []) {
|
||
const code = normalizeStockCodeKey(r?.sKodu)
|
||
if (!code) continue
|
||
if (seen.has(code)) continue
|
||
seen.add(code)
|
||
out.push(code)
|
||
}
|
||
return out
|
||
}
|
||
|
||
async function refreshTbStokMissingCodes () {
|
||
tbStokValidationLastError.value = ''
|
||
missingTbStokCodesMap.value = {}
|
||
}
|
||
|
||
function formatPercent (ratio) {
|
||
const n = Number(ratio)
|
||
if (!Number.isFinite(n)) return '-'
|
||
return `${(n * 100).toFixed(0)}%`
|
||
}
|
||
|
||
function downloadLast10WarningsExcel () {
|
||
const rows = Array.isArray(last10Warnings.value) ? last10Warnings.value : []
|
||
if (rows.length === 0) return
|
||
|
||
// Excel-friendly CSV (UTF-8 with BOM).
|
||
const header = [
|
||
'Kod',
|
||
'Aciklama',
|
||
'Doviz',
|
||
'Giris',
|
||
'Ort10',
|
||
'SapmaOran',
|
||
'SapmaYuzde',
|
||
'Sample',
|
||
'MinTarih',
|
||
'MaxTarih'
|
||
]
|
||
const lines = [header.join(';')]
|
||
for (const w of rows) {
|
||
const code = String(w?.item_code || '').trim()
|
||
const desc = String(w?.item_description || '').replaceAll('\n', ' ').replaceAll('\r', ' ').trim()
|
||
const cur = String(w?.currency_code || '').trim()
|
||
const inP = Number(w?.input_price || 0)
|
||
const avgP = Number(w?.avg_doc_price || 0)
|
||
const diff = Number(w?.diff_ratio || 0)
|
||
const sample = Number(w?.sample_count || 0)
|
||
const minD = String(w?.min_invoice_date || '').trim()
|
||
const maxD = String(w?.max_invoice_date || '').trim()
|
||
|
||
const safe = (s) => {
|
||
s = String(s ?? '')
|
||
if (s.includes(';') || s.includes('\"')) {
|
||
s = '\"' + s.replaceAll('\"', '\"\"') + '\"'
|
||
}
|
||
return s
|
||
}
|
||
lines.push([
|
||
safe(code),
|
||
safe(desc),
|
||
safe(cur),
|
||
String(Number.isFinite(inP) ? inP : 0),
|
||
String(Number.isFinite(avgP) ? avgP : 0),
|
||
String(Number.isFinite(diff) ? diff : 0),
|
||
`${Number.isFinite(diff) ? (diff * 100).toFixed(0) : '0'}%`,
|
||
String(Number.isFinite(sample) ? sample : 0),
|
||
safe(minD),
|
||
safe(maxD)
|
||
].join(';'))
|
||
}
|
||
|
||
const bom = '\uFEFF'
|
||
const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
const urun = String(detailHeader.value?.UrunKodu || productCode.value || '').trim()
|
||
const onml = String(onMLNo.value || '').trim()
|
||
const date = String(costDate.value || '').trim()
|
||
a.href = url
|
||
a.download = `fiyat_uyari_${urun || 'urun'}_${onml || 'onml'}_${date || 'tarih'}.csv`
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
a.remove()
|
||
URL.revokeObjectURL(url)
|
||
}
|
||
|
||
async function refreshLast10Warnings () {
|
||
if (!onMLNo.value) {
|
||
last10Warnings.value = []
|
||
return
|
||
}
|
||
try {
|
||
const resp = await get('/pricing/production-product-costing/last10-warnings', { n_onml_no: onMLNo.value })
|
||
const items = Array.isArray(resp?.items) ? resp.items : []
|
||
last10Warnings.value = items
|
||
} catch (err) {
|
||
// non-blocking
|
||
}
|
||
}
|
||
|
||
function scheduleTbStokValidation () {
|
||
// no-op (tbStok kontrolu devre disi)
|
||
if (tbStokValidationTimer) clearTimeout(tbStokValidationTimer)
|
||
tbStokValidationTimer = null
|
||
}
|
||
const purchaseAvgUSDCachedByCode = ref({})
|
||
const last10Warnings = ref([])
|
||
const last10WarningDialogOpen = ref(false)
|
||
const last10WarningCount = computed(() => (Array.isArray(last10Warnings.value) ? last10Warnings.value.length : 0))
|
||
const headerInfoCollapsed = ref(false)
|
||
const subHeaderTop = ref(140)
|
||
const stickyStackRef = ref(null)
|
||
const saveToolbarRef = ref(null)
|
||
const groupOpenState = ref({})
|
||
const productionTypes = ref([])
|
||
|
||
const onMLNo = computed(() => String(route.query?.n_onml_no || '').trim())
|
||
const productCode = computed(() => String(route.query?.urun_kodu || '').trim())
|
||
const recipeCode = computed(() => String(route.query?.recete_kodu || '').trim())
|
||
const detailSource = computed(() => String(route.query?.detail_source || '').trim().toLowerCase())
|
||
const isNoCostDetail = computed(() => detailSource.value === 'no-cost')
|
||
const generatedTraceId = ref(createTraceId('pcd-detail'))
|
||
const traceId = computed(() => String(route.query?.trace_id || generatedTraceId.value).trim())
|
||
const pageMode = computed(() => (isNoCostDetail.value ? 'new' : 'edit'))
|
||
const pageVars = computed(() => ({
|
||
'--pcd-subheader-top': `${subHeaderTop.value}px`
|
||
}))
|
||
const lineHistoryCanFetchSimilar = computed(() => (
|
||
Boolean(lineHistoryTargetItemCode.value) &&
|
||
lineHistorySearchMode.value !== 'prefix'
|
||
))
|
||
const lineHistoryCanFetchAlternative = computed(() => (
|
||
Boolean(lineHistoryTargetHammaddeTuruNo.value) &&
|
||
lineHistorySearchMode.value !== 'alternative'
|
||
))
|
||
const lineHistoryShowingFallback = computed(() => (
|
||
lineHistorySearchMode.value === 'exact' &&
|
||
lineHistoryRows.value.some(r => r.priceType === 'BNZ')
|
||
))
|
||
const priceCurrencyOptions = [
|
||
{ label: 'USD', value: 'USD' },
|
||
{ label: 'TRY', value: 'TRY' },
|
||
{ label: 'EUR', value: 'EUR' },
|
||
{ label: 'GBP', value: 'GBP' }
|
||
]
|
||
const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : []))
|
||
const priceDeviationAllSelected = computed(() => {
|
||
const list = Array.isArray(priceDeviationRows.value) ? priceDeviationRows.value : []
|
||
return list.length > 0 && list.every((row) => row.selected)
|
||
})
|
||
|
||
const showFabricCopyBtn = computed(() => normalizeGroupName(rowEditorForm.value?.sAciklama3 || '') === 'FABRIC')
|
||
|
||
const fabricCopySourceRows = computed(() => {
|
||
const currentKey = String(rowEditorTargetRowKey.value || '').trim()
|
||
return (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
|
||
.filter(r => normalizeGroupName(r?.sAciklama3 || '') === 'FABRIC')
|
||
.filter(r => String(r?.__rowKey || '').trim() && String(r?.__rowKey || '').trim() !== currentKey)
|
||
})
|
||
|
||
const fabricCopyOptions = computed(() => {
|
||
// Deduplicate for UX: group by (code + color + hammaddeNo). Keep most recently changed first.
|
||
const list = (Array.isArray(fabricCopySourceRows.value) ? fabricCopySourceRows.value : [])
|
||
.slice()
|
||
.sort((a, b) => {
|
||
const da = new Date(a?.dteIslemTarihiDeg || a?.dteIslemTarihi || 0).getTime()
|
||
const db = new Date(b?.dteIslemTarihiDeg || b?.dteIslemTarihi || 0).getTime()
|
||
return db - da
|
||
})
|
||
|
||
const seen = new Set()
|
||
const out = []
|
||
for (const r of list) {
|
||
const code = String(r?.sKodu || '').trim()
|
||
if (!code) continue
|
||
const color = String(resolveRowColorCode(r) || '').trim()
|
||
const hNo = String(r?.nHammaddeTuruNo || '').trim()
|
||
const hName = String(r?.sHammaddeTuruAdi || '').trim()
|
||
const key = `${hNo}|${code}|${color}`
|
||
if (seen.has(key)) continue
|
||
seen.add(key)
|
||
const price = resolveNumericRowInputPrice(r)
|
||
const cur = resolveInputCurrency(r)
|
||
const desc = String(r?.sAciklama || '').trim()
|
||
const hLabel = `${hNo}${hName ? ` - ${hName}` : ''}`
|
||
const label = `${hLabel} | ${code}${desc ? ` - ${desc}` : ''}${color ? ` | ${color}` : ''}${price > 0 ? ` | ${price} ${cur}` : ''}`
|
||
out.push({ value: key, label })
|
||
}
|
||
return out
|
||
})
|
||
|
||
// no-cost: required parca slots (from Maliyet Parca Eslestirme)
|
||
const requiredParcaMappings = ref([])
|
||
const requiredAttentionRowKeys = ref({})
|
||
const requiredHammaddeMetaCache = ref({}) // hammaddeNo -> { groupName, parcaAdi }
|
||
// When user explicitly deletes a requiredPlaceholder row, we should not keep warning/recreating it
|
||
// for this draft/session. Key format: `${mtBolumID}:${hNo}`.
|
||
const suppressedRequiredKeys = ref({})
|
||
|
||
// Bump this when draft payload semantics change, to avoid hydrating stale rows after backend logic updates.
|
||
const DRAFT_STORAGE_VERSION = 'v2'
|
||
|
||
const draftStorageKey = computed(() => {
|
||
if (isNoCostDetail.value) {
|
||
if (!recipeCode.value) return ''
|
||
return `pcd-costing:${DRAFT_STORAGE_VERSION}:no-cost:${String(productCode.value || '').trim()}:${String(recipeCode.value || '').trim()}`
|
||
}
|
||
if (!onMLNo.value) return ''
|
||
return `pcd-costing:${DRAFT_STORAGE_VERSION}:has-cost:${String(onMLNo.value).trim()}`
|
||
})
|
||
|
||
const currentHeaderSnapshot = computed(() => JSON.stringify({
|
||
UretimSekliID: String(detailHeader.value?.UretimSekliID || '').trim(),
|
||
UretimSekli: String(detailHeader.value?.UretimSekli || '').trim()
|
||
}))
|
||
|
||
const headerHasUnsavedChanges = computed(() => {
|
||
if (!initialHeaderSnapshot.value) return false
|
||
return currentHeaderSnapshot.value !== initialHeaderSnapshot.value
|
||
})
|
||
|
||
const hasUnsavedChanges = computed(() => {
|
||
if (deletedDetailRows.value.length > 0) return true
|
||
if (headerHasUnsavedChanges.value) return true
|
||
return flatDetailRows.value.some(row => Boolean(row?.draftChanged))
|
||
})
|
||
|
||
const canDeleteCosting = computed(() => {
|
||
// Allow delete only for records created by this app's no-cost flow (we generate >= 100000).
|
||
// This avoids accidental deletion of legacy OnML records.
|
||
const n = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0
|
||
return n >= 100000
|
||
})
|
||
|
||
function persistLocalDraftNow () {
|
||
const key = draftStorageKey.value
|
||
if (!key) return
|
||
try {
|
||
const payload = {
|
||
version: 1,
|
||
savedAt: new Date().toISOString(),
|
||
mode: pageMode.value,
|
||
detail_source: detailSource.value || 'has-cost',
|
||
n_onml_no: onMLNo.value,
|
||
urun_kodu: productCode.value,
|
||
recete_kodu: recipeCode.value,
|
||
header: detailHeader.value ? {
|
||
UretimSekliID: String(detailHeader.value?.UretimSekliID || '').trim(),
|
||
UretimSekli: String(detailHeader.value?.UretimSekli || '').trim()
|
||
} : null,
|
||
deletedDetailRows: Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : [],
|
||
suppressedRequiredKeys: suppressedRequiredKeys.value || {},
|
||
detailGroups: Array.isArray(detailGroups.value) ? detailGroups.value : []
|
||
}
|
||
localStorage.setItem(key, JSON.stringify(payload))
|
||
} catch (err) {
|
||
slog.error('production-product-costing.detail', 'draft:persist:error', {
|
||
trace_id: traceId.value,
|
||
key: draftStorageKey.value,
|
||
error: String(err?.message || err)
|
||
})
|
||
}
|
||
}
|
||
|
||
function schedulePersistLocalDraft () {
|
||
try {
|
||
if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value)
|
||
localDraftTimer.value = window.setTimeout(() => {
|
||
localDraftTimer.value = 0
|
||
persistLocalDraftNow()
|
||
}, 400)
|
||
} catch {
|
||
persistLocalDraftNow()
|
||
}
|
||
}
|
||
|
||
function clearLocalDraft () {
|
||
const key = draftStorageKey.value
|
||
if (!key) return
|
||
try {
|
||
localStorage.removeItem(key)
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function tryHydrateFromLocalDraft () {
|
||
const key = draftStorageKey.value
|
||
if (!key) return false
|
||
try {
|
||
const raw = localStorage.getItem(key)
|
||
if (!raw) return false
|
||
const payload = JSON.parse(raw)
|
||
if (!payload || typeof payload !== 'object') return false
|
||
if (Array.isArray(payload.deletedDetailRows)) deletedDetailRows.value = payload.deletedDetailRows
|
||
if (payload.suppressedRequiredKeys && typeof payload.suppressedRequiredKeys === 'object') suppressedRequiredKeys.value = payload.suppressedRequiredKeys
|
||
if (Array.isArray(payload.detailGroups)) detailGroups.value = normalizeDetailGroups(payload.detailGroups)
|
||
if (payload.header && detailHeader.value) {
|
||
detailHeader.value.UretimSekliID = String(payload.header.UretimSekliID || '').trim()
|
||
detailHeader.value.UretimSekli = String(payload.header.UretimSekli || '').trim()
|
||
}
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
function ensureBeforeUnloadGuard (enabled) {
|
||
if (!enabled) {
|
||
window.onbeforeunload = null
|
||
return
|
||
}
|
||
window.onbeforeunload = (e) => {
|
||
e.preventDefault()
|
||
e.returnValue = ''
|
||
return ''
|
||
}
|
||
}
|
||
function shouldIncludeRowInCostingTotal (row) {
|
||
// The explicit "Maliyete Dahil" selection is authoritative for the top
|
||
// header and the amount persisted to spUrtOnMLMas, including CM rows.
|
||
return normalizeBooleanFlag(row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil)
|
||
}
|
||
|
||
const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => {
|
||
if (!shouldIncludeRowInCostingTotal(row)) return acc
|
||
acc.tryTotal += resolveRowTRYTutar(row)
|
||
acc.usdTotal += resolveRowUSDTutar(row)
|
||
acc.eurTotal += resolveRowEURTutar(row)
|
||
acc.gbpTotal += resolveRowGBPTutar(row)
|
||
return acc
|
||
}, {
|
||
tryTotal: 0,
|
||
usdTotal: 0,
|
||
eurTotal: 0,
|
||
gbpTotal: 0
|
||
}))
|
||
|
||
const summaryOpen = ref(false)
|
||
|
||
function makeEmptySummaryRow (part) {
|
||
return {
|
||
part,
|
||
inputAmountLabel: '-',
|
||
inputCurrencyLabel: '-',
|
||
inputUnitLabel: '-',
|
||
meterLabel: '-',
|
||
meterQty: 0,
|
||
meterUom: '',
|
||
usd: 0,
|
||
try: 0,
|
||
hasCmtOrMalzemeli: false
|
||
}
|
||
}
|
||
|
||
function accumulateInputAmount (bucket, row) {
|
||
const qty = resolveNumericRowQuantity(row)
|
||
const inputPrice = resolveNumericRowInputPrice(row)
|
||
const cur = resolveInputCurrency(row) || 'USD'
|
||
const amount = (Number.isFinite(inputPrice) ? inputPrice : 0) * (Number.isFinite(qty) ? qty : 0)
|
||
if (!Number.isFinite(amount) || amount === 0) return
|
||
if (!bucket.inputByCur) bucket.inputByCur = {}
|
||
bucket.inputByCur[cur] = (bucket.inputByCur[cur] || 0) + amount
|
||
}
|
||
|
||
function accumulateUnitPrice (bucket, row) {
|
||
// For fabric summary: show a representative unit input price (first non-zero).
|
||
const inputPrice = resolveNumericRowInputPrice(row)
|
||
const cur = resolveInputCurrency(row) || 'USD'
|
||
if (!Number.isFinite(inputPrice) || inputPrice <= 0) return
|
||
if (bucket.unitPrice === undefined || bucket.unitPrice === null) {
|
||
bucket.unitPrice = inputPrice
|
||
bucket.unitCur = cur
|
||
}
|
||
}
|
||
|
||
function finalizeInputLabel (bucket) {
|
||
if (Number(bucket.meterQty || 0) > 0) {
|
||
const uom = String(bucket.meterUom || '').trim()
|
||
bucket.meterLabel = `${formatBarQuantity(bucket.meterQty)}${uom ? ' ' + uom : ''}`
|
||
} else {
|
||
bucket.meterLabel = bucket.meterLabel || '-'
|
||
}
|
||
const byCur = bucket.inputByCur || {}
|
||
const currencies = Object.keys(byCur).filter(Boolean)
|
||
if (currencies.length === 0) {
|
||
bucket.inputAmountLabel = '-'
|
||
bucket.inputCurrencyLabel = '-'
|
||
bucket.inputUnitLabel = bucket.inputUnitLabel || '-'
|
||
return bucket
|
||
}
|
||
if (currencies.length === 1) {
|
||
const cur = currencies[0]
|
||
bucket.inputCurrencyLabel = cur
|
||
bucket.inputAmountLabel = formatMoney(byCur[cur] || 0)
|
||
if (bucket.unitPrice !== undefined && bucket.unitCur) {
|
||
bucket.inputUnitLabel = formatMoney(bucket.unitPrice)
|
||
bucket.inputCurrencyLabel = String(bucket.unitCur || cur).toUpperCase()
|
||
}
|
||
return bucket
|
||
}
|
||
// Multiple currencies mixed: keep labels short.
|
||
bucket.inputCurrencyLabel = 'MIX'
|
||
bucket.inputAmountLabel = formatMoney(currencies.reduce((acc, c) => acc + (byCur[c] || 0), 0))
|
||
return bucket
|
||
}
|
||
|
||
const mailSummary = computed(() => {
|
||
// Parts must be dynamic. Use sParcaAdi coming from backend (spUrtMTBolum.sAdi),
|
||
// and keep a stable order: common parts first, then the rest by first appearance.
|
||
const preferredParts = ['CEKET', 'PANTOLON', 'YELEK', 'AKSESUAR', 'YAKA']
|
||
const seenParts = new Set()
|
||
const dynamicParts = []
|
||
flatDetailRows.value.forEach((row) => {
|
||
const partRaw = normalizeGroupName(row?.sParcaAdi)
|
||
const part = String(partRaw || '').trim().toUpperCase()
|
||
if (!part) return
|
||
if (seenParts.has(part)) return
|
||
seenParts.add(part)
|
||
dynamicParts.push(part)
|
||
})
|
||
const parts = [
|
||
...preferredParts.filter(p => seenParts.has(p)),
|
||
...dynamicParts.filter(p => !preferredParts.includes(p))
|
||
]
|
||
const base = {
|
||
headerTotals: {
|
||
usd: toolbarSummary.value.usdTotal || 0,
|
||
try: toolbarSummary.value.tryTotal || 0,
|
||
eur: toolbarSummary.value.eurTotal || 0,
|
||
gbp: toolbarSummary.value.gbpTotal || 0
|
||
},
|
||
laborByPart: {},
|
||
materialByPart: {},
|
||
fabricByPart: {}
|
||
}
|
||
|
||
parts.forEach((p) => {
|
||
base.laborByPart[p] = makeEmptySummaryRow(p)
|
||
base.materialByPart[p] = makeEmptySummaryRow(p)
|
||
base.fabricByPart[p] = makeEmptySummaryRow(p)
|
||
})
|
||
|
||
flatDetailRows.value.forEach((row) => {
|
||
const partRaw = normalizeGroupName(row?.sParcaAdi)
|
||
const part = String(partRaw || '').trim().toUpperCase()
|
||
if (!part || !parts.includes(part)) return
|
||
|
||
const group = String(normalizeGroupName(row?.sAciklama3)).trim().toUpperCase()
|
||
const included = Boolean(row?.maliyeteDahil) || isCMGroupName(group)
|
||
if (!included) return
|
||
|
||
const usdAmount = resolveRowUSDTutar(row)
|
||
const tryAmount = resolveRowTRYTutar(row)
|
||
|
||
if (isCMGroupName(group)) {
|
||
const b = base.laborByPart[part]
|
||
b.usd += usdAmount
|
||
b.try += tryAmount
|
||
accumulateInputAmount(b, row)
|
||
// Tick should reflect actual checkbox state (only when user selected "malzemeli"/type=2).
|
||
if (resolveCMPriceTypeChecked(row)) b.hasCmtOrMalzemeli = true
|
||
return
|
||
}
|
||
|
||
if (group === 'DT' || group.includes(' DT') || group === 'TP' || group.includes(' TP')) {
|
||
const b = base.materialByPart[part]
|
||
b.usd += usdAmount
|
||
b.try += tryAmount
|
||
accumulateInputAmount(b, row)
|
||
return
|
||
}
|
||
|
||
if (group === 'FABRIC' || group.includes('FABRIC')) {
|
||
const b = base.fabricByPart[part]
|
||
b.usd += usdAmount
|
||
b.try += tryAmount
|
||
accumulateInputAmount(b, row)
|
||
accumulateUnitPrice(b, row)
|
||
const qty = resolveNumericRowQuantity(row)
|
||
if (Number.isFinite(qty) && qty > 0) {
|
||
b.meterQty += qty
|
||
if (!b.meterUom) b.meterUom = String(row?.sBirim || '').trim()
|
||
}
|
||
}
|
||
})
|
||
|
||
const laborRows = parts.map((p) => finalizeInputLabel(base.laborByPart[p]))
|
||
const materialRows = parts.map((p) => finalizeInputLabel(base.materialByPart[p]))
|
||
const fabricRows = parts.map((p) => finalizeInputLabel(base.fabricByPart[p]))
|
||
|
||
return {
|
||
headerTotals: base.headerTotals,
|
||
laborRows,
|
||
materialRows,
|
||
fabricRows
|
||
}
|
||
})
|
||
|
||
const partSummary = computed(() => {
|
||
const summary = {}
|
||
flatDetailRows.value.forEach(row => {
|
||
if (!row?.maliyeteDahil) return
|
||
const part = String(row?.sParcaAdi || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||
if (!summary[part]) {
|
||
summary[part] = { try: 0, usd: 0, eur: 0, gbp: 0 }
|
||
}
|
||
summary[part].try += resolveRowTRYTutar(row)
|
||
summary[part].usd += resolveRowUSDTutar(row)
|
||
summary[part].eur += resolveRowEURTutar(row)
|
||
summary[part].gbp += resolveRowGBPTutar(row)
|
||
})
|
||
return Object.entries(summary).map(([name, totals]) => ({ name, ...totals }))
|
||
})
|
||
const lineHistoryColumns = [
|
||
// Not: fixed percentage widths were overflowing at 100% zoom.
|
||
// Let the table layout decide widths so the "Sec" column stays visible.
|
||
{ name: 'sourceLabel', label: 'Kaynak', field: 'sourceLabel', align: 'left', sortable: false },
|
||
{ name: 'dateLabel', label: 'Tarih', field: 'dateLabel', align: 'left', sortable: true },
|
||
{ name: 'invoiceCode', label: 'Fatura/OnML', field: 'invoiceCode', align: 'left', sortable: true },
|
||
{ name: 'companyCode', label: 'Firma Kodu', field: 'companyCode', align: 'left', sortable: true },
|
||
{ name: 'companyDescription', label: 'Firma Aciklama', field: 'companyDescription', align: 'left', sortable: true },
|
||
{ name: 'itemCode', label: 'Masraf/sKodu', field: 'itemCode', align: 'left', sortable: true },
|
||
{ name: 'itemDescription', label: 'Masraf Detay', field: 'itemDescription', align: 'left', sortable: true },
|
||
{ name: 'colorCode', label: 'Renk', field: 'colorCode', align: 'left', sortable: true },
|
||
{ name: 'colorDescription', label: 'Renk Aciklama', field: 'colorDescription', align: 'left', sortable: true },
|
||
{ name: 'itemDim1Code', label: 'Dim1', field: 'itemDim1Code', align: 'left', sortable: true },
|
||
{ name: 'itemDim1Description', label: 'Dim1 Aciklama', field: 'itemDim1Description', align: 'left', sortable: true },
|
||
{ name: 'quantity', label: 'Miktar', field: 'quantity', align: 'right', sortable: true },
|
||
{ name: 'unit', label: 'Birim', field: 'unit', align: 'left', sortable: true },
|
||
{ name: 'price', label: 'Fiyat', field: 'price', align: 'right', sortable: true },
|
||
{ name: 'amount', label: 'Tutar', field: 'amount', align: 'right', sortable: true },
|
||
{ name: 'currency', label: 'Pr. Br.', field: 'currency', align: 'left', sortable: true },
|
||
{ name: 'select', label: '', field: 'select', align: 'right', sortable: false }
|
||
]
|
||
|
||
function resolveLineHistoryRowClass (row) {
|
||
if (row?.priceType === 'BNZ') return 'pcd-history-row-similar'
|
||
if (row?.sourceType === 'recipe') return 'pcd-history-row-recipe'
|
||
if (row?.sourceType === 'purchase') return 'pcd-history-row-purchase'
|
||
return ''
|
||
}
|
||
|
||
const detailColumns = [
|
||
{ name: 'actions', label: '', field: 'actions', align: 'left', style: 'width:60px', headerStyle: 'width:60px' },
|
||
{ name: 'nOnMLDetNo', label: 'No', field: 'nOnMLDetNo', align: 'left', sortable: true },
|
||
{ name: 'sParcaAdi', label: 'Parça Adı', field: 'sParcaAdi', align: 'left', sortable: true },
|
||
{ name: 'nHammaddeTuruNo', label: 'Hammadde Türü', field: 'nHammaddeTuruNo', align: 'left', sortable: true },
|
||
{ name: 'sKodu', label: 'Kod', field: 'sKodu', align: 'left', sortable: true },
|
||
{ name: 'sAciklama', label: 'Açıklama', field: 'sAciklama', align: 'left', sortable: true },
|
||
{ name: 'sRenk', label: 'Renk', field: 'sRenk', align: 'left', sortable: true },
|
||
{ name: 'lMiktar', label: 'Miktar', field: 'lMiktar', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width: 80px', headerStyle: 'width: 80px' },
|
||
{ name: 'inputPrice', label: 'Fiyat Giriş', field: 'inputPrice', align: 'right', sortable: false, style: 'width: 80px', headerStyle: 'width: 80px' },
|
||
{ name: 'inputPricePrBr', label: 'Fiyat Giriş Pr.Br.', field: 'inputPricePrBr', align: 'left', sortable: false, style: 'width: 92px', headerStyle: 'width: 92px' },
|
||
{ name: 'maliyeteDahil', label: 'Maliyete Dahil', field: 'maliyeteDahil', align: 'center', sortable: false },
|
||
{ name: 'cmPriceType', label: 'CMT', field: 'cm_price_type_id', align: 'center', sortable: false, style: 'width: 72px', headerStyle: 'width: 72px' },
|
||
{ name: 'lFiyat', label: 'lFiyat', field: 'lFiyat', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||
{ name: 'lTutar', label: 'lTutar', field: 'lTutar', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||
{ name: 'sFiyatTipi', label: 'sFiyatTipi', field: 'sFiyatTipi', align: 'left', sortable: true },
|
||
{ name: 'sDovizCinsi', label: 'Döviz', field: 'sDovizCinsi', align: 'left', sortable: true },
|
||
{ name: 'lDovizKuru', label: 'Kur', field: 'lDovizKuru', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||
{ name: 'lDovizFiyati', label: 'Döviz Fiyatı', field: 'lDovizFiyati', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||
{ name: 'priceUpdateState', label: 'Fiyat Tipi', field: 'priceUpdateState', align: 'left', sortable: true },
|
||
{
|
||
name: 'usdTutar',
|
||
label: 'USD TUTAR',
|
||
field: row => resolveRowUSDTutar(row),
|
||
align: 'right',
|
||
sortable: true,
|
||
format: val => formatMoney(val)
|
||
},
|
||
{ name: 'sBirim', label: 'Birim', field: 'sBirim', align: 'left', sortable: true }
|
||
]
|
||
|
||
function formatDateTR (value) {
|
||
const s = String(value || '').trim()
|
||
if (!s) return ''
|
||
const m = /^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2}))?/.exec(s)
|
||
if (!m) return s
|
||
const datePart = `${m[3]}.${m[2]}.${m[1]}`
|
||
if (m[4] && m[5]) return `${datePart} ${m[4]}:${m[5]}`
|
||
return datePart
|
||
}
|
||
|
||
function formatUretimSekli (header) {
|
||
const id = String(header?.UretimSekliID || '').trim()
|
||
const aciklama = String(header?.UretimSekli || '').trim()
|
||
|
||
if (id && aciklama) return `${id}-${aciklama}`
|
||
return id || aciklama || '-'
|
||
}
|
||
|
||
function normalizeDateInput (value) {
|
||
const s = String(value || '').trim()
|
||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s)
|
||
if (!m) return ''
|
||
return `${m[1]}-${m[2]}-${m[3]}`
|
||
}
|
||
|
||
function isCMGroupName (value) {
|
||
const normalizedValue = String(value || '').trim().toUpperCase()
|
||
return normalizedValue.includes('CM1') || normalizedValue.includes('CM2')
|
||
}
|
||
|
||
function isKnownGroupName (value) {
|
||
const v = String(value || '').trim().toUpperCase()
|
||
if (!v) return false
|
||
return v === 'DT' || v === 'TP' || v === 'FABRIC' || v === 'CM1' || v === 'CM2' ||
|
||
v.includes(' DT') || v.includes(' TP') || v.includes('FABRIC') || v.includes('CM1') || v.includes('CM2')
|
||
}
|
||
|
||
function normalizeLegacyParcaAndGroup (seed = {}) {
|
||
const parca = String(seed?.sParcaAdi || '').trim()
|
||
const group = String(seed?.sAciklama3 || '').trim()
|
||
if (!parca && !group) return seed
|
||
|
||
// Some legacy rows were saved with parca/group swapped. Fix it at the UI layer:
|
||
// - Parca: CEKET/PANTOLON/YELEK...
|
||
// - Group: DT/TP/CM1/CM2/FABRIC...
|
||
if (isKnownGroupName(parca) && !isKnownGroupName(group)) {
|
||
return { ...seed, sParcaAdi: group, sAciklama3: parca }
|
||
}
|
||
return seed
|
||
}
|
||
|
||
function createEmptyExchangeRates () {
|
||
return {
|
||
rateDate: '',
|
||
tryRate: 1,
|
||
usdRate: 0,
|
||
eurRate: 0,
|
||
gbpRate: 0
|
||
}
|
||
}
|
||
|
||
function createRowEditorForm (seed = {}) {
|
||
seed = normalizeLegacyParcaAndGroup(seed)
|
||
const defaultCurrency = normalizePriceCurrency(seed?.inputPricePrBr || seed?.fiyat_doviz || detailHeader.value?.sDovizCinsi) || 'USD'
|
||
const cmPriceTypeId = normalizeCMPriceTypeId(seed?.cmPriceTypeId ?? seed?.cm_price_type_id, seed?.sAciklama3 ?? seed?.sParcaAdi)
|
||
return {
|
||
__rowKey: String(seed?.__rowKey || '').trim(),
|
||
isNew: Boolean(seed?.isNew),
|
||
nStokID: String(seed?.nStokID || '').trim(),
|
||
sModel: String(seed?.sModel || '').trim(),
|
||
nOnMLDetNo: String(seed?.nOnMLDetNo || '').trim(),
|
||
sParcaAdi: String(seed?.sParcaAdi || '').trim(),
|
||
nHammaddeTuruNo: String(seed?.nHammaddeTuruNo || '').trim(),
|
||
sHammaddeTuruAdi: String(seed?.sHammaddeTuruAdi || '').trim(),
|
||
sAciklama3: String(seed?.sAciklama3 || '').trim(),
|
||
sKodu: String(seed?.sKodu || '').trim(),
|
||
sAciklama: String(seed?.sAciklama || '').trim(),
|
||
sRenk: String(seed?.sRenk || seed?.ColorCode || '').trim(),
|
||
ColorCode: String(seed?.ColorCode || seed?.sRenk || '').trim(),
|
||
ColorDescription: String(seed?.ColorDescription || '').trim(),
|
||
miktarInput: normalizeQuantityInput(seed?.miktarInput ?? seed?.lMiktar ?? 1),
|
||
inputPrice: normalizeInputPrice(seed?.inputPrice ?? seed?.fiyat_girilen ?? 0),
|
||
inputPricePrBr: defaultCurrency,
|
||
maliyeteDahil: seed?.maliyeteDahil ?? normalizeBooleanFlag(seed?.maliyete_dahil ?? true),
|
||
cmPriceTypeChecked: cmPriceTypeId === 2,
|
||
sBirim: extractPrimaryUnitValue(seed?.sBirim || 'AD') || 'AD'
|
||
}
|
||
}
|
||
|
||
function formatMoney (value) {
|
||
return parseMoneyInput(value).toLocaleString('tr-TR', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2
|
||
})
|
||
}
|
||
|
||
function formatQuantity (value) {
|
||
return parseMoneyInput(value).toLocaleString('tr-TR', {
|
||
minimumFractionDigits: 4,
|
||
maximumFractionDigits: 4
|
||
})
|
||
}
|
||
|
||
function formatQuantity2 (value) {
|
||
return parseMoneyInput(value).toLocaleString('tr-TR', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2
|
||
})
|
||
}
|
||
|
||
function formatBarMoney (value) {
|
||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
||
return formatMoney(roundedValue)
|
||
}
|
||
|
||
function formatBarQuantity (value) {
|
||
// Bar/summary quantities: show 2 decimals (requested for MT totals).
|
||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
||
return formatQuantity2(roundedValue)
|
||
}
|
||
|
||
function convertPriceToUSD (price, currency) {
|
||
const p = Number(price || 0)
|
||
if (!Number.isFinite(p) || p <= 0) return 0
|
||
const cur = String(currency || '').trim().toUpperCase()
|
||
const usdRate = Number(exchangeRates.value?.usdRate || 0) || 0
|
||
const eurRate = Number(exchangeRates.value?.eurRate || 0) || 0
|
||
const gbpRate = Number(exchangeRates.value?.gbpRate || 0) || 0
|
||
// If we don't have rates, fall back to assuming USD.
|
||
if (!(usdRate > 0)) return p
|
||
switch (cur) {
|
||
case 'USD':
|
||
return p
|
||
case 'TRY':
|
||
case 'TL':
|
||
case '':
|
||
return p / usdRate
|
||
case 'EUR': {
|
||
const tryVal = (eurRate > 0 ? p * eurRate : p)
|
||
return tryVal / usdRate
|
||
}
|
||
case 'GBP': {
|
||
const tryVal = (gbpRate > 0 ? p * gbpRate : p)
|
||
return tryVal / usdRate
|
||
}
|
||
default:
|
||
return p
|
||
}
|
||
}
|
||
|
||
function normalizePriceCurrency (value) {
|
||
const normalizedValue = String(value || '').trim().toUpperCase()
|
||
return ['USD', 'TRY', 'EUR', 'GBP'].includes(normalizedValue) ? normalizedValue : ''
|
||
}
|
||
|
||
function normalizeCodeValue (value) {
|
||
return String(value ?? '').trim().toUpperCase()
|
||
}
|
||
|
||
function extractPrimaryUnitValue (value) {
|
||
const normalizedValue = String(value || '').trim()
|
||
if (!normalizedValue) return ''
|
||
return normalizedValue.split(/[\s,;|/]+/).filter(Boolean)[0] || normalizedValue
|
||
}
|
||
|
||
function normalizeInputPrice (value) {
|
||
const numericValue = parseMoneyInput(value)
|
||
if (value === null || value === undefined || value === '' || !Number.isFinite(numericValue)) {
|
||
return formatMoney(0)
|
||
}
|
||
return formatMoney(numericValue)
|
||
}
|
||
|
||
function normalizeQuantityInput (value) {
|
||
const numericValue = parseMoneyInput(value)
|
||
if (value === null || value === undefined || value === '' || !Number.isFinite(numericValue)) {
|
||
return formatQuantity(0)
|
||
}
|
||
return formatQuantity(numericValue)
|
||
}
|
||
|
||
function normalizeSingleSeparatorNumber (rawValue, separator) {
|
||
const parts = String(rawValue || '').split(separator)
|
||
if (parts.length <= 1) return String(rawValue || '')
|
||
if (parts.length === 2) {
|
||
return separator === ',' ? `${parts[0]}.${parts[1]}` : `${parts[0]}.${parts[1]}`
|
||
}
|
||
|
||
const headParts = parts.slice(0, -1)
|
||
const lastPart = parts[parts.length - 1]
|
||
const looksLikeGroupedInteger = lastPart.length === 3 && headParts.every((part, index) => {
|
||
if (index === 0) return part.length >= 1 && part.length <= 3
|
||
return part.length === 3
|
||
})
|
||
|
||
if (looksLikeGroupedInteger) {
|
||
return parts.join('')
|
||
}
|
||
|
||
return `${headParts.join('')}.${lastPart}`
|
||
}
|
||
|
||
function parseMoneyInput (value) {
|
||
const rawValue = String(value ?? '').trim()
|
||
if (!rawValue) return 0
|
||
|
||
const compactValue = rawValue.replace(/\s+/g, '')
|
||
const lastDotIndex = compactValue.lastIndexOf('.')
|
||
const lastCommaIndex = compactValue.lastIndexOf(',')
|
||
const hasDot = lastDotIndex >= 0
|
||
const hasComma = lastCommaIndex >= 0
|
||
|
||
let normalizedValue = compactValue
|
||
|
||
if (hasDot && hasComma) {
|
||
const decimalSeparator = lastDotIndex > lastCommaIndex ? '.' : ','
|
||
const thousandSeparator = decimalSeparator === '.' ? ',' : '.'
|
||
normalizedValue = compactValue.replace(new RegExp(`\\${thousandSeparator}`, 'g'), '')
|
||
if (decimalSeparator === ',') {
|
||
normalizedValue = normalizedValue.replace(',', '.')
|
||
}
|
||
} else if (hasComma) {
|
||
normalizedValue = normalizeSingleSeparatorNumber(compactValue, ',')
|
||
} else if (hasDot) {
|
||
normalizedValue = normalizeSingleSeparatorNumber(compactValue, '.')
|
||
}
|
||
|
||
const numericValue = Number(normalizedValue)
|
||
return Number.isFinite(numericValue) ? numericValue : 0
|
||
}
|
||
|
||
function normalizeBooleanFlag (value) {
|
||
if (typeof value === 'boolean') return value
|
||
if (typeof value === 'number') return value === 1
|
||
|
||
const normalizedValue = String(value ?? '').trim().toLowerCase()
|
||
return normalizedValue === '1' || normalizedValue === 'true' || normalizedValue === 'evet'
|
||
}
|
||
|
||
function normalizeCMPriceTypeId (value, groupName) {
|
||
if (!isCMGroupName(groupName)) return null
|
||
if (value === null || value === undefined || value === '') return 1
|
||
|
||
const numericValue = Number(value)
|
||
if (numericValue === 2) return 2
|
||
return 1
|
||
}
|
||
|
||
function resolveCMPriceTypeChecked (row) {
|
||
return normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, row?.sAciklama3) === 2
|
||
}
|
||
|
||
function firstDefinedValue (source, keys) {
|
||
for (const key of keys) {
|
||
const value = source?.[key]
|
||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||
return value
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function normalizeExchangeRatesPayload (payload, fallbackDate = '') {
|
||
return {
|
||
rateDate: normalizeDateInput(firstDefinedValue(payload, ['rateDate', 'RateDate', 'date', 'Date'])) || fallbackDate,
|
||
tryRate: 1,
|
||
usdRate: parseMoneyInput(firstDefinedValue(payload, ['usdRate', 'USDRate', 'usd', 'USD'])),
|
||
eurRate: parseMoneyInput(firstDefinedValue(payload, ['eurRate', 'EURRate', 'eur', 'EUR'])),
|
||
gbpRate: parseMoneyInput(firstDefinedValue(payload, ['gbpRate', 'GBPRate', 'gbp', 'GBP']))
|
||
}
|
||
}
|
||
|
||
function buildRowEditorHammaddeOption (source) {
|
||
const value = String(source?.value || source?.nHammaddeTuruNo || '').trim()
|
||
const name = String(source?.sHammaddeTuruAdi || '').trim()
|
||
const groupName = String(source?.sAciklama3 || '').trim()
|
||
return {
|
||
kind: 'hammadde',
|
||
value,
|
||
label: String(source?.label || `${value}${name ? ` - ${name}` : ''}`).trim(),
|
||
nHammaddeTuruNo: value,
|
||
sHammaddeTuruAdi: name,
|
||
sAciklama3: groupName,
|
||
sParcaAdi: String(source?.sParcaAdi || groupName).trim()
|
||
}
|
||
}
|
||
|
||
function buildRowEditorItemOption (source) {
|
||
const value = String(source?.value || source?.sKodu || '').trim()
|
||
const desc = String(source?.sAciklama || '').trim()
|
||
return {
|
||
kind: 'item',
|
||
value,
|
||
nStokID: String(source?.nStokID || '').trim(),
|
||
sModel: String(source?.sModel || '').trim(),
|
||
label: String(source?.label || `${value}${desc ? ` - ${desc}` : ''}`).trim(),
|
||
sKodu: value,
|
||
sAciklama: desc,
|
||
sBirim: extractPrimaryUnitValue(source?.sBirim)
|
||
}
|
||
}
|
||
|
||
function buildRowEditorColorOption (source) {
|
||
const value = String(source?.value || source?.colorCode || source?.ColorCode || source?.sRenk || '').trim()
|
||
const desc = String(source?.colorDescription || source?.ColorDescription || '').trim()
|
||
return {
|
||
kind: 'color',
|
||
value,
|
||
label: String(source?.label || `${value}${desc ? ` - ${desc}` : ''}`).trim(),
|
||
colorCode: value,
|
||
colorDescription: desc
|
||
}
|
||
}
|
||
|
||
function upsertEditorOption (optionsRef, option) {
|
||
if (!option?.value) return
|
||
const nextOptions = [...optionsRef.value]
|
||
const idx = nextOptions.findIndex(x => String(x?.value || '').trim() === option.value)
|
||
if (idx >= 0) {
|
||
nextOptions[idx] = { ...nextOptions[idx], ...option }
|
||
} else {
|
||
nextOptions.unshift(option)
|
||
}
|
||
optionsRef.value = nextOptions
|
||
}
|
||
|
||
async function fetchRowEditorOptions (kind, search = '', extraParams = {}) {
|
||
const response = await get('/pricing/production-product-costing/detail-editor-options', {
|
||
kind,
|
||
search,
|
||
limit: ROW_EDITOR_OPTIONS_LIMIT,
|
||
trace_id: traceId.value,
|
||
...extraParams
|
||
})
|
||
return Array.isArray(response) ? response : []
|
||
}
|
||
|
||
function setRowEditorOptionsByKind (kind, rows) {
|
||
if (kind === 'hammadde') {
|
||
rowEditorHammaddeAllOptions.value = rows
|
||
rowEditorHammaddeOptions.value = rows
|
||
return
|
||
}
|
||
if (kind === 'item') {
|
||
rowEditorItemAllOptions.value = rows
|
||
rowEditorItemOptions.value = rows
|
||
return
|
||
}
|
||
rowEditorColorAllOptions.value = rows
|
||
rowEditorColorOptions.value = rows
|
||
}
|
||
|
||
function setRowEditorLookupLoading (kind, isLoading) {
|
||
if (kind === 'hammadde') {
|
||
rowEditorHammaddeLoading.value = isLoading
|
||
return
|
||
}
|
||
if (kind === 'item') {
|
||
rowEditorItemLoading.value = isLoading
|
||
return
|
||
}
|
||
rowEditorColorLoading.value = isLoading
|
||
}
|
||
|
||
async function refreshRowEditorOptions (kind, search = '') {
|
||
const extraParams = kind === 'color'
|
||
? { model_code: String(rowEditorForm.value.sModel || '').trim() }
|
||
: {}
|
||
|
||
if (kind === 'color' && !extraParams.model_code) {
|
||
setRowEditorOptionsByKind(kind, [])
|
||
primeRowEditorOptionsFromForm()
|
||
return []
|
||
}
|
||
|
||
setRowEditorLookupLoading(kind, true)
|
||
try {
|
||
const rows = await fetchRowEditorOptions(kind, search, extraParams)
|
||
setRowEditorOptionsByKind(kind, rows)
|
||
primeRowEditorOptionsFromForm()
|
||
return rows
|
||
} finally {
|
||
setRowEditorLookupLoading(kind, false)
|
||
}
|
||
}
|
||
|
||
async function filterRowEditorOptions (kind, val, update, abort) {
|
||
const normalizedSearch = String(val || '').trim()
|
||
if (kind === 'item' && normalizedSearch.length < ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH) {
|
||
update(() => {
|
||
rowEditorItemAllOptions.value = []
|
||
rowEditorItemOptions.value = []
|
||
primeRowEditorOptionsFromForm()
|
||
})
|
||
return
|
||
}
|
||
|
||
try {
|
||
const rows = await refreshRowEditorOptions(kind, normalizedSearch)
|
||
update(() => {
|
||
setRowEditorOptionsByKind(kind, rows)
|
||
primeRowEditorOptionsFromForm()
|
||
})
|
||
} catch (err) {
|
||
abort()
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: `Lookup getirilemedi: ${await extractApiErrorDetail(err)}`,
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
|
||
function filterRowEditorHammaddeOptions (val, update, abort) {
|
||
return filterRowEditorOptions('hammadde', val, update, abort)
|
||
}
|
||
|
||
function filterRowEditorItemOptions (val, update, abort) {
|
||
return filterRowEditorOptions('item', val, update, abort)
|
||
}
|
||
|
||
function filterRowEditorColorOptions (val, update, abort) {
|
||
const modelCode = String(rowEditorForm.value.sModel || '').trim()
|
||
if (!modelCode) {
|
||
update(() => {
|
||
rowEditorColorAllOptions.value = []
|
||
rowEditorColorOptions.value = []
|
||
})
|
||
return
|
||
}
|
||
return filterRowEditorOptions('color', val, update, abort)
|
||
}
|
||
|
||
async function loadRowEditorColorOptions () {
|
||
const modelCode = String(rowEditorForm.value.sModel || '').trim()
|
||
if (!modelCode) return []
|
||
try {
|
||
return await fetchRowEditorOptions('color', '', { model_code: modelCode })
|
||
} catch (err) {
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: `Renk lookup getirilemedi: ${await extractApiErrorDetail(err)}`,
|
||
position: 'top-right'
|
||
})
|
||
return []
|
||
}
|
||
}
|
||
|
||
function primeRowEditorOptionsFromForm () {
|
||
upsertEditorOption(rowEditorHammaddeAllOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||
upsertEditorOption(rowEditorHammaddeOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||
// Do not inject invalid free-typed values (e.g. "GAMBOÇ") into the item select options.
|
||
// Item options must represent real tbStok model codes (usually like "M.X", "K.X", "I.X").
|
||
const itemCandidate = buildRowEditorItemOption(rowEditorForm.value)
|
||
const itemKey = String(itemCandidate?.value || '').trim()
|
||
const looksLikeModel = /^[A-Za-z0-9]\./.test(itemKey)
|
||
if (looksLikeModel) {
|
||
upsertEditorOption(rowEditorItemAllOptions, itemCandidate)
|
||
upsertEditorOption(rowEditorItemOptions, itemCandidate)
|
||
}
|
||
upsertEditorOption(rowEditorColorAllOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||
upsertEditorOption(rowEditorColorOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||
}
|
||
|
||
function getNextDetailNo () {
|
||
const maxNo = flatDetailRows.value.reduce((acc, row) => {
|
||
const rowNo = parseInt(String(row?.nOnMLDetNo || '').trim(), 10)
|
||
return Number.isFinite(rowNo) ? Math.max(acc, rowNo) : acc
|
||
}, 0)
|
||
return String(maxNo + 1)
|
||
}
|
||
|
||
function resolveRowColorCode (row) {
|
||
return normalizeCodeValue(firstDefinedValue(row, [
|
||
'ColorCode',
|
||
'colorCode',
|
||
'sRenk',
|
||
's_renk',
|
||
'renk'
|
||
]))
|
||
}
|
||
|
||
function resolveRowItemDim1Code (row) {
|
||
return normalizeCodeValue(firstDefinedValue(row, [
|
||
'ItemDim1Code',
|
||
'itemDim1Code',
|
||
'sBeden',
|
||
's_beden'
|
||
]))
|
||
}
|
||
|
||
function extractNamedArray (payload, keys) {
|
||
for (const key of keys) {
|
||
if (Array.isArray(payload?.[key])) return payload[key]
|
||
if (Array.isArray(payload?.data?.[key])) return payload.data[key]
|
||
}
|
||
return []
|
||
}
|
||
|
||
function parseDateSortValue (value) {
|
||
const rawValue = String(value || '').trim()
|
||
if (!rawValue) return 0
|
||
const normalizedValue = rawValue.replace(' ', 'T')
|
||
const timeValue = Date.parse(normalizedValue)
|
||
return Number.isFinite(timeValue) ? timeValue : 0
|
||
}
|
||
|
||
function sortHistoryRowsByDateDesc (rows) {
|
||
return [...rows].sort((a, b) => parseDateSortValue(b?.dateValue) - parseDateSortValue(a?.dateValue))
|
||
}
|
||
|
||
function resolveHistorySourceType (item, forcedType = '') {
|
||
const normalizedType = String(forcedType || firstDefinedValue(item, [
|
||
'sourceType',
|
||
'source',
|
||
'kaynak',
|
||
'historyType',
|
||
'tip'
|
||
])).trim().toLowerCase()
|
||
|
||
if (['recipe', 'recete', 'uretim'].includes(normalizedType)) return 'recipe'
|
||
if (['purchase', 'invoice', 'satinalma', 'baggi_v3', 'baggi-v3'].includes(normalizedType)) return 'purchase'
|
||
if (firstDefinedValue(item, ['MasrafKodu', 'FaturaKodu', 'EvrakFiyat', 'ItemDim1Code']) !== '') return 'purchase'
|
||
if (firstDefinedValue(item, ['nOnMLNo', 'dteIslemTarihi', 'lDovizFiyati', 'lDovizTutari']) !== '') return 'recipe'
|
||
return normalizedType || 'history'
|
||
}
|
||
|
||
function resolveHistorySourceLabel (sourceType) {
|
||
if (sourceType === 'purchase') return formatDataSourceLabel('BAGGI_V3')
|
||
if (sourceType === 'recipe') return formatDataSourceLabel('URETIM')
|
||
return 'HISTORY'
|
||
}
|
||
|
||
function normalizeHistoryRowFromItem (item, index, forcedType = '') {
|
||
const sourceType = resolveHistorySourceType(item, forcedType)
|
||
const price = parseMoneyInput(firstDefinedValue(item, [
|
||
'EvrakFiyat',
|
||
'evrakFiyat',
|
||
'fiyat_girilen',
|
||
'fiyatGirilen',
|
||
'price',
|
||
'fiyat',
|
||
'birimFiyat',
|
||
'unitPrice',
|
||
'lDovizFiyati',
|
||
'lFiyat'
|
||
]))
|
||
const quantity = parseMoneyInput(firstDefinedValue(item, [
|
||
'Miktar',
|
||
'miktar',
|
||
'quantity',
|
||
'qty',
|
||
'adet',
|
||
'lMiktar'
|
||
]))
|
||
const amountValue = firstDefinedValue(item, [
|
||
'EvrakTutar',
|
||
'evrakTutar',
|
||
'tutar',
|
||
'amount',
|
||
'toplamTutar',
|
||
'lTutar',
|
||
'lDovizTutari'
|
||
])
|
||
const currency = normalizePriceCurrency(firstDefinedValue(item, [
|
||
'EvrakDoviz',
|
||
'evrakDoviz',
|
||
'fiyat_doviz',
|
||
'fiyatDoviz',
|
||
'currency',
|
||
'priceCurrency',
|
||
'prBr',
|
||
'pb',
|
||
'sDovizCinsi',
|
||
'USD'
|
||
]))
|
||
const dateValue = String(firstDefinedValue(item, [
|
||
'Tarih',
|
||
'tarih',
|
||
'InvoiceDate',
|
||
'invoiceDate',
|
||
'dteIslemTarihi',
|
||
'islemTarihi',
|
||
'dteKayitTarihi',
|
||
'date'
|
||
])).trim()
|
||
const amount = amountValue === ''
|
||
? price * quantity
|
||
: parseMoneyInput(amountValue)
|
||
|
||
return {
|
||
__historyKey: String(firstDefinedValue(item, ['id', '__historyKey', 'historyKey'])).trim() || `${sourceType}-${index}`,
|
||
raw: item,
|
||
sourceType,
|
||
sourceLabel: resolveHistorySourceLabel(sourceType),
|
||
dateValue,
|
||
dateLabel: formatDateTR(dateValue),
|
||
invoiceCode: String(firstDefinedValue(item, [
|
||
'FaturaKodu',
|
||
'faturaKodu',
|
||
'InvoiceNumber',
|
||
'invoiceCode',
|
||
'FaturaNo',
|
||
'nOnMLNo',
|
||
'n_onml_no'
|
||
])).trim(),
|
||
companyCode: String(firstDefinedValue(item, [
|
||
'FirmaKodu',
|
||
'firmaKodu',
|
||
'CurrAccCode',
|
||
'currAccCode',
|
||
'accountCode',
|
||
'vendorCode'
|
||
])).trim(),
|
||
companyDescription: String(firstDefinedValue(item, [
|
||
'FirmaAciklama',
|
||
'firmaAciklama',
|
||
'CurrAccDescription',
|
||
'currAccDescription',
|
||
'companyDescription',
|
||
'vendorDescription'
|
||
])).trim(),
|
||
itemCode: String(firstDefinedValue(item, [
|
||
'MasrafKodu',
|
||
'masrafKodu',
|
||
'sKodu',
|
||
's_kodu',
|
||
'ItemCode',
|
||
'itemCode'
|
||
])).trim(),
|
||
itemDescription: String(firstDefinedValue(item, [
|
||
'MasrafDetay',
|
||
'masrafDetay',
|
||
'sAciklama',
|
||
's_aciklama',
|
||
'ItemDescription',
|
||
'itemDescription',
|
||
'description'
|
||
])).trim(),
|
||
colorCode: String(firstDefinedValue(item, [
|
||
'ColorCode',
|
||
'colorCode',
|
||
'sRenk',
|
||
's_renk'
|
||
])).trim(),
|
||
colorDescription: String(firstDefinedValue(item, [
|
||
'ColorDescription',
|
||
'colorDescription',
|
||
'sRenkAdi',
|
||
's_renk_adi'
|
||
])).trim(),
|
||
itemDim1Code: String(firstDefinedValue(item, [
|
||
'ItemDim1Code',
|
||
'itemDim1Code',
|
||
'sBeden',
|
||
's_beden'
|
||
])).trim(),
|
||
itemDim1Description: String(firstDefinedValue(item, [
|
||
'ItemDim1Description',
|
||
'itemDim1Description',
|
||
'sAciklama2',
|
||
's_aciklama2'
|
||
])).trim(),
|
||
priceType: String(item?.priceType || '').trim(),
|
||
quantity,
|
||
unit: String(firstDefinedValue(item, [
|
||
'BIRIM',
|
||
'birim',
|
||
'unit',
|
||
'Unit',
|
||
'sBirim',
|
||
's_birim'
|
||
])).trim(),
|
||
price,
|
||
amount,
|
||
currency: currency || 'USD',
|
||
recipeInfo: String(firstDefinedValue(item, [
|
||
'recipeInfo',
|
||
'receteBilgisi',
|
||
'recete',
|
||
'DUMMY',
|
||
'nUrtReceteID',
|
||
'sAciklama3',
|
||
'Description',
|
||
'Aciklama',
|
||
'Aciklama1',
|
||
'LineDescription',
|
||
'note'
|
||
])).trim()
|
||
}
|
||
}
|
||
|
||
function normalizeBulkPriceItems (payload) {
|
||
const list = Array.isArray(payload)
|
||
? payload
|
||
: extractNamedArray(payload, ['items', 'data', 'updates', 'matchedItems', 'rows'])
|
||
|
||
return list.map((item, index) => {
|
||
const price = parseMoneyInput(firstDefinedValue(item, [
|
||
'EvrakFiyat',
|
||
'evrakFiyat',
|
||
'fiyat_girilen',
|
||
'fiyatGirilen',
|
||
'price',
|
||
'fiyat',
|
||
'unitPrice',
|
||
'birimFiyat',
|
||
'lDovizFiyati',
|
||
'lFiyat'
|
||
]))
|
||
const currency = normalizePriceCurrency(firstDefinedValue(item, [
|
||
'EvrakDoviz',
|
||
'evrakDoviz',
|
||
'fiyat_doviz',
|
||
'fiyatDoviz',
|
||
'currency',
|
||
'priceCurrency',
|
||
'prBr',
|
||
'pb',
|
||
'sDovizCinsi'
|
||
]))
|
||
|
||
return {
|
||
__updateKey: `upd-${index}`,
|
||
__rowKey: String(firstDefinedValue(item, ['__rowKey', 'rowKey'])).trim(),
|
||
nOnMLDetNo: String(firstDefinedValue(item, ['nOnMLDetNo', 'n_onml_det_no'])).trim(),
|
||
nHammaddeTuruNo: String(firstDefinedValue(item, ['nHammaddeTuruNo', 'n_hammadde_turu_no'])).trim(),
|
||
sKodu: normalizeCodeValue(firstDefinedValue(item, ['sKodu', 's_kodu', 'stokKodu', 'MasrafKodu', 'masrafKodu', 'ItemCode', 'itemCode'])),
|
||
colorCode: normalizeCodeValue(firstDefinedValue(item, ['ColorCode', 'colorCode', 'sRenk', 's_renk', 'renk'])),
|
||
itemDim1Code: normalizeCodeValue(firstDefinedValue(item, ['ItemDim1Code', 'itemDim1Code', 'item_dim1_code', 'sBeden', 's_beden'])),
|
||
inputPrice: normalizeInputPrice(price),
|
||
fiyat_girilen: price,
|
||
inputPricePrBr: currency || 'USD',
|
||
fiyat_doviz: currency || 'USD'
|
||
}
|
||
})
|
||
}
|
||
|
||
function normalizeLineHistoryRows (payload) {
|
||
const purchaseRows = extractNamedArray(payload, [
|
||
'purchaseRows',
|
||
'purchase_items',
|
||
'purchaseItems',
|
||
'invoiceRows',
|
||
'invoice_items',
|
||
'invoiceItems',
|
||
'baggiV3Rows',
|
||
'baggi_v3_rows',
|
||
'baggiRows'
|
||
])
|
||
const recipeRows = extractNamedArray(payload, [
|
||
'recipeRows',
|
||
'recipe_items',
|
||
'recipeItems',
|
||
'uretimRows',
|
||
'uretim_rows',
|
||
'productionRows'
|
||
])
|
||
|
||
if (purchaseRows.length > 0 || recipeRows.length > 0) {
|
||
const normalizedPurchaseRows = sortHistoryRowsByDateDesc(
|
||
purchaseRows.map((item, index) => normalizeHistoryRowFromItem(item, index, 'purchase'))
|
||
)
|
||
const normalizedRecipeRows = sortHistoryRowsByDateDesc(
|
||
recipeRows.map((item, index) => normalizeHistoryRowFromItem(item, index, 'recipe'))
|
||
)
|
||
return sortHistoryRowsByDateDesc([
|
||
...normalizedPurchaseRows,
|
||
...normalizedRecipeRows
|
||
]).slice(0, LINE_HISTORY_COMBINED_ROW_LIMIT)
|
||
}
|
||
|
||
const flatList = Array.isArray(payload)
|
||
? payload
|
||
: extractNamedArray(payload, ['items', 'data', 'rows'])
|
||
|
||
const normalizedRows = flatList.map((item, index) => normalizeHistoryRowFromItem(item, index))
|
||
const purchaseHistoryRows = sortHistoryRowsByDateDesc(normalizedRows.filter(row => row.sourceType === 'purchase'))
|
||
const recipeHistoryRows = sortHistoryRowsByDateDesc(normalizedRows.filter(row => row.sourceType === 'recipe'))
|
||
const otherHistoryRows = sortHistoryRowsByDateDesc(normalizedRows.filter(row => !['purchase', 'recipe'].includes(row.sourceType)))
|
||
|
||
return sortHistoryRowsByDateDesc([
|
||
...purchaseHistoryRows,
|
||
...recipeHistoryRows,
|
||
...otherHistoryRows
|
||
]).slice(0, LINE_HISTORY_COMBINED_ROW_LIMIT)
|
||
}
|
||
|
||
function rowMatchesBulkUpdate (row, update) {
|
||
if (update.__rowKey && update.__rowKey === row.__rowKey) return true
|
||
if (update.nOnMLDetNo && String(row?.nOnMLDetNo || '').trim() === update.nOnMLDetNo) return true
|
||
|
||
const rowKodu = normalizeCodeValue(row?.sKodu)
|
||
const rowRenk = resolveRowColorCode(row)
|
||
const rowTur = String(row?.nHammaddeTuruNo || '').trim()
|
||
const rowItemDim1Code = resolveRowItemDim1Code(row)
|
||
|
||
if (update.nHammaddeTuruNo && update.sKodu) {
|
||
if (rowTur === update.nHammaddeTuruNo && rowKodu === update.sKodu) return true
|
||
}
|
||
|
||
if (update.sKodu && update.colorCode && update.itemDim1Code) {
|
||
if (rowKodu === update.sKodu && rowRenk === update.colorCode && rowItemDim1Code === update.itemDim1Code) return true
|
||
}
|
||
|
||
if (update.sKodu && update.colorCode) {
|
||
if (rowKodu === update.sKodu && rowRenk === update.colorCode) return true
|
||
}
|
||
|
||
if (update.sKodu && update.itemDim1Code) {
|
||
if (rowKodu === update.sKodu && rowItemDim1Code === update.itemDim1Code) return true
|
||
}
|
||
|
||
if (update.sKodu) {
|
||
return rowKodu === update.sKodu
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
function resolveExchangeRateValue (currency) {
|
||
const normalizedCurrency = normalizePriceCurrency(currency)
|
||
if (normalizedCurrency === 'TRY') return 1
|
||
if (normalizedCurrency === 'USD') return parseMoneyInput(exchangeRates.value?.usdRate)
|
||
if (normalizedCurrency === 'EUR') return parseMoneyInput(exchangeRates.value?.eurRate)
|
||
if (normalizedCurrency === 'GBP') return parseMoneyInput(exchangeRates.value?.gbpRate)
|
||
return 0
|
||
}
|
||
|
||
function resolveInputCurrency (row) {
|
||
return normalizePriceCurrency(row?.inputPricePrBr || row?.fiyat_doviz) || 'USD'
|
||
}
|
||
|
||
function resolveAutoOrICodeHighlightClass (row) {
|
||
// Priority: auto-filled from previous costing (already firm-aware).
|
||
if (row?.__autoFilledFromPrev) {
|
||
return row?.__autoFilledFromPrevSameFirma ? 'text-positive text-weight-bold' : 'text-negative text-weight-bold'
|
||
}
|
||
// Manual selection for I.* codes: highlight same/other firm like the auto-fill convention.
|
||
const code = String(row?.sKodu || '').trim().toUpperCase()
|
||
if (code.startsWith('I.') && row?.__iCodeSelected) {
|
||
return row?.__iCodeSameFirma ? 'text-positive text-weight-bold' : 'text-negative text-weight-bold'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function resolveNumericRowQuantity (row) {
|
||
return parseMoneyInput(row?.miktarInput ?? row?.lMiktar)
|
||
}
|
||
|
||
function resolveNumericRowInputPrice (row) {
|
||
return parseMoneyInput(row?.inputPrice ?? row?.fiyat_girilen)
|
||
}
|
||
|
||
function resolveTRYUnitPriceByInput (inputPrice, inputCurrency, usdRate, eurRate, gbpRate) {
|
||
if (inputPrice <= 0) return 0
|
||
if (inputCurrency === 'TRY') return inputPrice
|
||
if (inputCurrency === 'USD') return usdRate > 0 ? inputPrice * usdRate : 0
|
||
if (inputCurrency === 'EUR') return eurRate > 0 ? inputPrice * eurRate : 0
|
||
if (inputCurrency === 'GBP') return gbpRate > 0 ? inputPrice * gbpRate : 0
|
||
return 0
|
||
}
|
||
|
||
function resolveUSDUnitPriceByInput (inputPrice, inputCurrency, usdRate, eurRate, gbpRate) {
|
||
if (inputPrice <= 0) return 0
|
||
if (inputCurrency === 'USD') return inputPrice
|
||
if (inputCurrency === 'TRY') return usdRate > 0 ? inputPrice / usdRate : 0
|
||
if (inputCurrency === 'EUR' || inputCurrency === 'GBP') {
|
||
const tryEquivalent = resolveTRYUnitPriceByInput(inputPrice, inputCurrency, usdRate, eurRate, gbpRate)
|
||
return usdRate > 0 ? tryEquivalent / usdRate : 0
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function normalizeDetailRows (items, groupName = '') {
|
||
const list = Array.isArray(items) ? items : []
|
||
return list.map((x, i) => {
|
||
// Determine the original currency from backend fields.
|
||
const originalCurrency = normalizePriceCurrency(x?.fiyat_doviz || x?.sDovizCinsi || x?.inputPricePrBr) || 'USD'
|
||
return {
|
||
...x,
|
||
__rowKey: x?.__rowKey || `${x?.nOnMLNo || ''}-${x?.nOnMLDetNo || ''}-${i}`,
|
||
miktarInput: x?.miktarInput ?? normalizeQuantityInput(x?.lMiktar),
|
||
// If inputPrice is missing, it means we are loading fresh from backend.
|
||
// Use fiyat_girilen which is the unit price in original currency.
|
||
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
|
||
inputPricePrBr: originalCurrency,
|
||
maliyeteDahil: normalizeBooleanFlag(x?.maliyeteDahil ?? x?.maliyete_dahil ?? x?.Maliyete_dahil),
|
||
__lastValidTRYAmount: parseMoneyInput(x?.lTutar ?? x?.lTutarTL),
|
||
__lastValidUSDAmount: parseMoneyInput(x?.usdTutar ?? x?.lTutarUSD),
|
||
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
|
||
draftChanged: Boolean(x?.draftChanged),
|
||
priceUpdateState: String(x?.priceUpdateState || '').trim()
|
||
}
|
||
})
|
||
}
|
||
|
||
function normalizeDetailGroups (groups) {
|
||
const list = Array.isArray(groups) ? groups : []
|
||
const out = list.map(grp => {
|
||
const groupName = String(grp?.sAciklama3 || '').trim()
|
||
const items = normalizeDetailRows(grp?.items, groupName).map(row => ({
|
||
...row,
|
||
sAciklama3: String(grp?.sAciklama3 || row?.sAciklama3 || '').trim(),
|
||
cmPriceTypeId: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, groupName || row?.sAciklama3)
|
||
}))
|
||
// USD TUTAR (DESC) sıralama
|
||
if (isNoCostDetail.value) {
|
||
items.sort((a, b) => {
|
||
const ha = parseInt(String(a?.nHammaddeTuruNo || '0'), 10) || 0
|
||
const hb = parseInt(String(b?.nHammaddeTuruNo || '0'), 10) || 0
|
||
if (ha !== hb) return ha - hb
|
||
const ka = String(a?.sKodu || '').trim()
|
||
const kb = String(b?.sKodu || '').trim()
|
||
return ka.localeCompare(kb, 'tr')
|
||
})
|
||
} else {
|
||
items.sort((a, b) => {
|
||
const valA = resolveRowUSDTutar(a)
|
||
const valB = resolveRowUSDTutar(b)
|
||
return valB - valA
|
||
})
|
||
}
|
||
return {
|
||
...grp,
|
||
items
|
||
}
|
||
})
|
||
return sortDetailGroups(out)
|
||
}
|
||
|
||
function groupOrderIndex (name) {
|
||
const v = String(name || '').trim().toUpperCase()
|
||
if (!v) return 999
|
||
if (v.includes('CM2')) return 0
|
||
if (v.includes('FABRIC')) return 1
|
||
if (v === 'TP' || v.includes(' TP')) return 2
|
||
if (v === 'DT' || v.includes(' DT')) return 3
|
||
if (v.includes('CM1')) return 4
|
||
return 999
|
||
}
|
||
|
||
function sortDetailGroups (groups) {
|
||
const list = Array.isArray(groups) ? [...groups] : []
|
||
list.sort((a, b) => {
|
||
const ga = String(a?.sAciklama3 || '').trim()
|
||
const gb = String(b?.sAciklama3 || '').trim()
|
||
const ia = groupOrderIndex(ga)
|
||
const ib = groupOrderIndex(gb)
|
||
if (ia !== ib) return ia - ib
|
||
return ga.localeCompare(gb, 'tr')
|
||
})
|
||
return list
|
||
}
|
||
|
||
function recalculateDetailRow (row, options = {}) {
|
||
const quantity = resolveNumericRowQuantity(row)
|
||
const inputPrice = resolveNumericRowInputPrice(row)
|
||
const inputCurrency = resolveInputCurrency(row)
|
||
const usdRate = resolveExchangeRateValue('USD')
|
||
const eurRate = resolveExchangeRateValue('EUR')
|
||
const gbpRate = resolveExchangeRateValue('GBP')
|
||
const unitTRY = resolveTRYUnitPriceByInput(inputPrice, inputCurrency, usdRate, eurRate, gbpRate)
|
||
const unitUSD = resolveUSDUnitPriceByInput(inputPrice, inputCurrency, usdRate, eurRate, gbpRate)
|
||
const unitEUR = eurRate > 0 ? unitTRY / eurRate : 0
|
||
const unitGBP = gbpRate > 0 ? unitTRY / gbpRate : 0
|
||
const maliyeteDahil = normalizeBooleanFlag(row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil)
|
||
const cmPriceTypeId = normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, row?.sAciklama3)
|
||
|
||
row.lMiktar = quantity
|
||
row.fiyat_girilen = inputPrice
|
||
row.fiyat_doviz = inputCurrency
|
||
row.inputPricePrBr = inputCurrency
|
||
row.maliyeteDahil = maliyeteDahil
|
||
row.maliyete_dahil = maliyeteDahil ? 1 : 0
|
||
row.Maliyete_dahil = maliyeteDahil ? 1 : 0
|
||
row.cmPriceTypeId = cmPriceTypeId
|
||
row.cm_price_type_id = cmPriceTypeId
|
||
row.sDovizCinsi = 'USD'
|
||
row.lDovizKuru = usdRate
|
||
row.lDovizFiyati = unitUSD
|
||
row.lFiyat = unitTRY
|
||
row.lTutar = unitTRY * quantity
|
||
row.usdTutar = unitUSD * quantity
|
||
row.eurTutar = unitEUR * quantity
|
||
row.gbpTutar = unitGBP * quantity
|
||
row.lTutarUSD = row.usdTutar
|
||
row.lTutarEURO = row.eurTutar
|
||
row.lTutarGBP = row.gbpTutar
|
||
row.lTutarTL = row.lTutar
|
||
|
||
if (row.lTutar !== 0 || options.markChanged) {
|
||
row.__lastValidTRYAmount = row.lTutar
|
||
}
|
||
if (row.usdTutar !== 0 || options.markChanged) {
|
||
row.__lastValidUSDAmount = row.usdTutar
|
||
}
|
||
|
||
if (!options.preserveInputs) {
|
||
row.miktarInput = normalizeQuantityInput(quantity)
|
||
row.inputPrice = normalizeInputPrice(inputPrice)
|
||
}
|
||
|
||
if (options.markChanged) {
|
||
row.draftChanged = true
|
||
}
|
||
|
||
if (options.priceType !== undefined) {
|
||
row.sFiyatTipi = options.priceType
|
||
}
|
||
|
||
if (options.updateState !== undefined) {
|
||
row.priceUpdateState = options.updateState
|
||
}
|
||
|
||
return row
|
||
}
|
||
|
||
function recalculateAllDetailRows () {
|
||
detailGroups.value = detailGroups.value.map(grp => ({
|
||
...grp,
|
||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => recalculateDetailRow({ ...row }, { preserveInputs: true }))
|
||
}))
|
||
refreshLiveGroupTotals()
|
||
}
|
||
|
||
function resolveRowTRYTutar (row) {
|
||
const quantity = resolveNumericRowQuantity(row)
|
||
const inputPrice = resolveNumericRowInputPrice(row)
|
||
const inputCurrency = resolveInputCurrency(row)
|
||
const liveAmount = resolveTRYUnitPriceByInput(
|
||
inputPrice,
|
||
inputCurrency,
|
||
resolveExchangeRateValue('USD'),
|
||
resolveExchangeRateValue('EUR'),
|
||
resolveExchangeRateValue('GBP')
|
||
) * quantity
|
||
|
||
if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
|
||
return parseMoneyInput(row?.lTutar)
|
||
}
|
||
|
||
function resolveRowEURTutar (row) {
|
||
return parseMoneyInput(row?.eurTutar)
|
||
}
|
||
|
||
function resolveRowGBPTutar (row) {
|
||
return parseMoneyInput(row?.gbpTutar)
|
||
}
|
||
|
||
function resolveRowUSDTutar (row) {
|
||
const quantity = resolveNumericRowQuantity(row)
|
||
const inputPrice = resolveNumericRowInputPrice(row)
|
||
const inputCurrency = resolveInputCurrency(row)
|
||
const liveAmount = resolveUSDUnitPriceByInput(
|
||
inputPrice,
|
||
inputCurrency,
|
||
resolveExchangeRateValue('USD'),
|
||
resolveExchangeRateValue('EUR'),
|
||
resolveExchangeRateValue('GBP')
|
||
) * quantity
|
||
|
||
if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
|
||
|
||
const rawUSDAmount = row?.usdTutar
|
||
if (rawUSDAmount !== undefined && rawUSDAmount !== null && String(rawUSDAmount).trim() !== '') {
|
||
return parseMoneyInput(rawUSDAmount)
|
||
}
|
||
|
||
const dovizFiyati = parseMoneyInput(row?.lDovizFiyati)
|
||
const calc = quantity * dovizFiyati
|
||
return Number.isFinite(calc) ? calc : 0
|
||
}
|
||
|
||
function shouldIncludeRowInGroupTotal (grp, row) {
|
||
if (!row) return false
|
||
|
||
// CM1/CM2 sub-headers always show their full group value. For every other
|
||
// group, the explicit "Maliyete Dahil" selection controls the subtotal.
|
||
if (isCMGroupName(grp?.sAciklama3 || row?.sAciklama3)) return true
|
||
|
||
const inclusionValue = row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil
|
||
if (inclusionValue === false || inclusionValue === 0) return false
|
||
const normalizedValue = String(inclusionValue ?? '').trim().toLowerCase()
|
||
return normalizedValue !== '0' && normalizedValue !== 'false' && normalizedValue !== 'hayir'
|
||
}
|
||
|
||
function refreshLiveGroupTotals () {
|
||
// Totals are derived directly from reactive rows. The revision only forces
|
||
// Quasar's table/sub-header render after an in-place row edit.
|
||
groupTotalsRevision.value += 1
|
||
}
|
||
|
||
const pendingRenderedAmounts = new Map()
|
||
let renderedAmountFlushScheduled = false
|
||
|
||
function captureRenderedGroupAmount (groupIndex, grp, row, currency, value) {
|
||
const rowKey = String(row?.__rowKey || row?.nOnMLDetNo || row?.sKodu || '').trim()
|
||
const cacheKey = `${groupIndex}:${rowKey}`
|
||
const previousPending = pendingRenderedAmounts.get(cacheKey) || {}
|
||
pendingRenderedAmounts.set(cacheKey, {
|
||
...previousPending,
|
||
groupIndex,
|
||
rowKey,
|
||
included: shouldIncludeRowInGroupTotal(grp, row),
|
||
[currency]: parseMoneyInput(value)
|
||
})
|
||
|
||
if (!renderedAmountFlushScheduled) {
|
||
renderedAmountFlushScheduled = true
|
||
Promise.resolve().then(() => {
|
||
const next = { ...renderedGroupAmounts.value }
|
||
let changed = false
|
||
pendingRenderedAmounts.forEach(entry => {
|
||
const groupRows = { ...(next[entry.groupIndex] || {}) }
|
||
const previous = groupRows[entry.rowKey] || {}
|
||
if (previous.try !== entry.try || previous.usd !== entry.usd || previous.included !== entry.included) {
|
||
groupRows[entry.rowKey] = { ...previous, ...entry }
|
||
changed = true
|
||
}
|
||
next[entry.groupIndex] = groupRows
|
||
})
|
||
pendingRenderedAmounts.clear()
|
||
renderedAmountFlushScheduled = false
|
||
if (changed) {
|
||
renderedGroupAmounts.value = next
|
||
groupTotalsRevision.value += 1
|
||
}
|
||
})
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function renderedRowsForGroup (groupIndex) {
|
||
return Object.values(renderedGroupAmounts.value[groupIndex] || {})
|
||
}
|
||
|
||
function resolveRenderedGroupIncludedCount (groupIndex) {
|
||
const grp = detailGroups.value[groupIndex]
|
||
return (Array.isArray(grp?.items) ? grp.items : []).filter(row => shouldIncludeRowInGroupTotal(grp, row)).length
|
||
}
|
||
|
||
function resolveRenderedGroupRowCount (groupIndex) {
|
||
const grp = detailGroups.value[groupIndex]
|
||
return Array.isArray(grp?.items) ? grp.items.length : 0
|
||
}
|
||
|
||
function resolveGroupTRYTutar (groupIndex, _revision) {
|
||
const grp = detailGroups.value[groupIndex]
|
||
return (Array.isArray(grp?.items) ? grp.items : []).reduce((total, row) => {
|
||
if (!shouldIncludeRowInGroupTotal(grp, row)) return total
|
||
return total + parseMoneyInput(row?.lTutar)
|
||
}, 0)
|
||
}
|
||
|
||
function resolveGroupUSDTutar (groupIndex, _revision) {
|
||
const grp = detailGroups.value[groupIndex]
|
||
return (Array.isArray(grp?.items) ? grp.items : []).reduce((total, row) => {
|
||
if (!shouldIncludeRowInGroupTotal(grp, row)) return total
|
||
return total + parseMoneyInput(row?.usdTutar ?? row?.lTutarUSD)
|
||
}, 0)
|
||
}
|
||
|
||
function resolveGroupQuantity (grp) {
|
||
return (Array.isArray(grp?.items) ? grp.items : []).reduce((total, row) => (
|
||
total + resolveNumericRowQuantity(row)
|
||
), 0)
|
||
}
|
||
|
||
function groupKey (grp, gi) {
|
||
return `${String(grp?.sAciklama3 || 'TANIMSIZ')}-${gi}`
|
||
}
|
||
|
||
function isGroupOpen (grp, gi) {
|
||
return groupOpenState.value[groupKey(grp, gi)] !== false
|
||
}
|
||
|
||
function toggleGroup (grp, gi) {
|
||
const key = groupKey(grp, gi)
|
||
groupOpenState.value = {
|
||
...groupOpenState.value,
|
||
[key]: !isGroupOpen(grp, gi)
|
||
}
|
||
}
|
||
|
||
function resolveElHeight (refVal) {
|
||
const el = refVal?.$el || refVal
|
||
return Number(el?.offsetHeight || 0)
|
||
}
|
||
|
||
function updateStickyTop () {
|
||
const stackH = resolveElHeight(stickyStackRef.value)
|
||
subHeaderTop.value = (stackH || 0) + 50
|
||
}
|
||
|
||
function toggleHeaderInfo () {
|
||
headerInfoCollapsed.value = !headerInfoCollapsed.value
|
||
nextTick(() => {
|
||
updateStickyTop()
|
||
})
|
||
}
|
||
|
||
function onUretimSekliChange (newId) {
|
||
if (!detailHeader.value) return
|
||
const found = productionTypes.value.find(t => String(t.id) === String(newId))
|
||
if (found) {
|
||
detailHeader.value.UretimSekliID = found.id
|
||
detailHeader.value.UretimSekli = found.aciklama
|
||
schedulePersistLocalDraft()
|
||
}
|
||
}
|
||
|
||
function buildDetailFetchParams () {
|
||
if (isNoCostDetail.value) {
|
||
return {
|
||
detail_source: 'no-cost',
|
||
urun_kodu: productCode.value,
|
||
recete_kodu: recipeCode.value,
|
||
trace_id: traceId.value
|
||
}
|
||
}
|
||
|
||
return {
|
||
n_onml_no: onMLNo.value,
|
||
urun_kodu: productCode.value,
|
||
trace_id: traceId.value
|
||
}
|
||
}
|
||
|
||
async function fetchExchangeRatesForCostDate (targetDate = costDate.value) {
|
||
const normalizedDate = normalizeDateInput(targetDate)
|
||
if (!normalizedDate) {
|
||
exchangeRates.value = createEmptyExchangeRates()
|
||
recalculateAllDetailRows()
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await get('/pricing/production-product-costing/has-cost-detail-exchange-rates', {
|
||
maliyet_tarihi: normalizedDate,
|
||
trace_id: traceId.value
|
||
})
|
||
exchangeRates.value = normalizeExchangeRatesPayload(response, normalizedDate)
|
||
} catch (err) {
|
||
exchangeRates.value = {
|
||
...createEmptyExchangeRates(),
|
||
rateDate: normalizedDate
|
||
}
|
||
slog.error('production-product-costing.detail', 'exchange-rates:error', {
|
||
trace_id: traceId.value,
|
||
maliyet_tarihi: normalizedDate,
|
||
detail: await extractApiErrorDetail(err)
|
||
})
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: await extractApiErrorDetail(err),
|
||
position: 'top-right'
|
||
})
|
||
} finally {
|
||
recalculateAllDetailRows()
|
||
}
|
||
}
|
||
|
||
async function fetchDetail (options = {}) {
|
||
const hydrateDraft = options.hydrateDraft !== false
|
||
const clearDraft = options.clearDraft === true
|
||
if (isNoCostDetail.value && !recipeCode.value) {
|
||
detailError.value = 'Recete kodu bulunamadi'
|
||
detailGroups.value = []
|
||
detailHeader.value = null
|
||
exchangeRates.value = createEmptyExchangeRates()
|
||
return
|
||
}
|
||
|
||
if (!isNoCostDetail.value && !onMLNo.value) {
|
||
detailError.value = 'nOnMLNo bulunamadi'
|
||
detailGroups.value = []
|
||
detailHeader.value = null
|
||
exchangeRates.value = createEmptyExchangeRates()
|
||
return
|
||
}
|
||
|
||
detailLoading.value = true
|
||
detailError.value = ''
|
||
detailGroups.value = []
|
||
renderedGroupAmounts.value = {}
|
||
pendingRenderedAmounts.clear()
|
||
detailHeader.value = null
|
||
costDate.value = ''
|
||
exchangeRates.value = createEmptyExchangeRates()
|
||
newRowSequence.value = 0
|
||
rowEditorDialogOpen.value = false
|
||
rowEditorMode.value = 'new'
|
||
rowEditorTargetRowKey.value = ''
|
||
rowEditorForm.value = createRowEditorForm()
|
||
rowEditorHammaddeAllOptions.value = []
|
||
rowEditorHammaddeOptions.value = []
|
||
rowEditorHammaddeLoading.value = false
|
||
rowEditorItemAllOptions.value = []
|
||
rowEditorItemOptions.value = []
|
||
rowEditorItemLoading.value = false
|
||
rowEditorColorAllOptions.value = []
|
||
rowEditorColorOptions.value = []
|
||
rowEditorColorLoading.value = false
|
||
lineHistoryDialogOpen.value = false
|
||
lineHistoryLoading.value = false
|
||
lineHistoryError.value = ''
|
||
lineHistoryRows.value = []
|
||
lineHistoryTargetRowKey.value = ''
|
||
lineHistoryTargetHammaddeTuruNo.value = ''
|
||
lineHistoryTargetItemCode.value = ''
|
||
lineHistoryTargetSummary.value = ''
|
||
lineHistorySearchMode.value = 'exact'
|
||
lineHistoryLastPurchaseMatchStage.value = ''
|
||
lineHistoryLastRecipeMatchStage.value = ''
|
||
|
||
try {
|
||
if (clearDraft) {
|
||
clearLocalDraft()
|
||
}
|
||
const detailParams = buildDetailFetchParams()
|
||
slog.info('production-product-costing.detail', 'fetch-detail:start', {
|
||
trace_id: traceId.value,
|
||
detail_source: detailSource.value || 'has-cost',
|
||
n_onml_no: onMLNo.value,
|
||
urun_kodu: productCode.value,
|
||
recete_kodu: recipeCode.value
|
||
})
|
||
const [headerData, groupsData, typesData] = await Promise.all([
|
||
get('/pricing/production-product-costing/has-cost-detail-header', detailParams),
|
||
get('/pricing/production-product-costing/has-cost-detail-groups', detailParams),
|
||
get('/pricing/production-product-costing/production-types', {
|
||
trace_id: traceId.value
|
||
})
|
||
])
|
||
detailHeader.value = headerData && typeof headerData === 'object' ? headerData : null
|
||
productionTypes.value = Array.isArray(typesData) ? typesData : []
|
||
// Prefer true costing date (spUrtOnMLMas.Tarihi) over record create/update timestamps.
|
||
costDate.value = normalizeDateInput(detailHeader.value?.maliyetTarihi || detailHeader.value?.dteKayitTarihi)
|
||
detailGroups.value = normalizeDetailGroups(groupsData)
|
||
initialHeaderSnapshot.value = currentHeaderSnapshot.value
|
||
// Optional: hydrate local draft after base data load.
|
||
if (hydrateDraft) {
|
||
tryHydrateFromLocalDraft()
|
||
}
|
||
// Ensure required placeholder rows exist (based on mapping screen) only for the "new/no-cost" flow.
|
||
// For has-cost records (existing), we do not inject placeholders after load.
|
||
// Placeholders should be created only when detail_source=no-cost (route query),
|
||
// so saved records always render exactly what backend has.
|
||
if (isNoCostDetail.value) {
|
||
try {
|
||
const mappings = await fetchRequiredParcaMappings()
|
||
await ensureNoCostRequiredRowsFromMappings(mappings)
|
||
} catch (err) {
|
||
slog.error('production-product-costing.detail', 'required-mapping:error', {
|
||
trace_id: traceId.value,
|
||
detail: await extractApiErrorDetail(err)
|
||
})
|
||
}
|
||
}
|
||
const initialOpen = {}
|
||
detailGroups.value.forEach((grp, gi) => {
|
||
initialOpen[groupKey(grp, gi)] = true
|
||
})
|
||
groupOpenState.value = initialOpen
|
||
slog.info('production-product-costing.detail', 'fetch-detail:ok', {
|
||
trace_id: traceId.value,
|
||
detail_source: detailSource.value || 'has-cost',
|
||
group_count: detailGroups.value.length,
|
||
item_count: flatDetailRows.value.length,
|
||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || ''
|
||
})
|
||
// tbStok exists-bulk kontrolu devre disi (manual giris kapali).
|
||
// Load last10 avg deviation warnings panel (non-blocking).
|
||
await refreshLast10Warnings()
|
||
// (eski tbStok missing dialog/notify kaldirildi)
|
||
} catch (err) {
|
||
detailError.value = await extractApiErrorDetail(err)
|
||
slog.error('production-product-costing.detail', 'fetch-detail:error', {
|
||
trace_id: traceId.value,
|
||
detail_source: detailSource.value || 'has-cost',
|
||
n_onml_no: onMLNo.value,
|
||
urun_kodu: productCode.value,
|
||
recete_kodu: recipeCode.value,
|
||
detail: detailError.value
|
||
})
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function goBack () {
|
||
if (isNoCostDetail.value) {
|
||
router.push({ name: 'production-product-costing-no-cost' })
|
||
return
|
||
}
|
||
|
||
const urunKodu = productCode.value
|
||
if (!urunKodu) {
|
||
router.push({ name: 'production-product-costing-has-cost' })
|
||
return
|
||
}
|
||
router.push({
|
||
name: 'production-product-costing-has-cost-history',
|
||
query: { urun_kodu: urunKodu }
|
||
})
|
||
}
|
||
|
||
function buildDetailItemRequestPayload (row) {
|
||
return {
|
||
__rowKey: row.__rowKey,
|
||
n_onml_no: String(row?.nOnMLNo || detailHeader.value?.nOnMLNo || onMLNo.value || '').trim(),
|
||
n_onml_det_no: String(row?.nOnMLDetNo || '').trim(),
|
||
n_hammadde_turu_no: String(row?.nHammaddeTuruNo || '').trim(),
|
||
s_kodu: String(row?.sKodu || '').trim(),
|
||
s_aciklama: String(row?.sAciklama || '').trim(),
|
||
s_renk: String(row?.sRenk || '').trim(),
|
||
color_code: String(firstDefinedValue(row, ['ColorCode', 'colorCode', 'sRenk', 's_renk'])).trim(),
|
||
color_description: String(firstDefinedValue(row, ['ColorDescription', 'colorDescription', 'sRenkAdi', 's_renk_adi'])).trim(),
|
||
item_dim1_code: String(firstDefinedValue(row, ['ItemDim1Code', 'itemDim1Code', 'sBeden', 's_beden'])).trim(),
|
||
item_dim1_description: String(firstDefinedValue(row, ['ItemDim1Description', 'itemDim1Description', 'sAciklama2', 's_aciklama2'])).trim(),
|
||
s_birim: String(row?.sBirim || '').trim(),
|
||
l_miktar: resolveNumericRowQuantity(row),
|
||
fiyat_girilen: resolveNumericRowInputPrice(row),
|
||
fiyat_doviz: resolveInputCurrency(row),
|
||
maliyete_dahil: row?.maliyeteDahil ? 1 : 0,
|
||
cm_price_type_id: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, row?.sAciklama3)
|
||
}
|
||
}
|
||
|
||
function applyPriceSelectionToRow (targetRowKey, price, currency, priceType, itemCode, itemDescription, historyCompanyCode) {
|
||
const normalizedCurrency = normalizePriceCurrency(currency) || 'USD'
|
||
const normalizedPrice = parseMoneyInput(price)
|
||
const finalPriceType = priceType || 'SAF'
|
||
const normalizedCode = normalizeCodeValue(itemCode)
|
||
const normalizedDesc = String(itemDescription || '').trim()
|
||
|
||
const isICode = String(normalizedCode || '').toUpperCase().startsWith('I.')
|
||
const selectedFirma = String(detailHeader.value?.FirmaKodu || '').trim().toUpperCase()
|
||
const historyFirma = String(historyCompanyCode || '').trim().toUpperCase()
|
||
const isSameFirma = Boolean(selectedFirma && historyFirma && selectedFirma === historyFirma)
|
||
|
||
detailGroups.value = detailGroups.value.map(grp => ({
|
||
...grp,
|
||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||
if (row.__rowKey !== targetRowKey) return row
|
||
return recalculateDetailRow({
|
||
...row,
|
||
...(normalizedCode ? { sKodu: normalizedCode } : {}),
|
||
...(normalizedDesc ? { sAciklama: normalizedDesc } : {}),
|
||
...(isICode ? { __iCodeSelected: true, __iCodeSameFirma: isSameFirma } : { __iCodeSelected: false, __iCodeSameFirma: false }),
|
||
inputPrice: normalizeInputPrice(normalizedPrice),
|
||
fiyat_girilen: normalizedPrice,
|
||
inputPricePrBr: normalizedCurrency,
|
||
fiyat_doviz: normalizedCurrency
|
||
}, {
|
||
priceType: finalPriceType,
|
||
updateState: 'selection',
|
||
markChanged: true
|
||
})
|
||
})
|
||
}))
|
||
}
|
||
|
||
async function fetchSimilarItemHistory (mode = 'prefix') {
|
||
if (mode === 'exact') {
|
||
const row = flatDetailRows.value.find(r => r.__rowKey === lineHistoryTargetRowKey.value)
|
||
if (row) {
|
||
await openLineHistory(row)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (!lineHistoryTargetHammaddeTuruNo.value && !lineHistoryTargetItemCode.value) return
|
||
|
||
const normalizedMode = mode === 'alternative' ? 'alternative' : 'prefix'
|
||
lineHistorySearchMode.value = normalizedMode
|
||
lineHistoryLoading.value = true
|
||
lineHistoryError.value = ''
|
||
|
||
try {
|
||
slog.info('production-product-costing.detail', 'line-history-similar:start', {
|
||
trace_id: traceId.value,
|
||
search_mode: normalizedMode,
|
||
n_hammadde_turu_no: lineHistoryTargetHammaddeTuruNo.value,
|
||
s_kodu: lineHistoryTargetItemCode.value,
|
||
maliyet_tarihi: costDate.value,
|
||
limit: LINE_HISTORY_ROW_LIMIT
|
||
})
|
||
|
||
const response = await get('/pricing/production-product-costing/has-cost-detail-similar-history', {
|
||
n_hammadde_turu_no: lineHistoryTargetHammaddeTuruNo.value,
|
||
s_kodu: lineHistoryTargetItemCode.value,
|
||
maliyet_tarihi: costDate.value,
|
||
limit: LINE_HISTORY_ROW_LIMIT,
|
||
search_mode: normalizedMode,
|
||
trace_id: traceId.value
|
||
})
|
||
|
||
lineHistoryRows.value = normalizeLineHistoryRows(response)
|
||
lineHistoryLastPurchaseMatchStage.value = String(response?.purchase_match_stage || '').trim()
|
||
lineHistoryLastRecipeMatchStage.value = String(response?.recipe_match_stage || '').trim()
|
||
slog.info('production-product-costing.detail', 'line-history-similar:ok', {
|
||
trace_id: traceId.value,
|
||
search_mode: normalizedMode,
|
||
purchase_match_stage: response?.purchase_match_stage || '',
|
||
recipe_match_stage: response?.recipe_match_stage || '',
|
||
similar_code_prefix: response?.similar_code_prefix || '',
|
||
row_count: lineHistoryRows.value.length
|
||
})
|
||
|
||
if (lineHistoryRows.value.length === 0) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Bu hammadde türü için de benzer ürün kaydı bulunamadı.',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
} catch (err) {
|
||
lineHistoryError.value = await extractApiErrorDetail(err)
|
||
slog.error('production-product-costing.detail', 'line-history-similar:error', {
|
||
trace_id: traceId.value,
|
||
search_mode: normalizedMode,
|
||
n_hammadde_turu_no: lineHistoryTargetHammaddeTuruNo.value,
|
||
s_kodu: lineHistoryTargetItemCode.value,
|
||
error: lineHistoryError.value
|
||
})
|
||
} finally {
|
||
lineHistoryLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onRowQuantityInput (row, value) {
|
||
row.miktarInput = value
|
||
recalculateDetailRow(row, {
|
||
preserveInputs: true,
|
||
markChanged: true
|
||
})
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
|
||
function triggerUIUpdate () {
|
||
refreshLiveGroupTotals()
|
||
}
|
||
|
||
function normalizeRowQuantityDisplay (row) {
|
||
row.miktarInput = normalizeQuantityInput(row?.miktarInput)
|
||
recalculateDetailRow(row, {
|
||
preserveInputs: true
|
||
})
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
|
||
function onRowInputPriceChange (row, value) {
|
||
row.inputPrice = value
|
||
recalculateDetailRow(row, {
|
||
preserveInputs: true,
|
||
priceType: 'MAN',
|
||
updateState: 'manual',
|
||
markChanged: true
|
||
})
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
|
||
function normalizeRowPriceDisplay (row) {
|
||
row.inputPrice = normalizeInputPrice(row?.inputPrice)
|
||
recalculateDetailRow(row, {
|
||
preserveInputs: true
|
||
})
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
|
||
function onRowInputPriceCurrencyChange (row, value) {
|
||
row.inputPricePrBr = normalizePriceCurrency(value) || 'USD'
|
||
recalculateDetailRow(row, {
|
||
preserveInputs: true,
|
||
priceType: 'MAN',
|
||
updateState: 'manual',
|
||
markChanged: true
|
||
})
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
|
||
function onRowMaliyeteDahilChange (row, value) {
|
||
row.maliyeteDahil = Boolean(value)
|
||
row.maliyete_dahil = value ? 1 : 0
|
||
row.Maliyete_dahil = value ? 1 : 0
|
||
row.draftChanged = true
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
|
||
function onRowCMPriceTypeChange (row, value) {
|
||
row.cmPriceTypeId = isCMGroupName(row?.sAciklama3) ? (value ? 2 : 1) : null
|
||
row.cm_price_type_id = row.cmPriceTypeId
|
||
row.draftChanged = true
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
|
||
async function fetchBulkItemPrices () {
|
||
const flatRows = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||
if (flatRows.length === 0) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Toplu fiyat cagrisi icin satir bulunamadi.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
bulkPriceLoading.value = true
|
||
try {
|
||
const response = await post('/pricing/production-product-costing/has-cost-detail-bulk-prices', {
|
||
n_onml_no: onMLNo.value,
|
||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || '',
|
||
maliyet_tarihi: costDate.value,
|
||
items: flatRows.map(buildDetailItemRequestPayload)
|
||
}, {
|
||
params: {
|
||
trace_id: traceId.value
|
||
}
|
||
})
|
||
|
||
const updates = normalizeBulkPriceItems(response)
|
||
if (updates.length === 0) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Toplu fiyat cagrisindan uygulanacak veri donmedi.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
let appliedCount = 0
|
||
detailGroups.value = detailGroups.value.map(grp => ({
|
||
...grp,
|
||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||
const match = updates.find(update => rowMatchesBulkUpdate(row, update))
|
||
if (!match) return row
|
||
appliedCount += 1
|
||
return recalculateDetailRow({
|
||
...row,
|
||
inputPrice: match.inputPrice,
|
||
fiyat_girilen: match.fiyat_girilen,
|
||
inputPricePrBr: match.inputPricePrBr,
|
||
fiyat_doviz: match.fiyat_doviz
|
||
}, {
|
||
priceType: match.priceType || 'SAF',
|
||
updateState: 'bulk',
|
||
markChanged: true
|
||
})
|
||
})
|
||
}))
|
||
// Update sub-headers immediately after the bulk response is applied. Do
|
||
// not wait for the optional previous-cost autofill request below.
|
||
triggerUIUpdate()
|
||
|
||
$q.notify({
|
||
type: appliedCount > 0 ? 'positive' : 'warning',
|
||
message: appliedCount > 0
|
||
? `${appliedCount} kalemin fiyat ve pr. br. bilgisi guncellendi.`
|
||
: 'Donen veriler satirlarla eslestirilemedi.',
|
||
position: 'top-right'
|
||
})
|
||
|
||
// Autofill missing required codes from last OnML history (URETIM spUrtOnMLMasDet)
|
||
try {
|
||
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||
const targets = flatNow.filter(r =>
|
||
Boolean(r?.requiredPlaceholder) &&
|
||
// CM1/CM2 and FABRIC rows must be chosen by user; don't auto-fill from previous costing.
|
||
!isCMGroupName(r?.sAciklama3) &&
|
||
normalizeGroupName(r?.sAciklama3) !== 'FABRIC' &&
|
||
(String(r?.sKodu || '').trim() === '' || Number(resolveNumericRowInputPrice(r) || 0) <= 0)
|
||
)
|
||
const hNos = Array.from(new Set(targets
|
||
.map(r => parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10))
|
||
.filter(n => n > 0)
|
||
))
|
||
if (hNos.length > 0) {
|
||
const lastRows = await post('/pricing/production-product-costing/has-cost-detail/last-detail', {
|
||
nHammaddeTuruNos: hNos,
|
||
before_date: normalizeDateInput(costDate.value),
|
||
exclude_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
|
||
n_firma_id: parseInt(String(detailHeader.value?.nFirmaID || detailHeader.value?.NFirmaID || '0').trim() || '0', 10) || 0
|
||
}, { params: { trace_id: traceId.value } })
|
||
|
||
const list = Array.isArray(lastRows) ? lastRows : []
|
||
const byNo = {}
|
||
list.forEach(x => {
|
||
const no = parseInt(String(x?.nHammaddeTuruNo || '0'), 10) || 0
|
||
if (no > 0) byNo[no] = x
|
||
})
|
||
|
||
let filled = 0
|
||
detailGroups.value = detailGroups.value.map(grp => ({
|
||
...grp,
|
||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||
if (!row?.requiredPlaceholder) return row
|
||
if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return row
|
||
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
|
||
const hit = byNo[no]
|
||
if (!hit) return row
|
||
|
||
const next = { ...row }
|
||
const hasCode = String(next.sKodu || '').trim() !== ''
|
||
const hasPrice = Number(resolveNumericRowInputPrice(next) || 0) > 0
|
||
|
||
if (!hasCode && String(hit?.sKodu || '').trim()) {
|
||
next.sKodu = String(hit.sKodu || '').trim()
|
||
next.sAciklama = String(hit?.sAciklama || '').trim()
|
||
}
|
||
if (!hasPrice && Number(hit?.fiyat_girilen || 0) > 0) {
|
||
next.inputPrice = String(hit.fiyat_girilen)
|
||
next.fiyat_girilen = Number(hit.fiyat_girilen)
|
||
const pr = String(hit?.fiyat_doviz || '').trim() || 'USD'
|
||
next.inputPricePrBr = pr
|
||
next.fiyat_doviz = pr
|
||
}
|
||
|
||
// Only mark if something changed
|
||
const changed = (next.sKodu !== row.sKodu) || (next.sAciklama !== row.sAciklama) || (next.inputPrice !== row.inputPrice) || (next.inputPricePrBr !== row.inputPricePrBr)
|
||
if (!changed) return row
|
||
filled += 1
|
||
return recalculateDetailRow({
|
||
...next,
|
||
__autoFilledFromPrev: true
|
||
,
|
||
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
|
||
}, {
|
||
priceType: 'PREV',
|
||
updateState: 'autofill',
|
||
markChanged: true
|
||
})
|
||
})
|
||
}))
|
||
triggerUIUpdate()
|
||
|
||
if (filled > 0) {
|
||
$q.notify({
|
||
type: 'info',
|
||
message: `${filled} satirda kod/aciklama ve fiyat bilgisi onceki maliyetten otomatik getirildi.`,
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Non-blocking
|
||
slog.error('production-product-costing.detail', 'bulk:autofill-prev:error', {
|
||
trace_id: traceId.value,
|
||
error: String(e?.message || e || '')
|
||
})
|
||
}
|
||
// Bulk price and previous-cost autofill paths can update many rows at once.
|
||
// Recalculate once more as a whole so group sub-header totals react immediately.
|
||
recalculateAllDetailRows()
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
} catch (err) {
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: await extractApiErrorDetail(err),
|
||
position: 'top-right'
|
||
})
|
||
} finally {
|
||
bulkPriceLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openLineHistory (row) {
|
||
const rowCode = String(row?.sKodu || '').trim()
|
||
if (!rowCode) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'History acmak icin satirda kod bilgisi olmali.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
lineHistoryDialogOpen.value = true
|
||
lineHistoryLoading.value = true
|
||
lineHistoryError.value = ''
|
||
lineHistoryRows.value = []
|
||
lineHistoryTargetRowKey.value = row.__rowKey
|
||
lineHistoryTargetHammaddeTuruNo.value = String(row?.nHammaddeTuruNo || '').trim()
|
||
lineHistoryTargetItemCode.value = rowCode
|
||
lineHistoryTargetSummary.value = `${rowCode} | ${String(row?.sAciklama || '').trim() || 'TANIMSIZ'}`
|
||
lineHistorySearchMode.value = 'exact'
|
||
lineHistoryLastPurchaseMatchStage.value = ''
|
||
lineHistoryLastRecipeMatchStage.value = ''
|
||
|
||
try {
|
||
const response = await get('/pricing/production-product-costing/has-cost-detail-line-history', {
|
||
n_onml_no: onMLNo.value,
|
||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || '',
|
||
n_onml_det_no: String(row?.nOnMLDetNo || '').trim(),
|
||
n_hammadde_turu_no: String(row?.nHammaddeTuruNo || '').trim(),
|
||
s_kodu: rowCode,
|
||
s_renk: String(row?.sRenk || '').trim(),
|
||
color_code: String(firstDefinedValue(row, ['ColorCode', 'colorCode', 'sRenk', 's_renk'])).trim(),
|
||
item_dim1_code: String(firstDefinedValue(row, ['ItemDim1Code', 'itemDim1Code', 'sBeden', 's_beden'])).trim(),
|
||
maliyet_tarihi: costDate.value,
|
||
trace_id: traceId.value
|
||
})
|
||
lineHistoryRows.value = normalizeLineHistoryRows(response)
|
||
} catch (err) {
|
||
lineHistoryError.value = await extractApiErrorDetail(err)
|
||
} finally {
|
||
lineHistoryLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onDetailRowClick (evt, row) {
|
||
// Grid satırı tıklanabilir değil, artık ikonlar ve butonlar kullanılıyor.
|
||
}
|
||
|
||
function applyLineHistorySelection (historyRow) {
|
||
const targetKey = String(lineHistoryTargetRowKey.value || '').trim()
|
||
if (!targetKey) {
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: 'Hedef satir bulunamadi (rowKey bos).',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
applyPriceSelectionToRow(
|
||
targetKey,
|
||
historyRow?.price,
|
||
historyRow?.currency,
|
||
historyRow?.priceType,
|
||
historyRow?.itemCode,
|
||
historyRow?.itemDescription,
|
||
historyRow?.companyCode
|
||
)
|
||
lineHistoryDialogOpen.value = false
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: `Secilen fiyat satira uygulandi: ${formatMoney(historyRow?.price)} ${String(historyRow?.currency || '').trim() || 'USD'}`,
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
|
||
function resolveDetailRowClass (row) {
|
||
const key = String(row?.__rowKey || '').trim()
|
||
if (key && requiredAttentionRowKeys.value?.[key]) return 'pcd-detail-row-required'
|
||
if (row?.requiredPlaceholder) return 'pcd-detail-row-required'
|
||
const code = String(row?.sKodu || '').trim()
|
||
if (code && isTbStokMissingCode(code)) return 'pcd-detail-row-missing-stock'
|
||
return row?.draftChanged ? 'pcd-detail-row-secondary' : ''
|
||
}
|
||
|
||
async function bootstrapRowEditorOptions () {
|
||
try {
|
||
const hammaddeSearch = String(rowEditorForm.value.nHammaddeTuruNo || '').trim()
|
||
const itemSearch = String(rowEditorForm.value.sKodu || '').trim()
|
||
const colorSearch = String(rowEditorForm.value.ColorCode || '').trim()
|
||
const [hammaddeRows, itemRows] = await Promise.all([
|
||
refreshRowEditorOptions('hammadde', hammaddeSearch),
|
||
itemSearch.length >= ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH
|
||
? refreshRowEditorOptions('item', itemSearch)
|
||
: Promise.resolve([])
|
||
])
|
||
if (itemSearch.length < ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH) {
|
||
rowEditorItemAllOptions.value = []
|
||
rowEditorItemOptions.value = []
|
||
primeRowEditorOptionsFromForm()
|
||
}
|
||
const selectedHammadde = hammaddeRows.find(opt => String(opt?.value || '') === hammaddeSearch)
|
||
if (selectedHammadde) {
|
||
rowEditorForm.value.sHammaddeTuruAdi = String(selectedHammadde.sHammaddeTuruAdi || rowEditorForm.value.sHammaddeTuruAdi || '').trim()
|
||
rowEditorForm.value.sAciklama3 = String(selectedHammadde.sAciklama3 || rowEditorForm.value.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||
// Parca adi should come from spUrtMTBolum.sAdi (backend sends as sParcaAdi). Never fall back to group label (DT/TP/...).
|
||
rowEditorForm.value.sParcaAdi = String(selectedHammadde.sParcaAdi || rowEditorForm.value.sParcaAdi || '').trim()
|
||
}
|
||
const selectedItem = itemRows.find(opt => String(opt?.value || '') === itemSearch)
|
||
if (selectedItem) {
|
||
rowEditorForm.value.nStokID = String(selectedItem.nStokID || rowEditorForm.value.nStokID || '').trim()
|
||
rowEditorForm.value.sModel = String(selectedItem.sModel || rowEditorForm.value.sModel || '').trim()
|
||
rowEditorForm.value.sAciklama = String(selectedItem.sAciklama || rowEditorForm.value.sAciklama || '').trim()
|
||
rowEditorForm.value.sBirim = extractPrimaryUnitValue(selectedItem.sBirim || rowEditorForm.value.sBirim || 'AD') || 'AD'
|
||
}
|
||
await refreshRowEditorOptions('color', colorSearch)
|
||
primeRowEditorOptionsFromForm()
|
||
} catch (err) {
|
||
rowEditorHammaddeAllOptions.value = []
|
||
rowEditorHammaddeOptions.value = []
|
||
rowEditorItemAllOptions.value = []
|
||
rowEditorItemOptions.value = []
|
||
rowEditorColorAllOptions.value = []
|
||
rowEditorColorOptions.value = []
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: await extractApiErrorDetail(err),
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
|
||
function onRowEditorHammaddeChange (value) {
|
||
const selected = rowEditorHammaddeOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||
rowEditorForm.value.nHammaddeTuruNo = String(value || '').trim()
|
||
if (!selected) return
|
||
rowEditorForm.value.sHammaddeTuruAdi = String(selected.sHammaddeTuruAdi || '').trim()
|
||
rowEditorForm.value.sAciklama3 = String(selected.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||
// Parca adi should come from spUrtMTBolum.sAdi (backend sends as sParcaAdi). Never fall back to group label (DT/TP/...).
|
||
rowEditorForm.value.sParcaAdi = String(selected.sParcaAdi || '').trim()
|
||
if (!isCMGroupName(rowEditorForm.value.sAciklama3)) {
|
||
rowEditorForm.value.cmPriceTypeChecked = false
|
||
}
|
||
}
|
||
|
||
function openFabricCopyDialog () {
|
||
fabricCopySelectedKey.value = ''
|
||
fabricCopyDialogOpen.value = true
|
||
}
|
||
|
||
async function applyFabricCopySelection () {
|
||
const key = String(fabricCopySelectedKey.value || '').trim()
|
||
if (!key) return
|
||
const match = fabricCopySourceRows.value.find(r => {
|
||
const code = String(r?.sKodu || '').trim()
|
||
const color = String(resolveRowColorCode(r) || '').trim()
|
||
const hNo = String(r?.nHammaddeTuruNo || '').trim()
|
||
return `${hNo}|${code}|${color}` === key
|
||
})
|
||
if (!match) {
|
||
fabricCopyDialogOpen.value = false
|
||
return
|
||
}
|
||
|
||
// Copy: Code + Price + Currency. Leave color as-is (user may be copying price only).
|
||
rowEditorForm.value.sKodu = String(match.sKodu || '').trim()
|
||
// Ensure select can render the chosen value even if it isn't in the current options list.
|
||
upsertEditorOption(rowEditorItemAllOptions, buildRowEditorItemOption({ ...match, value: String(match.sKodu || '').trim() }))
|
||
upsertEditorOption(rowEditorItemOptions, buildRowEditorItemOption({ ...match, value: String(match.sKodu || '').trim() }))
|
||
try {
|
||
await onRowEditorItemChange(String(match.sKodu || '').trim())
|
||
} catch {}
|
||
|
||
const price = resolveNumericRowInputPrice(match)
|
||
const cur = resolveInputCurrency(match) || 'USD'
|
||
rowEditorForm.value.inputPrice = normalizeInputPrice(price)
|
||
rowEditorForm.value.fiyat_girilen = parseMoneyInput(price)
|
||
rowEditorForm.value.inputPricePrBr = normalizePriceCurrency(cur) || 'USD'
|
||
rowEditorForm.value.fiyat_doviz = normalizePriceCurrency(cur) || 'USD'
|
||
|
||
fabricCopyDialogOpen.value = false
|
||
}
|
||
|
||
async function onRowEditorItemChange (value) {
|
||
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||
// Prevent free-typed values from being accepted as "code". User must pick from list.
|
||
if (!selected) {
|
||
const typed = String(value || '').trim()
|
||
// If user cleared the field, allow clearing (other validations will catch blank-on-save if needed).
|
||
if (!typed) {
|
||
rowEditorForm.value.sKodu = ''
|
||
rowEditorLastValidItemValue.value = ''
|
||
return
|
||
}
|
||
// Revert to last valid selection.
|
||
rowEditorForm.value.sKodu = String(rowEditorLastValidItemValue.value || '').trim()
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: 'Kod secilmeden serbest metin girilemez. Listeden secim yapin.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
rowEditorForm.value.sKodu = String(value || '').trim()
|
||
rowEditorLastValidItemValue.value = String(value || '').trim()
|
||
const previousColorCode = String(rowEditorForm.value.ColorCode || '').trim()
|
||
rowEditorForm.value.nStokID = String(selected.nStokID || '').trim()
|
||
rowEditorForm.value.sModel = String(selected.sModel || '').trim()
|
||
rowEditorForm.value.sAciklama = String(selected.sAciklama || '').trim()
|
||
rowEditorForm.value.sBirim = extractPrimaryUnitValue(selected.sBirim || rowEditorForm.value.sBirim || 'AD') || 'AD'
|
||
rowEditorForm.value.ColorCode = ''
|
||
rowEditorForm.value.sRenk = ''
|
||
rowEditorForm.value.ColorDescription = ''
|
||
try {
|
||
await refreshRowEditorOptions('color', previousColorCode)
|
||
} catch (err) {
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: `Renk lookup getirilemedi: ${await extractApiErrorDetail(err)}`,
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
const matchedColor = rowEditorColorOptions.value.find(opt => String(opt?.value || '') === previousColorCode)
|
||
if (matchedColor) {
|
||
rowEditorForm.value.ColorCode = previousColorCode
|
||
rowEditorForm.value.sRenk = previousColorCode
|
||
rowEditorForm.value.ColorDescription = String(matchedColor.colorDescription || '').trim()
|
||
}
|
||
}
|
||
|
||
function onRowEditorColorChange (value) {
|
||
const selected = rowEditorColorOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||
const normalizedValue = String(value || '').trim()
|
||
rowEditorForm.value.ColorCode = normalizedValue
|
||
rowEditorForm.value.sRenk = normalizedValue
|
||
if (!selected) return
|
||
rowEditorForm.value.ColorDescription = String(selected.colorDescription || '').trim()
|
||
}
|
||
|
||
function buildRowFromEditorForm () {
|
||
const form = normalizeLegacyParcaAndGroup(rowEditorForm.value)
|
||
const existingRow = flatDetailRows.value.find(row => row.__rowKey === rowEditorTargetRowKey.value)
|
||
const cmPriceTypeId = normalizeCMPriceTypeId(form.cmPriceTypeChecked ? 2 : 1, form.sAciklama3)
|
||
if (!existingRow) {
|
||
newRowSequence.value += 1
|
||
}
|
||
|
||
return recalculateDetailRow({
|
||
...(existingRow || {}),
|
||
__rowKey: existingRow?.__rowKey || `new-editor-row-${newRowSequence.value}`,
|
||
isNew: Boolean(existingRow?.isNew) || rowEditorMode.value === 'new',
|
||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||
nStokID: String(form.nStokID || '').trim(),
|
||
sModel: String(form.sModel || '').trim(),
|
||
nOnMLDetNo: String(form.nOnMLDetNo || '').trim(),
|
||
// Keep Parca Adi and Parca Grubu distinct. sAciklama3 is the group key (DT/TP/CM2/FABRIC).
|
||
sParcaAdi: String(form.sParcaAdi || '').trim(),
|
||
sAciklama3: String(form.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ',
|
||
nHammaddeTuruNo: String(form.nHammaddeTuruNo || '').trim(),
|
||
sHammaddeTuruAdi: String(form.sHammaddeTuruAdi || '').trim(),
|
||
sKodu: String(form.sKodu || '').trim(),
|
||
sAciklama: String(form.sAciklama || '').trim(),
|
||
sRenk: String(form.sRenk || form.ColorCode || '').trim(),
|
||
ColorCode: String(form.ColorCode || form.sRenk || '').trim(),
|
||
ColorDescription: String(form.ColorDescription || '').trim(),
|
||
lMiktar: parseMoneyInput(form.miktarInput),
|
||
miktarInput: normalizeQuantityInput(form.miktarInput),
|
||
inputPrice: normalizeInputPrice(form.inputPrice),
|
||
inputPricePrBr: normalizePriceCurrency(form.inputPricePrBr) || 'USD',
|
||
fiyat_girilen: parseMoneyInput(form.inputPrice),
|
||
fiyat_doviz: normalizePriceCurrency(form.inputPricePrBr) || 'USD',
|
||
maliyeteDahil: Boolean(form.maliyeteDahil),
|
||
maliyete_dahil: form.maliyeteDahil ? 1 : 0,
|
||
Maliyete_dahil: form.maliyeteDahil ? 1 : 0,
|
||
cmPriceTypeId,
|
||
cm_price_type_id: cmPriceTypeId,
|
||
sBirim: String(form.sBirim || 'AD').trim() || 'AD',
|
||
draftChanged: true
|
||
}, {
|
||
preserveInputs: true,
|
||
priceType: 'MAN',
|
||
updateState: 'editor',
|
||
markChanged: true
|
||
})
|
||
}
|
||
|
||
function applyEditorRowToGroups (nextRow) {
|
||
const targetGroupName = String(nextRow?.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||
const nextGroups = detailGroups.value
|
||
.map(grp => ({
|
||
...grp,
|
||
items: (Array.isArray(grp?.items) ? grp.items : []).filter(row => row.__rowKey !== nextRow.__rowKey)
|
||
}))
|
||
.filter(grp => (Array.isArray(grp?.items) ? grp.items.length : 0) > 0 || String(grp?.sAciklama3 || '').trim() === targetGroupName)
|
||
|
||
const targetIndex = nextGroups.findIndex(grp => String(grp?.sAciklama3 || '').trim() === targetGroupName)
|
||
if (targetIndex >= 0) {
|
||
nextGroups[targetIndex] = {
|
||
...nextGroups[targetIndex],
|
||
items: [...(Array.isArray(nextGroups[targetIndex].items) ? nextGroups[targetIndex].items : []), nextRow]
|
||
.sort((left, right) => {
|
||
if (isNoCostDetail.value) {
|
||
const ha = parseInt(String(left?.nHammaddeTuruNo || '0'), 10) || 0
|
||
const hb = parseInt(String(right?.nHammaddeTuruNo || '0'), 10) || 0
|
||
if (ha !== hb) return ha - hb
|
||
}
|
||
return parseInt(String(left?.nOnMLDetNo || '0'), 10) - parseInt(String(right?.nOnMLDetNo || '0'), 10)
|
||
})
|
||
}
|
||
} else {
|
||
nextGroups.push({
|
||
sAciklama3: targetGroupName,
|
||
totalTutar: 0,
|
||
totalUSDTutar: 0,
|
||
items: [nextRow]
|
||
})
|
||
}
|
||
|
||
detailGroups.value = sortDetailGroups(nextGroups)
|
||
syncAllGroupsOpen()
|
||
schedulePersistLocalDraft()
|
||
// tbStok validation devre disi
|
||
}
|
||
|
||
function syncAllGroupsOpen () {
|
||
const openState = {}
|
||
detailGroups.value.forEach((grp, gi) => {
|
||
openState[groupKey(grp, gi)] = true
|
||
})
|
||
groupOpenState.value = openState
|
||
}
|
||
|
||
function normalizeHammaddeNo (value) {
|
||
return String(value || '').trim()
|
||
}
|
||
|
||
function normalizeGroupName (value) {
|
||
return String(value || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||
}
|
||
|
||
async function fetchRequiredParcaMappings () {
|
||
const ilk = String(detailHeader.value?.UrunIlkGrubu || '').trim()
|
||
const ana = String(detailHeader.value?.UrunAnaGrubu || '').trim()
|
||
// Some sources return NULL/'' for "no alt group". Mapping screen stores it as '-'.
|
||
const altRaw = String(detailHeader.value?.UrunAltGrubu || '').trim()
|
||
const alt = altRaw || '-'
|
||
if (!ilk || !ana) return []
|
||
|
||
const data = await get('/pricing/production-product-costing/maliyet-parca-eslestirme', {
|
||
trace_id: traceId.value,
|
||
only_active: 1,
|
||
urun_ilk_grubu: ilk,
|
||
urun_ana_grubu: ana,
|
||
urun_alt_grubu: alt
|
||
})
|
||
return Array.isArray(data) ? data : []
|
||
}
|
||
|
||
async function resolveHammaddeMetaForRequired (hNo) {
|
||
const key = String(hNo || '').trim()
|
||
if (!key) return null
|
||
// Always attempt a fresh resolve; backend rules for active/inactive hammadde types can change.
|
||
// We still store into cache as a last-known-good for the current page load.
|
||
const cached = requiredHammaddeMetaCache.value?.[key]
|
||
|
||
try {
|
||
const rows = await get('/pricing/production-product-costing/detail-editor-options', {
|
||
kind: 'hammadde',
|
||
search: key,
|
||
limit: 25,
|
||
trace_id: traceId.value
|
||
})
|
||
const list = Array.isArray(rows) ? rows : []
|
||
const hit = list.find(x => String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim() === key)
|
||
const meta = hit
|
||
? {
|
||
// In URETIM lookups, DT/TP/CM2/FABRIC is often stored in sAciklama2 (and sometimes in sAciklama3).
|
||
groupName: String(hit?.sAciklama3 || hit?.sAciklama2 || '').trim(),
|
||
// Hammadde type description is usually sAciklama (e.g. "CKT KUMAS").
|
||
// NOTE: detail-editor-options returns `sHammaddeTuruAdi` already without the number. Do not fall back to `label`,
|
||
// otherwise we end up rendering "3300 - 3300 - CKT ASKI".
|
||
hammaddeAdi: String(hit?.sHammaddeTuruAdi || hit?.sAciklama || '').trim(),
|
||
// Part info: spUrtOnMLHammaddeTuru.nUrtMTBolumID -> spUrtMTBolum.sAdi
|
||
mtBolumID: parseInt(String(hit?.mtUrtMTBolumID ?? hit?.MTUrtMTBolumID ?? hit?.nUrtMTBolumID ?? hit?.NUrtMTBolumID ?? '0'), 10) || 0,
|
||
parcaAdi: String(hit?.sParcaAdi || '').trim()
|
||
}
|
||
: null
|
||
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: meta || { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
|
||
return meta
|
||
} catch {
|
||
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
|
||
return cached || null
|
||
}
|
||
}
|
||
|
||
async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||
const list = Array.isArray(mappings) ? mappings : []
|
||
requiredParcaMappings.value = list
|
||
if (list.length === 0) return
|
||
|
||
// Defensive: The mapping payload may contain duplicate hammadde numbers across rows (or even inside a single row).
|
||
// IMPORTANT: Placeholders are keyed by (nUrtMTBolumID + nHammaddeTuruNo). The same hammadde type can be required
|
||
// for multiple parts (Ceket/Pantolon/Yelek...), so de-duping only by hNo is incorrect.
|
||
const processedRequiredKeys = new Set()
|
||
|
||
// Prefetch default quantities for all required hammadde types (1 call).
|
||
const allRequiredNos = []
|
||
for (const mapping of list) {
|
||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||
for (const hNoRaw of hList) {
|
||
const hNo = parseInt(String(hNoRaw || '').trim(), 10)
|
||
if (hNo > 0) allRequiredNos.push(hNo)
|
||
}
|
||
}
|
||
let defaultQtyByNo = {}
|
||
try {
|
||
const defaults = await post('/pricing/production-product-costing/default-quantities/lookup', {
|
||
nHammaddeTuruNos: allRequiredNos
|
||
})
|
||
const arr = Array.isArray(defaults) ? defaults : []
|
||
arr.forEach(it => {
|
||
const no = parseInt(String(it?.nHammaddeTuruNo ?? '0'), 10) || 0
|
||
const qty = Number(it?.lDefaultMiktar || 0)
|
||
if (no > 0 && qty > 0) defaultQtyByNo[no] = qty
|
||
})
|
||
} catch (err) {
|
||
defaultQtyByNo = {}
|
||
slog.error('production-product-costing.detail', 'default-qty:lookup:error', {
|
||
trace_id: traceId.value,
|
||
error: String(err?.message || err)
|
||
})
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Varsayilan miktarlar alinmadi (lookup hatasi). Miktarlar 1 olarak geldi.',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
|
||
// Add missing placeholder rows (qty=1, price=0) to remind user
|
||
for (const mapping of list) {
|
||
// Parca adi (CEKET/PANTOLON/YELEK...) comes from the MT bolum description (joined in backend as parcaBolumAdi).
|
||
// sAciklama3 is reserved for the group header (DT/TP/CM2/FABRIC).
|
||
const mappingParcaAdi = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi)
|
||
const mappingMtBolumID = parseInt(String(mapping?.nUrtMTBolumID ?? mapping?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||
for (const hNoRaw of hList) {
|
||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||
if (!hNo) continue
|
||
const reqKey = `${mappingMtBolumID}:${hNo}`
|
||
if (suppressedRequiredKeys.value?.[reqKey]) continue
|
||
if (processedRequiredKeys.has(reqKey)) continue
|
||
processedRequiredKeys.add(reqKey)
|
||
|
||
// Prefer the hammadde lookup's groupName (DT/TP/CM2/FABRIC)
|
||
// so placeholder rows land under the correct group headers.
|
||
const meta = await resolveHammaddeMetaForRequired(hNo)
|
||
const groupName = normalizeGroupName(meta?.groupName || '')
|
||
const hammaddeAdi = String(meta?.hammaddeAdi || '').trim()
|
||
const effectiveGroupName = groupName || 'TANIMSIZ'
|
||
// The product-type mapping is authoritative for the required part. A raw
|
||
// material type can be required for multiple parts, while its master
|
||
// record only carries one default MT section. Using that default here
|
||
// incorrectly made an existing recipe row satisfy every mapped part.
|
||
const mtBolumID = mappingMtBolumID > 0 ? mappingMtBolumID : (meta?.mtBolumID || 0)
|
||
const desiredParcaAdi = mappingMtBolumID > 0
|
||
? mappingParcaAdi
|
||
: normalizeGroupName(meta?.parcaAdi || mappingParcaAdi)
|
||
|
||
const anyMatch = flatDetailRows.value.find(r => {
|
||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
if (rowMtBolumID > 0 && mtBolumID > 0) return rowMtBolumID === mtBolumID
|
||
// If we don't have row's nUrtMTBolumID (legacy rows / older placeholders), fall back to matching by part name.
|
||
// Also accept legacy placeholders where sParcaAdi accidentally contains the group label (DT/TP/...), so we can move/fix them.
|
||
const rowParca = normalizeGroupName(r?.sParcaAdi)
|
||
return rowParca === desiredParcaAdi || isKnownGroupName(rowParca)
|
||
})
|
||
if (anyMatch) {
|
||
// If we previously created a placeholder under a wrong group name, move it instead of duplicating.
|
||
if (anyMatch?.requiredPlaceholder) {
|
||
const ensuredDetNo = String(anyMatch?.nOnMLDetNo || '').trim() || getNextDetailNo()
|
||
const currentGroup = normalizeGroupName(anyMatch?.sAciklama3)
|
||
const currentParcaAdi = normalizeGroupName(anyMatch?.sParcaAdi || '')
|
||
const currentHammaddeAdi = String(anyMatch?.sHammaddeTuruAdi || '').trim()
|
||
if (
|
||
String(anyMatch?.nOnMLDetNo || '').trim() !== ensuredDetNo ||
|
||
currentGroup !== effectiveGroupName ||
|
||
currentParcaAdi !== desiredParcaAdi ||
|
||
(hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)
|
||
) {
|
||
const moved = recalculateDetailRow({
|
||
...anyMatch,
|
||
nOnMLDetNo: ensuredDetNo,
|
||
sAciklama3: effectiveGroupName,
|
||
sParcaAdi: desiredParcaAdi,
|
||
nUrtMTBolumID: mtBolumID || anyMatch?.nUrtMTBolumID || anyMatch?.NUrtMTBolumID || 0,
|
||
sHammaddeTuruAdi: hammaddeAdi || String(anyMatch?.sHammaddeTuruAdi || '').trim(),
|
||
requiredPlaceholder: true,
|
||
draftChanged: true
|
||
}, {
|
||
preserveInputs: true,
|
||
priceType: 'REQ',
|
||
updateState: 'required',
|
||
markChanged: true
|
||
})
|
||
applyEditorRowToGroups(moved)
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
newRowSequence.value += 1
|
||
const rowKey = `req-auto-row-${newRowSequence.value}`
|
||
const placeholder = recalculateDetailRow({
|
||
__rowKey: rowKey,
|
||
isNew: true,
|
||
nUrtMTBolumID: mtBolumID,
|
||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||
// IMPORTANT: Backend skips upserts where nOnMLDetNo <= 0. Required placeholders must have a real number.
|
||
nOnMLDetNo: getNextDetailNo(),
|
||
// Group header key
|
||
sAciklama3: effectiveGroupName,
|
||
// Parca adi column value (CEKET/PANTOLON/YELEK...)
|
||
sParcaAdi: desiredParcaAdi,
|
||
nHammaddeTuruNo: hNo,
|
||
// Make "Hammadde Turu" render like existing rows: "no - aciklama"
|
||
sHammaddeTuruAdi: hammaddeAdi,
|
||
sKodu: '',
|
||
sAciklama: '',
|
||
sRenk: '',
|
||
ColorCode: '',
|
||
ColorDescription: '',
|
||
lMiktar: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? Number(defaultQtyByNo[parseInt(hNo, 10)]) : 1),
|
||
miktarInput: (Number(defaultQtyByNo[parseInt(hNo, 10)] || 0) > 0 ? String(defaultQtyByNo[parseInt(hNo, 10)]) : '1'),
|
||
inputPrice: '0',
|
||
inputPricePrBr: 'USD',
|
||
fiyat_girilen: 0,
|
||
fiyat_doviz: 'USD',
|
||
maliyeteDahil: true,
|
||
maliyete_dahil: 1,
|
||
Maliyete_dahil: 1,
|
||
cmPriceTypeId: normalizeCMPriceTypeId(1, groupName),
|
||
cm_price_type_id: normalizeCMPriceTypeId(1, groupName),
|
||
sBirim: 'AD',
|
||
draftChanged: true,
|
||
requiredPlaceholder: true
|
||
}, {
|
||
preserveInputs: true,
|
||
priceType: 'REQ',
|
||
updateState: 'required',
|
||
markChanged: true
|
||
})
|
||
|
||
applyEditorRowToGroups(placeholder)
|
||
}
|
||
}
|
||
|
||
// CM2 auto-required: include all active CM2 hammadde types (bAktif=1) as required placeholders too.
|
||
// This is independent of the mapping screen, because CM2 labor lines are managed in OnML detail.
|
||
try {
|
||
// Filter CM2 types by the parts that exist for this product (MTBolumIDs coming from mapping list).
|
||
// This prevents irrelevant CM2 types (e.g. GML/KBN/MNT) from showing up for products that don't have those parts.
|
||
const allowedMtBolumIDs = new Set()
|
||
for (const m of list) {
|
||
const mt = parseInt(String(m?.nUrtMTBolumID ?? m?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
if (mt > 0) allowedMtBolumIDs.add(mt)
|
||
}
|
||
if (allowedMtBolumIDs.size === 0) {
|
||
// No parts to scope CM2 by -> don't add global CM2 placeholders.
|
||
return
|
||
}
|
||
|
||
const cm2Types = await get('/pricing/production-product-costing/detail-editor-options', {
|
||
kind: 'hammadde',
|
||
group: 'CM2',
|
||
only_active: 1,
|
||
search: '',
|
||
limit: 200,
|
||
trace_id: traceId.value
|
||
})
|
||
const cm2List = Array.isArray(cm2Types) ? cm2Types : []
|
||
for (const it of cm2List) {
|
||
const hNo = normalizeHammaddeNo(it?.nHammaddeTuruNo ?? it?.value)
|
||
if (!hNo) continue
|
||
const meta = {
|
||
groupName: String(it?.sAciklama3 || it?.sAciklama2 || 'CM2').trim(),
|
||
hammaddeAdi: String(it?.sHammaddeTuruAdi || it?.sAciklama || '').trim(),
|
||
mtBolumID: parseInt(String(it?.mtUrtMTBolumID ?? it?.MTUrtMTBolumID ?? it?.nUrtMTBolumID ?? it?.NUrtMTBolumID ?? '0'), 10) || 0,
|
||
parcaAdi: String(it?.sParcaAdi || '').trim()
|
||
}
|
||
if (!(meta.mtBolumID > 0) || !allowedMtBolumIDs.has(meta.mtBolumID)) continue
|
||
const effectiveGroupName = normalizeGroupName(meta.groupName || 'CM2') || 'CM2'
|
||
const mtBolumID = meta.mtBolumID || 0
|
||
const desiredParcaAdi = normalizeGroupName(meta.parcaAdi || '') || 'CM2'
|
||
const reqKey = `${mtBolumID}:${hNo}`
|
||
if (mtBolumID > 0 && suppressedRequiredKeys.value?.[reqKey]) continue
|
||
|
||
const anyMatch = flatDetailRows.value.find(r => {
|
||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
if (rowMtBolumID > 0 && mtBolumID > 0) return rowMtBolumID === mtBolumID
|
||
return normalizeGroupName(r?.sParcaAdi) === desiredParcaAdi
|
||
})
|
||
if (anyMatch) {
|
||
// Ensure existing required CM2 placeholder has a valid detNo, otherwise backend will skip it on save.
|
||
if (anyMatch?.requiredPlaceholder && String(anyMatch?.nOnMLDetNo || '').trim() === '') {
|
||
const patched = recalculateDetailRow({
|
||
...anyMatch,
|
||
nOnMLDetNo: getNextDetailNo(),
|
||
draftChanged: true
|
||
}, {
|
||
preserveInputs: true,
|
||
priceType: 'REQ',
|
||
updateState: 'required-cm2',
|
||
markChanged: true
|
||
})
|
||
applyEditorRowToGroups(patched)
|
||
}
|
||
continue
|
||
}
|
||
|
||
newRowSequence.value += 1
|
||
const rowKey = `req-auto-cm2-row-${newRowSequence.value}`
|
||
const placeholder = recalculateDetailRow({
|
||
__rowKey: rowKey,
|
||
isNew: true,
|
||
nUrtMTBolumID: mtBolumID,
|
||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||
// IMPORTANT: Backend skips upserts where nOnMLDetNo <= 0. Required placeholders must have a real number.
|
||
nOnMLDetNo: getNextDetailNo(),
|
||
sAciklama3: effectiveGroupName,
|
||
sParcaAdi: desiredParcaAdi,
|
||
nHammaddeTuruNo: hNo,
|
||
sHammaddeTuruAdi: meta.hammaddeAdi,
|
||
sKodu: '',
|
||
sAciklama: '',
|
||
sRenk: '',
|
||
ColorCode: '',
|
||
ColorDescription: '',
|
||
lMiktar: 1,
|
||
miktarInput: '1',
|
||
inputPrice: '0',
|
||
inputPricePrBr: 'USD',
|
||
fiyat_girilen: 0,
|
||
fiyat_doviz: 'USD',
|
||
maliyeteDahil: true,
|
||
maliyete_dahil: 1,
|
||
Maliyete_dahil: 1,
|
||
cmPriceTypeId: normalizeCMPriceTypeId(1, effectiveGroupName),
|
||
cm_price_type_id: normalizeCMPriceTypeId(1, effectiveGroupName),
|
||
sBirim: 'AD',
|
||
draftChanged: true,
|
||
requiredPlaceholder: true
|
||
}, {
|
||
preserveInputs: true,
|
||
priceType: 'REQ',
|
||
updateState: 'required-cm2',
|
||
markChanged: true
|
||
})
|
||
applyEditorRowToGroups(placeholder)
|
||
}
|
||
} catch (e) {
|
||
slog.error('production-product-costing.detail', 'cm2:required:error', {
|
||
trace_id: traceId.value,
|
||
error: String(e?.message || e)
|
||
})
|
||
}
|
||
|
||
// Debug: required placeholders must always have a valid detNo.
|
||
try {
|
||
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||
const reqRows = flatNow.filter(r => Boolean(r?.requiredPlaceholder))
|
||
const missingDetNo = reqRows.filter(r => (parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0) <= 0)
|
||
slog.info('production-product-costing.detail', 'required:placeholders:ensured', {
|
||
trace_id: traceId.value,
|
||
required_count: reqRows.length,
|
||
required_missing_detno: missingDetNo.length,
|
||
required_cm2_count: reqRows.filter(r => normalizeGroupName(r?.sAciklama3) === 'CM2').length
|
||
})
|
||
if (missingDetNo.length > 0) {
|
||
slog.warn('production-product-costing.detail', 'required:placeholders:missing-detno', {
|
||
trace_id: traceId.value,
|
||
sample: missingDetNo.slice(0, 10).map(r => ({
|
||
detNo: String(r?.nOnMLDetNo || ''),
|
||
hNo: String(r?.nHammaddeTuruNo || ''),
|
||
mtBolumID: String(r?.nUrtMTBolumID || ''),
|
||
group: String(r?.sAciklama3 || ''),
|
||
rowKey: String(r?.__rowKey || '')
|
||
}))
|
||
})
|
||
}
|
||
} catch {}
|
||
|
||
// CM2 special case:
|
||
// Bulk autofill is disabled for CM2 (and FABRIC) to prevent wrong price pulls,
|
||
// but we still want the I.* code templates (code + description) to appear on page open.
|
||
// So we prefill ONLY code + description for required CM2 placeholders from last OnML history.
|
||
try {
|
||
const flatNow = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||
const cm2Targets = flatNow.filter(r =>
|
||
Boolean(r?.requiredPlaceholder) &&
|
||
normalizeGroupName(r?.sAciklama3) === 'CM2' &&
|
||
String(r?.sKodu || '').trim() === ''
|
||
)
|
||
const cm2Nos = Array.from(new Set(cm2Targets
|
||
.map(r => parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10))
|
||
.filter(n => n > 0)
|
||
))
|
||
if (cm2Nos.length > 0) {
|
||
const lastRows = await post('/pricing/production-product-costing/has-cost-detail/last-detail', {
|
||
nHammaddeTuruNos: cm2Nos,
|
||
before_date: normalizeDateInput(costDate.value),
|
||
exclude_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
|
||
n_firma_id: parseInt(String(detailHeader.value?.nFirmaID || detailHeader.value?.NFirmaID || '0').trim() || '0', 10) || 0,
|
||
only_i_code: true
|
||
}, { params: { trace_id: traceId.value } })
|
||
|
||
const list = Array.isArray(lastRows) ? lastRows : []
|
||
const byNo = {}
|
||
list.forEach(x => {
|
||
const no = parseInt(String(x?.nHammaddeTuruNo || '0'), 10) || 0
|
||
if (no > 0) byNo[no] = x
|
||
})
|
||
|
||
let filled = 0
|
||
detailGroups.value = detailGroups.value.map(grp => ({
|
||
...grp,
|
||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||
if (!row?.requiredPlaceholder) return row
|
||
if (normalizeGroupName(row?.sAciklama3) !== 'CM2') return row
|
||
if (String(row?.sKodu || '').trim() !== '') return row
|
||
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
|
||
const hit = byNo[no]
|
||
if (!hit) return row
|
||
const code = String(hit?.sKodu || '').trim()
|
||
if (!code) return row
|
||
|
||
const next = recalculateDetailRow({
|
||
...row,
|
||
sKodu: code,
|
||
sAciklama: String(hit?.sAciklama || '').trim(),
|
||
__autoFilledFromPrev: true,
|
||
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
|
||
}, {
|
||
priceType: 'PREV',
|
||
updateState: 'autofill-icode',
|
||
markChanged: true
|
||
})
|
||
filled += 1
|
||
return next
|
||
})
|
||
}))
|
||
if (filled > 0) {
|
||
$q.notify({
|
||
type: 'info',
|
||
message: `${filled} CM2 satirinda I.* kod/aciklama onceki maliyetten otomatik getirildi.`,
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Non-blocking
|
||
slog.error('production-product-costing.detail', 'cm2:autofill-icode:error', {
|
||
trace_id: traceId.value,
|
||
error: String(e?.message || e)
|
||
})
|
||
}
|
||
}
|
||
|
||
function computeMissingRequiredSlots () {
|
||
const list = Array.isArray(requiredParcaMappings.value) ? requiredParcaMappings.value : []
|
||
const missing = []
|
||
if (list.length === 0) return missing
|
||
|
||
// Expand suppression keys: support both mappingMtBolumID and meta-derived mtBolumID.
|
||
const suppressed = suppressedRequiredKeys.value || {}
|
||
|
||
list.forEach(mapping => {
|
||
// Required slots are defined per (ParcaBolum / MTBolumID) + (HammaddeTuruNo).
|
||
// Do NOT match by row.sAciklama3, because that field is the "group header" (DT/TP/CM2/FABRIC), not the part.
|
||
const mappingMtBolumID = parseInt(String(mapping?.nUrtMTBolumID ?? mapping?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
const mappingParcaAdi = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || '')
|
||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||
hList.forEach(hNoRaw => {
|
||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||
if (!hNo) return
|
||
const reqKeyByMapping = `${mappingMtBolumID}:${hNo}`
|
||
// Also allow suppression by the mtBolumID that the placeholder row itself uses (meta override).
|
||
const reqKeyByRowMt = (() => {
|
||
const rowLike = flatDetailRows.value.find(r => normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo && Boolean(r?.requiredPlaceholder))
|
||
const rowMt = parseInt(String(rowLike?.nUrtMTBolumID ?? rowLike?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
return rowMt > 0 ? `${rowMt}:${hNo}` : ''
|
||
})()
|
||
|
||
// User explicitly removed this required placeholder in the UI; don't keep warning about it.
|
||
if (suppressed?.[reqKeyByMapping] || (reqKeyByRowMt && suppressed?.[reqKeyByRowMt])) {
|
||
// Auto-clear suppression if a real row exists again.
|
||
const existing = flatDetailRows.value.find(r => {
|
||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
if (mappingMtBolumID > 0 && rowMtBolumID > 0) return mappingMtBolumID === rowMtBolumID
|
||
return normalizeGroupName(r?.sParcaAdi) === mappingParcaAdi
|
||
})
|
||
if (existing) {
|
||
const next = { ...(suppressedRequiredKeys.value || {}) }
|
||
delete next[reqKeyByMapping]
|
||
if (reqKeyByRowMt) delete next[reqKeyByRowMt]
|
||
suppressedRequiredKeys.value = next
|
||
schedulePersistLocalDraft()
|
||
} else {
|
||
return
|
||
}
|
||
}
|
||
|
||
const match = flatDetailRows.value.find(r => {
|
||
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
|
||
const rowMtBolumID = parseInt(String(r?.nUrtMTBolumID ?? r?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
if (mappingMtBolumID > 0 && rowMtBolumID > 0) return mappingMtBolumID === rowMtBolumID
|
||
// Fallback: older rows might not have mtBolumID; use part name match.
|
||
return normalizeGroupName(r?.sParcaAdi) === mappingParcaAdi
|
||
})
|
||
const price = resolveNumericRowInputPrice(match)
|
||
if (!match || !(price > 0)) {
|
||
missing.push({
|
||
mtBolumID: mappingMtBolumID,
|
||
parcaAdi: mappingParcaAdi,
|
||
nHammaddeTuruNo: hNo,
|
||
rowKey: match?.__rowKey || ''
|
||
})
|
||
}
|
||
})
|
||
})
|
||
return missing
|
||
}
|
||
|
||
function removeDetailRowByKey (rowKey) {
|
||
const normalizedRowKey = String(rowKey || '').trim()
|
||
if (!normalizedRowKey) return false
|
||
|
||
const existingRow = flatDetailRows.value.find(r => String(r?.__rowKey || '').trim() === normalizedRowKey)
|
||
|
||
const nextGroups = detailGroups.value
|
||
.map(grp => ({
|
||
...grp,
|
||
items: (Array.isArray(grp?.items) ? grp.items : []).filter(row => row.__rowKey !== normalizedRowKey)
|
||
}))
|
||
.filter(grp => (Array.isArray(grp?.items) ? grp.items.length : 0) > 0)
|
||
|
||
const hasChanged = nextGroups.length !== detailGroups.value.length ||
|
||
nextGroups.some((grp, index) => (grp?.items?.length || 0) !== (detailGroups.value[index]?.items?.length || 0))
|
||
|
||
if (!hasChanged) return false
|
||
|
||
detailGroups.value = nextGroups
|
||
syncAllGroupsOpen()
|
||
// If user deletes an auto-created required placeholder, suppress the corresponding required key
|
||
// so we don't keep recreating/warning about it for this draft.
|
||
if (existingRow?.requiredPlaceholder) {
|
||
const hNo = normalizeHammaddeNo(existingRow?.nHammaddeTuruNo)
|
||
const mtBolumID = parseInt(String(existingRow?.nUrtMTBolumID ?? existingRow?.NUrtMTBolumID ?? '0'), 10) || 0
|
||
if (hNo && mtBolumID > 0) {
|
||
const reqKey = `${mtBolumID}:${hNo}`
|
||
suppressedRequiredKeys.value = { ...(suppressedRequiredKeys.value || {}), [reqKey]: true }
|
||
}
|
||
}
|
||
if (existingRow && !existingRow?.isNew) {
|
||
deletedDetailRows.value = [
|
||
...(Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []),
|
||
{
|
||
__rowKey: normalizedRowKey,
|
||
nOnMLNo: String(existingRow?.nOnMLNo || '').trim(),
|
||
nOnMLDetNo: String(existingRow?.nOnMLDetNo || '').trim(),
|
||
nHammaddeTuruNo: String(existingRow?.nHammaddeTuruNo || '').trim(),
|
||
sKodu: String(existingRow?.sKodu || '').trim(),
|
||
sRenk: String(existingRow?.sRenk || '').trim()
|
||
}
|
||
]
|
||
}
|
||
schedulePersistLocalDraft()
|
||
return true
|
||
}
|
||
|
||
function openNewRowDialog () {
|
||
if (!detailHeader.value) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Yeni satir eklemek icin once detay verisi olmali.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
rowEditorMode.value = 'new'
|
||
rowEditorTargetRowKey.value = ''
|
||
rowEditorForm.value = createRowEditorForm({
|
||
isNew: true,
|
||
nOnMLDetNo: getNextDetailNo(),
|
||
inputPricePrBr: normalizePriceCurrency(detailHeader.value?.sDovizCinsi) || 'USD',
|
||
maliyeteDahil: true,
|
||
sBirim: 'AD'
|
||
})
|
||
rowEditorLastValidItemValue.value = ''
|
||
primeRowEditorOptionsFromForm()
|
||
rowEditorDialogOpen.value = true
|
||
void bootstrapRowEditorOptions()
|
||
}
|
||
|
||
function openRowEditorForEdit (row) {
|
||
rowEditorMode.value = 'edit'
|
||
rowEditorTargetRowKey.value = String(row?.__rowKey || '').trim()
|
||
rowEditorForm.value = createRowEditorForm({
|
||
...row,
|
||
sAciklama3: row?.sAciklama3 || ''
|
||
})
|
||
rowEditorLastValidItemValue.value = String(rowEditorForm.value?.sKodu || '').trim()
|
||
primeRowEditorOptionsFromForm()
|
||
rowEditorDialogOpen.value = true
|
||
void bootstrapRowEditorOptions()
|
||
}
|
||
|
||
function deleteRowEditor () {
|
||
const targetRowKey = String(rowEditorTargetRowKey.value || rowEditorForm.value.__rowKey || '').trim()
|
||
if (!targetRowKey) return
|
||
|
||
$q.dialog({
|
||
title: 'Satiri Sil',
|
||
message: 'Bu satir gridden kaldirilacak. Devam edilsin mi?',
|
||
cancel: {
|
||
flat: true,
|
||
color: 'grey-7',
|
||
label: 'Vazgec'
|
||
},
|
||
ok: {
|
||
unelevated: true,
|
||
color: 'negative',
|
||
label: 'Sil'
|
||
},
|
||
persistent: true
|
||
}).onOk(() => {
|
||
if (!removeDetailRowByKey(targetRowKey)) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Silinecek satir bulunamadi.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
rowEditorDialogOpen.value = false
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: 'Satir gridden kaldirildi.',
|
||
position: 'top-right'
|
||
})
|
||
})
|
||
}
|
||
|
||
function saveRowEditor () {
|
||
if (!String(rowEditorForm.value.nOnMLDetNo || '').trim()) {
|
||
rowEditorForm.value.nOnMLDetNo = getNextDetailNo()
|
||
}
|
||
if (!String(rowEditorForm.value.nHammaddeTuruNo || '').trim()) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Hammadde Turu secilmeden satir kaydedilemez.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
if (!String(rowEditorForm.value.sKodu || '').trim()) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Kod secilmeden satir kaydedilemez.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
const nextRow = buildRowFromEditorForm()
|
||
applyEditorRowToGroups(nextRow)
|
||
rowEditorDialogOpen.value = false
|
||
}
|
||
|
||
function round1 (n) {
|
||
const x = Number(n || 0)
|
||
if (!Number.isFinite(x)) return 0
|
||
return Math.round(x * 10) / 10
|
||
}
|
||
|
||
function round4 (n) {
|
||
const x = Number(n || 0)
|
||
if (!Number.isFinite(x)) return 0
|
||
return Math.round(x * 10000) / 10000
|
||
}
|
||
|
||
function formatSignedPercent (value) {
|
||
const n = Number(value || 0)
|
||
const sign = n >= 0 ? '+' : ''
|
||
return `${sign}${round1(n)}%`
|
||
}
|
||
|
||
function escapeHtml (input) {
|
||
const s = String(input ?? '')
|
||
return s
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''')
|
||
}
|
||
|
||
function convertUSDToPriceCurrency (usdPrice, currency) {
|
||
const usd = Number(usdPrice || 0)
|
||
if (!Number.isFinite(usd) || usd <= 0) return 0
|
||
const cur = normalizePriceCurrency(currency) || 'USD'
|
||
if (cur === 'USD') return usd
|
||
const usdRate = resolveExchangeRateValue('USD')
|
||
if (!(usdRate > 0)) return usd
|
||
const tryPrice = usd * usdRate
|
||
if (cur === 'TRY') return tryPrice
|
||
const targetRate = resolveExchangeRateValue(cur)
|
||
return targetRate > 0 ? (tryPrice / targetRate) : usd
|
||
}
|
||
|
||
function setAllPriceDeviationRowsSelected (value) {
|
||
priceDeviationRows.value = priceDeviationRows.value.map(row => ({
|
||
...row,
|
||
selected: Boolean(value)
|
||
}))
|
||
}
|
||
|
||
function applySelectedPriceDeviationAverages () {
|
||
priceDeviationRows.value = priceDeviationRows.value.map(row => {
|
||
if (!row.selected) return row
|
||
const currency = normalizePriceCurrency(row.newCurrency) || 'USD'
|
||
const price = convertUSDToPriceCurrency(row.avgUSD, currency)
|
||
return {
|
||
...row,
|
||
newPrice: normalizeInputPrice(round4(price)),
|
||
newCurrency: currency
|
||
}
|
||
})
|
||
}
|
||
|
||
function applyPriceDeviationRowEdits () {
|
||
const selectedRows = priceDeviationRows.value.filter(row => row.selected)
|
||
for (const item of selectedRows) {
|
||
const target = item.row
|
||
if (!target) continue
|
||
target.inputPrice = normalizeInputPrice(item.newPrice)
|
||
target.inputPricePrBr = normalizePriceCurrency(item.newCurrency) || 'USD'
|
||
recalculateDetailRow(target, {
|
||
preserveInputs: true,
|
||
priceType: 'MAN',
|
||
updateState: 'manual',
|
||
markChanged: true
|
||
})
|
||
}
|
||
if (selectedRows.length > 0) {
|
||
schedulePersistLocalDraft()
|
||
triggerUIUpdate()
|
||
}
|
||
}
|
||
|
||
function closePriceDeviationDialog (result) {
|
||
const resolve = priceDeviationResolver.value
|
||
priceDeviationResolver.value = null
|
||
priceDeviationDialogOpen.value = false
|
||
if (typeof resolve === 'function') resolve(Boolean(result))
|
||
}
|
||
|
||
function cancelPriceDeviationDialog () {
|
||
closePriceDeviationDialog(false)
|
||
}
|
||
|
||
function confirmPriceDeviationDialog () {
|
||
applyPriceDeviationRowEdits()
|
||
closePriceDeviationDialog(true)
|
||
}
|
||
|
||
async function confirmDefaultQtyDeviationIfNeeded () {
|
||
// Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type.
|
||
// Rule: if deviation > 10% (abs), require user confirmation.
|
||
const qtyByNo = {}
|
||
flatDetailRows.value.forEach(r => {
|
||
const no = parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
|
||
const qty = Number(r?.lMiktar || 0)
|
||
if (!(no > 0) || !(qty > 0)) return
|
||
qtyByNo[no] = (qtyByNo[no] || 0) + qty
|
||
})
|
||
const nos = Object.keys(qtyByNo).map(k => parseInt(k, 10)).filter(n => n > 0)
|
||
if (nos.length === 0) return true
|
||
|
||
let defaults = []
|
||
try {
|
||
defaults = await post('/pricing/production-product-costing/default-quantities/lookup', {
|
||
nHammaddeTuruNos: nos
|
||
})
|
||
} catch {
|
||
// If lookup fails, don't block save.
|
||
return true
|
||
}
|
||
const defMap = {}
|
||
const descMap = {}
|
||
;(Array.isArray(defaults) ? defaults : []).forEach(it => {
|
||
const no = parseInt(String(it?.nHammaddeTuruNo || '0'), 10) || 0
|
||
const d = Number(it?.lDefaultMiktar || 0)
|
||
const desc = String(it?.sAciklama || '').trim()
|
||
if (no > 0 && d > 0) {
|
||
defMap[no] = d
|
||
if (desc) descMap[no] = desc
|
||
}
|
||
})
|
||
|
||
const outliers = []
|
||
Object.keys(defMap).forEach(k => {
|
||
const no = parseInt(k, 10) || 0
|
||
const defQty = Number(defMap[no] || 0)
|
||
const enteredQty = Number(qtyByNo[no] || 0)
|
||
if (!(defQty > 0) || !(enteredQty > 0)) return
|
||
const pct = ((enteredQty - defQty) / defQty) * 100
|
||
if (Math.abs(pct) > 10) {
|
||
outliers.push({ no, defQty, enteredQty, pct })
|
||
}
|
||
})
|
||
if (outliers.length === 0) return true
|
||
|
||
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
|
||
|
||
const maxRows = 30
|
||
const rowsHtml = outliers.slice(0, maxRows).map(x => {
|
||
const sign = x.pct >= 0 ? '+' : ''
|
||
const pct = `${sign}${round1(x.pct)}%`
|
||
const cls = x.pct >= 0 ? 'color:#b71c1c;' : 'color:#1b5e20;'
|
||
const desc = String(descMap[x.no] || '').trim()
|
||
const noLabel = desc ? `${x.no} - ${escapeHtml(desc)}` : String(x.no)
|
||
return `
|
||
<tr>
|
||
<td style="padding:6px 8px; white-space:nowrap; font-weight:600;">${noLabel}</td>
|
||
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${round4(x.defQty)}</td>
|
||
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${round4(x.enteredQty)}</td>
|
||
<td style="padding:6px 8px; text-align:right; white-space:nowrap; ${cls} font-weight:600;">${pct}</td>
|
||
</tr>
|
||
`
|
||
}).join('')
|
||
const truncatedNote = outliers.length > maxRows
|
||
? `<div style="margin-top:8px; color:#666;">Toplam ${outliers.length} satir var. Ilk ${maxRows} gosterildi.</div>`
|
||
: ''
|
||
|
||
const ok = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Varsayilan Miktar Kontrolu',
|
||
html: true,
|
||
message: `
|
||
<div style="margin-bottom:10px;">
|
||
Bazi hammadde turlerinde varsayilan miktardan <b>%10</b>'dan fazla sapma var.
|
||
</div>
|
||
<div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;">
|
||
<table style="width:100%; border-collapse:collapse; font-size:13px;">
|
||
<thead>
|
||
<tr style="background:#f5f5f5; position: sticky; top: 0;">
|
||
<th style="text-align:left; padding:6px 8px;">Hammadde</th>
|
||
<th style="text-align:right; padding:6px 8px;">Varsayilan</th>
|
||
<th style="text-align:right; padding:6px 8px;">Girilen</th>
|
||
<th style="text-align:right; padding:6px 8px;">Fark %</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rowsHtml}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
${truncatedNote}
|
||
<div style="margin-top:10px;">
|
||
Onayliyorsaniz <b>Onayla ve Kaydet</b>'e basın. Duzenlemek icin <b>Geri Don</b>.
|
||
</div>
|
||
`,
|
||
cancel: { label: 'Geri Don' },
|
||
ok: { label: 'Onayla ve Kaydet', color: 'primary' },
|
||
persistent: true
|
||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||
})
|
||
return ok
|
||
}
|
||
|
||
async function getPurchaseAvgUSDForCode (code) {
|
||
const normalized = String(code || '').trim()
|
||
if (!normalized) return { ok: false, code: '', avgUSD: 0, n: 0 }
|
||
const cached = purchaseAvgUSDCachedByCode.value?.[normalized]
|
||
if (cached && cached.ok) return cached
|
||
|
||
try {
|
||
const response = await get('/pricing/production-product-costing/has-cost-detail-line-history', {
|
||
n_onml_no: parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0,
|
||
s_kodu: normalized,
|
||
maliyet_tarihi: normalizeDateInput(costDate.value),
|
||
trace_id: traceId.value
|
||
})
|
||
const rows = Array.isArray(response?.purchaseRows) ? response.purchaseRows : []
|
||
const picked = []
|
||
for (const r of rows) {
|
||
const p = Number(r?.EvrakFiyat || 0)
|
||
if (!(p > 0)) continue
|
||
const cur = String(r?.EvrakDoviz || '').trim().toUpperCase() || 'USD'
|
||
const usd = convertPriceToUSD(p, cur)
|
||
if (!(usd > 0)) continue
|
||
picked.push(usd)
|
||
if (picked.length >= 10) break
|
||
}
|
||
const avgUSD = picked.length > 0 ? (picked.reduce((a, b) => a + b, 0) / picked.length) : 0
|
||
const result = { ok: picked.length > 0, code: normalized, avgUSD, n: picked.length }
|
||
purchaseAvgUSDCachedByCode.value = { ...(purchaseAvgUSDCachedByCode.value || {}), [normalized]: result }
|
||
return result
|
||
} catch (e) {
|
||
const result = { ok: false, code: normalized, avgUSD: 0, n: 0, error: String(e?.message || e) }
|
||
purchaseAvgUSDCachedByCode.value = { ...(purchaseAvgUSDCachedByCode.value || {}), [normalized]: result }
|
||
return result
|
||
}
|
||
}
|
||
|
||
async function confirmBrPriceDeviationIfNeeded () {
|
||
const rows = Array.isArray(flatDetailRows.value) ? flatDetailRows.value : []
|
||
if (rows.length === 0) return true
|
||
|
||
// Only consider rows that have a code + a non-zero entered price.
|
||
const candidates = rows
|
||
.filter(r => String(r?.sKodu || '').trim() !== '')
|
||
.map(r => {
|
||
const price = Number(resolveNumericRowInputPrice(r) || 0)
|
||
const cur = String(resolveInputCurrency(r) || '').trim().toUpperCase() || 'USD'
|
||
return { row: r, code: String(r?.sKodu || '').trim(), price, cur }
|
||
})
|
||
.filter(x => x.price > 0)
|
||
|
||
if (candidates.length === 0) return true
|
||
|
||
const uniqueCodes = Array.from(new Set(candidates.map(x => x.code)))
|
||
// Fetch averages with simple batching to avoid hammering the API.
|
||
const avgByCode = {}
|
||
const batchSize = 8
|
||
for (let i = 0; i < uniqueCodes.length; i += batchSize) {
|
||
const batch = uniqueCodes.slice(i, i + batchSize)
|
||
const results = await Promise.all(batch.map(c => getPurchaseAvgUSDForCode(c)))
|
||
results.forEach(r => { avgByCode[r.code] = r })
|
||
}
|
||
|
||
const outliers = []
|
||
for (const c of candidates) {
|
||
const avg = avgByCode[c.code]
|
||
if (!avg || !avg.ok || !(avg.avgUSD > 0) || avg.n < 3) continue // too little history -> ignore
|
||
const enteredUSD = convertPriceToUSD(c.price, c.cur)
|
||
if (!(enteredUSD > 0)) continue
|
||
const pct = ((enteredUSD - avg.avgUSD) / avg.avgUSD) * 100
|
||
if (Math.abs(pct) > 10) {
|
||
outliers.push({
|
||
row: c.row,
|
||
key: `${String(c.row?.__rowKey || c.code)}|${outliers.length}`,
|
||
code: c.code,
|
||
avgUSD: avg.avgUSD,
|
||
enteredUSD,
|
||
pct,
|
||
originalPrice: c.price,
|
||
originalCurrency: c.cur
|
||
})
|
||
}
|
||
}
|
||
|
||
if (outliers.length === 0) return true
|
||
|
||
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
|
||
priceDeviationRows.value = outliers.map(x => ({
|
||
...x,
|
||
selected: true,
|
||
newPrice: normalizeInputPrice(x.originalPrice),
|
||
newCurrency: normalizePriceCurrency(x.originalCurrency) || 'USD'
|
||
}))
|
||
return await new Promise(resolve => {
|
||
priceDeviationResolver.value = resolve
|
||
priceDeviationDialogOpen.value = true
|
||
})
|
||
|
||
const maxRows = 30
|
||
const rowsHtml = outliers.slice(0, maxRows).map(x => {
|
||
const sign = x.pct >= 0 ? '+' : ''
|
||
const pct = `${sign}${round1(x.pct)}%`
|
||
const cls = x.pct >= 0 ? 'color:#b71c1c;' : 'color:#1b5e20;'
|
||
return `
|
||
<tr>
|
||
<td style="padding:6px 8px; white-space:nowrap; font-weight:600;">${escapeHtml(x.code)}</td>
|
||
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${formatMoney(x.avgUSD)}</td>
|
||
<td style="padding:6px 8px; text-align:right; white-space:nowrap;">${formatMoney(x.enteredUSD)}</td>
|
||
<td style="padding:6px 8px; text-align:right; white-space:nowrap; ${cls} font-weight:600;">${pct}</td>
|
||
</tr>
|
||
`
|
||
}).join('')
|
||
|
||
const truncatedNote = outliers.length > maxRows
|
||
? `<div style="margin-top:8px; color:#666;">Toplam ${outliers.length} satir var. Ilk ${maxRows} gosterildi.</div>`
|
||
: ''
|
||
|
||
const ok = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Fiyat Kontrolu (Satinalma Ortalama)',
|
||
html: true,
|
||
message: `
|
||
<div style="margin-bottom:10px;">
|
||
Bazı satırlarda girilen fiyat, NEBIM_V3 satınalma geçmişindeki <b>son 10</b> kaydın USD ortalamasından <b>%10</b>'dan fazla sapıyor.
|
||
</div>
|
||
<div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;">
|
||
<table style="width:100%; border-collapse:collapse; font-size:13px;">
|
||
<thead>
|
||
<tr style="background:#f5f5f5; position: sticky; top: 0;">
|
||
<th style="text-align:left; padding:6px 8px;">Kod</th>
|
||
<th style="text-align:right; padding:6px 8px;">Ort USD</th>
|
||
<th style="text-align:right; padding:6px 8px;">Girilen USD</th>
|
||
<th style="text-align:right; padding:6px 8px;">Fark %</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rowsHtml}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
${truncatedNote}
|
||
<div style="margin-top:10px;">
|
||
Onayliyorsaniz <b>Onayla ve Kaydet</b>'e basın. Duzenlemek icin <b>Geri Don</b>.
|
||
</div>
|
||
`,
|
||
cancel: { label: 'Geri Don' },
|
||
ok: { label: 'Onayla ve Kaydet', color: 'primary' },
|
||
persistent: true
|
||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||
})
|
||
return ok
|
||
}
|
||
|
||
async function deleteCosting () {
|
||
if (!detailHeader.value) return
|
||
const n = parseInt(String(detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || onMLNo.value || '0'), 10) || 0
|
||
if (!(n > 0)) {
|
||
$q.notify({ type: 'warning', message: 'Silinecek kayit bulunamadi.', position: 'top-right' })
|
||
return
|
||
}
|
||
|
||
const ok = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Kaydi Sil',
|
||
message: `Bu maliyet kaydi silinecek. (OnMLNo=${n}) Devam edilsin mi?`,
|
||
cancel: true,
|
||
persistent: true
|
||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||
})
|
||
if (!ok) return
|
||
|
||
saveLoading.value = true
|
||
try {
|
||
await post('/pricing/production-product-costing/onml/delete', {
|
||
n_onml_no: n
|
||
})
|
||
clearLocalDraft()
|
||
$q.notify({ type: 'positive', message: 'Kayit silindi.', position: 'top-right' })
|
||
router.replace({ name: 'production-product-costing-no-cost' })
|
||
} catch (e) {
|
||
$q.notify({ type: 'negative', message: String(e?.message || e || 'Silinemedi'), position: 'top-right' })
|
||
} finally {
|
||
saveLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function confirmRefresh () {
|
||
if (detailLoading.value) return
|
||
const ok = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Yenile',
|
||
message: hasUnsavedChanges.value
|
||
? 'Bu islem sayfayi sifirlar ve kaydedilmemis degisiklikler kaybolur. Devam edilsin mi?'
|
||
: 'Bu islem sayfayi sifirlar. Devam edilsin mi?',
|
||
cancel: { label: 'Geri Don' },
|
||
ok: { label: 'Onayla', color: 'negative' },
|
||
persistent: true
|
||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||
})
|
||
if (!ok) return
|
||
clearLocalDraft()
|
||
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||
}
|
||
|
||
function scrollToFirstRequiredAttentionRow () {
|
||
// Best-effort DOM scroll: q-table renders rowClass on the <tr>.
|
||
// This avoids users having to hunt for missing rows after we block save.
|
||
try {
|
||
const el = document.querySelector('.pcd-detail-row-required')
|
||
if (el && typeof el.scrollIntoView === 'function') {
|
||
el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
async function saveChanges () {
|
||
saveLoading.value = true
|
||
try {
|
||
requiredAttentionRowKeys.value = {}
|
||
// tbStok exists-bulk kontrolu devre disi (manual giris kapali): save bloke edilmez.
|
||
if (isNoCostDetail.value) {
|
||
const missing = computeMissingRequiredSlots()
|
||
slog.info('production-product-costing.detail', 'required:missing:computed', {
|
||
trace_id: traceId.value,
|
||
missing_count: missing.length,
|
||
sample: missing.slice(0, 20).map(m => ({
|
||
mtBolumID: m?.mtBolumID,
|
||
parcaAdi: String(m?.parcaAdi || '').trim(),
|
||
hNo: String(m?.nHammaddeTuruNo || '').trim(),
|
||
rowKey: String(m?.rowKey || '').trim()
|
||
}))
|
||
})
|
||
if (missing.length > 0) {
|
||
const rowsHtml = missing.slice(0, 40).map((m, i) => {
|
||
const key = String(m?.rowKey || '').trim()
|
||
const row = key ? flatDetailRows.value.find(r => String(r?.__rowKey || '').trim() === key) : null
|
||
const code = String(row?.sKodu || '').trim()
|
||
const desc = String(row?.sAciklama || '').trim()
|
||
return `
|
||
<tr>
|
||
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${i + 1}</td>
|
||
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${String(m?.parcaAdi || '').trim()}</td>
|
||
<td style="padding:4px 6px; border-bottom:1px solid #eee;">${String(m?.nHammaddeTuruNo || '').trim()}</td>
|
||
<td style="padding:4px 6px; border-bottom:1px solid #eee; color:#b71c1c;">${code || '-'}</td>
|
||
<td style="padding:4px 6px; border-bottom:1px solid #eee; color:#b71c1c;">${desc || '-'}</td>
|
||
</tr>
|
||
`
|
||
}).join('')
|
||
const truncatedNote = missing.length > 40
|
||
? `<div style="margin-top:8px; color:#777;">Ilk 40 satir gosterildi. Toplam: ${missing.length}</div>`
|
||
: ''
|
||
const ok = await new Promise(resolve => {
|
||
$q.dialog({
|
||
title: 'Eksik Maliyet Parcalari',
|
||
html: true,
|
||
message: `
|
||
<div>
|
||
Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz?
|
||
<div style="margin-top:6px; font-weight:600;">Eksik: ${missing.length}</div>
|
||
<div style="margin-top:8px; max-height:280px; overflow:auto; border:1px solid #eee;">
|
||
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||
<thead>
|
||
<tr style="text-align:left; background:#fafafa;">
|
||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">No</th>
|
||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Parca</th>
|
||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Hammadde</th>
|
||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Kod</th>
|
||
<th style="padding:4px 6px; border-bottom:1px solid #eee;">Aciklama</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rowsHtml}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
${truncatedNote}
|
||
</div>
|
||
`,
|
||
cancel: true,
|
||
persistent: true
|
||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||
})
|
||
if (!ok) {
|
||
const next = {}
|
||
missing.forEach(x => {
|
||
if (x.rowKey) next[String(x.rowKey)] = true
|
||
})
|
||
requiredAttentionRowKeys.value = next
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Eksik parcalar isaretlendi. Fiyat girip tekrar deneyin.',
|
||
position: 'top-right'
|
||
})
|
||
// Scroll to first highlighted row.
|
||
nextTick(() => scrollToFirstRequiredAttentionRow())
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
const okDefaultQty = await confirmDefaultQtyDeviationIfNeeded()
|
||
if (!okDefaultQty) return
|
||
|
||
const okBrPrice = await confirmBrPriceDeviationIfNeeded()
|
||
if (!okBrPrice) return
|
||
|
||
if (!detailHeader.value) {
|
||
$q.notify({ type: 'negative', message: 'Header bulunamadi.', position: 'top-right' })
|
||
return
|
||
}
|
||
|
||
// Validate required header fields
|
||
const selectedUretimSekliID = parseInt(String(detailHeader.value?.UretimSekliID || '0').trim() || '0', 10) || 0
|
||
if (!(selectedUretimSekliID > 0)) {
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: 'Uretim Sekli secilmeden kayit yapilamaz.',
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
// Validate: "Kod" (sKodu) is mandatory for any row that has a hammadde type.
|
||
// We block saving if there are rows with empty/whitespace code, to avoid sending blank rows to backend.
|
||
const blankCodeRows = (Array.isArray(flatDetailRows.value) ? flatDetailRows.value : [])
|
||
.filter(r => {
|
||
const hNo = parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
|
||
if (!(hNo > 0)) return false
|
||
return String(r?.sKodu || '').trim() === ''
|
||
})
|
||
if (blankCodeRows.length > 0) {
|
||
const next = {}
|
||
blankCodeRows.forEach(r => {
|
||
const key = String(r?.__rowKey || '').trim()
|
||
if (key) next[key] = true
|
||
})
|
||
requiredAttentionRowKeys.value = next
|
||
$q.notify({
|
||
type: 'warning',
|
||
message: `Kod bos birakilan satirlar var. Kaydetmeden once Kod alanini doldurun. (Adet: ${blankCodeRows.length})`,
|
||
position: 'top-right'
|
||
})
|
||
return
|
||
}
|
||
|
||
const header = detailHeader.value
|
||
// Debug: backend skips rows where n_onml_det_no <= 0. Log this before sending.
|
||
try {
|
||
const rows = Array.isArray(flatDetailRows.value) ? flatDetailRows.value : []
|
||
const detNoZero = rows.filter(r => (parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0) <= 0)
|
||
slog.info('production-product-costing.detail', 'save:payload:summary', {
|
||
trace_id: traceId.value,
|
||
row_count: rows.length,
|
||
det_no_zero_count: detNoZero.length,
|
||
det_no_zero_sample: detNoZero.slice(0, 10).map(r => ({
|
||
rowKey: String(r?.__rowKey || ''),
|
||
detNo: String(r?.nOnMLDetNo || ''),
|
||
hNo: String(r?.nHammaddeTuruNo || ''),
|
||
group: String(r?.sAciklama3 || ''),
|
||
code: String(r?.sKodu || '').trim()
|
||
}))
|
||
})
|
||
} catch {}
|
||
|
||
const upserts = flatDetailRows.value.map(r => ({
|
||
n_onml_det_no: parseInt(String(r?.nOnMLDetNo || '').trim() || '0', 10) || 0,
|
||
n_hammadde_turu_no: parseInt(String(r?.nHammaddeTuruNo || '').trim() || '0', 10) || 0,
|
||
n_urt_mt_bolum_id: parseInt(String(r?.nUrtMTBolumID || '0').trim() || '0', 10) || 0,
|
||
s_kodu: String(r?.sKodu || '').trim(),
|
||
s_aciklama: String(r?.sAciklama || '').trim(),
|
||
s_renk: String(r?.sRenk || '').trim(),
|
||
s_beden: String(r?.sBeden || '').trim(),
|
||
s_aciklama2: String(r?.sAciklama2 || '').trim(),
|
||
s_birim: String(r?.sBirim || '').trim(),
|
||
l_miktar: Number(r?.lMiktar || 0),
|
||
fiyat_girilen: Number(resolveNumericRowInputPrice(r) || 0),
|
||
fiyat_doviz: String(resolveInputCurrency(r) || '').trim(),
|
||
maliyete_dahil: (r?.maliyeteDahil || r?.maliyete_dahil) ? 1 : 0,
|
||
cm_price_type_id: r?.cmPriceTypeId ?? r?.cm_price_type_id ?? null,
|
||
s_aciklama3: String(r?.sAciklama3 || '').trim()
|
||
}))
|
||
|
||
const deletes = (Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []).map(d => ({
|
||
n_onml_det_no: parseInt(String(d?.nOnMLDetNo || '').trim() || '0', 10) || 0
|
||
})).filter(x => x.n_onml_det_no > 0)
|
||
|
||
const payload = {
|
||
detail_source: isNoCostDetail.value ? 'no-cost' : 'has-cost',
|
||
header: {
|
||
n_onml_no: parseInt(String(header?.nOnMLNo || onMLNo.value || '0').trim() || '0', 10) || 0,
|
||
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim(),
|
||
urun_adi: String(header?.UrunAdi || '').trim(),
|
||
maliyet_tarihi: normalizeDateInput(costDate.value),
|
||
n_urt_recete_id: parseInt(String(header?.nUrtReceteID || recipeCode.value || '0').trim() || '0', 10) || 0,
|
||
uretim_sekli_id: parseInt(String(header?.UretimSekliID || '0').trim() || '0', 10) || 0,
|
||
s_aciklama: String(header?.sAciklama || header?.SAciklama || '').trim(),
|
||
firma_kodu: String(header?.FirmaKodu || '').trim(),
|
||
n_firma_id: parseInt(String(header?.nFirmaID || header?.NFirmaID || '0').trim() || '0', 10) || 0
|
||
},
|
||
detail: { upserts, deletes }
|
||
}
|
||
|
||
const response = await post('/pricing/production-product-costing/onml/save', payload, {
|
||
params: { trace_id: traceId.value }
|
||
})
|
||
const newOnMLNo = parseInt(String(response?.n_onml_no || '0'), 10) || 0
|
||
|
||
$q.notify({ type: 'positive', message: 'Kaydedildi.', position: 'top-right' })
|
||
|
||
// Force clear local draft before fetching fresh data to ensure we don't re-hydrate old inputs.
|
||
clearLocalDraft()
|
||
|
||
// If we created a new OnML (no-cost), switch to has-cost detail mode.
|
||
if (isNoCostDetail.value && newOnMLNo > 0) {
|
||
await router.replace({
|
||
name: 'production-product-costing-has-cost-detail',
|
||
query: {
|
||
n_onml_no: String(newOnMLNo),
|
||
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim()
|
||
}
|
||
})
|
||
|
||
$q.dialog({
|
||
title: 'Maliyet Kaydedildi',
|
||
message: 'Maliyeti olmayan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
|
||
ok: {
|
||
label: 'Listeye Don',
|
||
color: 'primary',
|
||
icon: 'arrow_back'
|
||
},
|
||
cancel: {
|
||
label: 'Bu Sayfada Devam Et',
|
||
color: 'grey-7',
|
||
flat: true
|
||
},
|
||
persistent: true
|
||
}).onOk(() => {
|
||
router.push({ name: 'production-product-costing-no-cost' })
|
||
})
|
||
return
|
||
}
|
||
|
||
// For existing costing, just refresh the detail.
|
||
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||
// last10 warnings are computed async on backend; re-check shortly after save.
|
||
window.setTimeout(() => {
|
||
try { refreshLast10Warnings() } catch {}
|
||
}, 1200)
|
||
|
||
$q.dialog({
|
||
title: 'Maliyet Guncellendi',
|
||
message: 'Mevcut maliyeti olan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
|
||
ok: {
|
||
label: 'Listeye Don',
|
||
color: 'primary',
|
||
icon: 'arrow_back'
|
||
},
|
||
cancel: {
|
||
label: 'Bu Sayfada Devam Et',
|
||
color: 'grey-7',
|
||
flat: true
|
||
},
|
||
persistent: true
|
||
}).onOk(() => {
|
||
router.push({ name: 'production-product-costing-has-cost' })
|
||
})
|
||
} catch (e) {
|
||
// Surface backend message (http.Error text) when available.
|
||
const msg = String(
|
||
e?.response?.data?.message ||
|
||
e?.response?.data ||
|
||
e?.message ||
|
||
e ||
|
||
'Kaydedilemedi'
|
||
)
|
||
slog.error('production-product-costing.detail', 'save:error', {
|
||
trace_id: traceId.value,
|
||
status: e?.response?.status,
|
||
error: msg
|
||
})
|
||
$q.notify({ type: 'negative', message: msg, position: 'top-right' })
|
||
return
|
||
} finally {
|
||
saveLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function exportCostingPDF () {
|
||
try {
|
||
const n = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0
|
||
if (!(n > 0)) {
|
||
$q.notify({ type: 'warning', message: 'OnML No bulunamadi.', position: 'top-right' })
|
||
return
|
||
}
|
||
const blob = await download('/pricing/production-product-costing/onml/pdf', {
|
||
n_onml_no: n,
|
||
trace_id: traceId.value
|
||
})
|
||
const url = URL.createObjectURL(blob)
|
||
window.open(url, '_blank')
|
||
window.setTimeout(() => {
|
||
try { URL.revokeObjectURL(url) } catch {}
|
||
}, 60_000)
|
||
} catch (e) {
|
||
$q.notify({ type: 'negative', message: await extractApiErrorDetail(e), position: 'top-right' })
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => [onMLNo.value, productCode.value, recipeCode.value, detailSource.value],
|
||
() => {
|
||
fetchDetail()
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => costDate.value,
|
||
value => {
|
||
const normalizedDate = normalizeDateInput(value)
|
||
if (detailHeader.value) {
|
||
detailHeader.value.dteKayitTarihi = normalizedDate
|
||
}
|
||
fetchExchangeRatesForCostDate(normalizedDate)
|
||
}
|
||
)
|
||
|
||
onMounted(() => {
|
||
if (!canReadOrder.value) return
|
||
fetchDetail()
|
||
nextTick(() => {
|
||
updateStickyTop()
|
||
})
|
||
window.addEventListener('resize', updateStickyTop)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('resize', updateStickyTop)
|
||
ensureBeforeUnloadGuard(false)
|
||
if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value)
|
||
// Defensive: if component unmounts without router guard (rare), drop the draft.
|
||
clearLocalDraft()
|
||
})
|
||
|
||
watch(
|
||
() => hasUnsavedChanges.value,
|
||
(value) => {
|
||
ensureBeforeUnloadGuard(Boolean(value))
|
||
}
|
||
)
|
||
|
||
onBeforeRouteLeave((to, from, next) => {
|
||
if (!hasUnsavedChanges.value) {
|
||
// Always clear local draft on page exit; backend is source of truth.
|
||
clearLocalDraft()
|
||
next()
|
||
return
|
||
}
|
||
|
||
$q.dialog({
|
||
title: 'Sayfadan ayriliyorsunuz',
|
||
message: pageMode.value === 'edit'
|
||
? 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?'
|
||
: 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?',
|
||
ok: { label: 'Evet', color: 'negative' },
|
||
cancel: { label: 'Hayir' },
|
||
persistent: true
|
||
})
|
||
.onOk(() => {
|
||
clearLocalDraft()
|
||
next()
|
||
})
|
||
.onCancel(() => next(false))
|
||
})
|
||
|
||
watch(
|
||
() => [detailLoading.value, detailError.value, !!detailHeader.value, headerInfoCollapsed.value],
|
||
() => {
|
||
nextTick(() => {
|
||
updateStickyTop()
|
||
})
|
||
}
|
||
)
|
||
|
||
</script>
|
||
|
||
<style scoped>
|
||
.pcd-page {
|
||
padding: 0;
|
||
}
|
||
|
||
.pcd-content-body {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.pcd-sticky-stack {
|
||
position: sticky;
|
||
top: 50px;
|
||
z-index: 1000;
|
||
background: #fff;
|
||
margin-bottom: 0;
|
||
border-bottom: 1px solid #ddd;
|
||
width: 100%;
|
||
}
|
||
|
||
.pcd-save-toolbar {
|
||
border-top: 0;
|
||
}
|
||
|
||
.pcd-toolbar-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.pcd-toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
}
|
||
|
||
.pcd-toolbar-title {
|
||
flex: 0 0 auto;
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
color: #223048;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pcd-toolbar-summary {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
flex: 1 1 auto;
|
||
}
|
||
|
||
.pcd-toolbar-summary-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.pcd-toolbar-pill {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
padding: 5px 8px;
|
||
border-radius: 8px;
|
||
border: 1px solid #d7e0ea;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pcd-toolbar-pill-emphasis {
|
||
background: var(--q-primary);
|
||
border-color: #145ea8;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.pcd-toolbar-pill-neutral {
|
||
background: #f5f7fa;
|
||
color: #2b3c54;
|
||
}
|
||
|
||
.pcd-toolbar-pill-warn {
|
||
background: #ef6c00;
|
||
border-color: #ef6c00;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.pcd-summary-expansion :deep(.q-expansion-item__container) {
|
||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
}
|
||
|
||
.pcd-missing-stock-code {
|
||
background: #c62828 !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.pcd-missing-stock-code :deep(*) {
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.pcd-detail-row-missing-stock td {
|
||
background: #c62828 !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.pcd-detail-row-missing-stock td :deep(*) {
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.pcd-summary-title {
|
||
font-weight: 800;
|
||
font-size: 12px;
|
||
color: #2b3c54;
|
||
margin: 2px 0 6px;
|
||
}
|
||
|
||
.pcd-summary-table td,
|
||
.pcd-summary-table th {
|
||
font-size: 12px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.pcd-summary-total-row td {
|
||
border-top: 2px solid rgba(0, 0, 0, 0.15);
|
||
background: #fbfbfd;
|
||
}
|
||
|
||
.pcd-summary-k {
|
||
width: 60px;
|
||
color: #6b7680;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.pcd-summary-v {
|
||
font-weight: 800;
|
||
}
|
||
|
||
.pcd-toolbar-pill-label {
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.02em;
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.pcd-toolbar-pill-value {
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
min-width: 0;
|
||
}
|
||
|
||
.pcd-toolbar-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 6px;
|
||
flex: 0 0 auto;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.pcd-toolbar-btn {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.pcd-toolbar-btn :deep(.q-btn__content) {
|
||
min-height: 34px;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.pcd-add-row-dialog {
|
||
min-width: 360px;
|
||
}
|
||
|
||
.pcd-row-editor-dialog {
|
||
width: min(960px, 96vw);
|
||
max-width: 96vw;
|
||
}
|
||
|
||
.pcd-row-editor-drag-handle {
|
||
cursor: move;
|
||
user-select: none;
|
||
}
|
||
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__control) {
|
||
background: color-mix(in srgb, var(--q-secondary) 18%, white) !important;
|
||
border: 1px solid var(--q-secondary) !important;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__label),
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__native),
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__input),
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__marginal),
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-select__dropdown-icon),
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__selection) {
|
||
color: var(--q-secondary) !important;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry.q-field--focused .q-field__control) {
|
||
box-shadow: 0 0 0 1px var(--q-secondary), 0 0 0 3px color-mix(in srgb, var(--q-secondary) 24%, white);
|
||
}
|
||
|
||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-placeholder) {
|
||
color: var(--q-secondary) !important;
|
||
opacity: 0.76;
|
||
}
|
||
|
||
.pcd-price-deviation-dialog {
|
||
width: min(1120px, 96vw);
|
||
max-width: 96vw;
|
||
}
|
||
|
||
.pcd-price-deviation-table-wrap {
|
||
max-height: min(62vh, 620px);
|
||
overflow: auto;
|
||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.pcd-price-deviation-table {
|
||
width: 100%;
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.pcd-price-deviation-table th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
background: #f4f6f8;
|
||
color: #263238;
|
||
font-weight: 700;
|
||
padding: 6px 8px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pcd-price-deviation-table td {
|
||
padding: 5px 8px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.pcd-price-deviation-table td:nth-child(6) {
|
||
min-width: 120px;
|
||
}
|
||
|
||
.pcd-price-deviation-table td:nth-child(7) {
|
||
min-width: 96px;
|
||
}
|
||
|
||
.pcd-row-editor-flag {
|
||
background: color-mix(in srgb, var(--q-secondary) 10%, white);
|
||
border: 1px solid color-mix(in srgb, var(--q-secondary) 35%, white);
|
||
border-radius: 8px;
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
.pcd-history-dialog {
|
||
background: #fff;
|
||
}
|
||
|
||
.pcd-detail-header-bar {
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.pcd-emphasis-field :deep(.q-field__control) {
|
||
background: var(--q-primary) !important;
|
||
border: 1px solid #145ea8;
|
||
}
|
||
|
||
.pcd-emphasis-field :deep(.q-field__label) {
|
||
color: #eaf3ff !important;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.pcd-emphasis-field :deep(.q-field__native),
|
||
.pcd-emphasis-field :deep(.q-field__input) {
|
||
color: #ffffff !important;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.pcd-emphasis-field-alt :deep(.q-field__control) {
|
||
background: color-mix(in srgb, var(--q-secondary) 25%, white) !important;
|
||
border: 1px solid var(--q-secondary);
|
||
}
|
||
|
||
.pcd-emphasis-field-alt :deep(.q-field__label) {
|
||
color: var(--q-primary) !important;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.pcd-emphasis-field-alt :deep(.q-field__native),
|
||
.pcd-emphasis-field-alt :deep(.q-field__input),
|
||
.pcd-emphasis-field-alt :deep(.q-field__selection) {
|
||
color: var(--q-primary) !important;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.pcd-readonly-header-input :deep(.q-field__control) {
|
||
background: #f8f9fa !important;
|
||
}
|
||
|
||
.pcd-group-card {
|
||
border: 1px solid #dfe3e8;
|
||
border-radius: 6px;
|
||
overflow: visible;
|
||
position: relative;
|
||
margin: 0 10px 15px;
|
||
}
|
||
|
||
.pcd-sub-header {
|
||
position: sticky !important;
|
||
top: var(--pcd-subheader-top) !important;
|
||
z-index: 990 !important;
|
||
display: flex !important;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
padding: 8px 10px;
|
||
min-height: 46px;
|
||
height: auto;
|
||
border-top: 1px solid #d6c06a;
|
||
border-bottom: 1px solid #d6c06a;
|
||
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.pcd-sub-header .sub-left {
|
||
font-weight: 800;
|
||
color: #2b1f05;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.pcd-sub-header .sub-right {
|
||
font-weight: 900;
|
||
color: #3b2f09;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
text-align: right;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
flex-wrap: wrap;
|
||
row-gap: 4px;
|
||
max-width: 70%;
|
||
}
|
||
.pcd-sub-header .sub-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
.pcd-sub-title {
|
||
flex: 0 1 auto;
|
||
min-width: 0;
|
||
}
|
||
.pcd-sub-right-clickable {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.pcd-group-total-text {
|
||
color: #2f2408;
|
||
font-size: 13px;
|
||
font-weight: 900;
|
||
white-space: nowrap;
|
||
}
|
||
.pcd-group-total-divider {
|
||
margin: 0 4px;
|
||
color: #8d6d13;
|
||
}
|
||
.pcd-group-debug {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 2px 7px;
|
||
border: 1px solid rgba(180, 35, 24, 0.35);
|
||
border-radius: 5px;
|
||
color: #8f1d14;
|
||
background: rgba(255, 255, 255, 0.78);
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
text-transform: none;
|
||
white-space: nowrap;
|
||
}
|
||
.pcd-sub-mt-qty {
|
||
flex: 0 0 auto;
|
||
white-space: nowrap;
|
||
opacity: 0.9;
|
||
font-weight: 900;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.pcd-sub-header {
|
||
align-items: flex-start;
|
||
}
|
||
.pcd-sub-header .sub-right {
|
||
max-width: 78%;
|
||
}
|
||
.pcd-group-debug {
|
||
flex-basis: auto;
|
||
}
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table__middle) {
|
||
overflow: visible !important;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table) {
|
||
table-layout: fixed;
|
||
width: 100%;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table th),
|
||
.pcd-detail-table :deep(.q-table td) {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table tbody td) {
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table thead) {
|
||
display: table-header-group !important;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table thead tr:first-child th) {
|
||
position: sticky !important;
|
||
top: calc(var(--pcd-subheader-top) + 42px) !important;
|
||
z-index: 980 !important;
|
||
background: #f8f9fa !important;
|
||
opacity: 1 !important;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table thead th) {
|
||
font-size: 11px;
|
||
padding: 3px 4px;
|
||
white-space: normal;
|
||
line-height: 1.2;
|
||
vertical-align: bottom;
|
||
border-bottom: 1px solid rgba(0,0,0,0.12);
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table tbody td) {
|
||
font-size: 11px;
|
||
padding: 2px 4px;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.q-table tbody tr) {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-detail-row-secondary td) {
|
||
background: color-mix(in srgb, var(--q-secondary) 14%, white) !important;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-detail-row-required td) {
|
||
background: color-mix(in srgb, #ff9800 18%, white) !important;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-entry-header) {
|
||
background: color-mix(in srgb, var(--q-primary) 16%, #f8f9fa) !important;
|
||
color: var(--q-primary) !important;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-secondary-header) {
|
||
background: color-mix(in srgb, var(--q-secondary) 18%, #f8f9fa) !important;
|
||
color: var(--q-secondary) !important;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-secondary-cell) {
|
||
background: color-mix(in srgb, var(--q-secondary) 10%, white) !important;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-entry-input .q-field__control) {
|
||
background: color-mix(in srgb, var(--q-primary) 12%, white) !important;
|
||
border: 1px solid var(--q-primary) !important;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-entry-input .q-field__native),
|
||
.pcd-detail-table :deep(.pcd-entry-input .q-field__input),
|
||
.pcd-detail-table :deep(.pcd-entry-input .q-field__marginal),
|
||
.pcd-detail-table :deep(.pcd-entry-input .q-select__dropdown-icon) {
|
||
color: var(--q-primary) !important;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-entry-input.q-field--focused .q-field__control) {
|
||
box-shadow: 0 0 0 1px var(--q-primary), 0 0 0 3px color-mix(in srgb, var(--q-primary) 28%, white);
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-entry-input .q-placeholder) {
|
||
color: var(--q-primary) !important;
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-inline-input .q-field__control) {
|
||
min-height: 30px;
|
||
}
|
||
|
||
.pcd-detail-table :deep(.pcd-inline-input .q-field__native),
|
||
.pcd-detail-table :deep(.pcd-inline-input .q-field__input) {
|
||
padding-top: 0;
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.pcd-detail-table {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.pcd-part-summary-card {
|
||
margin-top: 8px;
|
||
border: 1px solid #d7e0ea;
|
||
border-radius: 8px;
|
||
background: #fcfdfe;
|
||
padding: 8px;
|
||
}
|
||
.pcd-part-summary-title {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
color: #2b3c54;
|
||
margin-bottom: 6px;
|
||
padding-left: 4px;
|
||
border-left: 3px solid var(--q-primary);
|
||
}
|
||
.pcd-part-summary-table {
|
||
background: transparent !important;
|
||
}
|
||
.pcd-part-summary-table thead th {
|
||
background: #f5f7fa;
|
||
font-weight: 800;
|
||
color: #44556c;
|
||
}
|
||
.pcd-part-summary-table tbody td {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.pcd-history-table :deep(.q-table thead th) {
|
||
font-size: 9.5px;
|
||
line-height: 1.1;
|
||
white-space: normal;
|
||
padding: 3px 2px;
|
||
}
|
||
|
||
.pcd-history-table :deep(.q-table tbody td) {
|
||
font-size: 9.5px;
|
||
vertical-align: top;
|
||
padding: 2px;
|
||
white-space: normal;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.pcd-history-table :deep(.q-table__middle) {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.pcd-history-table :deep(.q-table) {
|
||
table-layout: fixed;
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.pcd-history-table :deep(.q-table th),
|
||
.pcd-history-table :deep(.q-table td) {
|
||
min-width: 0 !important;
|
||
}
|
||
|
||
.pcd-history-table :deep(.q-table th:nth-child(1)), .pcd-history-table :deep(.q-table td:nth-child(1)) { width: 5% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(2)), .pcd-history-table :deep(.q-table td:nth-child(2)) { width: 6% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(3)), .pcd-history-table :deep(.q-table td:nth-child(3)) { width: 7% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(4)), .pcd-history-table :deep(.q-table td:nth-child(4)) { width: 6% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(5)), .pcd-history-table :deep(.q-table td:nth-child(5)) { width: 8% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(6)), .pcd-history-table :deep(.q-table td:nth-child(6)) { width: 7% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(7)), .pcd-history-table :deep(.q-table td:nth-child(7)) { width: 10% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(8)), .pcd-history-table :deep(.q-table td:nth-child(8)) { width: 5% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(9)), .pcd-history-table :deep(.q-table td:nth-child(9)) { width: 7% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(10)), .pcd-history-table :deep(.q-table td:nth-child(10)) { width: 5% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(11)), .pcd-history-table :deep(.q-table td:nth-child(11)) { width: 7% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(12)), .pcd-history-table :deep(.q-table td:nth-child(12)) { width: 5% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(13)), .pcd-history-table :deep(.q-table td:nth-child(13)) { width: 3% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(14)), .pcd-history-table :deep(.q-table td:nth-child(14)) { width: 5% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(15)), .pcd-history-table :deep(.q-table td:nth-child(15)) { width: 6% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(16)), .pcd-history-table :deep(.q-table td:nth-child(16)) { width: 4% !important; }
|
||
.pcd-history-table :deep(.q-table th:nth-child(17)), .pcd-history-table :deep(.q-table td:nth-child(17)) { width: 4% !important; }
|
||
|
||
.pcd-history-table :deep(.pcd-history-source-chip) {
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
padding: 2px 4px;
|
||
font-size: 9px;
|
||
}
|
||
|
||
.pcd-history-table :deep(.q-btn) {
|
||
min-width: 0;
|
||
padding-left: 5px;
|
||
padding-right: 5px;
|
||
font-size: 9px;
|
||
}
|
||
|
||
.pcd-history-table :deep(.pcd-history-row-purchase td) {
|
||
background: #ffffff;
|
||
}
|
||
|
||
.pcd-history-table :deep(.pcd-history-row-recipe td) {
|
||
background: color-mix(in srgb, var(--q-secondary) 10%, white);
|
||
}
|
||
|
||
.pcd-history-table :deep(.pcd-history-row-similar td) {
|
||
background: #fff8e1 !important;
|
||
border-bottom: 1px solid #ffe082 !important;
|
||
}
|
||
|
||
.pcd-history-source-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 72px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.pcd-history-source-chip--purchase {
|
||
background: color-mix(in srgb, var(--q-primary) 12%, white);
|
||
color: var(--q-primary);
|
||
}
|
||
|
||
.pcd-history-source-chip--bnz-v3,
|
||
.pcd-history-source-chip--bnz-rec {
|
||
background: #f3e5f5;
|
||
color: #7b1fa2;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.pcd-history-source-chip--recipe {
|
||
background: color-mix(in srgb, var(--q-secondary) 18%, white);
|
||
color: color-mix(in srgb, var(--q-secondary) 82%, black);
|
||
}
|
||
|
||
.pcd-history-source-chip--history {
|
||
background: #eceff3;
|
||
color: #44556c;
|
||
}
|
||
</style>
|