Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -51,7 +51,7 @@
|
||||
color="primary"
|
||||
class="pcd-toolbar-btn"
|
||||
:loading="detailLoading"
|
||||
@click="fetchDetail({ clearDraft: true, hydrateDraft: false })"
|
||||
@click="confirmRefresh"
|
||||
/>
|
||||
<q-btn
|
||||
label="Toplu Fiyat Cagir"
|
||||
@@ -84,6 +84,19 @@
|
||||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||||
@click="saveChanges"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="isNoCostDetail"
|
||||
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>
|
||||
@@ -359,13 +372,19 @@
|
||||
</template>
|
||||
|
||||
<template #body-cell-sKodu="props">
|
||||
<q-td :props="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
||||
>
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-sAciklama="props">
|
||||
<q-td :props="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
||||
>
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
@@ -443,9 +462,12 @@
|
||||
</div>
|
||||
</q-page>
|
||||
|
||||
<q-dialog v-model="rowEditorDialogOpen" persistent>
|
||||
<q-card class="pcd-row-editor-dialog">
|
||||
<q-card-section class="row items-center justify-between q-pb-sm">
|
||||
<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>
|
||||
@@ -752,6 +774,47 @@ const rowEditorDialogOpen = ref(false)
|
||||
const rowEditorMode = ref('new')
|
||||
const rowEditorTargetRowKey = ref('')
|
||||
const rowEditorForm = ref(createRowEditorForm())
|
||||
|
||||
// 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)
|
||||
@@ -843,6 +906,13 @@ const hasUnsavedChanges = computed(() => {
|
||||
return flatDetailRows.value.some(row => Boolean(row?.draftChanged))
|
||||
})
|
||||
|
||||
const canDeleteCosting = computed(() => {
|
||||
// Only allow delete for records created from no-cost flow.
|
||||
// UX expectation: delete button becomes available after a costing record exists.
|
||||
const n = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || '0'), 10) || 0
|
||||
return n > 0 && String(detailSource.value || '').trim().toLowerCase() === 'no-cost'
|
||||
})
|
||||
|
||||
function persistLocalDraftNow () {
|
||||
const key = draftStorageKey.value
|
||||
if (!key) return
|
||||
@@ -1796,6 +1866,19 @@ 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)
|
||||
}
|
||||
@@ -1826,17 +1909,23 @@ function resolveUSDUnitPriceByInput (inputPrice, inputCurrency, usdRate, eurRate
|
||||
|
||||
function normalizeDetailRows (items, groupName = '') {
|
||||
const list = Array.isArray(items) ? items : []
|
||||
return list.map((x, i) => ({
|
||||
...x,
|
||||
__rowKey: x?.__rowKey || `${x?.nOnMLNo || ''}-${x?.nOnMLDetNo || ''}-${i}`,
|
||||
miktarInput: x?.miktarInput ?? normalizeQuantityInput(x?.lMiktar),
|
||||
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
|
||||
inputPricePrBr: normalizePriceCurrency(x?.inputPricePrBr || x?.fiyat_doviz || x?.sDovizCinsi) || 'USD',
|
||||
maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil),
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
|
||||
draftChanged: Boolean(x?.draftChanged),
|
||||
priceUpdateState: String(x?.priceUpdateState || '').trim()
|
||||
}))
|
||||
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: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil),
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
|
||||
draftChanged: Boolean(x?.draftChanged),
|
||||
priceUpdateState: String(x?.priceUpdateState || '').trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeDetailGroups (groups) {
|
||||
@@ -2253,10 +2342,17 @@ function buildDetailItemRequestPayload (row) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyPriceSelectionToRow (targetRowKey, price, currency, priceType) {
|
||||
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,
|
||||
@@ -2264,6 +2360,9 @@ function applyPriceSelectionToRow (targetRowKey, price, currency, priceType) {
|
||||
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,
|
||||
@@ -2481,6 +2580,94 @@ async function fetchBulkItemPrices () {
|
||||
: '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
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
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 || '')
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
@@ -2542,11 +2729,28 @@ function onDetailRowClick (evt, row) {
|
||||
}
|
||||
|
||||
function applyLineHistorySelection (historyRow) {
|
||||
applyPriceSelectionToRow(lineHistoryTargetRowKey.value, historyRow?.price, historyRow?.currency, historyRow?.priceType)
|
||||
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 history fiyati satira uygulandi.',
|
||||
message: `Secilen fiyat satira uygulandi: ${formatMoney(historyRow?.price)} ${String(historyRow?.currency || '').trim() || 'USD'}`,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
@@ -2759,8 +2963,10 @@ function normalizeGroupName (value) {
|
||||
async function fetchRequiredParcaMappings () {
|
||||
const ilk = String(detailHeader.value?.UrunIlkGrubu || '').trim()
|
||||
const ana = String(detailHeader.value?.UrunAnaGrubu || '').trim()
|
||||
const alt = String(detailHeader.value?.UrunAltGrubu || '').trim()
|
||||
if (!ilk || !ana || !alt) return []
|
||||
// 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,
|
||||
@@ -2956,6 +3162,81 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
applyEditorRowToGroups(placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
@@ -2964,19 +3245,30 @@ function computeMissingRequiredSlots () {
|
||||
if (list.length === 0) return missing
|
||||
|
||||
list.forEach(mapping => {
|
||||
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3)
|
||||
// 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 match = flatDetailRows.value.find(r =>
|
||||
normalizeGroupName(r?.sAciklama3) === groupName &&
|
||||
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo
|
||||
)
|
||||
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({ groupName, nHammaddeTuruNo: hNo, rowKey: match?.__rowKey || '' })
|
||||
missing.push({
|
||||
mtBolumID: mappingMtBolumID,
|
||||
parcaAdi: mappingParcaAdi,
|
||||
nHammaddeTuruNo: hNo,
|
||||
rowKey: match?.__rowKey || ''
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -3131,6 +3423,16 @@ function round4 (n) {
|
||||
return Math.round(x * 10000) / 10000
|
||||
}
|
||||
|
||||
function escapeHtml (input) {
|
||||
const s = String(input ?? '')
|
||||
return s
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
async function confirmDefaultQtyDeviationIfNeeded () {
|
||||
// Compare entered qty vs default qty (mk_MaliyetParcaEslestirme_vmiktarlar) per hammadde type.
|
||||
// Rule: if deviation > 10% (abs), require user confirmation.
|
||||
@@ -3154,10 +3456,15 @@ async function confirmDefaultQtyDeviationIfNeeded () {
|
||||
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)
|
||||
if (no > 0 && d > 0) defMap[no] = d
|
||||
const desc = String(it?.sAciklama || '').trim()
|
||||
if (no > 0 && d > 0) {
|
||||
defMap[no] = d
|
||||
if (desc) descMap[no] = desc
|
||||
}
|
||||
})
|
||||
|
||||
const outliers = []
|
||||
@@ -3176,18 +3483,53 @@ async function confirmDefaultQtyDeviationIfNeeded () {
|
||||
outliers.sort((a, b) => Math.abs(b.pct) - Math.abs(a.pct))
|
||||
|
||||
const maxRows = 30
|
||||
const lines = outliers.slice(0, maxRows).map(x => {
|
||||
const rowsHtml = outliers.slice(0, maxRows).map(x => {
|
||||
const sign = x.pct >= 0 ? '+' : ''
|
||||
return `${x.no}: varsayilan ${round4(x.defQty)} | girilen ${round4(x.enteredQty)} | fark ${sign}${round1(x.pct)}%`
|
||||
})
|
||||
const truncated = outliers.length > maxRows
|
||||
? `\n... (Toplam ${outliers.length} satir. Ilk ${maxRows} gosterildi.)`
|
||||
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',
|
||||
message: `Bazi hammadde turlerinde varsayilan miktardan %10'dan fazla sapma var.\n\n${lines.join('\n')}${truncated}\n\nOnayliyorsaniz Kaydet'e basın. Duzenlemek icin Geri Don.`,
|
||||
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
|
||||
@@ -3196,21 +3538,72 @@ async function confirmDefaultQtyDeviationIfNeeded () {
|
||||
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 })
|
||||
}
|
||||
|
||||
async function saveChanges () {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
requiredAttentionRowKeys.value = {}
|
||||
if (isNoCostDetail.value) {
|
||||
const missing = computeMissingRequiredSlots()
|
||||
if (missing.length > 0) {
|
||||
const ok = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Eksik Maliyet Parcalari',
|
||||
message: `Eslestirilen parcalarda (fiyat > 0) girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||
})
|
||||
if (missing.length > 0) {
|
||||
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})`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||
})
|
||||
if (!ok) {
|
||||
const next = {}
|
||||
missing.forEach(x => {
|
||||
@@ -3250,7 +3643,8 @@ async function saveChanges () {
|
||||
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
|
||||
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 => ({
|
||||
@@ -3278,6 +3672,9 @@ async function saveChanges () {
|
||||
|
||||
$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) {
|
||||
router.replace({
|
||||
@@ -3289,6 +3686,25 @@ async function saveChanges () {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// For existing costing, just refresh the detail.
|
||||
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||||
} 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
|
||||
}
|
||||
@@ -3489,6 +3905,11 @@ watch(
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user