Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-05-08 11:37:01 +03:00
parent a21e38f56f
commit a4f7d5b071
3 changed files with 248 additions and 47 deletions

View File

@@ -28,26 +28,74 @@
dense
separator="cell"
row-key="__key"
:rows="mappings"
:rows="rows"
:columns="columns"
:loading="loading"
no-data-label="Kayit bulunamadi"
:rows-per-page-options="[0]"
hide-bottom
>
<template #header-cell="props">
<q-th :props="props">
<div class="pcmm-header-cell">
<div class="pcmm-head-wrap-2">{{ props.col.label }}</div>
<q-btn
v-if="props.col.name !== 'copy_select' && props.col.name !== 'save_select'"
dense
flat
round
size="sm"
icon="filter_alt"
:color="isColumnFilterActive(props.col.name) ? 'primary' : 'grey-6'"
>
<q-menu class="pcmm-filter-menu" fit>
<div class="pcmm-filter-menu-content">
<div class="text-caption text-weight-bold q-mb-sm">{{ props.col.label }}</div>
<q-input
v-model="getColumnFilter(props.col.name).text"
dense
outlined
clearable
label="Icerir"
/>
<q-select
v-model="getColumnFilter(props.col.name).selected"
class="q-mt-sm"
dense
outlined
multiple
use-chips
use-input
emit-value
map-options
:options="getColumnDistinctOptions(props.col.name)"
label="Deger Sec"
/>
<div class="row justify-end q-gutter-sm q-mt-sm">
<q-btn dense flat color="grey-7" label="Temizle" @click="clearColumnFilter(props.col.name)" />
</div>
</div>
</q-menu>
</q-btn>
</div>
</q-th>
</template>
<template #top-left>
<div class="row items-center q-gutter-sm">
<q-btn
label="Kolon Filtreleri"
icon="filter_alt_off"
color="grey-7"
flat
:disable="loading || saving"
@click="clearAllColumnFilters"
/>
</div>
</template>
<template #top-right>
<div class="row items-center q-gutter-sm">
<q-input
v-model="filters.search"
dense
filled
clearable
debounce="250"
label="Ara (Ana/Alt)"
style="min-width: 220px"
@update:model-value="fetchSheet"
@clear="onSearchCleared"
/>
<q-btn
color="secondary"
icon="content_copy"
@@ -72,7 +120,7 @@
@click="saveAll"
/>
<div class="text-caption text-grey-7 q-pl-sm">
Satir: {{ mappings.length }} | Degisen: {{ dirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ saveSelectedCount }}
Satir: {{ rows.length }} | Degisen: {{ dirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ saveSelectedCount }}
</div>
</div>
</template>
@@ -242,7 +290,7 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { useQuasar } from 'quasar'
import { get, post, del, extractApiErrorDetail } from 'src/services/api'
import { usePermission } from 'src/composables/usePermission'
@@ -261,9 +309,7 @@ const mappings = ref([])
const copySelectedKeys = ref([]) // ordered
const saveSelectedKeyMap = ref({}) // key -> true
const filters = ref({
search: ''
})
const allCombos = ref([])
const mtBolumOptions = ref([])
const hammaddeOptions = ref([])
@@ -274,8 +320,8 @@ const bolumByKey = ref({})
const hammaddeByKey = ref({})
const columns = [
{ name: 'copy_select', label: 'Kopya', field: 'copy_select', align: 'center' },
{ name: 'save_select', label: 'Sec', field: 'save_select', align: 'center' },
{ name: 'copy_select', label: '', field: 'copy_select', align: 'center' },
{ name: 'save_select', label: '', field: 'save_select', align: 'center' },
{ name: 'urunIlkGrubu', label: 'Urun Ilk Grubu', field: 'urunIlkGrubu', align: 'left', sortable: true },
{ name: 'urunAnaGrubu', label: 'Urun Ana Grubu', field: 'urunAnaGrubu', align: 'left', sortable: true },
{ name: 'urunAltGrubu', label: 'Urun Alt Grubu', field: 'urunAltGrubu', align: 'left', sortable: true },
@@ -290,6 +336,95 @@ const canCopySelected = computed(() => copySelectedCount.value >= 2)
const saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length)
const canSaveSelected = computed(() => saveSelectedCount.value > 0)
function normalizeSearch (value) {
const s = String(value ?? '').trim()
if (!s) return ''
// Case-insensitive + Turkish/English character folding.
// Goal: treat "İ/i/I/ı" as the same, and fold Turkish letters to their ASCII counterparts.
// This is intentionally simple and predictable for server-side LIKE/search endpoints.
return s
.toLowerCase()
.replace(/ı/g, 'i')
.replace(/İ/g, 'i')
.replace(/i̇/g, 'i') // defensive: dotted i from some lowercasing paths
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ş/g, 's')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
}
const columnFilters = reactive({})
function getColumnFilter (name) {
if (!columnFilters[name]) {
columnFilters[name] = { text: '', selected: [] }
}
return columnFilters[name]
}
function isColumnFilterActive (name) {
const cf = getColumnFilter(name)
return !!String(cf.text || '').trim() || (Array.isArray(cf.selected) && cf.selected.length > 0)
}
function clearColumnFilter (name) {
const cf = getColumnFilter(name)
cf.text = ''
cf.selected = []
}
function clearAllColumnFilters () {
for (const col of columns) {
if (col.name === 'copy_select' || col.name === 'save_select') continue
clearColumnFilter(col.name)
}
}
function getColumnComparableValue (row, colName) {
if (colName === 'parcaBolumAdi') {
return Array.isArray(row?.nUrtMTBolumIDs) ? row.nUrtMTBolumIDs.join(', ') : ''
}
if (colName === 'nHammaddeTurleri') {
return Array.isArray(row?.nHammaddeTurleri) ? row.nHammaddeTurleri.join(', ') : ''
}
return String(row?.[colName] ?? '').trim()
}
function getColumnDistinctOptions (colName) {
const set = new Set()
for (const row of mappings.value) {
const val = getColumnComparableValue(row, colName)
if (val) set.add(val)
}
return Array.from(set)
.sort((a, b) => a.localeCompare(b, 'tr'))
.map(v => ({ label: v, value: v }))
}
const rows = computed(() => {
let result = mappings.value
for (const col of columns) {
if (col.name === 'copy_select' || col.name === 'save_select') continue
const cf = getColumnFilter(col.name)
const text = normalizeSearch(cf.text)
const selected = Array.isArray(cf.selected) ? cf.selected : []
if (!text && selected.length === 0) continue
result = result.filter((row) => {
const value = getColumnComparableValue(row, col.name)
const valueNorm = normalizeSearch(value)
if (text && !valueNorm.includes(text)) return false
if (selected.length > 0 && !selected.includes(value)) return false
return true
})
}
return result
})
function markDirty (row) {
const key = String(row?.__key || '').trim()
if (!key) return
@@ -472,14 +607,7 @@ async function fetchSheet () {
copySelectedKeys.value = []
saveSelectedKeyMap.value = {}
const [combos, existing] = await Promise.all([
get('/pricing/production-product-costing/options/urun-ana-alt-combos', {
trace_id: traceId,
search: String(filters.value.search || '').trim(),
limit: 5000
}),
fetchMappings()
])
const existing = await fetchMappings()
const existingByKey = new Map()
;(Array.isArray(existing) ? existing : []).forEach(x => {
@@ -490,7 +618,8 @@ async function fetchSheet () {
existingByKey.set(k, nextList)
})
const rows = (Array.isArray(combos) ? combos : []).map((c, idx) => {
const combos = Array.isArray(allCombos.value) ? allCombos.value : []
const rows = combos.map((c, idx) => {
const ilk = String(c?.urunIlkGrubu || '').trim()
const ana = String(c?.urunAnaGrubu || '').trim()
const alt = String(c?.urunAltGrubu || '').trim()
@@ -534,28 +663,42 @@ async function fetchSheet () {
}
}
function onSearchCleared () {
filters.value.search = ''
copySelectedKeys.value = []
saveSelectedKeyMap.value = {}
clearDirty()
fetchSheet()
}
// Column filters handle "search" now. Use clearAllColumnFilters() from the toolbar.
async function refreshAll () {
await Promise.all([
fetchMTBolumOptions(''),
fetchHammaddeOptions('')
fetchHammaddeOptions(''),
fetchCombos()
])
await fetchSheet()
}
async function fetchCombos () {
try {
const data = await get('/pricing/production-product-costing/options/urun-ana-alt-combos', {
trace_id: traceId,
search: '',
limit: 5000
})
allCombos.value = Array.isArray(data) ? data : []
} catch (e) {
const detail = await extractApiErrorDetail(e)
allCombos.value = []
$q.notify({ type: 'negative', message: detail || 'Urun combo listesi okunamadi' })
slog.error('production-product-costing.mtbolum-map', 'fetchCombos:error', {
trace_id: traceId,
detail: detail || String(e?.message || e || '')
})
}
}
async function fetchMTBolumOptions (search) {
mtBolumLoading.value = true
try {
const data = await get('/pricing/production-product-costing/options/mtbolum', {
trace_id: traceId,
search: search || '',
search: normalizeSearch(search),
limit: 200
})
mtBolumOptions.value = Array.isArray(data)
@@ -583,7 +726,7 @@ async function fetchHammaddeOptions (search) {
const data = await get('/pricing/production-product-costing/detail-editor-options', {
trace_id: traceId,
kind: 'hammadde',
search: search || '',
search: normalizeSearch(search),
limit: 200
})
hammaddeOptions.value = Array.isArray(data)
@@ -686,8 +829,8 @@ async function saveKeys (keys) {
clearDirty()
// after saving, clear save selection to avoid accidental re-save
saveSelectedKeyMap.value = {}
// after saving, also clear the search input so the sheet reloads unfiltered
filters.value.search = ''
// after saving, also clear column filters to avoid carrying search context
clearAllColumnFilters()
await refreshAll()
} catch (e) {
const detail = await extractApiErrorDetail(e)
@@ -698,11 +841,21 @@ async function saveKeys (keys) {
}
onMounted(async () => {
await Promise.all([
fetchMTBolumOptions(''),
fetchHammaddeOptions(''),
fetchSheet()
])
try {
await Promise.all([
fetchMTBolumOptions(''),
fetchHammaddeOptions(''),
fetchCombos()
])
await fetchSheet()
} catch (e) {
const detail = await extractApiErrorDetail(e)
$q.notify({ type: 'negative', message: detail || 'Sayfa yuklenemedi' })
slog.error('production-product-costing.mtbolum-map', 'mounted:error', {
trace_id: traceId,
detail: detail || String(e?.message || e || '')
})
}
})
</script>
@@ -743,4 +896,39 @@ onMounted(async () => {
.pcmm-table :deep(.pcmm-multi-select .q-chip) {
max-width: 100%;
}
.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;
}
.pcmm-header-cell {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 4px;
width: 100%;
}
.pcmm-head-wrap-2 {
min-width: 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
line-height: 1.15;
white-space: normal;
word-break: break-word;
}
.pcmm-filter-menu {
min-width: 300px;
}
.pcmm-filter-menu-content {
padding: 10px;
}
</style>