Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -86,6 +86,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-banner
|
||||
v-if="csvImportStatus"
|
||||
dense
|
||||
class="q-mb-xs"
|
||||
:class="csvImportStatus.type === 'warning' ? 'bg-amber-2 text-amber-10' : 'bg-green-1 text-green-10'"
|
||||
>
|
||||
{{ csvImportStatus.message }}
|
||||
</q-banner>
|
||||
|
||||
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
||||
<q-table
|
||||
flat
|
||||
@@ -272,6 +281,34 @@
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-toggle
|
||||
v-else-if="col.name === 'calc_enabled'"
|
||||
v-model="props.row.calc_enabled"
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-toggle
|
||||
v-else-if="col.name === 'publish_postgres'"
|
||||
v-model="props.row.publish_postgres"
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-toggle
|
||||
v-else-if="col.name === 'publish_nebim'"
|
||||
v-model="props.row.publish_nebim"
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-select
|
||||
v-else-if="retailModeFields.has(col.name)"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:options="retailModeOptions.map(value => ({ label: value, value }))"
|
||||
:model-value="props.row[col.field]"
|
||||
@update:model-value="(value) => updateRetailMode(props.row, col.field, value)"
|
||||
/>
|
||||
<input
|
||||
v-else-if="numericFields.has(col.name)"
|
||||
class="native-cell-input text-right"
|
||||
@@ -287,6 +324,8 @@
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
|
||||
<q-inner-loading :showing="saving" label="Kaydediliyor..." />
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
@@ -306,6 +345,7 @@ const fileInputRef = ref(null)
|
||||
const selectedKeyMap = ref({})
|
||||
const copySelectedKeys = ref([])
|
||||
const tablePagination = ref({ rowsPerPage: 0, sortBy: 'urun_ilk_grubu', descending: false })
|
||||
const csvImportStatus = ref(null) // { type: 'positive'|'warning', message: string, at: string }
|
||||
let emptyRetryTimer = null
|
||||
|
||||
const numericFields = new Set([
|
||||
@@ -313,6 +353,8 @@ const numericFields = new Set([
|
||||
'usd_base', 'usd1', 'usd2', 'usd3', 'usd4', 'usd5', 'usd6', 'usd_wholesale_step', 'usd_retail_step',
|
||||
'eur_base', 'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6', 'eur_wholesale_step', 'eur_retail_step'
|
||||
])
|
||||
const retailModeFields = new Set(['try_retail_mode', 'usd_retail_mode', 'eur_retail_mode'])
|
||||
const retailModeOptions = ['STEP', 'END_99', 'END_49', 'BAND_99', 'BAND_49']
|
||||
|
||||
const importKeyFieldLabels = [
|
||||
['askili_yan', 'ASKILI YAN'],
|
||||
@@ -328,7 +370,12 @@ const importKeyFieldLabels = [
|
||||
|
||||
const importFieldMap = {
|
||||
AKTIF: 'is_active',
|
||||
'HESAP AKTIF': 'calc_enabled',
|
||||
'PG YAYIN': 'publish_postgres',
|
||||
'NEBIM YAYIN': 'publish_nebim',
|
||||
'TRY TOPTAN YUVARLAMA': 'try_wholesale_step',
|
||||
'TRY PERAKENDE MODU': 'try_retail_mode',
|
||||
'TRY PERAKENDE DEGERI': 'try_retail_step',
|
||||
'TRY PERAKENDE YUVARLAMA': 'try_retail_step',
|
||||
'TRY YUVARLAMA': 'try_wholesale_step',
|
||||
'TRY TABAN': 'try_base',
|
||||
@@ -339,6 +386,8 @@ const importFieldMap = {
|
||||
'TRY 5': 'try5',
|
||||
'TRY 6': 'try6',
|
||||
'USD TOPTAN YUVARLAMA': 'usd_wholesale_step',
|
||||
'USD PERAKENDE MODU': 'usd_retail_mode',
|
||||
'USD PERAKENDE DEGERI': 'usd_retail_step',
|
||||
'USD PERAKENDE YUVARLAMA': 'usd_retail_step',
|
||||
'USD YUVARLAMA': 'usd_wholesale_step',
|
||||
'USD TABAN': 'usd_base',
|
||||
@@ -349,6 +398,8 @@ const importFieldMap = {
|
||||
'USD 5': 'usd5',
|
||||
'USD 6': 'usd6',
|
||||
'EUR TOPTAN YUVARLAMA': 'eur_wholesale_step',
|
||||
'EUR PERAKENDE MODU': 'eur_retail_mode',
|
||||
'EUR PERAKENDE DEGERI': 'eur_retail_step',
|
||||
'EUR PERAKENDE YUVARLAMA': 'eur_retail_step',
|
||||
'EUR YUVARLAMA': 'eur_wholesale_step',
|
||||
'EUR TABAN': 'eur_base',
|
||||
@@ -362,7 +413,8 @@ const importFieldMap = {
|
||||
|
||||
const multiFilterFields = [
|
||||
'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
|
||||
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group',
|
||||
'try_retail_mode', 'usd_retail_mode', 'eur_retail_mode'
|
||||
]
|
||||
const multiSelectFilterFieldSet = new Set(multiFilterFields)
|
||||
const numberRangeFilterFieldSet = new Set(numericFields)
|
||||
@@ -387,56 +439,64 @@ function col (name, label, field, width, extra = {}) {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
col('copy_select', 'KOPYA', 'copy_select', 86, { sortable: false, classes: 'copy-selection-col', headerClasses: 'copy-selection-col' }),
|
||||
col('select', 'KAYDET', 'select', 72, { sortable: false, classes: 'save-selection-col', headerClasses: 'save-selection-col' }),
|
||||
col('has_rule', 'DURUM', 'has_rule', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('is_active', 'AKTIF', 'is_active', 48, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('askili_yan', 'ASKILI YAN', 'askili_yan', 86, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 92, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ilk_grubu', 'URUN ILK GRUBU', 'urun_ilk_grubu', 100, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ana_grubu', 'URUN ANA GRUBU', 'urun_ana_grubu', 110, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_alt_grubu', 'URUN ALT GRUBU', 'urun_alt_grubu', 110, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('marka', 'MARKA', 'marka', 100, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_code', 'BRAND CODE', 'brand_code', 78, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_group', 'MARKA GRUBU', 'brand_group', 88, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('copy_select', 'KOPYA', 'copy_select', 68, { sortable: false, classes: 'copy-selection-col', headerClasses: 'copy-selection-col' }),
|
||||
col('select', 'KAYDET', 'select', 58, { sortable: false, classes: 'save-selection-col', headerClasses: 'save-selection-col' }),
|
||||
col('has_rule', 'DURUM', 'has_rule', 54, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('is_active', 'AKTIF', 'is_active', 42, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('askili_yan', 'ASKILI YAN', 'askili_yan', 72, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 76, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ilk_grubu', 'URUN ILK GRUBU', 'urun_ilk_grubu', 84, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ana_grubu', 'URUN ANA GRUBU', 'urun_ana_grubu', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_alt_grubu', 'URUN ALT GRUBU', 'urun_alt_grubu', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 72, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('marka', 'MARKA', 'marka', 80, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_code', 'BRAND CODE', 'brand_code', 68, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_group', 'MARKA GRUBU', 'brand_group', 76, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('anchor_mode', 'ANCHOR MODE', 'anchor_mode', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('calc_enabled', 'HESAP AKTIF', 'calc_enabled', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('publish_postgres', 'PG YAYIN', 'publish_postgres', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('publish_nebim', 'NEBIM YAYIN', 'publish_nebim', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
|
||||
col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 92, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_retail_step', 'TRY PERAKENDE YUVARLAMA', 'try_retail_step', 98, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_base', 'TRY TABAN', 'try_base', 70, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 76, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_retail_mode', 'TRY PERAKENDE MODU', 'try_retail_mode', 76, { classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_retail_step', 'TRY PERAKENDE DEGERI', 'try_retail_step', 78, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_base', 'TRY TABAN', 'try_base', 58, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
|
||||
col('usd_wholesale_step', 'USD TOPTAN YUVARLAMA', 'usd_wholesale_step', 92, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_retail_step', 'USD PERAKENDE YUVARLAMA', 'usd_retail_step', 98, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_base', 'USD TABAN', 'usd_base', 70, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_wholesale_step', 'USD TOPTAN YUVARLAMA', 'usd_wholesale_step', 76, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_retail_mode', 'USD PERAKENDE MODU', 'usd_retail_mode', 76, { classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_retail_step', 'USD PERAKENDE DEGERI', 'usd_retail_step', 78, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_base', 'USD TABAN', 'usd_base', 58, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
|
||||
col('eur_wholesale_step', 'EUR TOPTAN YUVARLAMA', 'eur_wholesale_step', 92, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_retail_step', 'EUR PERAKENDE YUVARLAMA', 'eur_retail_step', 98, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_base', 'EUR TABAN', 'eur_base', 70, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' })
|
||||
col('eur_wholesale_step', 'EUR TOPTAN YUVARLAMA', 'eur_wholesale_step', 76, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_retail_mode', 'EUR PERAKENDE MODU', 'eur_retail_mode', 76, { classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_retail_step', 'EUR PERAKENDE DEGERI', 'eur_retail_step', 78, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_base', 'EUR TABAN', 'eur_base', 58, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' })
|
||||
]
|
||||
|
||||
const stickyColumnNames = [
|
||||
'copy_select', 'select', 'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu',
|
||||
'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||
'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group',
|
||||
'anchor_mode', 'calc_enabled', 'publish_postgres', 'publish_nebim'
|
||||
]
|
||||
const stickyBoundaryColumnName = 'brand_group'
|
||||
const stickyBoundaryColumnName = 'publish_nebim'
|
||||
const stickyColumnNameSet = new Set(stickyColumnNames)
|
||||
|
||||
const stickyLeftMap = computed(() => {
|
||||
@@ -466,6 +526,7 @@ const tableStyle = computed(() => ({
|
||||
function filterDisplayValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
|
||||
if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
|
||||
if (retailModeFields.has(field)) return String(row?.[field] || 'STEP').trim() || 'STEP'
|
||||
return String(row?.[field] ?? '').trim()
|
||||
}
|
||||
|
||||
@@ -562,6 +623,10 @@ function normalizeWorksheetRow (source) {
|
||||
_row_key: String(source?.scope_key || source?.pricing_parameter_id || ''),
|
||||
has_rule: Boolean(source?.has_rule),
|
||||
id: String(rule?.id || ''),
|
||||
anchor_mode: String(rule?.anchor_mode || 'USD'),
|
||||
calc_enabled: rule?.calc_enabled !== false,
|
||||
publish_postgres: rule?.publish_postgres !== false,
|
||||
publish_nebim: rule?.publish_nebim !== false,
|
||||
is_active: rule?.is_active !== false,
|
||||
askili_yan: String(source?.askili_yan || ''),
|
||||
kategori: String(source?.kategori || ''),
|
||||
@@ -572,6 +637,9 @@ function normalizeWorksheetRow (source) {
|
||||
marka: String(source?.marka || ''),
|
||||
brand_code: String(source?.brand_code || ''),
|
||||
brand_group: String(source?.brand_group || ''),
|
||||
try_retail_mode: String(rule?.try_retail_mode || 'STEP'),
|
||||
usd_retail_mode: String(rule?.usd_retail_mode || 'STEP'),
|
||||
eur_retail_mode: String(rule?.eur_retail_mode || 'STEP'),
|
||||
_dirty: false
|
||||
}
|
||||
for (const key of numericFields) {
|
||||
@@ -607,6 +675,11 @@ function isRowSelected (row) {
|
||||
return !!selectedKeyMap.value?.[row._row_key]
|
||||
}
|
||||
|
||||
function clearSelections () {
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
}
|
||||
|
||||
function isCopySelected (row) {
|
||||
return copySelectedKeySet.value.has(row._row_key)
|
||||
}
|
||||
@@ -668,9 +741,17 @@ function updateNumber (row, field, value) {
|
||||
markDirty(row)
|
||||
}
|
||||
|
||||
function updateRetailMode (row, field, value) {
|
||||
row[field] = retailModeOptions.includes(String(value || '').trim()) ? String(value).trim() : 'STEP'
|
||||
markDirty(row)
|
||||
}
|
||||
|
||||
function exportSortValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 1 : 0
|
||||
if (field === 'is_active') return row?.is_active ? 1 : 0
|
||||
if (field === 'calc_enabled') return row?.calc_enabled ? 1 : 0
|
||||
if (field === 'publish_postgres') return row?.publish_postgres ? 1 : 0
|
||||
if (field === 'publish_nebim') return row?.publish_nebim ? 1 : 0
|
||||
if (numericFields.has(field)) return finiteNumber(row?.[field], 0)
|
||||
return String(row?.[field] ?? '')
|
||||
}
|
||||
@@ -678,6 +759,16 @@ function exportSortValue (row, field) {
|
||||
function exportCellValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
|
||||
if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
|
||||
if (field === 'calc_enabled') return row?.calc_enabled ? 'Aktif' : 'Pasif'
|
||||
if (field === 'publish_postgres') return row?.publish_postgres ? 'Evet' : 'Hayir'
|
||||
if (field === 'publish_nebim') return row?.publish_nebim ? 'Evet' : 'Hayir'
|
||||
// Excel often coerces numeric-looking codes/names; wrap to keep as text when opened/edited in Excel.
|
||||
if (field === 'brand_code' || field === 'marka') {
|
||||
const text = String(row?.[field] ?? '').trim()
|
||||
if (!text) return ''
|
||||
return `="${text.replaceAll('"', '""')}"`
|
||||
}
|
||||
if (retailModeFields.has(field)) return String(row?.[field] || 'STEP').trim() || 'STEP'
|
||||
if (numericFields.has(field)) {
|
||||
const value = row?.[field]
|
||||
if (value === '' || value === null || value === undefined) return '0'
|
||||
@@ -858,10 +949,17 @@ async function onImportFileChange (event) {
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
console.info('[pricing-rules][ui] csv-import:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
name: file?.name || '',
|
||||
size: file?.size || 0
|
||||
})
|
||||
const text = await file.text()
|
||||
const matrix = parseCsvRows(text).filter(row => row.some(cell => String(cell || '').trim() !== ''))
|
||||
if (matrix.length < 2) {
|
||||
Notify.create({ type: 'negative', message: 'CSV bos veya gecersiz' })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: 'CSV bos veya gecersiz.' }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -883,6 +981,7 @@ async function onImportFileChange (event) {
|
||||
let matched = 0
|
||||
let updated = 0
|
||||
let skipped = 0
|
||||
const updatedRowKeys = []
|
||||
|
||||
for (let i = 1; i < matrix.length; i++) {
|
||||
const csvRow = matrix[i]
|
||||
@@ -912,7 +1011,22 @@ async function onImportFileChange (event) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (field === 'calc_enabled' || field === 'publish_postgres' || field === 'publish_nebim') {
|
||||
const next = parseImportedBoolean(rawValue)
|
||||
if (next !== null && Boolean(target[field]) !== next) {
|
||||
target[field] = next
|
||||
rowChanged = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (retailModeFields.has(field)) {
|
||||
const next = retailModeOptions.includes(normalizeImportText(rawValue)) ? normalizeImportText(rawValue) : 'STEP'
|
||||
if (String(target[field] || 'STEP') !== next) {
|
||||
target[field] = next
|
||||
rowChanged = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
const next = parseImportedNumber(rawValue)
|
||||
if (Number(target[field] ?? 0) !== next) {
|
||||
target[field] = next
|
||||
@@ -922,21 +1036,48 @@ async function onImportFileChange (event) {
|
||||
|
||||
if (rowChanged) {
|
||||
markDirty(target)
|
||||
updatedRowKeys.push(String(target._row_key || '').trim())
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
if (matched === 0) {
|
||||
Notify.create({ type: 'warning', message: 'CSV icindeki satirlar ekrandaki kayitlarla eslesmedi' })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: 'CSV icindeki satirlar ekrandaki kayitlarla eslesmedi.' }
|
||||
return
|
||||
}
|
||||
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: `CSV yuklendi. Islenen: ${matrix.length - 1}, eslesen: ${matched}, guncellenen: ${updated}, atlanan: ${skipped}`
|
||||
// Ensure: CSV'den degisen satirlar hem dirty hem de "Kaydet" secimi (checkbox) olarak isaretlensin.
|
||||
// Bazı render edge-case'lerinde sadece sayac artip checkbox guncellenmiyor gibi gorunebiliyor;
|
||||
// burada selection map'i explicit guncelleyip senkronu garanti ediyoruz.
|
||||
if (updatedRowKeys.length > 0) {
|
||||
const next = { ...(selectedKeyMap.value || {}) }
|
||||
for (const key of updatedRowKeys) {
|
||||
if (!key) continue
|
||||
next[key] = true
|
||||
}
|
||||
selectedKeyMap.value = next
|
||||
}
|
||||
|
||||
const summary = `CSV yuklendi. Islenen: ${matrix.length - 1}, eslesen: ${matched}, guncellenen: ${updated}, atlanan: ${skipped}`
|
||||
if (updated === 0) {
|
||||
Notify.create({ type: 'warning', message: summary })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: summary }
|
||||
} else {
|
||||
Notify.create({ type: 'positive', message: summary })
|
||||
csvImportStatus.value = { type: 'positive', at: new Date().toISOString(), message: summary }
|
||||
}
|
||||
|
||||
console.info('[pricing-rules][ui] csv-import:done', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
processed: matrix.length - 1,
|
||||
matched,
|
||||
updated,
|
||||
skipped
|
||||
})
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: err?.message || 'CSV okunamadi' }
|
||||
} finally {
|
||||
if (input) input.value = ''
|
||||
}
|
||||
@@ -953,6 +1094,12 @@ function copySelectedToSelected () {
|
||||
const target = rows.value.find(row => row._row_key === keys[i])
|
||||
if (!target) continue
|
||||
target.is_active = Boolean(source.is_active)
|
||||
target.calc_enabled = Boolean(source.calc_enabled)
|
||||
target.publish_postgres = Boolean(source.publish_postgres)
|
||||
target.publish_nebim = Boolean(source.publish_nebim)
|
||||
target.try_retail_mode = String(source.try_retail_mode || 'STEP')
|
||||
target.usd_retail_mode = String(source.usd_retail_mode || 'STEP')
|
||||
target.eur_retail_mode = String(source.eur_retail_mode || 'STEP')
|
||||
for (const field of numericFields) {
|
||||
target[field] = source[field]
|
||||
}
|
||||
@@ -1056,8 +1203,7 @@ async function refreshRows () {
|
||||
}
|
||||
|
||||
clearAllFilters()
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
clearSelections()
|
||||
await loadRows()
|
||||
}
|
||||
|
||||
@@ -1067,6 +1213,7 @@ async function loadRows () {
|
||||
emptyRetryTimer = null
|
||||
}
|
||||
loading.value = true
|
||||
let ok = false
|
||||
try {
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
@@ -1074,16 +1221,17 @@ async function loadRows () {
|
||||
timeout: 180000
|
||||
})
|
||||
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
clearSelections()
|
||||
if (rows.value.length === 0) {
|
||||
emptyRetryTimer = setTimeout(loadRows, 10000)
|
||||
}
|
||||
ok = true
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kural kombinasyonlari alinamadi' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
async function saveSelected () {
|
||||
@@ -1091,27 +1239,124 @@ async function saveSelected () {
|
||||
if (dirty.length === 0) return
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
items: dirty.map(row => {
|
||||
const item = {
|
||||
id: row.id,
|
||||
pricing_parameter_id: row.pricing_parameter_id,
|
||||
is_active: Boolean(row.is_active)
|
||||
}
|
||||
for (const key of numericFields) item[key] = finiteNumber(row[key], 0)
|
||||
return item
|
||||
})
|
||||
}
|
||||
await api.request({
|
||||
method: 'POST',
|
||||
url: '/pricing/pricing-rules/bulk-save',
|
||||
data: payload,
|
||||
timeout: 180000
|
||||
const startedAt = Date.now()
|
||||
console.info('[pricing-rules][ui] saveSelected:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
dirty_count: dirty.length
|
||||
})
|
||||
|
||||
const buildPayload = (list) => {
|
||||
return {
|
||||
items: list.map(row => {
|
||||
const item = {
|
||||
id: row.id,
|
||||
pricing_parameter_id: row.pricing_parameter_id,
|
||||
calc_enabled: Boolean(row.calc_enabled),
|
||||
publish_postgres: Boolean(row.publish_postgres),
|
||||
publish_nebim: Boolean(row.publish_nebim),
|
||||
is_active: Boolean(row.is_active),
|
||||
try_retail_mode: String(row.try_retail_mode || 'STEP'),
|
||||
usd_retail_mode: String(row.usd_retail_mode || 'STEP'),
|
||||
eur_retail_mode: String(row.eur_retail_mode || 'STEP')
|
||||
}
|
||||
for (const key of numericFields) item[key] = finiteNumber(row[key], 0)
|
||||
return item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const makeTraceId = () => `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const isTimeoutLikeError = (e) => {
|
||||
const status = e?.response?.status || null
|
||||
// With axios timeout disabled for bulk-save, treat only real upstream/proxy timeouts as retry-able.
|
||||
return status === 504
|
||||
}
|
||||
|
||||
let savedTotal = 0
|
||||
const failedKeys = []
|
||||
|
||||
const postBulkSave = async (list) => {
|
||||
const traceId = makeTraceId()
|
||||
const payload = buildPayload(list)
|
||||
await api.request({
|
||||
method: 'POST',
|
||||
url: '/pricing/pricing-rules/bulk-save',
|
||||
data: payload,
|
||||
// Disable axios timeout here: backend may legitimately run for several minutes on the first write after a full truncate/import.
|
||||
// Any upstream/proxy timeout will surface as 504 anyway.
|
||||
timeout: 0,
|
||||
headers: { 'X-Trace-ID': traceId }
|
||||
})
|
||||
return traceId
|
||||
}
|
||||
|
||||
// Prefer single request (fast path). Fallback to bisection only on proxy/timeout errors.
|
||||
try {
|
||||
const traceId = await postBulkSave(dirty)
|
||||
savedTotal = dirty.length
|
||||
console.info('[pricing-rules][ui] saveSelected:one-shot:done', { trace_id: traceId, total: dirty.length })
|
||||
} catch (e) {
|
||||
if (!isTimeoutLikeError(e)) throw e
|
||||
|
||||
const initialChunkSize = 50
|
||||
const queue = []
|
||||
for (let offset = 0; offset < dirty.length; offset += initialChunkSize) {
|
||||
queue.push(dirty.slice(offset, offset + initialChunkSize))
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const batch = queue.shift()
|
||||
if (!batch || batch.length === 0) continue
|
||||
|
||||
console.info('[pricing-rules][ui] saveSelected:batch:start', {
|
||||
batch_size: batch.length,
|
||||
saved_total: savedTotal,
|
||||
total: dirty.length
|
||||
})
|
||||
|
||||
try {
|
||||
const traceId = await postBulkSave(batch)
|
||||
savedTotal += batch.length
|
||||
Notify.create({ type: 'positive', message: `Kaydedildi: ${savedTotal} / ${dirty.length}` })
|
||||
console.info('[pricing-rules][ui] saveSelected:batch:done', {
|
||||
trace_id: traceId,
|
||||
batch_size: batch.length,
|
||||
saved_total: savedTotal,
|
||||
total: dirty.length
|
||||
})
|
||||
} catch (err2) {
|
||||
if (isTimeoutLikeError(err2) && batch.length > 1) {
|
||||
const mid = Math.ceil(batch.length / 2)
|
||||
queue.unshift(batch.slice(mid))
|
||||
queue.unshift(batch.slice(0, mid))
|
||||
continue
|
||||
}
|
||||
throw err2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reloaded = await loadRows()
|
||||
if (!reloaded) {
|
||||
Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Kaydetme tamamlandi, ancak liste yenilenemedi. Sayfayi yenileyip (F5) kontrol edin.'
|
||||
})
|
||||
return
|
||||
}
|
||||
Notify.create({ type: 'positive', message: `Kaydedildi: ${dirty.length}` })
|
||||
await loadRows()
|
||||
csvImportStatus.value = null
|
||||
console.info('[pricing-rules][ui] saveSelected:done', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
dirty_count: dirty.length,
|
||||
reloaded: true
|
||||
})
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kurallar kaydedilemedi' })
|
||||
console.error('[pricing-rules][ui] saveSelected:error', {
|
||||
status: err?.response?.status || null,
|
||||
message: err?.response?.data || err?.message || 'Kurallar kaydedilemedi'
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -1119,8 +1364,7 @@ async function saveSelected () {
|
||||
|
||||
function resetTransientState () {
|
||||
rows.value = []
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
clearSelections()
|
||||
}
|
||||
|
||||
onMounted(refreshRows)
|
||||
@@ -1133,10 +1377,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.pricing-rules-page {
|
||||
--rules-row-height: 31px;
|
||||
--rules-header-height: 72px;
|
||||
--rules-row-height: 27px;
|
||||
--rules-header-height: 58px;
|
||||
--rules-table-height: calc(100vh - 210px);
|
||||
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
@@ -1181,7 +1426,7 @@ onBeforeUnmount(() => {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin-right: var(--sticky-scroll-comp, 0px);
|
||||
@@ -1197,7 +1442,7 @@ onBeforeUnmount(() => {
|
||||
.rules-table :deep(th),
|
||||
.rules-table :deep(td) {
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
padding: 0 2px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1229,7 +1474,7 @@ onBeforeUnmount(() => {
|
||||
word-break: normal;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -1301,7 +1546,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
color: #bf5b04;
|
||||
}
|
||||
@@ -1375,7 +1620,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.rules-table :deep(.selection-col .q-checkbox__inner) {
|
||||
color: var(--q-primary);
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copy-cell-wrap {
|
||||
@@ -1389,7 +1634,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.rules-table :deep(.rule-select-checkbox .q-checkbox__inner) {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.rules-table :deep(th.usd-col),
|
||||
@@ -1419,18 +1664,19 @@ onBeforeUnmount(() => {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.1;
|
||||
padding: 0 4px;
|
||||
padding: 0 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.native-cell-input {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 3px;
|
||||
padding: 1px 2px;
|
||||
border: 1px solid #cfd8dc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1440,7 +1686,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.action-legend :deep(.q-chip) {
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user