Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user