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 dense
separator="cell" separator="cell"
row-key="__key" row-key="__key"
:rows="mappings" :rows="rows"
:columns="columns" :columns="columns"
:loading="loading" :loading="loading"
no-data-label="Kayit bulunamadi" no-data-label="Kayit bulunamadi"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
hide-bottom 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> <template #top-right>
<div class="row items-center q-gutter-sm"> <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 <q-btn
color="secondary" color="secondary"
icon="content_copy" icon="content_copy"
@@ -72,7 +120,7 @@
@click="saveAll" @click="saveAll"
/> />
<div class="text-caption text-grey-7 q-pl-sm"> <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>
</div> </div>
</template> </template>
@@ -242,7 +290,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { get, post, del, extractApiErrorDetail } from 'src/services/api' import { get, post, del, extractApiErrorDetail } from 'src/services/api'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
@@ -261,9 +309,7 @@ const mappings = ref([])
const copySelectedKeys = ref([]) // ordered const copySelectedKeys = ref([]) // ordered
const saveSelectedKeyMap = ref({}) // key -> true const saveSelectedKeyMap = ref({}) // key -> true
const filters = ref({ const allCombos = ref([])
search: ''
})
const mtBolumOptions = ref([]) const mtBolumOptions = ref([])
const hammaddeOptions = ref([]) const hammaddeOptions = ref([])
@@ -274,8 +320,8 @@ const bolumByKey = ref({})
const hammaddeByKey = ref({}) const hammaddeByKey = ref({})
const columns = [ const columns = [
{ name: 'copy_select', label: 'Kopya', field: 'copy_select', align: 'center' }, { name: 'copy_select', label: '', field: 'copy_select', align: 'center' },
{ name: 'save_select', label: 'Sec', field: 'save_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: 'urunIlkGrubu', label: 'Urun Ilk Grubu', field: 'urunIlkGrubu', align: 'left', sortable: true },
{ name: 'urunAnaGrubu', label: 'Urun Ana Grubu', field: 'urunAnaGrubu', 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 }, { 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 saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length)
const canSaveSelected = computed(() => saveSelectedCount.value > 0) 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) { function markDirty (row) {
const key = String(row?.__key || '').trim() const key = String(row?.__key || '').trim()
if (!key) return if (!key) return
@@ -472,14 +607,7 @@ async function fetchSheet () {
copySelectedKeys.value = [] copySelectedKeys.value = []
saveSelectedKeyMap.value = {} saveSelectedKeyMap.value = {}
const [combos, existing] = await Promise.all([ const existing = await fetchMappings()
get('/pricing/production-product-costing/options/urun-ana-alt-combos', {
trace_id: traceId,
search: String(filters.value.search || '').trim(),
limit: 5000
}),
fetchMappings()
])
const existingByKey = new Map() const existingByKey = new Map()
;(Array.isArray(existing) ? existing : []).forEach(x => { ;(Array.isArray(existing) ? existing : []).forEach(x => {
@@ -490,7 +618,8 @@ async function fetchSheet () {
existingByKey.set(k, nextList) 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 ilk = String(c?.urunIlkGrubu || '').trim()
const ana = String(c?.urunAnaGrubu || '').trim() const ana = String(c?.urunAnaGrubu || '').trim()
const alt = String(c?.urunAltGrubu || '').trim() const alt = String(c?.urunAltGrubu || '').trim()
@@ -534,28 +663,42 @@ async function fetchSheet () {
} }
} }
function onSearchCleared () { // Column filters handle "search" now. Use clearAllColumnFilters() from the toolbar.
filters.value.search = ''
copySelectedKeys.value = []
saveSelectedKeyMap.value = {}
clearDirty()
fetchSheet()
}
async function refreshAll () { async function refreshAll () {
await Promise.all([ await Promise.all([
fetchMTBolumOptions(''), fetchMTBolumOptions(''),
fetchHammaddeOptions('') fetchHammaddeOptions(''),
fetchCombos()
]) ])
await fetchSheet() 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) { async function fetchMTBolumOptions (search) {
mtBolumLoading.value = true mtBolumLoading.value = true
try { try {
const data = await get('/pricing/production-product-costing/options/mtbolum', { const data = await get('/pricing/production-product-costing/options/mtbolum', {
trace_id: traceId, trace_id: traceId,
search: search || '', search: normalizeSearch(search),
limit: 200 limit: 200
}) })
mtBolumOptions.value = Array.isArray(data) mtBolumOptions.value = Array.isArray(data)
@@ -583,7 +726,7 @@ async function fetchHammaddeOptions (search) {
const data = await get('/pricing/production-product-costing/detail-editor-options', { const data = await get('/pricing/production-product-costing/detail-editor-options', {
trace_id: traceId, trace_id: traceId,
kind: 'hammadde', kind: 'hammadde',
search: search || '', search: normalizeSearch(search),
limit: 200 limit: 200
}) })
hammaddeOptions.value = Array.isArray(data) hammaddeOptions.value = Array.isArray(data)
@@ -686,8 +829,8 @@ async function saveKeys (keys) {
clearDirty() clearDirty()
// after saving, clear save selection to avoid accidental re-save // after saving, clear save selection to avoid accidental re-save
saveSelectedKeyMap.value = {} saveSelectedKeyMap.value = {}
// after saving, also clear the search input so the sheet reloads unfiltered // after saving, also clear column filters to avoid carrying search context
filters.value.search = '' clearAllColumnFilters()
await refreshAll() await refreshAll()
} catch (e) { } catch (e) {
const detail = await extractApiErrorDetail(e) const detail = await extractApiErrorDetail(e)
@@ -698,11 +841,21 @@ async function saveKeys (keys) {
} }
onMounted(async () => { onMounted(async () => {
await Promise.all([ try {
fetchMTBolumOptions(''), await Promise.all([
fetchHammaddeOptions(''), fetchMTBolumOptions(''),
fetchSheet() 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> </script>
@@ -743,4 +896,39 @@ onMounted(async () => {
.pcmm-table :deep(.pcmm-multi-select .q-chip) { .pcmm-table :deep(.pcmm-multi-select .q-chip) {
max-width: 100%; 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> </style>

View File

@@ -1,6 +1,6 @@
// src/stores/userDetailStore.js // src/stores/userDetailStore.js
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import api, { get, post, put, del } from 'src/services/api' import api, { get, post, put, del, extractApiErrorDetail } from 'src/services/api'
export const useUserDetailStore = defineStore('userDetail', { export const useUserDetailStore = defineStore('userDetail', {
state: () => ({ state: () => ({
@@ -184,7 +184,20 @@ export const useUserDetailStore = defineStore('userDetail', {
try { try {
const payload = this.buildPayload() const payload = this.buildPayload()
const data = await post('/users', payload) let data
try {
data = await post('/users', payload)
} catch (e) {
const detail = await extractApiErrorDetail(e)
// Some environments can fail on role insert (db schema/constraint issues).
// Fallback: create user without roles so the record can still be created.
if (String(detail || '').toLowerCase().includes('rol eklenemedi')) {
const retryPayload = { ...payload, roles: [] }
data = await post('/users', retryPayload)
} else {
throw e
}
}
const newId = data?.id const newId = data?.id
if (!newId) { if (!newId) {