Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-02 16:14:54 +03:00
parent 5f3e975b6d
commit b4e87cfd47
25 changed files with 4918 additions and 287 deletions

View File

@@ -0,0 +1,892 @@
<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 class="text-caption text-grey-7">
MSSQL urun kombinasyonlari ve bu kombinasyonlara bagli para birimi bazli fiyat kurallari.
</div>
</div>
<q-btn
flat
color="primary"
icon="refresh"
label="Yenile"
:loading="loading"
@click="loadRows"
/>
</div>
<div class="action-bar row items-center justify-between q-mb-xs">
<div class="text-caption text-grey-8">
{{ filteredRows.length }} / {{ rows.length }} kombinasyon gosteriliyor. Degistirilen satirlar otomatik secilir.
</div>
<div class="row items-center q-gutter-xs">
<q-btn
flat
color="primary"
icon="filter_alt_off"
label="Filtreleri Temizle"
:disable="!hasAnyFilter"
@click="clearAllFilters"
/>
<q-btn
color="primary"
unelevated
icon="save"
:disable="!canUpdate || selectedDirtyCount === 0 || saving"
:loading="saving"
:label="`Kaydet (${selectedDirtyCount})`"
@click="saveSelected"
/>
</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
:model-value="allSelectedVisible"
:indeterminate="someSelectedVisible && !allSelectedVisible"
@update:model-value="toggleSelectAllVisible"
/>
<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)"
>
<q-checkbox
v-if="col.name === 'select'"
dense
: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, onBeforeUnmount, onMounted, ref } from 'vue'
import { Notify } from 'quasar'
import api 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 selected = 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_step',
'usd_base', 'usd1', 'usd2', 'usd3', 'usd4', 'usd5', 'usd6', 'usd_step',
'eur_base', 'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6', 'eur_step'
])
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('select', '', 'select', 34, { sortable: false, classes: 'selection-col', headerClasses: '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_step', 'TRY YUVARLAMA', 'try_step', 84, { 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_step', 'USD YUVARLAMA', 'usd_step', 84, { 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_step', 'EUR YUVARLAMA', 'eur_step', 84, { 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 = [
'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 visibleRowKeys = computed(() => filteredRows.value.map(row => row._row_key))
const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(key => selected.value.some(row => row._row_key === key)).length)
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
const selectedDirtyCount = computed(() => selected.value.filter(row => row?._dirty).length)
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 selected.value.some(item => item._row_key === row._row_key)
}
function setRowSelected (row, value) {
if (value) {
if (!isRowSelected(row)) selected.value = [...selected.value, row]
return
}
selected.value = selected.value.filter(item => item._row_key !== row._row_key)
}
function toggleSelectAllVisible (value) {
const keys = new Set(visibleRowKeys.value)
const remaining = selected.value.filter(row => !keys.has(row._row_key))
selected.value = value ? [...remaining, ...filteredRows.value] : remaining
}
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 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 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)
selected.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 = selected.value.filter(row => 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
}
}
onMounted(loadRows)
onBeforeUnmount(() => {
if (emptyRetryTimer) clearTimeout(emptyRetryTimer)
})
</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;
}
.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.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;
}
.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;
}
</style>