Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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 () => {
|
||||||
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchMTBolumOptions(''),
|
fetchMTBolumOptions(''),
|
||||||
fetchHammaddeOptions(''),
|
fetchHammaddeOptions(''),
|
||||||
fetchSheet()
|
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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user