Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-15 19:43:44 +03:00
parent dacd3aefa9
commit c9a2004248
16 changed files with 1500 additions and 601 deletions

View File

@@ -1,75 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { Quasar } from 'quasar'
import { markRaw } from 'vue'
import RootComponent from 'app/src/App.vue'
import createStore from 'app/src/stores/index'
import createRouter from 'app/src/router/index'
export default async function (createAppFn, quasarUserOptions) {
// Create the app instance.
// Here we inject into it the Quasar UI, the router & possibly the store.
const app = createAppFn(RootComponent)
app.use(Quasar, quasarUserOptions)
const store = typeof createStore === 'function'
? await createStore({})
: createStore
app.use(store)
const router = markRaw(
typeof createRouter === 'function'
? await createRouter({store})
: createRouter
)
// make router instance available in store
store.use(({ store }) => { store.router = router })
// Expose the app, the router and the store.
// Note that we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}

View File

@@ -1,158 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { createApp } from 'vue'
import '@quasar/extras/roboto-font/roboto-font.css'
import '@quasar/extras/material-icons/material-icons.css'
// We load Quasar stylesheet file
import 'quasar/dist/quasar.sass'
import 'src/css/app.css'
import createQuasarApp from './app.js'
import quasarUserOptions from './quasar-user-options.js'
const publicPath = `/`
async function start ({
app,
router
, store
}, bootFiles) {
let hasRedirected = false
const getRedirectUrl = url => {
try { return router.resolve(url).href }
catch (err) {}
return Object(url) === url
? null
: url
}
const redirect = url => {
hasRedirected = true
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
window.location.href = url
return
}
const href = getRedirectUrl(url)
// continue if we didn't fail to resolve the url
if (href !== null) {
window.location.href = href
window.location.reload()
}
}
const urlPath = window.location.href.replace(window.location.origin, '')
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
try {
await bootFiles[i]({
app,
router,
store,
ssrContext: null,
redirect,
urlPath,
publicPath
})
}
catch (err) {
if (err && err.url) {
redirect(err.url)
return
}
console.error('[Quasar] boot error:', err)
return
}
}
if (hasRedirected === true) return
app.use(router)
app.mount('#q-app')
}
createQuasarApp(createApp, quasarUserOptions)
.then(app => {
// eventually remove this when Cordova/Capacitor/Electron support becomes old
const [ method, mapFn ] = Promise.allSettled !== void 0
? [
'allSettled',
bootFiles => bootFiles.map(result => {
if (result.status === 'rejected') {
console.error('[Quasar] boot error:', result.reason)
return
}
return result.value.default
})
]
: [
'all',
bootFiles => bootFiles.map(entry => entry.default)
]
return Promise[ method ]([
import(/* webpackMode: "eager" */ 'boot/dayjs'),
import(/* webpackMode: "eager" */ 'boot/locale'),
import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard')
]).then(bootFiles => {
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
start(app, boot)
})
})

View File

@@ -1,116 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import App from 'app/src/App.vue'
let appPrefetch = typeof App.preFetch === 'function'
? App.preFetch
: (
// Class components return the component options (and the preFetch hook) inside __c property
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
? App.__c.preFetch
: false
)
function getMatchedComponents (to, router) {
const route = to
? (to.matched ? to : router.resolve(to).route)
: router.currentRoute.value
if (!route) { return [] }
const matched = route.matched.filter(m => m.components !== void 0)
if (matched.length === 0) { return [] }
return Array.prototype.concat.apply([], matched.map(m => {
return Object.keys(m.components).map(key => {
const comp = m.components[key]
return {
path: m.path,
c: comp
}
})
}))
}
export function addPreFetchHooks ({ router, store, publicPath }) {
// Add router hook for handling preFetch.
// Doing it after initial route is resolved so that we don't double-fetch
// the data that we already have. Using router.beforeResolve() so that all
// async components are resolved.
router.beforeResolve((to, from, next) => {
const
urlPath = window.location.href.replace(window.location.origin, ''),
matched = getMatchedComponents(to, router),
prevMatched = getMatchedComponents(from, router)
let diffed = false
const preFetchList = matched
.filter((m, i) => {
return diffed || (diffed = (
!prevMatched[i] ||
prevMatched[i].c !== m.c ||
m.path.indexOf('/:') > -1 // does it has params?
))
})
.filter(m => m.c !== void 0 && (
typeof m.c.preFetch === 'function'
// Class components return the component options (and the preFetch hook) inside __c property
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
))
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
if (appPrefetch !== false) {
preFetchList.unshift(appPrefetch)
appPrefetch = false
}
if (preFetchList.length === 0) {
return next()
}
let hasRedirected = false
const redirect = url => {
hasRedirected = true
next(url)
}
const proceed = () => {
if (hasRedirected === false) { next() }
}
preFetchList.reduce(
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
store,
currentRoute: to,
previousRoute: from,
redirect,
urlPath,
publicPath
})),
Promise.resolve()
)
.then(proceed)
.catch(e => {
console.error(e)
proceed()
})
})
}

View File

@@ -1,23 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import lang from 'quasar/lang/tr.js'
import {Loading,Dialog,Notify} from 'quasar'
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }

View File

@@ -1,6 +1,6 @@
<template>
<q-page class="q-pa-md">
<div class="sticky-stack">
<q-page class="pcdq-page q-pa-md">
<div class="pcdq-top sticky-top">
<div class="save-toolbar">
<div class="row items-center justify-between q-col-gutter-sm">
<div class="col-auto text-weight-bold">Maliyet Varsayilan Miktarlar</div>
@@ -52,17 +52,20 @@
{{ error }}
</q-banner>
<q-table
flat
bordered
dense
row-key="nHammaddeTuruNo"
:rows="rows"
:columns="columns"
:loading="loading"
:rows-per-page-options="[0]"
hide-bottom
>
<div class="pcdq-table-wrap">
<q-table
class="pcdq-table"
flat
bordered
dense
row-key="nHammaddeTuruNo"
:rows="rows"
:columns="columns"
:loading="loading"
:rows-per-page-options="[0]"
hide-bottom
sticky-header
>
<template #body-cell-actions="props">
<q-td :props="props">
<q-btn
@@ -89,15 +92,8 @@
/>
</q-td>
</template>
<template #body-cell-bAktif="props">
<q-td :props="props">
<q-toggle
:model-value="Boolean(props.row.bAktif)"
@update:model-value="val => onEditActive(props.row, val)"
/>
</q-td>
</template>
</q-table>
</q-table>
</div>
</q-page>
</template>
@@ -120,7 +116,6 @@ const columns = [
{ name: 'sAciklama', label: 'Aciklama', field: 'sAciklama', align: 'left', sortable: true },
{ name: 'lDefaultMiktar', label: 'Varsayilan Miktar', field: 'lDefaultMiktar', align: 'right', sortable: true },
{ name: 'dteCalcTarihi', label: 'Hesap Tarihi', field: 'dteCalcTarihi', align: 'left', sortable: true },
{ name: 'bAktif', label: 'Aktif', field: 'bAktif', align: 'center', sortable: true },
{ name: 'actions', label: '', field: '__actions', align: 'right', sortable: false }
]
@@ -145,13 +140,6 @@ function onEditQty (row, val) {
row.lDefaultMiktar = qty
}
function onEditActive (row, val) {
const no = Number(row?.nHammaddeTuruNo || 0)
if (!(no > 0)) return
store.setDraft(no, { bAktif: Boolean(val) })
row.bAktif = Boolean(val)
}
async function onCalcAvg (row) {
const no = Number(row?.nHammaddeTuruNo || 0)
if (!(no > 0)) return
@@ -204,7 +192,7 @@ function ensureBeforeUnloadGuard (enabled) {
}
}
watch(hasUnsavedChanges, (v) => ensureBeforeUnloadGuard(Boolean(v)))
watch(() => Boolean(hasUnsavedChanges.value), (v) => ensureBeforeUnloadGuard(Boolean(v)))
onBeforeRouteLeave((to, from, next) => {
if (!hasUnsavedChanges.value) return next()
@@ -225,11 +213,62 @@ onUnmounted(() => {
</script>
<style scoped>
.sticky-stack {
.pcdq-page {
background: #fafafa;
display: flex;
flex-direction: column;
height: calc(100vh - 56px);
min-height: 0;
overflow: hidden;
padding-top: 10px;
}
.sticky-top {
flex: 0 0 auto;
z-index: 10;
background: #fafafa;
}
.pcdq-top {
flex: 0 0 auto;
}
.pcdq-table-wrap {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.pcdq-table {
width: 100%;
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.pcdq-table :deep(.q-table__container) {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.pcdq-table :deep(.q-table__middle) {
flex: 1 1 auto;
min-height: 0;
overflow: auto !important;
}
.pcdq-table :deep(.q-table thead th) {
background: #f0f0f0;
}
.pcdq-table :deep(.q-table__middle thead tr th) {
position: sticky;
top: var(--header-h);
z-index: 100;
background: #fff;
padding-top: 8px;
top: 0;
z-index: 10;
}
</style>

View File

@@ -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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
}
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;

View File

@@ -87,6 +87,43 @@
<template #top-left>
<div class="row items-center q-gutter-sm">
<q-select
v-model="commonHammaddeSelected"
dense
filled
multiple
use-chips
clearable
emit-value
map-options
option-label="label"
option-value="value"
:options="commonHammaddeOptions"
:disable="loading || saving || commonHammaddeOptions.length === 0"
class="pcmm-multi-select"
style="min-width: 420px"
>
<template #selected-item="scope">
<q-chip
class="q-mr-xs"
dense
removable
@remove="scope.removeAtIndex(scope.index)"
>
{{ scope.opt.label }}
</q-chip>
</template>
<template #hint>
Ekrandaki satirlarda bulunan hammadde turleri (distinct)
</template>
</q-select>
<q-btn
color="negative"
icon="delete_sweep"
label="Butun Parcalardan Kaldir"
:disable="loading || saving || (commonHammaddeSelected || []).length === 0"
@click="confirmRemoveCommonHammadde"
/>
<q-btn
label="Kolon Filtreleri"
icon="filter_alt_off"
@@ -327,6 +364,8 @@ const hammaddeLoading = ref(false)
const bolumByKey = ref({})
const hammaddeByKey = ref({})
const commonHammaddeSelected = ref([])
const hammaddeLabelCache = ref({}) // no -> "NO - ACIKLAMA"
const columns = [
{ name: 'copy_select', label: '', field: 'copy_select', align: 'center' },
@@ -345,6 +384,22 @@ const canCopySelected = computed(() => copySelectedCount.value >= 2)
const saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length)
const canSaveSelected = computed(() => saveSelectedCount.value > 0)
function findHammaddeLabel (no) {
const n = Number(no || 0)
if (!(n > 0)) return ''
const cached = String(hammaddeLabelCache.value?.[String(n)] || '').trim()
if (cached) return cached
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === n)
const raw = String(opt?.label || '').trim()
if (raw) {
// Ensure "NO - ACIKLAMA" format. Some option sources may only provide the description.
if (/^\d+\s*-\s*/.test(raw)) return raw
if (/^\d+\s*$/.test(raw)) return `${n}`
return `${n} - ${raw}`
}
return `${n}`
}
function normalizeSearch (value) {
const s = String(value ?? '').trim()
if (!s) return ''
@@ -459,6 +514,33 @@ const rows = computed(() => {
return result
})
const commonHammaddeOptions = computed(() => {
const listRows = Array.isArray(rows.value) ? rows.value : []
if (listRows.length === 0) return []
const keys = listRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
if (keys.length === 0) return []
// DISTINCT across visible rows (union).
const set = new Set()
for (const k of keys) {
const ham = normalizeIntList(hammaddeByKey.value?.[k] || [])
ham.forEach(n => { if (n > 0) set.add(n) })
}
return Array.from(set)
.sort((a, b) => a - b)
.map(n => ({ value: n, label: findHammaddeLabel(n) }))
})
watch(commonHammaddeOptions, (opts) => {
// Ensure labels for large/rare nos that won't be in the first 200 option fetch.
try {
const nos = (Array.isArray(opts) ? opts : []).map(o => Number(o?.value || 0)).filter(n => n > 0)
ensureHammaddeLabelsForNos(nos)
} catch {
// ignore
}
}, { immediate: true })
function hardResetAndRefresh () {
// reset view state (filters + selections + dirty)
clearAllColumnFilters()
@@ -626,9 +708,14 @@ function updateBolumSelection (key, newValue) {
function updateHammaddeSelection (key, newValue) {
const k = String(key || '').trim()
if (!k) return
hammaddeByKey.value = {
...(hammaddeByKey.value || {}),
[k]: normalizeIntList(newValue)
const nextList = normalizeIntList(newValue)
hammaddeByKey.value = { ...(hammaddeByKey.value || {}), [k]: nextList }
// Keep table field in sync for column filtering/sorting/export behavior.
const idx = (Array.isArray(mappings.value) ? mappings.value : []).findIndex(r => String(r?.__key || '') === k)
if (idx >= 0) {
const copy = [...mappings.value]
copy[idx] = { ...(copy[idx] || {}), nHammaddeTurleri: [...nextList] }
mappings.value = copy
}
}
@@ -653,6 +740,62 @@ function pruneHammaddeSelection (rowKey, list) {
return allowed
}
function confirmRemoveCommonHammadde () {
const selected = normalizeIntList(commonHammaddeSelected.value || [])
if (selected.length === 0) return
const listRows = Array.isArray(rows.value) ? rows.value : []
const keys = listRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
if (keys.length === 0) return
// Affected "parcalar": only rows that currently contain at least one selected hammadde.
const affectedRows = listRows.filter(r => {
const k = String(r?.__key || '').trim()
if (!k) return false
const current = normalizeIntList(hammaddeByKey.value?.[k] || [])
return current.some(v => selected.includes(v))
})
if (affectedRows.length === 0) {
$q.notify({ type: 'info', message: 'Secilen hammadde turleri bu ekrandaki satirlarda bulunmuyor.', position: 'top-right' })
return
}
const affected = affectedRows
.map(r => `${String(r?.urunIlkGrubu || '').trim() || '-'} | ${String(r?.urunAnaGrubu || '').trim() || '-'} | ${String(r?.urunAltGrubu || '').trim() || '-'}`)
.filter(Boolean)
const removedLabels = selected.map(n => findHammaddeLabel(n))
const htmlList = affected.slice(0, 30).map(x => `<div>${x}</div>`).join('')
const more = affected.length > 30 ? `<div class="text-grey-7 q-mt-sm">(+${affected.length - 30} satir daha)</div>` : ''
$q.dialog({
title: 'Butun Parcalardan Kaldir',
message: `
<div class="q-mb-sm"><b>Kaldirilacak hammadde turleri:</b></div>
<div class="q-mb-sm">${removedLabels.map(x => `<div>${x}</div>`).join('')}</div>
<div class="q-mb-sm"><b>Etkilenecek parcalar:</b></div>
<div style="max-height: 240px; overflow:auto; border: 1px solid #eee; padding: 8px;">${htmlList}${more}</div>
<div class="q-mt-sm text-grey-7">Onaylarsaniz secilen hammadde turleri sadece bu satirlardan kaldirilacak ve Degisenleri Kaydet ile kaydedilebilecek.</div>
`,
html: true,
cancel: true,
persistent: true,
ok: { label: 'Onayla', color: 'negative' },
cancelLabel: 'Geri Don'
}).onOk(() => {
const affectedKeys = affectedRows.map(r => String(r?.__key || '').trim()).filter(Boolean)
for (const k of affectedKeys) {
const current = normalizeIntList(hammaddeByKey.value?.[k] || [])
const next = current.filter(v => !selected.includes(v))
if (String(next) === String(current)) continue
updateHammaddeSelection(k, next)
const row = listRows.find(r => String(r?.__key || '') === k) || (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === k)
if (row) markDirty(row)
}
commonHammaddeSelected.value = []
$q.notify({ type: 'positive', message: 'Secilen hammadde turleri etkilenen satirlardan kaldirildi (taslak).', position: 'top-right' })
})
}
// label resolution now handled by options' `label` field + selected-item slot (see UserDetail.vue "Piyasalar").
async function fetchMappings () {
loading.value = true
@@ -816,11 +959,55 @@ async function fetchHammaddeOptions (search) {
.filter(x => Number.isFinite(x.value) && x.value > 0)
.sort((a, b) => a.value - b.value)
: []
// Prime cache with currently loaded options.
try {
const next = { ...(hammaddeLabelCache.value || {}) }
;(Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).forEach(opt => {
const no = Number(opt?.value || 0)
const label = String(opt?.label || '').trim()
if (no > 0 && label) next[String(no)] = label
})
hammaddeLabelCache.value = next
} catch {
// ignore
}
} finally {
hammaddeLoading.value = false
}
}
async function ensureHammaddeLabelsForNos (nos) {
const list = normalizeIntList(nos || [])
if (list.length === 0) return
const missing = list.filter(n => {
const cached = String(hammaddeLabelCache.value?.[String(n)] || '').trim()
if (cached) return false
const opt = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : []).find(o => Number(o?.value) === n)
return !String(opt?.label || '').trim()
})
if (missing.length === 0) return
try {
const data = await post('/pricing/production-product-costing/options/hammadde-by-nos', {
nHammaddeTuruNos: missing
}, { trace_id: traceId })
const rows = Array.isArray(data) ? data : []
const next = { ...(hammaddeLabelCache.value || {}) }
rows.forEach(r => {
const no = Number(r?.nHammaddeTuruNo || 0)
const name = String(r?.sAciklama || '').trim()
if (no > 0 && name) next[String(no)] = `${no} - ${name}`
})
hammaddeLabelCache.value = next
} catch (e) {
// Non-blocking: fallback will show only the number
slog.error('production-product-costing.mtbolum-map', 'hammadde-by-nos:error', {
trace_id: traceId,
detail: await extractApiErrorDetail(e)
})
}
}
function onFilterMTBolum (val, update) {
update(async () => {
await fetchMTBolumOptions(val)

View File

@@ -18,7 +18,7 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
const saving = ref(false)
const error = ref('')
// draftByNo: { [nHammaddeTuruNo]: { lDefaultMiktar, bAktif } }
// draftByNo: { [nHammaddeTuruNo]: { lDefaultMiktar } }
const draftByNo = ref({})
const persistTimer = ref(0)
@@ -94,12 +94,11 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
return {
...row,
lDefaultMiktar: typeof draft.lDefaultMiktar === 'number' ? draft.lDefaultMiktar : row?.lDefaultMiktar,
bAktif: typeof draft.bAktif === 'boolean' ? draft.bAktif : row?.bAktif,
__dirty: true
}
}
async function fetch ({ search = '', onlyActive = true, limit = 2000 } = {}) {
async function fetch ({ search = '', limit = 2000 } = {}) {
loading.value = true
error.value = ''
try {
@@ -136,8 +135,7 @@ export const useProductionProductCostingDefaultQtyStore = defineStore('productio
const patch = draftByNo.value?.[String(no)] || {}
return {
nHammaddeTuruNo: Number(no),
lDefaultMiktar: Number(patch.lDefaultMiktar || 0),
bAktif: typeof patch.bAktif === 'boolean' ? patch.bAktif : undefined
lDefaultMiktar: Number(patch.lDefaultMiktar || 0)
}
}).filter(it => it.nHammaddeTuruNo > 0 && it.lDefaultMiktar > 0)