@@ -3,37 +3,65 @@
< 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" >
< 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" > 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 class = "pcd-toolbar-pill pcd-toolbar-pill-neutr al" >
< 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 . gbp Rate) } } < / span >
< / div >
< / div >
< / div >
< 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 . eurTot al ) } } < / 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 . usd Rate) } } < / 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 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
@@ -114,6 +142,178 @@
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 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" > 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 . 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 = "7" 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" >
@@ -189,7 +389,7 @@
< q-input dense filled readonly label = "nUrtReceteID" : model -value = " detailHeader.nUrtReceteID | | ' - ' " / >
< / div >
< div v-if = "partSummary && partSummary.length > 0" class="col-12" >
< 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" >
@@ -386,7 +586,7 @@
<template #body-cell-sKodu=" props ">
<q-td
:props=" props "
:class=" resolveAutoOrICodeHighlightClass ( props . row ) "
:class=" [ resolveAutoOrICodeHighlightClass ( props . row ) , resolveMissingStockCodeClass ( props . row ) ] "
>
<span>{{ props.value }}</span>
</q-td>
@@ -395,7 +595,7 @@
<template #body-cell-sAciklama=" props ">
<q-td
:props=" props "
:class=" resolveAutoOrICodeHighlightClass ( props . row ) "
:class=" [ resolveAutoOrICodeHighlightClass ( props . row ) , resolveMissingStockCodeClass ( props . row ) ] "
>
<span>{{ props.value }}</span>
</q-td>
@@ -880,6 +1080,7 @@ 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)
@@ -894,7 +1095,89 @@ 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)}%`
}
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)
@@ -1114,6 +1397,167 @@ const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) =>
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 => {
@@ -1129,25 +1573,27 @@ const partSummary = computed(() => {
})
return Object.entries(summary).map(([name, totals]) => ({ name, ...totals }))
})
const lineHistoryColumns = [
{ name: 'sourceLabel', label: 'Kaynak', field: 'sourceLabel', align: 'left', sortable: false, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'dateLabel', label: 'Tarih', field: 'dateLabel', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
{ name: 'invoiceCode', label: 'Fatura/OnML', field: 'invoiceCode', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
{ name: 'companyCode ', label: 'Firma Kodu ', field: 'companyCode ', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
{ name: 'companyDescription', label: 'Firma Aciklama', field: 'companyDescription', align: 'left', sortable: true, style: 'width:12%', headerStyle: 'width:12%' },
{ name: 'item Code', label: 'Masraf/s Kodu', field: 'item Code', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
{ name: 'item Description', label: 'Masraf Detay ', field: 'item Description', align: 'left', sortable: true, style: 'width:11%', headerStyle: 'width:11%' },
{ name: 'color Code', label: 'Renk ', field: 'color Code', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
{ name: 'color Description', label: 'Renk Aciklama ', field: 'color Description', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
{ name: 'itemDim1 Code', label: 'Dim1 ', field: 'itemDim1 Code', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
{ name: 'itemDim1 Description', label: 'Dim1 Aciklama', field: 'itemDim1 Description', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
{ name: 'quantity ', label: 'Miktar ', field: 'quantity ', align: 'righ t', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'unit', label: 'Birim', field: 'unit', align: 'left', sortable: true, style: 'width:4%', headerStyle: 'width:4%' },
{ name: 'price ', label: 'Fiyat ', field: 'price ', align: 'right', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'amo unt', label: 'Tutar ', field: 'amo unt', align: 'righ t', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'currency ', label: 'Pr. Br. ', field: 'currency ', align: 'lef t', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
{ name: 'selec t', label: '', field: 'selec t', align: 'right', sortable: false, style: 'width:6%', headerStyle: 'width:6%' }
]
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: 'company Code', label: 'Firma Kodu', field: 'company Code', align: 'left', sortable: true },
{ name: 'company Description', label: 'Firma Aciklama ', field: 'company Description', align: 'left', sortable: true },
{ name: 'item Code', label: 'Masraf/sKodu ', field: 'item Code', align: 'left', sortable: true },
{ name: 'item Description', label: 'Masraf Detay ', field: 'item Description', align: 'left', sortable: true },
{ name: 'color Code', label: 'Renk ', field: 'color Code', align: 'left', sortable: true },
{ name: 'color Description', label: 'Renk Aciklama', field: 'color Description', align: 'left', sortable: true },
{ name: 'itemDim1Code ', label: 'Dim1 ', field: 'itemDim1Code ', align: 'lef t', sortable: true },
{ name: 'itemDim1Description', label: 'Dim1 Aciklama', field: 'itemDim1Description', align: 'left', sortable: true },
{ name: 'quantity ', label: 'Miktar ', field: 'quantity ', align: 'right', sortable: true },
{ name: 'uni t', label: 'Birim ', field: 'uni t', align: 'lef t', sortable: true },
{ name: 'price ', label: 'Fiyat ', field: 'price ', align: 'righ t', sortable: true },
{ name: 'amoun t', label: 'Tutar ', field: 'amoun t', 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'
@@ -1166,7 +1612,7 @@ const detailColumns = [
{ 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: 80 px', headerStyle: 'width: 80 px' },
{ name: 'inputPricePrBr', label: 'Fiyat Giriş Pr.Br.', field: 'inputPricePrBr', align: 'left', sortable: false, style: 'width: 92 px', headerStyle: 'width: 92 px' },
{ 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) },
@@ -1290,14 +1736,22 @@ function formatQuantity (value) {
})
}
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) {
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000
return formatQuantity(roundedValue)
// 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) {
@@ -1630,8 +2084,15 @@ async function loadRowEditorColorOptions () {
function primeRowEditorOptionsFromForm () {
upsertEditorOption(rowEditorHammaddeAllOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
upsertEditorOption(rowEditorHammaddeOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
upsertEditorOption(rowEditorItemAllOptions, buildRowEditorItemOption(rowEditorForm.value))
upsertEditorOption(rowEditor ItemO ptions, buildRowEditorItemOption(rowEditorForm.value))
// Do not inject invalid free-typed values (e.g. " GAMBOÇ ") into the item select options.
// Item o ptions 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))
}
@@ -2402,7 +2863,8 @@ async function fetchDetail (options = {}) {
])
detailHeader.value = headerData && typeof headerData === 'object' ? headerData : null
productionTypes.value = Array.isArray(typesData) ? typesData : []
costDate.value = normalizeDateInput(detailHeader.value?.dteKayitTarihi)
// 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.
@@ -2437,7 +2899,11 @@ async function fetchDetail (options = {}) {
urun _kodu : detailHeader . value ? . UrunKodu || productCode . value ,
n _urt _recete _id : detailHeader . value ? . nUrtReceteID || ''
} )
} catch ( err ) {
// 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 ,
@@ -2908,6 +3374,8 @@ 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' : ''
}
@@ -3011,8 +3479,27 @@ async function applyFabricCopySelection () {
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 ( )
if ( ! selected ) return
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 ( )
@@ -3129,7 +3616,8 @@ function applyEditorRowToGroups (nextRow) {
detailGroups . value = sortDetailGroups ( nextGroups )
syncAllGroupsOpen ( )
schedulePersistLocalDraft ( )
}
// tbStok validation devre disi
}
function syncAllGroupsOpen ( ) {
const openState = { }
@@ -3704,6 +4192,7 @@ function openNewRowDialog () {
maliyeteDahil : true ,
sBirim : 'AD'
} )
rowEditorLastValidItemValue . value = ''
primeRowEditorOptionsFromForm ( )
rowEditorDialogOpen . value = true
void bootstrapRowEditorOptions ( )
@@ -3716,6 +4205,7 @@ function openRowEditorForEdit (row) {
... row ,
sAciklama3 : row ? . sAciklama3 || ''
} )
rowEditorLastValidItemValue . value = String ( rowEditorForm . value ? . sKodu || '' ) . trim ( )
primeRowEditorOptionsFromForm ( )
rowEditorDialogOpen . value = true
void bootstrapRowEditorOptions ( )
@@ -4113,6 +4603,7 @@ 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' , {
@@ -4319,6 +4810,10 @@ async function saveChanges () {
// 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 )
} catch ( e ) {
// Surface backend message (http.Error text) when available.
const msg = String (
@@ -4485,14 +4980,22 @@ watch(
white - space : nowrap ;
}
. pcd - toolbar - summary {
display : flex ;
flex - wrap : nowrap ;
align - items : center ;
gap : 6 px ;
min - width : 0 ;
flex : 1 1 auto ;
}
. pcd - toolbar - summary {
display : flex ;
flex - direction : column ;
align - items : flex - start ;
gap : 6 px ;
min - width : 0 ;
flex : 1 1 auto ;
}
. pcd - toolbar - summary - row {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
gap : 6 px ;
min - width : 0 ;
}
. pcd - toolbar - pill {
display : flex ;
@@ -4511,9 +5014,67 @@ watch(
color : # ffffff ;
}
. pcd - toolbar - pill - neutral {
background : # f5f7fa ;
. pcd - toolbar - pill - neutral {
background : # f5f7fa ;
color : # 2 b3c54 ;
}
. pcd - toolbar - pill - warn {
background : # ef6c00 ;
border - color : # ef6c00 ;
color : # ffffff ;
}
. pcd - summary - expansion : deep ( . q - expansion - item _ _container ) {
border : 1 px solid rgba ( 0 , 0 , 0 , 0.08 ) ;
border - radius : 6 px ;
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 : 12 px ;
color : # 2 b3c54 ;
margin : 2 px 0 6 px ;
}
. pcd - summary - table td ,
. pcd - summary - table th {
font - size : 12 px ;
line - height : 1.2 ;
}
. pcd - summary - total - row td {
border - top : 2 px solid rgba ( 0 , 0 , 0 , 0.15 ) ;
background : # fbfbfd ;
}
. pcd - summary - k {
width : 60 px ;
color : # 6 b7680 ;
font - weight : 800 ;
}
. pcd - summary - v {
font - weight : 800 ;
}
. pcd - toolbar - pill - label {