1451 lines
46 KiB
Vue
1451 lines
46 KiB
Vue
<template>
|
|
<q-page class="q-pa-xs pricing-rules-page">
|
|
<div class="top-bar row items-center justify-between q-mb-xs">
|
|
<div>
|
|
<div class="text-subtitle1 text-weight-bold">Fiyat Carpani Kurallari</div>
|
|
</div>
|
|
<q-btn
|
|
flat
|
|
color="primary"
|
|
icon="refresh"
|
|
label="Yenile"
|
|
:loading="loading"
|
|
@click="refreshRows"
|
|
/>
|
|
</div>
|
|
|
|
<div class="action-bar row items-center justify-between q-mb-xs">
|
|
<div class="text-caption text-grey-8">
|
|
Satir: {{ filteredRows.length }} / {{ rows.length }} | Degisen: {{ selectedDirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ selectedCount }}
|
|
</div>
|
|
<div class="row items-center q-gutter-sm">
|
|
<div class="row items-center q-gutter-xs action-legend">
|
|
<q-chip dense square color="orange-1" text-color="deep-orange-8" icon="content_copy">
|
|
Kopya secimi
|
|
</q-chip>
|
|
<q-chip dense square color="light-green-1" text-color="green-9" icon="task_alt">
|
|
Kaydetme secimi
|
|
</q-chip>
|
|
</div>
|
|
<q-btn
|
|
flat
|
|
color="primary"
|
|
icon="filter_alt_off"
|
|
label="Filtreleri Temizle"
|
|
:disable="!hasAnyFilter"
|
|
@click="clearAllFilters"
|
|
/>
|
|
<q-btn
|
|
color="secondary"
|
|
unelevated
|
|
icon="content_copy"
|
|
label="Kopyala"
|
|
:disable="!canCopySelected"
|
|
@click="copySelectedToSelected"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
flat
|
|
icon="download"
|
|
label="Sayfayi Excel'e Aktar"
|
|
:disable="filteredRows.length === 0"
|
|
@click="exportCurrentView"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
outline
|
|
icon="download_for_offline"
|
|
label="Tum Filtreyi Excel'e Aktar"
|
|
:disable="rows.length === 0"
|
|
@click="exportAllFiltered"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
outline
|
|
icon="upload_file"
|
|
label="Verileri CSV'den Yukle"
|
|
:disable="loading || rows.length === 0"
|
|
@click="openImportDialog"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
unelevated
|
|
icon="save"
|
|
:disable="!canUpdate || selectedDirtyCount === 0 || saving"
|
|
:loading="saving"
|
|
:label="`Kaydet (${selectedDirtyCount})`"
|
|
@click="saveSelected"
|
|
/>
|
|
<input
|
|
ref="fileInputRef"
|
|
type="file"
|
|
accept=".csv,text/csv"
|
|
class="hidden-file-input"
|
|
@change="onImportFileChange"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
|
<q-table
|
|
flat
|
|
dense
|
|
row-key="_row_key"
|
|
:rows="filteredRows"
|
|
:columns="columns"
|
|
:loading="loading"
|
|
:rows-per-page-options="[0]"
|
|
v-model:pagination="tablePagination"
|
|
binary-state-sort
|
|
hide-bottom
|
|
:table-style="tableStyle"
|
|
class="pane-table rules-table"
|
|
>
|
|
<template #no-data>
|
|
<div class="full-width row flex-center q-pa-lg text-grey-7">
|
|
Parametre cache'i henuz dolmadi veya aktif filtrelerle eslesen kayit bulunamadi.
|
|
</div>
|
|
</template>
|
|
|
|
<template #header="props">
|
|
<q-tr :props="props" class="header-row-fixed">
|
|
<q-th
|
|
v-for="col in props.cols"
|
|
:key="col.name"
|
|
:props="props"
|
|
:class="[col.headerClasses, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
|
:style="getHeaderCellStyle(col)"
|
|
>
|
|
<q-checkbox
|
|
v-if="col.name === 'select'"
|
|
dense
|
|
size="24px"
|
|
class="rule-select-checkbox"
|
|
color="positive"
|
|
:model-value="allSelectedVisible"
|
|
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
|
@update:model-value="toggleSelectAllVisible"
|
|
/>
|
|
<div v-else-if="col.name === 'copy_select'" class="selection-header-copy">
|
|
<q-icon name="content_copy" size="16px" />
|
|
<span>Kopya</span>
|
|
</div>
|
|
<div v-else class="header-with-filter">
|
|
<span>{{ col.label }}</span>
|
|
<q-btn
|
|
v-if="isHeaderFilterField(col.field)"
|
|
dense
|
|
flat
|
|
round
|
|
size="8px"
|
|
icon="filter_alt"
|
|
:color="hasFilter(col.field) ? 'primary' : 'grey-7'"
|
|
class="header-filter-btn"
|
|
@click.stop
|
|
>
|
|
<q-badge
|
|
v-if="hasFilter(col.field)"
|
|
floating
|
|
rounded
|
|
color="primary"
|
|
:label="getFilterBadgeValue(col.field)"
|
|
/>
|
|
<q-menu anchor="bottom right" self="top right" @click.stop>
|
|
<div v-if="isMultiSelectFilterField(col.field)" class="excel-filter-menu">
|
|
<q-input
|
|
v-model="columnFilterSearch[col.field]"
|
|
dense
|
|
outlined
|
|
clearable
|
|
debounce="150"
|
|
placeholder="Ara"
|
|
/>
|
|
<div class="excel-filter-actions row items-center justify-between q-mt-xs">
|
|
<q-btn flat dense size="sm" color="primary" label="Tumunu Sec" @click="selectAllColumnFilterOptions(col.field)" />
|
|
<q-btn flat dense size="sm" color="grey-8" label="Temizle" @click="clearColumnFilter(col.field)" />
|
|
</div>
|
|
<q-virtual-scroll
|
|
v-if="getFilterOptionsForField(col.field).length > 0"
|
|
class="excel-filter-options"
|
|
:items="getFilterOptionsForField(col.field)"
|
|
:virtual-scroll-item-size="32"
|
|
separator
|
|
>
|
|
<template #default="{ item: option }">
|
|
<q-item
|
|
dense
|
|
clickable
|
|
class="excel-filter-option"
|
|
@click="toggleColumnFilterValue(col.field, option.value)"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-checkbox
|
|
dense
|
|
:model-value="isColumnFilterValueSelected(col.field, option.value)"
|
|
@click.stop
|
|
@update:model-value="toggleColumnFilterValue(col.field, option.value)"
|
|
/>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label>{{ option.label }}</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
</template>
|
|
</q-virtual-scroll>
|
|
<div v-else class="excel-filter-empty">Sonuc yok</div>
|
|
</div>
|
|
<div v-else-if="isNumberRangeFilterField(col.field)" class="excel-filter-menu">
|
|
<div class="range-filter-grid">
|
|
<q-input
|
|
dense
|
|
outlined
|
|
type="number"
|
|
label="Minimum"
|
|
:model-value="numberRangeFilters[col.field]?.min"
|
|
@update:model-value="(value) => updateNumberRangeFilter(col.field, 'min', value)"
|
|
/>
|
|
<q-input
|
|
dense
|
|
outlined
|
|
type="number"
|
|
label="Maksimum"
|
|
:model-value="numberRangeFilters[col.field]?.max"
|
|
@update:model-value="(value) => updateNumberRangeFilter(col.field, 'max', value)"
|
|
/>
|
|
</div>
|
|
<div class="excel-filter-actions row items-center justify-end q-mt-xs">
|
|
<q-btn flat dense size="sm" color="grey-8" label="Temizle" @click="clearRangeFilter(col.field)" />
|
|
</div>
|
|
</div>
|
|
</q-menu>
|
|
</q-btn>
|
|
<span v-else class="header-filter-ghost" />
|
|
</div>
|
|
</q-th>
|
|
</q-tr>
|
|
</template>
|
|
|
|
<template #body="props">
|
|
<q-tr :props="props">
|
|
<q-td
|
|
v-for="col in props.cols"
|
|
:key="col.name"
|
|
:props="props"
|
|
:class="[col.classes, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
|
:style="getBodyCellStyle(col)"
|
|
>
|
|
<div
|
|
v-if="col.name === 'copy_select'"
|
|
class="row items-center no-wrap justify-center copy-cell-wrap"
|
|
>
|
|
<q-checkbox
|
|
dense
|
|
size="24px"
|
|
class="rule-select-checkbox"
|
|
color="secondary"
|
|
:model-value="isCopySelected(props.row)"
|
|
@update:model-value="(value) => toggleCopySelected(props.row, value)"
|
|
/>
|
|
<q-badge
|
|
v-if="copyRoleLabel(props.row)"
|
|
color="deep-orange"
|
|
class="q-ml-xs"
|
|
>
|
|
{{ copyRoleLabel(props.row) }}
|
|
</q-badge>
|
|
</div>
|
|
<q-checkbox
|
|
v-else-if="col.name === 'select'"
|
|
dense
|
|
size="24px"
|
|
class="rule-select-checkbox"
|
|
color="positive"
|
|
:model-value="isRowSelected(props.row)"
|
|
@update:model-value="(value) => setRowSelected(props.row, value)"
|
|
/>
|
|
<q-badge v-else-if="col.name === 'has_rule'" :color="props.row.has_rule ? 'positive' : 'grey-6'">
|
|
{{ props.row.has_rule ? 'Tanimli' : 'Yeni' }}
|
|
</q-badge>
|
|
<q-toggle
|
|
v-else-if="col.name === 'is_active'"
|
|
v-model="props.row.is_active"
|
|
dense
|
|
@update:model-value="() => markDirty(props.row)"
|
|
/>
|
|
<input
|
|
v-else-if="numericFields.has(col.name)"
|
|
class="native-cell-input text-right"
|
|
inputmode="decimal"
|
|
:value="props.row[col.field]"
|
|
@input="(event) => updateNumber(props.row, col.field, event.target.value)"
|
|
>
|
|
<span v-else class="cell-text" :title="String(col.value ?? '')">
|
|
{{ col.value }}
|
|
</span>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
</q-table>
|
|
</div>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import { Dialog, Notify } from 'quasar'
|
|
import api, { download } from 'src/services/api'
|
|
import { usePermissionStore } from 'stores/permissionStore'
|
|
|
|
const perm = usePermissionStore()
|
|
const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))
|
|
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const rows = ref([])
|
|
const fileInputRef = ref(null)
|
|
const selectedKeyMap = ref({})
|
|
const copySelectedKeys = ref([])
|
|
const tablePagination = ref({ rowsPerPage: 0, sortBy: 'urun_ilk_grubu', descending: false })
|
|
let emptyRetryTimer = null
|
|
|
|
const numericFields = new Set([
|
|
'try_base', 'try1', 'try2', 'try3', 'try4', 'try5', 'try6', 'try_wholesale_step', 'try_retail_step',
|
|
'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 importKeyFieldLabels = [
|
|
['askili_yan', 'ASKILI YAN'],
|
|
['kategori', 'KATEGORI'],
|
|
['urun_ilk_grubu', 'URUN ILK GRUBU'],
|
|
['urun_ana_grubu', 'URUN ANA GRUBU'],
|
|
['urun_alt_grubu', 'URUN ALT GRUBU'],
|
|
['icerik', 'ICERIK'],
|
|
['marka', 'MARKA'],
|
|
['brand_code', 'BRAND CODE'],
|
|
['brand_group', 'MARKA GRUBU']
|
|
]
|
|
|
|
const importFieldMap = {
|
|
AKTIF: 'is_active',
|
|
'TRY TOPTAN YUVARLAMA': 'try_wholesale_step',
|
|
'TRY PERAKENDE YUVARLAMA': 'try_retail_step',
|
|
'TRY YUVARLAMA': 'try_wholesale_step',
|
|
'TRY TABAN': 'try_base',
|
|
'TRY 1': 'try1',
|
|
'TRY 2': 'try2',
|
|
'TRY 3': 'try3',
|
|
'TRY 4': 'try4',
|
|
'TRY 5': 'try5',
|
|
'TRY 6': 'try6',
|
|
'USD TOPTAN YUVARLAMA': 'usd_wholesale_step',
|
|
'USD PERAKENDE YUVARLAMA': 'usd_retail_step',
|
|
'USD YUVARLAMA': 'usd_wholesale_step',
|
|
'USD TABAN': 'usd_base',
|
|
'USD 1': 'usd1',
|
|
'USD 2': 'usd2',
|
|
'USD 3': 'usd3',
|
|
'USD 4': 'usd4',
|
|
'USD 5': 'usd5',
|
|
'USD 6': 'usd6',
|
|
'EUR TOPTAN YUVARLAMA': 'eur_wholesale_step',
|
|
'EUR PERAKENDE YUVARLAMA': 'eur_retail_step',
|
|
'EUR YUVARLAMA': 'eur_wholesale_step',
|
|
'EUR TABAN': 'eur_base',
|
|
'EUR 1': 'eur1',
|
|
'EUR 2': 'eur2',
|
|
'EUR 3': 'eur3',
|
|
'EUR 4': 'eur4',
|
|
'EUR 5': 'eur5',
|
|
'EUR 6': 'eur6'
|
|
}
|
|
|
|
const multiFilterFields = [
|
|
'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
|
|
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
|
]
|
|
const multiSelectFilterFieldSet = new Set(multiFilterFields)
|
|
const numberRangeFilterFieldSet = new Set(numericFields)
|
|
const headerFilterFieldSet = new Set([...multiFilterFields, ...numericFields])
|
|
|
|
const columnFilters = ref(Object.fromEntries(multiFilterFields.map(field => [field, []])))
|
|
const columnFilterSearch = ref(Object.fromEntries(multiFilterFields.map(field => [field, ''])))
|
|
const numberRangeFilters = ref(Object.fromEntries([...numericFields].map(field => [field, { min: '', max: '' }])))
|
|
|
|
function col (name, label, field, width, extra = {}) {
|
|
const size = `width:${width}px;min-width:${width}px;max-width:${width}px;`
|
|
return {
|
|
name,
|
|
label,
|
|
field,
|
|
sortable: name !== 'select',
|
|
align: 'left',
|
|
style: size,
|
|
headerStyle: size,
|
|
...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('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('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('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' })
|
|
]
|
|
|
|
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'
|
|
]
|
|
const stickyBoundaryColumnName = 'brand_group'
|
|
const stickyColumnNameSet = new Set(stickyColumnNames)
|
|
|
|
const stickyLeftMap = computed(() => {
|
|
const map = {}
|
|
let left = 0
|
|
for (const colName of stickyColumnNames) {
|
|
const column = columns.find(item => item.name === colName)
|
|
if (!column) continue
|
|
map[colName] = left
|
|
left += extractWidth(column.style)
|
|
}
|
|
return map
|
|
})
|
|
|
|
const stickyScrollComp = computed(() => {
|
|
const boundary = columns.find(item => item.name === stickyBoundaryColumnName)
|
|
return (stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundary?.style)
|
|
})
|
|
|
|
const tableMinWidth = computed(() => columns.reduce((sum, column) => sum + extractWidth(column.style), 0))
|
|
const tableStyle = computed(() => ({
|
|
width: `${tableMinWidth.value}px`,
|
|
minWidth: `${tableMinWidth.value}px`,
|
|
tableLayout: 'fixed'
|
|
}))
|
|
|
|
function filterDisplayValue (row, field) {
|
|
if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
|
|
if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
|
|
return String(row?.[field] ?? '').trim()
|
|
}
|
|
|
|
const multiFilterOptionMap = computed(() => {
|
|
const map = {}
|
|
for (const field of multiFilterFields) {
|
|
const uniq = new Set()
|
|
for (const row of rows.value) {
|
|
const value = filterDisplayValue(row, field)
|
|
if (value) uniq.add(value)
|
|
}
|
|
map[field] = [...uniq]
|
|
.sort((a, b) => a.localeCompare(b, 'tr'))
|
|
.map(value => ({ label: value, value }))
|
|
}
|
|
return map
|
|
})
|
|
|
|
const filteredFilterOptionMap = computed(() => {
|
|
const map = {}
|
|
for (const field of multiFilterFields) {
|
|
const search = String(columnFilterSearch.value[field] || '').trim().toLocaleLowerCase('tr')
|
|
const options = multiFilterOptionMap.value[field] || []
|
|
map[field] = search
|
|
? options.filter(option => option.label.toLocaleLowerCase('tr').includes(search))
|
|
: options
|
|
}
|
|
return map
|
|
})
|
|
|
|
const filteredRows = computed(() => {
|
|
return rows.value.filter(row => {
|
|
for (const field of multiFilterFields) {
|
|
const selectedValues = columnFilters.value[field] || []
|
|
if (selectedValues.length > 0 && !selectedValues.includes(filterDisplayValue(row, field))) return false
|
|
}
|
|
for (const field of numericFields) {
|
|
const value = Number(row?.[field] ?? 0)
|
|
const min = parseNullableNumber(numberRangeFilters.value[field]?.min)
|
|
const max = parseNullableNumber(numberRangeFilters.value[field]?.max)
|
|
if (min !== null && value < min) return false
|
|
if (max !== null && value > max) return false
|
|
}
|
|
return true
|
|
})
|
|
})
|
|
|
|
const exportableColumns = computed(() => columns.filter(col => col.name !== 'copy_select' && col.name !== 'select'))
|
|
const exportedRows = computed(() => {
|
|
const list = [...filteredRows.value]
|
|
const sortBy = String(tablePagination.value?.sortBy || '').trim()
|
|
const descending = Boolean(tablePagination.value?.descending)
|
|
if (!sortBy) return list
|
|
|
|
list.sort((a, b) => {
|
|
const av = exportSortValue(a, sortBy)
|
|
const bv = exportSortValue(b, sortBy)
|
|
if (typeof av === 'number' && typeof bv === 'number') return av - bv
|
|
return String(av ?? '').localeCompare(String(bv ?? ''), 'tr', { numeric: true, sensitivity: 'base' })
|
|
})
|
|
|
|
return descending ? list.reverse() : list
|
|
})
|
|
|
|
const copySelectedKeySet = computed(() => new Set(copySelectedKeys.value))
|
|
const visibleRowKeys = computed(() => filteredRows.value.map(row => row._row_key))
|
|
const selectedVisibleCount = computed(() => visibleRowKeys.value.reduce((count, key) => count + (selectedKeyMap.value?.[key] ? 1 : 0), 0))
|
|
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
|
|
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
|
const selectedDirtyCount = computed(() => rows.value.reduce((count, row) => count + (selectedKeyMap.value?.[row._row_key] && row?._dirty ? 1 : 0), 0))
|
|
const selectedCount = computed(() => Object.keys(selectedKeyMap.value || {}).length)
|
|
const copySelectedCount = computed(() => copySelectedKeys.value.length)
|
|
const canCopySelected = computed(() => copySelectedCount.value >= 2)
|
|
const hasUnsavedLocalChanges = computed(() => rows.value.some(row => Boolean(row?._dirty)))
|
|
const hasAnyFilter = computed(() => {
|
|
return [...headerFilterFieldSet].some(field => hasFilter(field))
|
|
})
|
|
|
|
function finiteNumber (value, fallback = 0) {
|
|
const parsed = Number(value)
|
|
return Number.isFinite(parsed) ? parsed : fallback
|
|
}
|
|
|
|
function parseNullableNumber (value) {
|
|
if (value === null || value === undefined || String(value).trim() === '') return null
|
|
const parsed = Number(value)
|
|
return Number.isFinite(parsed) ? parsed : null
|
|
}
|
|
|
|
function normalizeWorksheetRow (source) {
|
|
const rule = source?.rule || {}
|
|
const row = {
|
|
pricing_parameter_id: Number(source?.pricing_parameter_id || 0),
|
|
_row_key: String(source?.scope_key || source?.pricing_parameter_id || ''),
|
|
has_rule: Boolean(source?.has_rule),
|
|
id: String(rule?.id || ''),
|
|
is_active: rule?.is_active !== false,
|
|
askili_yan: String(source?.askili_yan || ''),
|
|
kategori: String(source?.kategori || ''),
|
|
urun_ilk_grubu: String(source?.urun_ilk_grubu || ''),
|
|
urun_ana_grubu: String(source?.urun_ana_grubu || ''),
|
|
urun_alt_grubu: String(source?.urun_alt_grubu || ''),
|
|
icerik: String(source?.icerik || ''),
|
|
marka: String(source?.marka || ''),
|
|
brand_code: String(source?.brand_code || ''),
|
|
brand_group: String(source?.brand_group || ''),
|
|
_dirty: false
|
|
}
|
|
for (const key of numericFields) {
|
|
row[key] = row.has_rule ? finiteNumber(rule?.[key], 0) : ''
|
|
}
|
|
return row
|
|
}
|
|
|
|
function extractWidth (style) {
|
|
const match = String(style || '').match(/width:(\d+)px/)
|
|
return match ? Number(match[1]) : 0
|
|
}
|
|
|
|
function isStickyCol (colName) {
|
|
return stickyColumnNameSet.has(colName)
|
|
}
|
|
|
|
function isStickyBoundary (colName) {
|
|
return colName === stickyBoundaryColumnName
|
|
}
|
|
|
|
function getHeaderCellStyle (column) {
|
|
if (!isStickyCol(column.name)) return undefined
|
|
return { left: `${stickyLeftMap.value[column.name] || 0}px`, zIndex: 35 }
|
|
}
|
|
|
|
function getBodyCellStyle (column) {
|
|
if (!isStickyCol(column.name)) return undefined
|
|
return { left: `${stickyLeftMap.value[column.name] || 0}px`, zIndex: 12 }
|
|
}
|
|
|
|
function isRowSelected (row) {
|
|
return !!selectedKeyMap.value?.[row._row_key]
|
|
}
|
|
|
|
function isCopySelected (row) {
|
|
return copySelectedKeySet.value.has(row._row_key)
|
|
}
|
|
|
|
function setRowSelected (row, value) {
|
|
const key = row?._row_key
|
|
if (!key) return
|
|
const next = { ...(selectedKeyMap.value || {}) }
|
|
if (value) {
|
|
next[key] = true
|
|
} else {
|
|
delete next[key]
|
|
}
|
|
selectedKeyMap.value = next
|
|
}
|
|
|
|
function toggleCopySelected (row, value) {
|
|
const key = row?._row_key
|
|
if (!key) return
|
|
const next = [...copySelectedKeys.value]
|
|
const idx = next.indexOf(key)
|
|
if (value) {
|
|
if (idx === -1) next.push(key)
|
|
} else if (idx >= 0) {
|
|
next.splice(idx, 1)
|
|
}
|
|
copySelectedKeys.value = next
|
|
}
|
|
|
|
function copyRoleLabel (row) {
|
|
const key = row?._row_key
|
|
if (!key) return ''
|
|
const idx = copySelectedKeys.value.indexOf(key)
|
|
if (idx === 0) return 'Kaynak'
|
|
if (idx > 0) return 'Hedef'
|
|
return ''
|
|
}
|
|
|
|
function toggleSelectAllVisible (value) {
|
|
const next = { ...(selectedKeyMap.value || {}) }
|
|
for (const key of visibleRowKeys.value) {
|
|
if (value) next[key] = true
|
|
else delete next[key]
|
|
}
|
|
selectedKeyMap.value = next
|
|
}
|
|
|
|
function selectDirtyRow (row) {
|
|
setRowSelected(row, true)
|
|
}
|
|
|
|
function markDirty (row) {
|
|
row._dirty = true
|
|
selectDirtyRow(row)
|
|
}
|
|
|
|
function updateNumber (row, field, value) {
|
|
row[field] = String(value ?? '').trim() === '' ? '' : finiteNumber(value, 0)
|
|
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 (numericFields.has(field)) return finiteNumber(row?.[field], 0)
|
|
return String(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 (numericFields.has(field)) {
|
|
const value = row?.[field]
|
|
if (value === '' || value === null || value === undefined) return '0'
|
|
return String(finiteNumber(value, 0))
|
|
}
|
|
return String(row?.[field] ?? '').trim()
|
|
}
|
|
|
|
function csvSafe (value) {
|
|
let text = String(value ?? '').replaceAll('\r', ' ').replaceAll('\n', ' ').trim()
|
|
if (text.includes(';') || text.includes('"')) {
|
|
text = `"${text.replaceAll('"', '""')}"`
|
|
}
|
|
return text
|
|
}
|
|
|
|
function exportCurrentView () {
|
|
const cols = exportableColumns.value
|
|
const list = exportedRows.value
|
|
if (cols.length === 0 || list.length === 0) return
|
|
|
|
const lines = [cols.map(col => csvSafe(col.label)).join(';')]
|
|
for (const row of list) {
|
|
lines.push(cols.map(col => csvSafe(exportCellValue(row, col.field))).join(';'))
|
|
}
|
|
|
|
const bom = '\uFEFF'
|
|
const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `pricing_rules_${new Date().toISOString().slice(0, 10)}.csv`
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
a.remove()
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
|
|
async function exportAllFiltered () {
|
|
try {
|
|
const params = {
|
|
askili_yan: (columnFilters.value.askili_yan || []).join(','),
|
|
kategori: (columnFilters.value.kategori || []).join(','),
|
|
urun_ilk_grubu: (columnFilters.value.urun_ilk_grubu || []).join(','),
|
|
urun_ana_grubu: (columnFilters.value.urun_ana_grubu || []).join(','),
|
|
urun_alt_grubu: (columnFilters.value.urun_alt_grubu || []).join(','),
|
|
icerik: (columnFilters.value.icerik || []).join(','),
|
|
marka: (columnFilters.value.marka || []).join(','),
|
|
brand_code: (columnFilters.value.brand_code || []).join(','),
|
|
brand_group: (columnFilters.value.brand_group || []).join(','),
|
|
sort_by: tablePagination.value?.sortBy || '',
|
|
desc: tablePagination.value?.descending ? 1 : 0
|
|
}
|
|
|
|
for (const field of numericFields) {
|
|
const min = String(numberRangeFilters.value[field]?.min || '').trim()
|
|
const max = String(numberRangeFilters.value[field]?.max || '').trim()
|
|
if (min) params[`${field}_min`] = min
|
|
if (max) params[`${field}_max`] = max
|
|
}
|
|
|
|
const blob = await download('/pricing/pricing-rules/export-all', params)
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `pricing_rules_all_${new Date().toISOString().slice(0, 10)}.csv`
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
a.remove()
|
|
URL.revokeObjectURL(url)
|
|
} catch (err) {
|
|
Notify.create({ type: 'negative', message: err?.message || 'Tum filtre export alinamadi' })
|
|
}
|
|
}
|
|
|
|
function openImportDialog () {
|
|
fileInputRef.value?.click?.()
|
|
}
|
|
|
|
function parseCsvRows (text) {
|
|
const clean = String(text || '').replace(/^\uFEFF/, '')
|
|
const rows = []
|
|
let cell = ''
|
|
let row = []
|
|
let inQuotes = false
|
|
|
|
for (let i = 0; i < clean.length; i++) {
|
|
const ch = clean[i]
|
|
if (inQuotes) {
|
|
if (ch === '"') {
|
|
if (clean[i + 1] === '"') {
|
|
cell += '"'
|
|
i += 1
|
|
} else {
|
|
inQuotes = false
|
|
}
|
|
} else {
|
|
cell += ch
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (ch === '"') {
|
|
inQuotes = true
|
|
continue
|
|
}
|
|
if (ch === ';') {
|
|
row.push(cell)
|
|
cell = ''
|
|
continue
|
|
}
|
|
if (ch === '\n') {
|
|
row.push(cell)
|
|
rows.push(row)
|
|
row = []
|
|
cell = ''
|
|
continue
|
|
}
|
|
if (ch !== '\r') {
|
|
cell += ch
|
|
}
|
|
}
|
|
|
|
if (cell !== '' || row.length > 0) {
|
|
row.push(cell)
|
|
rows.push(row)
|
|
}
|
|
|
|
return rows
|
|
}
|
|
|
|
function normalizeImportText (value) {
|
|
let text = String(value ?? '').trim()
|
|
if (text.startsWith("'")) text = text.slice(1).trim()
|
|
const formulaMatch = text.match(/^=\s*"([\s\S]*)"$/)
|
|
if (formulaMatch) text = formulaMatch[1]
|
|
return text.toLocaleUpperCase('tr')
|
|
}
|
|
|
|
function buildImportRowKeyFromObject (row) {
|
|
return importKeyFieldLabels
|
|
.map(([field]) => normalizeImportText(row?.[field] ?? ''))
|
|
.join('|')
|
|
}
|
|
|
|
function parseImportedNumber (value) {
|
|
const text = String(value ?? '').trim().replace(/\s/g, '')
|
|
if (!text) return 0
|
|
|
|
const lastComma = text.lastIndexOf(',')
|
|
const lastDot = text.lastIndexOf('.')
|
|
let normalized = text
|
|
|
|
if (lastComma >= 0 && lastDot >= 0) {
|
|
if (lastComma > lastDot) normalized = text.replace(/\./g, '').replace(',', '.')
|
|
else normalized = text.replace(/,/g, '')
|
|
} else if (lastComma >= 0) {
|
|
normalized = text.replace(/\./g, '').replace(',', '.')
|
|
} else {
|
|
normalized = text.replace(/,/g, '')
|
|
}
|
|
|
|
const parsed = Number(normalized)
|
|
return Number.isFinite(parsed) ? parsed : 0
|
|
}
|
|
|
|
function parseImportedBoolean (value) {
|
|
const normalized = normalizeImportText(value)
|
|
if (!normalized) return null
|
|
if (['AKTIF', 'TRUE', '1', 'EVET', 'YES'].includes(normalized)) return true
|
|
if (['PASIF', 'FALSE', '0', 'HAYIR', 'NO'].includes(normalized)) return false
|
|
return null
|
|
}
|
|
|
|
async function onImportFileChange (event) {
|
|
const input = event?.target
|
|
const file = input?.files?.[0]
|
|
if (!file) return
|
|
|
|
try {
|
|
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' })
|
|
return
|
|
}
|
|
|
|
const headers = matrix[0].map(cell => String(cell || '').trim())
|
|
const keyHeaderIndexes = {}
|
|
for (const [, label] of importKeyFieldLabels) {
|
|
keyHeaderIndexes[label] = headers.indexOf(label)
|
|
}
|
|
|
|
const missingKeyHeaders = importKeyFieldLabels
|
|
.map(([, label]) => label)
|
|
.filter(label => keyHeaderIndexes[label] < 0)
|
|
if (missingKeyHeaders.length > 0) {
|
|
Notify.create({ type: 'negative', message: `Eksik kolon: ${missingKeyHeaders.join(', ')}` })
|
|
return
|
|
}
|
|
|
|
const rowMap = new Map(rows.value.map(row => [buildImportRowKeyFromObject(row), row]))
|
|
let matched = 0
|
|
let updated = 0
|
|
let skipped = 0
|
|
|
|
for (let i = 1; i < matrix.length; i++) {
|
|
const csvRow = matrix[i]
|
|
const identity = {}
|
|
for (const [field, label] of importKeyFieldLabels) {
|
|
identity[field] = String(csvRow[keyHeaderIndexes[label]] ?? '').trim().replace(/^'/, '').replace(/^=\s*"([\s\S]*)"$/, '$1')
|
|
}
|
|
|
|
const target = rowMap.get(buildImportRowKeyFromObject(identity))
|
|
if (!target) {
|
|
skipped++
|
|
continue
|
|
}
|
|
matched++
|
|
|
|
let rowChanged = false
|
|
for (const [headerLabel, field] of Object.entries(importFieldMap)) {
|
|
const idx = headers.indexOf(headerLabel)
|
|
if (idx < 0) continue
|
|
const rawValue = csvRow[idx] ?? ''
|
|
|
|
if (field === 'is_active') {
|
|
const next = parseImportedBoolean(rawValue)
|
|
if (next !== null && Boolean(target.is_active) !== next) {
|
|
target.is_active = next
|
|
rowChanged = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
const next = parseImportedNumber(rawValue)
|
|
if (Number(target[field] ?? 0) !== next) {
|
|
target[field] = next
|
|
rowChanged = true
|
|
}
|
|
}
|
|
|
|
if (rowChanged) {
|
|
markDirty(target)
|
|
updated++
|
|
}
|
|
}
|
|
|
|
if (matched === 0) {
|
|
Notify.create({ type: 'warning', 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}`
|
|
})
|
|
} catch (err) {
|
|
Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })
|
|
} finally {
|
|
if (input) input.value = ''
|
|
}
|
|
}
|
|
|
|
function copySelectedToSelected () {
|
|
const keys = [...copySelectedKeys.value]
|
|
if (keys.length < 2) return
|
|
const sourceKey = keys[0]
|
|
const source = rows.value.find(row => row._row_key === sourceKey)
|
|
if (!source) return
|
|
|
|
for (let i = 1; i < keys.length; i++) {
|
|
const target = rows.value.find(row => row._row_key === keys[i])
|
|
if (!target) continue
|
|
target.is_active = Boolean(source.is_active)
|
|
for (const field of numericFields) {
|
|
target[field] = source[field]
|
|
}
|
|
markDirty(target)
|
|
}
|
|
|
|
copySelectedKeys.value = []
|
|
Notify.create({ type: 'positive', message: 'Kopyalama tamamlandi' })
|
|
}
|
|
|
|
function isHeaderFilterField (field) {
|
|
return headerFilterFieldSet.has(field)
|
|
}
|
|
|
|
function isMultiSelectFilterField (field) {
|
|
return multiSelectFilterFieldSet.has(field)
|
|
}
|
|
|
|
function isNumberRangeFilterField (field) {
|
|
return numberRangeFilterFieldSet.has(field)
|
|
}
|
|
|
|
function hasFilter (field) {
|
|
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
|
|
if (isNumberRangeFilterField(field)) {
|
|
const filter = numberRangeFilters.value[field]
|
|
return String(filter?.min || '').trim() !== '' || String(filter?.max || '').trim() !== ''
|
|
}
|
|
return false
|
|
}
|
|
|
|
function getFilterBadgeValue (field) {
|
|
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
|
|
if (isNumberRangeFilterField(field)) {
|
|
const filter = numberRangeFilters.value[field]
|
|
return [filter?.min, filter?.max].filter(value => String(value || '').trim() !== '').length
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function getFilterOptionsForField (field) {
|
|
return filteredFilterOptionMap.value[field] || []
|
|
}
|
|
|
|
function isColumnFilterValueSelected (field, value) {
|
|
return (columnFilters.value[field] || []).includes(value)
|
|
}
|
|
|
|
function toggleColumnFilterValue (field, value) {
|
|
const current = new Set(columnFilters.value[field] || [])
|
|
if (current.has(value)) current.delete(value)
|
|
else current.add(value)
|
|
columnFilters.value = { ...columnFilters.value, [field]: [...current] }
|
|
}
|
|
|
|
function selectAllColumnFilterOptions (field) {
|
|
columnFilters.value = {
|
|
...columnFilters.value,
|
|
[field]: getFilterOptionsForField(field).map(option => option.value)
|
|
}
|
|
}
|
|
|
|
function clearColumnFilter (field) {
|
|
columnFilters.value = { ...columnFilters.value, [field]: [] }
|
|
}
|
|
|
|
function updateNumberRangeFilter (field, side, value) {
|
|
numberRangeFilters.value = {
|
|
...numberRangeFilters.value,
|
|
[field]: { ...numberRangeFilters.value[field], [side]: value }
|
|
}
|
|
}
|
|
|
|
function clearRangeFilter (field) {
|
|
numberRangeFilters.value = {
|
|
...numberRangeFilters.value,
|
|
[field]: { min: '', max: '' }
|
|
}
|
|
}
|
|
|
|
function clearAllFilters () {
|
|
columnFilters.value = Object.fromEntries(multiFilterFields.map(field => [field, []]))
|
|
columnFilterSearch.value = Object.fromEntries(multiFilterFields.map(field => [field, '']))
|
|
numberRangeFilters.value = Object.fromEntries([...numericFields].map(field => [field, { min: '', max: '' }]))
|
|
}
|
|
|
|
async function refreshRows () {
|
|
if (hasUnsavedLocalChanges.value) {
|
|
const confirmed = await new Promise(resolve => {
|
|
Dialog.create({
|
|
title: 'Degisiklikler silinecek',
|
|
message: 'Kaydedilmemis tum degisiklikler silinecek. Devam etmek istiyor musunuz?',
|
|
cancel: true,
|
|
persistent: true
|
|
})
|
|
.onOk(() => resolve(true))
|
|
.onCancel(() => resolve(false))
|
|
.onDismiss(() => resolve(false))
|
|
})
|
|
if (!confirmed) return
|
|
}
|
|
|
|
clearAllFilters()
|
|
selectedKeyMap.value = {}
|
|
copySelectedKeys.value = []
|
|
await loadRows()
|
|
}
|
|
|
|
async function loadRows () {
|
|
if (emptyRetryTimer) {
|
|
clearTimeout(emptyRetryTimer)
|
|
emptyRetryTimer = null
|
|
}
|
|
loading.value = true
|
|
try {
|
|
const res = await api.request({
|
|
method: 'GET',
|
|
url: '/pricing/pricing-rules/parameters',
|
|
timeout: 180000
|
|
})
|
|
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
|
|
selectedKeyMap.value = {}
|
|
copySelectedKeys.value = []
|
|
if (rows.value.length === 0) {
|
|
emptyRetryTimer = setTimeout(loadRows, 10000)
|
|
}
|
|
} catch (err) {
|
|
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kural kombinasyonlari alinamadi' })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function saveSelected () {
|
|
const dirty = rows.value.filter(row => selectedKeyMap.value?.[row._row_key] && row?._dirty)
|
|
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
|
|
})
|
|
Notify.create({ type: 'positive', message: `Kaydedildi: ${dirty.length}` })
|
|
await loadRows()
|
|
} catch (err) {
|
|
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kurallar kaydedilemedi' })
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function resetTransientState () {
|
|
rows.value = []
|
|
selectedKeyMap.value = {}
|
|
copySelectedKeys.value = []
|
|
}
|
|
|
|
onMounted(refreshRows)
|
|
onActivated(refreshRows)
|
|
onBeforeUnmount(() => {
|
|
if (emptyRetryTimer) clearTimeout(emptyRetryTimer)
|
|
resetTransientState()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.pricing-rules-page {
|
|
--rules-row-height: 31px;
|
|
--rules-header-height: 72px;
|
|
--rules-table-height: calc(100vh - 210px);
|
|
|
|
min-width: 0;
|
|
height: calc(100vh - 120px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.top-bar,
|
|
.action-bar {
|
|
flex: 0 0 auto;
|
|
gap: 8px;
|
|
background: #fff;
|
|
position: relative;
|
|
z-index: 40;
|
|
}
|
|
|
|
.table-wrap {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
border-radius: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.pane-table {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.rules-table :deep(.q-table__middle) {
|
|
height: var(--rules-table-height);
|
|
min-height: var(--rules-table-height);
|
|
max-height: var(--rules-table-height);
|
|
overflow: auto !important;
|
|
scrollbar-gutter: stable both-edges;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.rules-table :deep(.q-table) {
|
|
width: max-content;
|
|
min-width: 100%;
|
|
table-layout: fixed;
|
|
font-size: 11px;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
margin-right: var(--sticky-scroll-comp, 0px);
|
|
}
|
|
|
|
.rules-table :deep(.q-table__container) {
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
background: transparent !important;
|
|
height: 100% !important;
|
|
}
|
|
|
|
.rules-table :deep(th),
|
|
.rules-table :deep(td) {
|
|
box-sizing: border-box;
|
|
padding: 0 4px;
|
|
overflow: hidden;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.rules-table :deep(td),
|
|
.rules-table :deep(.q-table tbody tr) {
|
|
height: var(--rules-row-height) !important;
|
|
min-height: var(--rules-row-height) !important;
|
|
max-height: var(--rules-row-height) !important;
|
|
line-height: var(--rules-row-height);
|
|
padding: 0 !important;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
}
|
|
|
|
.rules-table :deep(th),
|
|
.rules-table :deep(.q-table thead tr),
|
|
.rules-table :deep(.q-table thead tr.header-row-fixed),
|
|
.rules-table :deep(.q-table thead th),
|
|
.rules-table :deep(.q-table thead tr.header-row-fixed > th) {
|
|
height: var(--rules-header-height) !important;
|
|
min-height: var(--rules-header-height) !important;
|
|
max-height: var(--rules-header-height) !important;
|
|
}
|
|
|
|
.rules-table :deep(th) {
|
|
padding-top: 0;
|
|
padding-bottom: 0;
|
|
white-space: nowrap;
|
|
word-break: normal;
|
|
text-overflow: ellipsis;
|
|
text-align: center;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.rules-table :deep(.q-table thead th) {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 30;
|
|
background: #fff;
|
|
vertical-align: middle !important;
|
|
}
|
|
|
|
.rules-table :deep(.sticky-col) {
|
|
position: sticky !important;
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
.rules-table :deep(thead .sticky-col) {
|
|
z-index: 35 !important;
|
|
}
|
|
|
|
.rules-table :deep(tbody .sticky-col) {
|
|
z-index: 12 !important;
|
|
}
|
|
|
|
.rules-table :deep(.sticky-boundary) {
|
|
border-right: 2px solid rgba(25, 118, 210, 0.18) !important;
|
|
box-shadow: 8px 0 12px -10px rgba(15, 23, 42, 0.55);
|
|
}
|
|
|
|
.header-with-filter {
|
|
display: grid;
|
|
grid-template-columns: 1fr 20px;
|
|
align-items: center;
|
|
column-gap: 4px;
|
|
height: 100%;
|
|
line-height: 1.25;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header-with-filter > span {
|
|
min-width: 0;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
text-align: center;
|
|
text-overflow: ellipsis;
|
|
white-space: normal;
|
|
font-weight: 800;
|
|
line-height: 1.15;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.header-filter-btn {
|
|
width: 20px;
|
|
height: 20px;
|
|
min-width: 20px;
|
|
justify-self: end;
|
|
}
|
|
|
|
.header-filter-ghost {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.selection-header-copy {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
color: #bf5b04;
|
|
}
|
|
|
|
.excel-filter-menu {
|
|
min-width: 230px;
|
|
padding: 8px;
|
|
}
|
|
|
|
.range-filter-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.excel-filter-actions {
|
|
gap: 4px;
|
|
}
|
|
|
|
.excel-filter-options {
|
|
max-height: 220px;
|
|
margin-top: 8px;
|
|
overflow: auto;
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.excel-filter-option {
|
|
min-height: 32px;
|
|
}
|
|
|
|
.excel-filter-empty {
|
|
padding: 10px 8px;
|
|
color: #607d8b;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.rules-table :deep(th.ps-col),
|
|
.rules-table :deep(td.ps-col) {
|
|
background: #fff;
|
|
color: var(--q-primary);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.rules-table :deep(th.copy-selection-col),
|
|
.rules-table :deep(td.copy-selection-col) {
|
|
background: #fff3e8;
|
|
color: #bf5b04;
|
|
padding-left: 0 !important;
|
|
padding-right: 0 !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
.rules-table :deep(th.save-selection-col),
|
|
.rules-table :deep(td.save-selection-col) {
|
|
background: #eef9ef;
|
|
color: #1b7f3a;
|
|
padding-left: 0 !important;
|
|
padding-right: 0 !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
.rules-table :deep(th.selection-col),
|
|
.rules-table :deep(td.selection-col) {
|
|
background: #fff;
|
|
color: var(--q-primary);
|
|
padding-left: 0 !important;
|
|
padding-right: 0 !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
.rules-table :deep(.selection-col .q-checkbox__inner) {
|
|
color: var(--q-primary);
|
|
font-size: 16px;
|
|
}
|
|
|
|
.copy-cell-wrap {
|
|
min-height: 100%;
|
|
}
|
|
|
|
.rules-table :deep(.rule-select-checkbox) {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.rules-table :deep(.rule-select-checkbox .q-checkbox__inner) {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.rules-table :deep(th.usd-col),
|
|
.rules-table :deep(td.usd-col) {
|
|
background: #ecf9f0;
|
|
color: #178a3e;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.rules-table :deep(th.eur-col),
|
|
.rules-table :deep(td.eur-col) {
|
|
background: #fdeeee;
|
|
color: #c62828;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.rules-table :deep(th.try-col),
|
|
.rules-table :deep(td.try-col) {
|
|
background: #edf4ff;
|
|
color: #1e63c6;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.cell-text {
|
|
display: block;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
line-height: 1.1;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.native-cell-input {
|
|
width: 100%;
|
|
height: 22px;
|
|
box-sizing: border-box;
|
|
padding: 1px 3px;
|
|
border: 1px solid #cfd8dc;
|
|
border-radius: 4px;
|
|
background: #fff;
|
|
font-size: 11px;
|
|
margin: 0;
|
|
}
|
|
|
|
.native-cell-input:focus {
|
|
outline: none;
|
|
border-color: #1976d2;
|
|
}
|
|
|
|
.action-legend :deep(.q-chip) {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.hidden-file-input {
|
|
display: none;
|
|
}
|
|
</style>
|