Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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