Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-14 02:20:11 +03:00
parent 43f965a3cf
commit 7d1304b75a
8 changed files with 726 additions and 109 deletions
@@ -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 {