@@ -744,7 +802,7 @@ 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, extractApiErrorDetail } from 'src/services/api'
+import { get, post, download, extractApiErrorDetail } from 'src/services/api'
import { createTraceId, slog } from 'src/utils/slog'
const route = useRoute()
@@ -773,6 +831,8 @@ const rowEditorDialogOpen = ref(false)
const rowEditorMode = ref('new')
const rowEditorTargetRowKey = ref('')
const rowEditorForm = ref(createRowEditorForm())
+const fabricCopyDialogOpen = ref(false)
+const fabricCopySelectedKey = ref('')
// Draggable "Satir Duzenle" dialog (mouse drag by header).
const rowEditorDialogPos = ref({ x: 0, y: 0 })
@@ -872,10 +932,53 @@ const priceCurrencyOptions = [
]
const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : []))
+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'
@@ -929,6 +1032,7 @@ function persistLocalDraftNow () {
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))
@@ -972,6 +1076,7 @@ function tryHydrateFromLocalDraft () {
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()
@@ -1189,6 +1294,11 @@ function formatBarMoney (value) {
return formatMoney(roundedValue)
}
+function formatBarQuantity (value) {
+ const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000
+ return formatQuantity(roundedValue)
+}
+
function normalizePriceCurrency (value) {
const normalizedValue = String(value || '').trim().toUpperCase()
return ['USD', 'TRY', 'EUR', 'GBP'].includes(normalizedValue) ? normalizedValue : ''
@@ -2091,6 +2201,11 @@ function resolveGroupUSDTutar (grp) {
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0)
}
+function resolveGroupQuantity (grp) {
+ const items = Array.isArray(grp?.items) ? grp.items : []
+ return items.reduce((acc, row) => acc + (Number(row?.lMiktar || 0) || 0), 0)
+}
+
function groupKey (grp, gi) {
return `${String(grp?.sAciklama3 || 'TANIMSIZ')}-${gi}`
}
@@ -2264,15 +2379,20 @@ async function fetchDetail (options = {}) {
if (hydrateDraft) {
tryHydrateFromLocalDraft()
}
- // ensure required placeholder rows exist (based on mapping screen)
- 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)
- })
+ // 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) => {
@@ -2821,6 +2941,44 @@ function onRowEditorHammaddeChange (value) {
}
}
+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 || ''))
rowEditorForm.value.sKodu = String(value || '').trim()
@@ -2980,8 +3138,9 @@ async function fetchRequiredParcaMappings () {
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]
- if (cached) return cached
try {
const rows = await get('/pricing/production-product-costing/detail-editor-options', {
@@ -3009,7 +3168,7 @@ async function resolveHammaddeMetaForRequired (hNo) {
return meta
} catch {
requiredHammaddeMetaCache.value = { ...(requiredHammaddeMetaCache.value || {}), [key]: { groupName: '', hammaddeAdi: '', mtBolumID: 0, parcaAdi: '' } }
- return null
+ return cached || null
}
}
@@ -3067,6 +3226,7 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
const hNo = normalizeHammaddeNo(hNoRaw)
if (!hNo) continue
const reqKey = `${mappingMtBolumID}:${hNo}`
+ if (suppressedRequiredKeys.value?.[reqKey]) continue
if (processedRequiredKeys.has(reqKey)) continue
processedRequiredKeys.add(reqKey)
@@ -3093,12 +3253,19 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
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 (currentGroup !== effectiveGroupName || currentParcaAdi !== desiredParcaAdi || (hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)) {
+ 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,
@@ -3124,7 +3291,8 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
isNew: true,
nUrtMTBolumID: mtBolumID,
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
- nOnMLDetNo: '',
+ // 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...)
@@ -3162,6 +3330,142 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
}
}
+ // 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.
@@ -3243,6 +3547,9 @@ function computeMissingRequiredSlots () {
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.
@@ -3252,6 +3559,33 @@ function computeMissingRequiredSlots () {
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
@@ -3294,6 +3628,16 @@ function removeDetailRowByKey (rowKey) {
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 : []),
@@ -3588,17 +3932,79 @@ async function confirmRefresh () {
await fetchDetail({ clearDraft: true, hydrateDraft: false })
}
+function scrollToFirstRequiredAttentionRow () {
+ // Best-effort DOM scroll: q-table renders rowClass on the .
+ // 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 = {}
if (isNoCostDetail.value) {
const missing = computeMissingRequiredSlots()
- if (missing.length > 0) {
+ 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 `
+
+ | ${i + 1} |
+ ${String(m?.parcaAdi || '').trim()} |
+ ${String(m?.nHammaddeTuruNo || '').trim()} |
+ ${code || '-'} |
+ ${desc || '-'} |
+
+ `
+ }).join('')
+ const truncatedNote = missing.length > 40
+ ? `Ilk 40 satir gosterildi. Toplam: ${missing.length}
`
+ : ''
const ok = await new Promise(resolve => {
$q.dialog({
title: 'Eksik Maliyet Parcalari',
- message: `Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
+ html: true,
+ message: `
+
+ Eslestirilen parcalarda fiyat girilmemis satirlar var. Devam etmek istiyor musunuz?
+
Eksik: ${missing.length}
+
+
+
+
+ | No |
+ Parca |
+ Hammadde |
+ Kod |
+ Aciklama |
+
+
+
+ ${rowsHtml}
+
+
+
+ ${truncatedNote}
+
+ `,
cancel: true,
persistent: true
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
@@ -3614,6 +4020,8 @@ async function saveChanges () {
message: 'Eksik parcalar isaretlendi. Fiyat girip tekrar deneyin.',
position: 'top-right'
})
+ // Scroll to first highlighted row.
+ nextTick(() => scrollToFirstRequiredAttentionRow())
return
}
}
@@ -3627,6 +4035,17 @@ async function saveChanges () {
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 : [])
@@ -3651,6 +4070,24 @@ async function saveChanges () {
}
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,
@@ -3734,6 +4171,27 @@ async function saveChanges () {
}
}
+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],
() => {
@@ -4062,6 +4520,22 @@ watch(
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;
}