Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -44,7 +44,15 @@
|
||||
@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="fetchDetail" />
|
||||
<q-btn
|
||||
label="Yenile"
|
||||
icon="refresh"
|
||||
dense
|
||||
color="primary"
|
||||
class="pcd-toolbar-btn"
|
||||
:loading="detailLoading"
|
||||
@click="fetchDetail({ clearDraft: true, hydrateDraft: false })"
|
||||
/>
|
||||
<q-btn
|
||||
label="Toplu Fiyat Cagir"
|
||||
icon="playlist_add_check"
|
||||
@@ -107,7 +115,6 @@
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
v-if="!isNoCostDetail"
|
||||
v-model="detailHeader.UretimSekliID"
|
||||
:options="productionTypes"
|
||||
option-value="id"
|
||||
@@ -120,29 +127,6 @@
|
||||
class="pcd-emphasis-field-alt"
|
||||
@update:model-value="onUretimSekliChange"
|
||||
/>
|
||||
<q-input
|
||||
v-else
|
||||
dense
|
||||
filled
|
||||
readonly
|
||||
label="Uretim Sekli"
|
||||
:model-value="detailHeader.UretimSekli || '-'"
|
||||
class="pcd-emphasis-field-alt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="row q-col-gutter-xs">
|
||||
<div class="col-4">
|
||||
<q-input dense filled readonly label="USD Kuru" :model-value="formatMoney(exchangeRates.usdRate)" class="pcd-emphasis-field-alt" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-input dense filled readonly label="EUR Kuru" :model-value="formatMoney(exchangeRates.eurRate)" class="pcd-emphasis-field-alt" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-input dense filled readonly label="GBP Kuru" :model-value="formatMoney(exchangeRates.gbpRate)" class="pcd-emphasis-field-alt" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
@@ -160,6 +144,9 @@
|
||||
<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>
|
||||
@@ -180,9 +167,9 @@
|
||||
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
|
||||
</div>
|
||||
|
||||
<div v-if="!isNoCostDetail && partSummary && partSummary.length > 0" class="col-12">
|
||||
<div v-if="partSummary && partSummary.length > 0" class="col-12">
|
||||
<div class="pcd-part-summary-card">
|
||||
<div class="pcd-part-summary-title">Parça Bazlı Maliyet Özetleri</div>
|
||||
<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>
|
||||
@@ -359,7 +346,7 @@
|
||||
|
||||
<template #body-cell-sParcaAdi="props">
|
||||
<q-td :props="props">
|
||||
{{ props.value || props.row.sAciklama3 || '-' }}
|
||||
{{ props.value || '-' }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
@@ -826,14 +813,18 @@ const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.is
|
||||
// no-cost: required parca slots (from Maliyet Parca Eslestirme)
|
||||
const requiredParcaMappings = ref([])
|
||||
const requiredAttentionRowKeys = ref({})
|
||||
const requiredHammaddeMetaCache = ref({}) // hammaddeNo -> { groupName, parcaAdi }
|
||||
|
||||
// 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:no-cost:${String(productCode.value || '').trim()}:${String(recipeCode.value || '').trim()}`
|
||||
return `pcd-costing:${DRAFT_STORAGE_VERSION}:no-cost:${String(productCode.value || '').trim()}:${String(recipeCode.value || '').trim()}`
|
||||
}
|
||||
if (!onMLNo.value) return ''
|
||||
return `pcd-costing:has-cost:${String(onMLNo.value).trim()}`
|
||||
return `pcd-costing:${DRAFT_STORAGE_VERSION}:has-cost:${String(onMLNo.value).trim()}`
|
||||
})
|
||||
|
||||
const currentHeaderSnapshot = computed(() => JSON.stringify({
|
||||
@@ -1051,6 +1042,27 @@ function isCMGroupName (value) {
|
||||
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: '',
|
||||
@@ -1062,6 +1074,7 @@ function createEmptyExchangeRates () {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -1070,10 +1083,10 @@ function createRowEditorForm (seed = {}) {
|
||||
nStokID: String(seed?.nStokID || '').trim(),
|
||||
sModel: String(seed?.sModel || '').trim(),
|
||||
nOnMLDetNo: String(seed?.nOnMLDetNo || '').trim(),
|
||||
sParcaAdi: String(seed?.sParcaAdi || seed?.sAciklama3 || '').trim(),
|
||||
sParcaAdi: String(seed?.sParcaAdi || '').trim(),
|
||||
nHammaddeTuruNo: String(seed?.nHammaddeTuruNo || '').trim(),
|
||||
sHammaddeTuruAdi: String(seed?.sHammaddeTuruAdi || '').trim(),
|
||||
sAciklama3: String(seed?.sAciklama3 || seed?.sParcaAdi || '').trim(),
|
||||
sAciklama3: String(seed?.sAciklama3 || '').trim(),
|
||||
sKodu: String(seed?.sKodu || '').trim(),
|
||||
sAciklama: String(seed?.sAciklama || '').trim(),
|
||||
sRenk: String(seed?.sRenk || seed?.ColorCode || '').trim(),
|
||||
@@ -1828,7 +1841,7 @@ function normalizeDetailRows (items, groupName = '') {
|
||||
|
||||
function normalizeDetailGroups (groups) {
|
||||
const list = Array.isArray(groups) ? groups : []
|
||||
return list.map(grp => {
|
||||
const out = list.map(grp => {
|
||||
const groupName = String(grp?.sAciklama3 || '').trim()
|
||||
const items = normalizeDetailRows(grp?.items, groupName).map(row => ({
|
||||
...row,
|
||||
@@ -1836,16 +1849,52 @@ function normalizeDetailGroups (groups) {
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, groupName || row?.sAciklama3)
|
||||
}))
|
||||
// USD TUTAR (DESC) sıralama
|
||||
items.sort((a, b) => {
|
||||
const valA = resolveRowUSDTutar(a)
|
||||
const valB = resolveRowUSDTutar(b)
|
||||
return valB - valA
|
||||
})
|
||||
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 = {}) {
|
||||
@@ -1977,11 +2026,7 @@ function resolveElHeight (refVal) {
|
||||
|
||||
function updateStickyTop () {
|
||||
const stackH = resolveElHeight(stickyStackRef.value)
|
||||
// Quasar default header height is usually around 50px
|
||||
// If we are in a sub-layout or context where top header is not 50px, this might need adjustment
|
||||
const layoutHeader = document.querySelector('.q-header')
|
||||
const layoutHeaderH = layoutHeader ? layoutHeader.offsetHeight : 50
|
||||
subHeaderTop.value = (stackH || 0) + layoutHeaderH
|
||||
subHeaderTop.value = (stackH || 0) + 50
|
||||
}
|
||||
|
||||
function toggleHeaderInfo () {
|
||||
@@ -2052,7 +2097,9 @@ async function fetchExchangeRatesForCostDate (targetDate = costDate.value) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDetail () {
|
||||
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 = []
|
||||
@@ -2102,6 +2149,9 @@ async function fetchDetail () {
|
||||
lineHistoryLastRecipeMatchStage.value = ''
|
||||
|
||||
try {
|
||||
if (clearDraft) {
|
||||
clearLocalDraft()
|
||||
}
|
||||
const detailParams = buildDetailFetchParams()
|
||||
slog.info('production-product-costing.detail', 'fetch-detail:start', {
|
||||
trace_id: traceId.value,
|
||||
@@ -2123,11 +2173,13 @@ async function fetchDetail () {
|
||||
detailGroups.value = normalizeDetailGroups(groupsData)
|
||||
initialHeaderSnapshot.value = currentHeaderSnapshot.value
|
||||
// Optional: hydrate local draft after base data load.
|
||||
tryHydrateFromLocalDraft()
|
||||
if (hydrateDraft) {
|
||||
tryHydrateFromLocalDraft()
|
||||
}
|
||||
// ensure required placeholder rows exist (based on mapping screen)
|
||||
try {
|
||||
const mappings = await fetchRequiredParcaMappings()
|
||||
ensureNoCostRequiredRowsFromMappings(mappings)
|
||||
await ensureNoCostRequiredRowsFromMappings(mappings)
|
||||
} catch (err) {
|
||||
slog.error('production-product-costing.detail', 'required-mapping:error', {
|
||||
trace_id: traceId.value,
|
||||
@@ -2526,7 +2578,8 @@ async function bootstrapRowEditorOptions () {
|
||||
if (selectedHammadde) {
|
||||
rowEditorForm.value.sHammaddeTuruAdi = String(selectedHammadde.sHammaddeTuruAdi || rowEditorForm.value.sHammaddeTuruAdi || '').trim()
|
||||
rowEditorForm.value.sAciklama3 = String(selectedHammadde.sAciklama3 || rowEditorForm.value.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||||
rowEditorForm.value.sParcaAdi = String(selectedHammadde.sParcaAdi || selectedHammadde.sAciklama3 || rowEditorForm.value.sParcaAdi || '').trim()
|
||||
// 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) {
|
||||
@@ -2558,7 +2611,8 @@ function onRowEditorHammaddeChange (value) {
|
||||
if (!selected) return
|
||||
rowEditorForm.value.sHammaddeTuruAdi = String(selected.sHammaddeTuruAdi || '').trim()
|
||||
rowEditorForm.value.sAciklama3 = String(selected.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||||
rowEditorForm.value.sParcaAdi = String(selected.sParcaAdi || selected.sAciklama3 || '').trim()
|
||||
// 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
|
||||
}
|
||||
@@ -2603,7 +2657,7 @@ function onRowEditorColorChange (value) {
|
||||
}
|
||||
|
||||
function buildRowFromEditorForm () {
|
||||
const form = rowEditorForm.value
|
||||
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) {
|
||||
@@ -2618,8 +2672,9 @@ function buildRowFromEditorForm () {
|
||||
nStokID: String(form.nStokID || '').trim(),
|
||||
sModel: String(form.sModel || '').trim(),
|
||||
nOnMLDetNo: String(form.nOnMLDetNo || '').trim(),
|
||||
sParcaAdi: String(form.sParcaAdi || form.sAciklama3 || '').trim(),
|
||||
sAciklama3: String(form.sAciklama3 || form.sParcaAdi || 'TANIMSIZ').trim() || 'TANIMSIZ',
|
||||
// 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(),
|
||||
@@ -2662,7 +2717,14 @@ function applyEditorRowToGroups (nextRow) {
|
||||
nextGroups[targetIndex] = {
|
||||
...nextGroups[targetIndex],
|
||||
items: [...(Array.isArray(nextGroups[targetIndex].items) ? nextGroups[targetIndex].items : []), nextRow]
|
||||
.sort((left, right) => parseInt(String(left?.nOnMLDetNo || '0'), 10) - parseInt(String(right?.nOnMLDetNo || '0'), 10))
|
||||
.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({
|
||||
@@ -2673,7 +2735,7 @@ function applyEditorRowToGroups (nextRow) {
|
||||
})
|
||||
}
|
||||
|
||||
detailGroups.value = nextGroups
|
||||
detailGroups.value = sortDetailGroups(nextGroups)
|
||||
syncAllGroupsOpen()
|
||||
schedulePersistLocalDraft()
|
||||
}
|
||||
@@ -2695,49 +2757,143 @@ 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 (!ana || !alt) return []
|
||||
if (!ilk || !ana || !alt) 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 : []
|
||||
}
|
||||
|
||||
function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
async function resolveHammaddeMetaForRequired (hNo) {
|
||||
const key = String(hNo || '').trim()
|
||||
if (!key) return null
|
||||
const cached = requiredHammaddeMetaCache.value?.[key]
|
||||
if (cached) return cached
|
||||
|
||||
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 null
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
const list = Array.isArray(mappings) ? mappings : []
|
||||
requiredParcaMappings.value = list
|
||||
if (list.length === 0) return
|
||||
|
||||
// Add missing placeholder rows (qty=1, price=0) to remind user
|
||||
list.forEach(mapping => {
|
||||
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3)
|
||||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||||
hList.forEach(hNoRaw => {
|
||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||
if (!hNo) 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()
|
||||
|
||||
const exists = flatDetailRows.value.some(r =>
|
||||
normalizeGroupName(r?.sAciklama3) === groupName &&
|
||||
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo
|
||||
)
|
||||
if (exists) return
|
||||
// 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 (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'
|
||||
const mtBolumID = (meta?.mtBolumID > 0 ? meta.mtBolumID : mappingMtBolumID) || 0
|
||||
const metaParcaCandidate = normalizeGroupName((meta?.parcaAdi || '').trim())
|
||||
// Defensive: if backend/lookup accidentally returns group label (DT/TP/...) as part name, ignore it.
|
||||
const desiredParcaAdi = normalizeGroupName((metaParcaCandidate && !isKnownGroupName(metaParcaCandidate)) ? metaParcaCandidate : 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 currentGroup = normalizeGroupName(anyMatch?.sAciklama3)
|
||||
const currentParcaAdi = normalizeGroupName(anyMatch?.sParcaAdi || '')
|
||||
const currentHammaddeAdi = String(anyMatch?.sHammaddeTuruAdi || '').trim()
|
||||
if (currentGroup !== effectiveGroupName || currentParcaAdi !== desiredParcaAdi || (hammaddeAdi && currentHammaddeAdi !== hammaddeAdi)) {
|
||||
const moved = recalculateDetailRow({
|
||||
...anyMatch,
|
||||
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 || '',
|
||||
nOnMLDetNo: '',
|
||||
sParcaAdi: groupName,
|
||||
sAciklama3: groupName,
|
||||
// Group header key
|
||||
sAciklama3: effectiveGroupName,
|
||||
// Parca adi column value (CEKET/PANTOLON/YELEK...)
|
||||
sParcaAdi: desiredParcaAdi,
|
||||
nHammaddeTuruNo: hNo,
|
||||
sHammaddeTuruAdi: '',
|
||||
// Make "Hammadde Turu" render like existing rows: "no - aciklama"
|
||||
sHammaddeTuruAdi: hammaddeAdi,
|
||||
sKodu: '',
|
||||
sAciklama: '',
|
||||
sRenk: '',
|
||||
@@ -2765,8 +2921,8 @@ function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
})
|
||||
|
||||
applyEditorRowToGroups(placeholder)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function computeMissingRequiredSlots () {
|
||||
@@ -3061,6 +3217,9 @@ watch(
|
||||
}
|
||||
|
||||
.pcd-sticky-stack {
|
||||
position: sticky;
|
||||
top: 50px;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
@@ -3253,6 +3412,9 @@ watch(
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -3296,6 +3458,9 @@ watch(
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<q-page v-if="canReadOrder" class="pcmm-page q-pa-md">
|
||||
<div class="pcmm-top">
|
||||
<div class="pcmm-top sticky-top">
|
||||
<div class="pcmm-header row items-center q-col-gutter-md">
|
||||
<div class="col">
|
||||
<div class="text-h6">Maliyet Parca Eslestirme</div>
|
||||
@@ -9,18 +9,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="loading"
|
||||
@click="refreshAll"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="loading"
|
||||
@click="hardResetAndRefresh"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
<q-separator class="q-my-sm" />
|
||||
</div>
|
||||
|
||||
<div class="pcmm-table-wrap">
|
||||
@@ -37,6 +37,7 @@
|
||||
no-data-label="Kayit bulunamadi"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
sticky-header
|
||||
>
|
||||
<template #header-cell="props">
|
||||
<q-th :props="props">
|
||||
@@ -239,8 +240,9 @@
|
||||
map-options
|
||||
class="pcmm-multi-select"
|
||||
behavior="menu"
|
||||
:disable="(bolumByKey[props.row.__key] || []).length === 0"
|
||||
@filter="onFilterHammadde"
|
||||
@update:model-value="(val) => { updateHammaddeSelection(props.row.__key, val); markDirty(props.row) }"
|
||||
@update:model-value="(val) => { updateHammaddeSelection(props.row.__key, pruneHammaddeSelection(props.row.__key, val)); markDirty(props.row) }"
|
||||
style="min-width: 320px"
|
||||
>
|
||||
<template #before-options>
|
||||
@@ -265,10 +267,11 @@
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item v-bind="scope.itemProps" :disable="isHammaddeOptionDisabled(props.row.__key, scope.opt)">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="scope.selected"
|
||||
:disable="isHammaddeOptionDisabled(props.row.__key, scope.opt)"
|
||||
tabindex="-1"
|
||||
@update:model-value="() => scope.toggleOption(scope.opt)"
|
||||
@click.stop
|
||||
@@ -294,7 +297,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { get, post, del, extractApiErrorDetail } from 'src/services/api'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
@@ -305,6 +308,8 @@ const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
|
||||
const traceId = `pcd-mtbolum-map-${crypto?.randomUUID?.() || String(Date.now())}`
|
||||
const prevBodyOverflow = ref(null)
|
||||
const COLUMN_FILTERS_KEY = 'pcmm.mtbolum.columnFilters.v1'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
@@ -361,6 +366,29 @@ function normalizeSearch (value) {
|
||||
|
||||
const columnFilters = reactive({})
|
||||
|
||||
function loadSavedColumnFilters () {
|
||||
try {
|
||||
const raw = localStorage.getItem(COLUMN_FILTERS_KEY)
|
||||
if (!raw) return
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!parsed || typeof parsed !== 'object') return
|
||||
|
||||
// Replace-in-place for Vue reactivity.
|
||||
for (const k of Object.keys(columnFilters)) delete columnFilters[k]
|
||||
for (const [k, v] of Object.entries(parsed)) {
|
||||
const text = String(v?.text ?? '')
|
||||
const selected = Array.isArray(v?.selected) ? v.selected.map(x => String(x ?? '')) : []
|
||||
columnFilters[k] = { text, selected }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function persistColumnFilters () {
|
||||
try {
|
||||
localStorage.setItem(COLUMN_FILTERS_KEY, JSON.stringify(columnFilters))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getColumnFilter (name) {
|
||||
if (!columnFilters[name]) {
|
||||
columnFilters[name] = { text: '', selected: [] }
|
||||
@@ -377,6 +405,7 @@ function clearColumnFilter (name) {
|
||||
const cf = getColumnFilter(name)
|
||||
cf.text = ''
|
||||
cf.selected = []
|
||||
persistColumnFilters()
|
||||
}
|
||||
|
||||
function clearAllColumnFilters () {
|
||||
@@ -384,6 +413,7 @@ function clearAllColumnFilters () {
|
||||
if (col.name === 'copy_select' || col.name === 'save_select') continue
|
||||
clearColumnFilter(col.name)
|
||||
}
|
||||
persistColumnFilters()
|
||||
}
|
||||
|
||||
function getColumnComparableValue (row, colName) {
|
||||
@@ -429,6 +459,15 @@ const rows = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
function hardResetAndRefresh () {
|
||||
// reset view state (filters + selections + dirty)
|
||||
clearAllColumnFilters()
|
||||
copySelectedKeys.value = []
|
||||
saveSelectedKeyMap.value = {}
|
||||
clearDirty()
|
||||
refreshAll()
|
||||
}
|
||||
|
||||
function markDirty (row) {
|
||||
const key = String(row?.__key || '').trim()
|
||||
if (!key) return
|
||||
@@ -532,6 +571,7 @@ function selectAllHammadde (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
const all = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : [])
|
||||
.filter(opt => !isHammaddeOptionDisabled(key, opt))
|
||||
.map(o => Number(o?.value))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
updateHammaddeSelection(key, all)
|
||||
@@ -575,6 +615,12 @@ function updateBolumSelection (key, newValue) {
|
||||
...(bolumByKey.value || {}),
|
||||
[k]: normalizeIntList(newValue)
|
||||
}
|
||||
// When part selection changes, prune hammadde selection to what is valid for the selected parts.
|
||||
const currentHam = hammaddeByKey.value?.[k] || []
|
||||
const nextHam = pruneHammaddeSelection(k, currentHam)
|
||||
if (String(nextHam) !== String(currentHam)) {
|
||||
hammaddeByKey.value = { ...(hammaddeByKey.value || {}), [k]: nextHam }
|
||||
}
|
||||
}
|
||||
|
||||
function updateHammaddeSelection (key, newValue) {
|
||||
@@ -586,6 +632,27 @@ function updateHammaddeSelection (key, newValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function isHammaddeOptionDisabled (rowKey, opt) {
|
||||
const mtIds = normalizeIntList(bolumByKey.value?.[rowKey] || [])
|
||||
if (mtIds.length === 0) return true
|
||||
const mtBolumID = Number(opt?.mtBolumID || 0)
|
||||
// If lookup doesn't specify a part, allow everywhere (mtBolumID=0).
|
||||
if (!Number.isFinite(mtBolumID) || mtBolumID <= 0) return false
|
||||
return !mtIds.includes(mtBolumID)
|
||||
}
|
||||
|
||||
function pruneHammaddeSelection (rowKey, list) {
|
||||
const values = normalizeIntList(list || [])
|
||||
if (values.length === 0) return []
|
||||
const allowed = values.filter(v => {
|
||||
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === Number(v))
|
||||
// If option is not currently in the option list (search/paging), keep it; backend will still validate on save.
|
||||
if (!opt) return true
|
||||
return !isHammaddeOptionDisabled(rowKey, opt)
|
||||
})
|
||||
return allowed
|
||||
}
|
||||
|
||||
// label resolution now handled by options' `label` field + selected-item slot (see UserDetail.vue "Piyasalar").
|
||||
async function fetchMappings () {
|
||||
loading.value = true
|
||||
@@ -736,6 +803,8 @@ async function fetchHammaddeOptions (search) {
|
||||
hammaddeOptions.value = Array.isArray(data)
|
||||
? data.map(x => ({
|
||||
value: Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim()),
|
||||
// Part ownership: spUrtOnMLHammaddeTuru.MTnUrtMTBolumID (backend exposes as mtUrtMTBolumID / mtUrtMTBolumID).
|
||||
mtBolumID: Number(x?.mtUrtMTBolumID ?? x?.MTUrtMTBolumID ?? 0),
|
||||
label: (() => {
|
||||
const v = Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim())
|
||||
const name = String(x?.sHammaddeTuruAdi || x?.label || '').trim()
|
||||
@@ -833,8 +902,6 @@ async function saveKeys (keys) {
|
||||
clearDirty()
|
||||
// after saving, clear save selection to avoid accidental re-save
|
||||
saveSelectedKeyMap.value = {}
|
||||
// after saving, also clear column filters to avoid carrying search context
|
||||
clearAllColumnFilters()
|
||||
await refreshAll()
|
||||
} catch (e) {
|
||||
const detail = await extractApiErrorDetail(e)
|
||||
@@ -845,6 +912,25 @@ async function saveKeys (keys) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// This page is designed to scroll only inside the q-table body.
|
||||
// Prevent global/body scrolling while the page is mounted.
|
||||
try {
|
||||
prevBodyOverflow.value = document?.body?.style?.overflow ?? ''
|
||||
document.body.style.overflow = 'hidden'
|
||||
} catch {}
|
||||
|
||||
loadSavedColumnFilters()
|
||||
// Persist on changes (typing/selecting) with a small debounce.
|
||||
let persistTimer = null
|
||||
watch(
|
||||
() => columnFilters,
|
||||
() => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
persistTimer = setTimeout(() => persistColumnFilters(), 250)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchMTBolumOptions(''),
|
||||
@@ -861,11 +947,33 @@ onMounted(async () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
try {
|
||||
if (prevBodyOverflow.value !== null) {
|
||||
document.body.style.overflow = prevBodyOverflow.value || ''
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pcmm-page {
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Constrain to viewport so the table body can scroll. Quasar header is 56px in this app. */
|
||||
height: calc(100vh - 56px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
/* Small safe gap under q-header */
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sticky-top {
|
||||
flex: 0 0 auto;
|
||||
z-index: 10;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.pcmm-header {
|
||||
@@ -875,10 +983,16 @@ onMounted(async () => {
|
||||
|
||||
.pcmm-top {
|
||||
flex: 0 0 auto;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pcmm-table-wrap {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pcmm-form {
|
||||
@@ -887,8 +1001,30 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.pcmm-table {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.q-table__container) {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.q-table__middle) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.q-table) {
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
/* Allow multi-select chips to wrap and grow vertically (PowerBI-like) */
|
||||
@@ -909,17 +1045,35 @@ onMounted(async () => {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.q-table__top) {
|
||||
background: #fafafa;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20; /* Increased to be above thead th */
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.q-table thead th) {
|
||||
font-size: 11px;
|
||||
padding: 3px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Keep q-table top controls visible while scrolling (like sticky headers). */
|
||||
.pcmm-table :deep(.q-table__top) {
|
||||
background: #fafafa;
|
||||
/* Keep header fixed to the top of the scrollable middle area (directly under the top bar). */
|
||||
.pcmm-table :deep(.q-table__middle thead tr th) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Make sure body rows never paint over the sticky header while scrolling. */
|
||||
.pcmm-table :deep(.q-table__middle tbody td) {
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.pcmm-header-cell {
|
||||
|
||||
Reference in New Issue
Block a user