ui: update ProductPerformanceProfitability page to enhance table views and tabs management

This commit is contained in:
M_Kececi
2026-07-02 23:08:02 +03:00
parent f3d4661cff
commit d419f52e3c
6 changed files with 1373 additions and 134 deletions
+756 -89
View File
@@ -8,6 +8,36 @@
</div>
</div>
<div class="col-12 col-md-auto row items-center q-gutter-sm">
<q-option-group
v-model="selectedPeriods"
:options="periodOptions"
type="checkbox"
inline
dense
class="period-selector bg-white q-px-sm q-py-xs"
/>
<q-btn-dropdown
dense
outline
color="primary"
:icon="allProductGroupsExpanded ? 'unfold_less' : 'unfold_more'"
:label="allProductGroupsExpanded ? 'Detayları Kapat' : 'Tüm Detayları '"
@click="toggleAllProductGroups"
>
<div class="detail-level-menu">
<q-option-group
v-model="activeSelectedExpandLevelKeys"
:options="expandLevelOptions"
type="checkbox"
dense
/>
<q-separator class="q-my-sm" />
<div class="row items-center justify-end q-gutter-xs">
<q-btn dense flat color="grey-7" label="Kapat" @click="expandedGroups = {}" />
<q-btn dense color="primary" label="Seçileni Aç" @click="expandSelectedProductGroups" />
</div>
</div>
</q-btn-dropdown>
<q-btn outline color="secondary" icon="refresh" label="Yenile" :loading="loading" @click="reload" />
</div>
</div>
@@ -32,13 +62,13 @@
align="left"
>
<q-tab name="products" icon="inventory_2" label="Ürün KPI" />
<q-tab name="general" icon="analytics" label="KPI Genel" />
<q-tab name="order_product_customers" icon="assignment_ind" label="Ürün > Müşteri Sipariş" />
<q-tab name="order_market_details" icon="account_tree" label="Piyasa > Sipariş Detay" />
<q-tab name="sales_color_yaka_market_customer" icon="palette" label="Renk > Yaka > Piyasa > Müşteri" />
<q-tab name="idle" icon="warning" label="Atıl Stok / Maliyet" />
<q-tab name="markets" icon="query_stats" label="Piyasa Grup Kıyası" />
<q-tab name="countries" icon="public" label="Ülke / Segment" />
<q-tab name="customers" icon="groups" label="Müşteri Kırılımı" />
<q-tab name="sales_product_country_segment_market_customer" icon="account_tree" label="Ürün > Ülke > Segment > Piyasa > Müşteri" />
<q-tab name="sales_market_customer_product" icon="groups" label="Piyasa > Müşteri > Ürün Satış KPI" />
<q-tab name="sales_country_segment_market_customer_product" icon="public" label="Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI" />
<q-tab name="order_product_customers" icon="assignment_ind" label="Ürün > Piyasa > Müşteri Sipariş" />
<q-tab name="order_market_details" icon="receipt_long" label="Piyasa > Müşteri > Ürün Sipariş" />
</q-tabs>
<q-table
@@ -370,9 +400,9 @@
flat
bordered
row-key="row_key"
class="performance-table bg-white"
class="performance-table product-breakdown-table bg-white"
:rows="productTableRows"
:columns="columns"
:columns="productColumns"
:loading="loading"
:pagination="pagination"
@request="onRequest"
@@ -455,8 +485,23 @@
:class="['group-row', `group-row-level-${Math.min(Number(props.row.level || 0), 5)}`]"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
<template v-if="col.name === 'image' && groupShowsImage(props.row)">
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
<q-img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
fit="contain"
class="product-thumb"
no-spinner
/>
<div v-else class="product-thumb-placeholder">
<q-icon name="image" size="22px" />
</div>
<q-tooltip>Foto</q-tooltip>
</button>
</template>
<div
v-if="col.name === groupLabelColumnName(props.row)"
v-else-if="col.name === groupLabelColumnName(props.row)"
class="group-cell-label"
:style="{ paddingLeft: `${props.row.level * 18}px` }"
>
@@ -710,6 +755,111 @@
</template>
</q-table>
<q-table
v-else-if="salesBreakdownTabKeys.includes(activeTab)"
flat
bordered
row-key="row_key"
class="performance-table bg-white"
:rows="filteredActiveSalesBreakdownRows"
:columns="visibleSalesBreakdownColumns"
:loading="loading"
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score', descending: true }"
>
<template #header-cell="props">
<q-th :props="props" class="filterable-header-cell">
<div class="header-with-filter">
<span class="header-label" :title="props.col.label">{{ props.col.label }}</span>
<q-btn
v-if="isColumnFilterable(props.col.name)"
dense
flat
round
size="8px"
icon="filter_alt"
:color="hasColumnFilter(activeTab, props.col.name) ? 'primary' : 'grey-7'"
class="header-filter-btn"
@click.stop
>
<q-badge v-if="hasColumnFilter(activeTab, props.col.name)" color="primary" floating rounded>
{{ getColumnFilterBadgeValue(activeTab, props.col.name) }}
</q-badge>
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
<div class="excel-filter-menu">
<q-input
v-model="columnFilterSearch[filterKey(activeTab, props.col.name)]"
dense
outlined
clearable
class="excel-filter-select"
placeholder="Ara"
/>
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
<q-btn flat dense size="sm" label="Tümünü Seç" @click="selectAllColumnFilterOptions(activeTab, props.col.name)" />
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(activeTab, props.col.name)" />
</div>
<q-virtual-scroll
v-if="columnFilterOptions(activeTab, props.col.name).length > 0"
class="excel-filter-options"
:items="columnFilterOptions(activeTab, props.col.name)"
:virtual-scroll-item-size="32"
separator
>
<template #default="{ item: option }">
<q-item
:key="`${props.col.name}-${option.value}`"
dense
clickable
class="excel-filter-option"
@click="toggleColumnFilterValue(activeTab, props.col.name, option.value)"
>
<q-item-section avatar>
<q-checkbox
dense
size="sm"
:model-value="isColumnFilterValueSelected(activeTab, props.col.name, option.value)"
@update:model-value="() => toggleColumnFilterValue(activeTab, props.col.name, option.value)"
@click.stop
/>
</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">Sonuç yok</div>
</div>
</q-menu>
</q-btn>
<q-btn v-else dense flat round size="8px" icon="filter_alt" class="header-filter-btn header-filter-ghost" tabindex="-1" />
</div>
</q-th>
</template>
<template #body-cell-sales_usd_90d="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.sales_usd_90d, 'USD') }}
</q-td>
</template>
<template #body-cell-sales_usd_365d="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.sales_usd_365d, 'USD') }}
</q-td>
</template>
<template #body-cell-avg_price_usd_90d="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.avg_price_usd_90d, 'USD') }}
</q-td>
</template>
<template #body-cell-performance_bucket="props">
<q-td :props="props">
<q-badge :color="bucketColor(props.row.performance_bucket)">
{{ bucketLabel(props.row.performance_bucket) }}
</q-badge>
</q-td>
</template>
</q-table>
<div v-else-if="activeTab === 'customers'">
<q-card flat bordered class="q-mb-sm">
<q-card-section class="row q-col-gutter-sm items-center">
@@ -1034,9 +1184,16 @@ const orderAnalysisMode = ref('product')
const marketRows = ref([])
const countryRows = ref([])
const customerRows = ref([])
const salesBreakdownRows = reactive({
sales_color_yaka_market_customer: [],
sales_product_country_segment_market_customer: [],
sales_market_customer_product: [],
sales_country_segment_market_customer_product: []
})
const summary = ref({})
const activeTab = ref('products')
const customerBreakdown = ref('market_customer')
const selectedPeriods = ref(['90', '180', '360', 'total'])
const imageUrlByKey = ref({})
const imageListByKey = ref({})
const imageDialog = ref(false)
@@ -1055,6 +1212,16 @@ const detailStockSizes = ref([])
const columnFilters = reactive({})
const columnFilterSearch = reactive({})
const expandedGroups = ref({})
const selectedExpandLevelKeysByTab = reactive({
products: ['kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'market_key'],
sales_color_yaka_market_customer: ['color_code', 'yaka_kodu', 'market_key', 'customer_code'],
idle: ['kategori', 'askili_yan', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'product_code'],
sales_product_country_segment_market_customer: ['product_code', 'country', 'customer_segment', 'market_key', 'customer_code'],
sales_market_customer_product: ['market_key', 'customer_code', 'product_code', 'color_code', 'yaka_kodu'],
sales_country_segment_market_customer_product: ['country', 'customer_segment', 'market_key', 'customer_code', 'product_code', 'color_code', 'yaka_kodu'],
order_product_customers: ['product_code', 'market_key', 'customer_code'],
order_market_details: ['market_key', 'customer_code', 'product_code', 'color_code', 'yaka_kodu']
})
const productKpiFetchLimit = 50000
const pagination = ref({
@@ -1071,15 +1238,32 @@ const bucketOptions = [
{ label: 'Stok Riski', value: 'STOK_RISKI' },
{ label: 'Fiyat Baskısı', value: 'FIYAT_BASKISI' },
{ label: 'Fiyat Fırsatı', value: 'FIYAT_FIRSATI' },
{ label: 'Maliyet Yok', value: 'MALIYET_YOK' },
{ label: 'Takip', value: 'TAKIP' }
]
const periodOptions = [
{ label: '90G', value: '90' },
{ label: '180G', value: '180' },
{ label: '360G', value: '360' },
{ label: 'Genel', value: 'total' }
]
const customerBreakdownOptions = [
{ label: 'Piyasa > Müşteri', value: 'market_customer' },
{ label: 'Ülke > Müşteri', value: 'country_customer' },
{ label: 'Piyasa > Ülke > Müşteri', value: 'market_country_customer' }
]
const salesBreakdownModes = {
sales_color_yaka_market_customer: 'color_yaka_market_customer',
sales_product_country_segment_market_customer: 'product_country_segment_market_customer',
sales_market_customer_product: 'market_customer_product',
sales_country_segment_market_customer_product: 'country_segment_market_customer_product'
}
const salesBreakdownTabKeys = Object.keys(salesBreakdownModes)
const detailStockColumns = [
{ name: 'size_code', label: 'Beden', field: 'size_code', align: 'left', sortable: true },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true }
@@ -1088,7 +1272,7 @@ const detailStockColumns = [
const detailSalesColumns = [
{ name: 'sales_date', label: 'Tarih', field: 'sales_date', align: 'left', sortable: true },
{ name: 'ref_number', label: 'Ref', field: 'ref_number', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'country', label: 'Ülke', field: 'country', align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
@@ -1104,40 +1288,75 @@ const columns = [
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left' },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left' },
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_180d', label: '180G Satış', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_365d', label: '360G Satış', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_total', label: 'Genel Satış', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'sales_usd_180d', label: '180G USD', field: 'sales_usd_180d', align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '360G USD', field: 'sales_usd_365d', align: 'right', sortable: true },
{ name: 'sales_usd_total', label: 'Genel USD', field: 'sales_usd_total', align: 'right', sortable: true },
{ name: 'stock_days_90d', label: 'Stok Gün', field: row => formatNumber(row.stock_days_90d, 1), align: 'right', sortable: true },
{ name: 'stock_days_180d', label: '180G Stok Gün', field: row => formatNumber(row.stock_days_180d, 1), align: 'right', sortable: true },
{ name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_180d', label: '180G Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_365d', label: '360G Ort. USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right' },
{ name: 'avg_price_usd_total', label: 'Genel Ort. USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right' },
{ name: 'base_price_usd', label: 'Ort. USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd', label: 'Ort. USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_base_90d', label: '90G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_90d, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_cost_90d', label: '90G P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_90d, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_base_180d', label: '180G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_180d, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_cost_180d', label: '180G P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_180d, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_base_total', label: 'Genel P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_total, 'USD'), align: 'right', sortable: true },
{ name: 'unit_profit_cost_total', label: 'Genel P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_total, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_usd_90d', label: '90G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_usd_180d', label: '180G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_usd_total', label: 'Genel Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'gross_margin_base_90d', label: '90G Taban Marj', field: row => formatPercent(row.gross_margin_base_90d), align: 'right', sortable: true },
{ name: 'gross_margin_cost_90d', label: '90G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_90d), align: 'right', sortable: true },
{ name: 'gross_margin_base_180d', label: '180G Taban Marj', field: row => formatPercent(row.gross_margin_base_180d), align: 'right', sortable: true },
{ name: 'gross_margin_cost_180d', label: '180G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_180d), align: 'right', sortable: true },
{ name: 'gross_margin_base_total', label: 'Genel Taban Marj', field: row => formatPercent(row.gross_margin_base_total), align: 'right', sortable: true },
{ name: 'gross_margin_cost_total', label: 'Genel Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_total), align: 'right', sortable: true },
{ name: 'market_count_90d', label: '90G Piyasa', field: row => formatNumber(row.market_count_90d, 0), align: 'right', sortable: true },
{ name: 'customer_count_90d', label: '90G Müşteri', field: row => formatNumber(row.customer_count_90d, 0), align: 'right', sortable: true },
{ name: 'market_count_total', label: 'Genel Piyasa', field: row => formatNumber(row.market_count_total, 0), align: 'right', sortable: true },
{ name: 'customer_count_total', label: 'Genel Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
{ name: 'sales_index_90d', label: 'Piyasa End.', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
{ name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true },
{ name: 'performance_score', label: 'Skor', field: row => formatNumber(row.performance_score, 1), align: 'right', sortable: true },
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' },
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
]
const periodColumnRules = [
{ period: '90', match: name => name.includes('_90d') },
{ period: '180', match: name => name.includes('_180d') },
{ period: '360', match: name => name.includes('_365d') },
{ period: 'total', match: name => name.includes('_total') }
]
const selectedPeriodSet = computed(() => new Set(selectedPeriods.value))
function columnPeriod (name) {
return periodColumnRules.find(rule => rule.match(name))?.period || ''
}
function visiblePeriodColumn (col) {
const period = columnPeriod(col.name)
return !period || selectedPeriodSet.value.has(period)
}
const productColumns = computed(() => columns.filter(visiblePeriodColumn))
const generalColumns = [
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
@@ -1145,11 +1364,11 @@ const generalColumns = [
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left', sortable: true },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left', sortable: true },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'period_start', label: 'Başlangıç', field: 'period_start', align: 'left', sortable: true },
{ name: 'period_end', label: 'Bitiş', field: 'period_end', align: 'left', sortable: true },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
@@ -1183,11 +1402,11 @@ const orderAnalysisColumns = [
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left', sortable: true },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left', sortable: true },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
{ name: 'order_usd', label: 'Sipariş USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
{ name: 'avg_order_price_usd', label: 'Ort. Sipariş USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
@@ -1212,7 +1431,7 @@ const orderAnalysisColumns = [
const orderGroupColumns = [
{ name: 'group_key', label: 'Kırılım', field: row => orderGroupLabel(row), align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'product_count', label: 'Ürün', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
@@ -1242,7 +1461,7 @@ const orderProductCustomerColumns = [
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
@@ -1264,7 +1483,7 @@ const orderProductCustomerColumns = [
]
const orderMarketDetailColumns = [
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'order_number', label: 'Sipariş No', field: 'order_number', align: 'left', sortable: true },
@@ -1295,8 +1514,8 @@ const idleColumns = [
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left' },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left' },
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
@@ -1309,10 +1528,10 @@ const idleColumns = [
]
const marketColumns = [
{ name: 'market_key', label: 'Piyasa Grubu', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left' },
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left' },
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left' },
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left' },
{ name: 'product_count', label: 'Ürün', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
@@ -1330,19 +1549,19 @@ const marketColumns = [
const countryColumns = [
{ name: 'country', label: 'Ülke', field: 'country', align: 'left', sortable: true },
{ name: 'customer_segment', label: 'Müşteri Segmenti', field: 'customer_segment', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa Grubu', field: 'market_key', align: 'left' },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left' },
{ name: 'product_count', label: 'Ürün', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: row => formatMoney(row.sales_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'customer_count_90d', label: 'Müşteri', field: row => formatNumber(row.customer_count_90d, 0), align: 'right', sortable: true },
{ name: 'invoice_count_90d', label: 'Fatura', field: row => formatNumber(row.invoice_count_90d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_365d', label: '365G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '365G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true }
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true }
]
const customerColumns = [
{ name: 'market_key', label: 'Piyasa Grubu', field: 'market_key', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'country', label: 'Ülke', field: 'country', align: 'left', sortable: true },
{ name: 'customer_segment', label: 'Segment', field: 'customer_segment', align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
@@ -1351,12 +1570,74 @@ const customerColumns = [
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'base_price_usd_90d', label: '90G Taban', field: row => formatMoney(row.base_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_90d', label: '90G Çıplak', field: row => formatMoney(row.cost_price_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_90d', label: '90G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_cost_usd_90d', label: '90G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_90d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_margin_base_90d', label: '90G Taban Marj', field: row => formatPercent(row.gross_margin_base_90d), align: 'right', sortable: true },
{ name: 'gross_margin_cost_90d', label: '90G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_90d), align: 'right', sortable: true },
{ name: 'invoice_count_90d', label: 'Fatura', field: row => formatNumber(row.invoice_count_90d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_365d', label: '365G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '365G USD', field: 'sales_usd_365d', align: 'right', sortable: true },
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '360G USD', field: 'sales_usd_365d', align: 'right', sortable: true },
{ name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true }
]
const salesBreakdownColumns = [
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
{ name: 'country', label: 'Ülke', field: 'country', align: 'left', sortable: true },
{ name: 'customer_segment', label: 'Segment', field: 'customer_segment', align: 'left', sortable: true },
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
{ name: 'product_count', label: 'Ürün Sayısı', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
{ name: 'customer_count_90d', label: '90G Müşteri', field: row => formatNumber(row.customer_count_90d, 0), align: 'right', sortable: true },
{ name: 'invoice_count_90d', label: '90G Fatura', field: row => formatNumber(row.invoice_count_90d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_90d', label: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
{ name: 'avg_price_usd_90d', label: 'Ort. USD', field: 'avg_price_usd_90d', align: 'right', sortable: true },
{ name: 'customer_count_180d', label: '180G Müşteri', field: row => formatNumber(row.customer_count_180d, 0), align: 'right', sortable: true },
{ name: 'invoice_count_180d', label: '180G Fatura', field: row => formatNumber(row.invoice_count_180d, 0), align: 'right', sortable: true },
{ name: 'sales_qty_180d', label: '180G Adet', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_180d', label: '180G USD', field: row => formatMoney(row.sales_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_180d', label: '180G Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'base_price_usd_180d', label: '180G Taban', field: row => formatMoney(row.base_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_180d', label: '180G Çıplak', field: row => formatMoney(row.cost_price_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_180d', label: '180G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_cost_usd_180d', label: '180G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_180d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_margin_base_180d', label: '180G Taban Marj', field: row => formatPercent(row.gross_margin_base_180d), align: 'right', sortable: true },
{ name: 'gross_margin_cost_180d', label: '180G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_180d), align: 'right', sortable: true },
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
{ name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_365d', label: '360G Ort. USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'base_price_usd_365d', label: '360G Taban', field: row => formatMoney(row.base_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_365d', label: '360G Çıplak', field: row => formatMoney(row.cost_price_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_365d', label: '360G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_cost_usd_365d', label: '360G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_365d, 'USD'), align: 'right', sortable: true },
{ name: 'gross_margin_base_365d', label: '360G Taban Marj', field: row => formatPercent(row.gross_margin_base_365d), align: 'right', sortable: true },
{ name: 'gross_margin_cost_365d', label: '360G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_365d), align: 'right', sortable: true },
{ name: 'customer_count_total', label: 'Genel Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
{ name: 'invoice_count_total', label: 'Genel Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
{ name: 'sales_qty_total', label: 'Genel Adet', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
{ name: 'sales_usd_total', label: 'Genel USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'avg_price_usd_total', label: 'Genel Ort. USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'base_price_usd_total', label: 'Genel Taban', field: row => formatMoney(row.base_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'cost_price_usd_total', label: 'Genel Çıplak', field: row => formatMoney(row.cost_price_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_base_usd_total', label: 'Genel Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'gross_profit_cost_usd_total', label: 'Genel Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_total, 'USD'), align: 'right', sortable: true },
{ name: 'gross_margin_base_total', label: 'Genel Taban Marj', field: row => formatPercent(row.gross_margin_base_total), align: 'right', sortable: true },
{ name: 'gross_margin_cost_total', label: 'Genel Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_total), align: 'right', sortable: true },
{ name: 'sales_index_90d', label: 'Endeks', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
{ name: 'performance_score', label: 'Skor', field: row => formatNumber(row.performance_score, 1), align: 'right', sortable: true },
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
{ name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true },
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
]
const visibleSalesBreakdownColumns = computed(() => salesBreakdownColumns.filter(visiblePeriodColumn))
const summaryCards = computed(() => [
{ key: 'date', label: 'KPI Tarihi', value: summary.value.kpi_date || '-' },
{ key: 'rows', label: 'Satır', value: formatNumber(summary.value.total_rows, 0) },
@@ -1448,26 +1729,110 @@ const performanceCards = computed(() => {
const groupLevels = [
{ key: 'kategori', label: 'Kategori' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'color_code', label: 'Renk' }
{ key: 'market_key', label: 'Piyasa' },
{ key: 'product_code', label: 'Ürün Kodu' }
]
const tabGroupLevels = {
products: groupLevels,
sales_color_yaka_market_customer: [
{ key: 'color_code', label: 'Renk' },
{ key: 'yaka_kodu', label: 'Yaka' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' }
],
idle: [
{ key: 'kategori', label: 'Kategori' },
{ key: 'askili_yan', label: 'Askılı/Yan' },
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu', fallback: 'seri' },
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
{ key: 'product_code', label: 'Ürün Kodu' }
],
sales_product_country_segment_market_customer: [
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'country', label: 'Ülke' },
{ key: 'customer_segment', label: 'Segment' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' }
],
sales_market_customer_product: [
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' },
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'color_code', label: 'Renk' },
{ key: 'yaka_kodu', label: 'Yaka' }
],
sales_country_segment_market_customer_product: [
{ key: 'country', label: 'Ülke' },
{ key: 'customer_segment', label: 'Segment' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' },
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'color_code', label: 'Renk' },
{ key: 'yaka_kodu', label: 'Yaka' }
],
order_product_customers: [
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' }
],
order_market_details: [
{ key: 'market_key', label: 'Piyasa' },
{ key: 'customer_code', label: 'Müşteri' },
{ key: 'product_code', label: 'Ürün Kodu' },
{ key: 'color_code', label: 'Renk' },
{ key: 'yaka_kodu', label: 'Yaka' }
]
}
const activeGroupLevels = computed(() => tabGroupLevels[activeTab.value] || groupLevels)
const activeSelectedExpandLevelKeys = computed({
get: () => selectedExpandLevelKeysByTab[activeTab.value] || [],
set: value => {
selectedExpandLevelKeysByTab[activeTab.value] = Array.isArray(value) ? value : []
}
})
const expandLevelOptions = computed(() => activeGroupLevels.value.map(level => ({
label: level.label,
value: level.key
})))
const selectedExpandThroughLevel = computed(() => {
const selected = new Set(activeSelectedExpandLevelKeys.value)
const indexes = activeGroupLevels.value
.map((level, index) => selected.has(level.key) ? index : -1)
.filter(index => index >= 0)
return indexes.length ? Math.max(...indexes) : -1
})
const autoExpandThroughLevel = computed(() => {
const selectedLevel = selectedExpandThroughLevel.value
return selectedLevel >= 0 ? selectedLevel : activeGroupLevels.value.length - 1
})
const tableFilterSourceMap = computed(() => ({
products: { rows: rows.value, columns },
products: { rows: rows.value, columns: productColumns.value },
general: { rows: generalRows.value, columns: generalColumns },
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
idle: { rows: idleRows.value, columns: idleColumns },
markets: { rows: marketRows.value, columns: marketColumns },
countries: { rows: countryRows.value, columns: countryColumns },
customers: { rows: customerRows.value, columns: customerColumns }
customers: { rows: customerRows.value, columns: customerColumns },
sales_color_yaka_market_customer: { rows: salesBreakdownRows.sales_color_yaka_market_customer, columns: visibleSalesBreakdownColumns.value },
sales_product_country_segment_market_customer: { rows: salesBreakdownRows.sales_product_country_segment_market_customer, columns: visibleSalesBreakdownColumns.value },
sales_market_customer_product: { rows: salesBreakdownRows.sales_market_customer_product, columns: visibleSalesBreakdownColumns.value },
sales_country_segment_market_customer_product: { rows: salesBreakdownRows.sales_country_segment_market_customer_product, columns: visibleSalesBreakdownColumns.value }
}))
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, columns))
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, productColumns.value))
const filteredGeneralRows = computed(() => filterRowsForTable('general', generalRows.value, generalColumns))
const filteredOrderProductCustomerRows = computed(() => filterRowsForTable('order_product_customers', orderProductCustomerRows.value, orderProductCustomerColumns))
const filteredOrderMarketDetailRows = computed(() => filterRowsForTable('order_market_details', orderMarketDetailRows.value, orderMarketDetailColumns))
@@ -1475,6 +1840,7 @@ const filteredIdleRows = computed(() => filterRowsForTable('idle', idleRows.valu
const filteredMarketRows = computed(() => filterRowsForTable('markets', marketRows.value, marketColumns))
const filteredCountryRows = computed(() => filterRowsForTable('countries', countryRows.value, countryColumns))
const filteredCustomerRows = computed(() => filterRowsForTable('customers', customerRows.value, customerColumns))
const filteredActiveSalesBreakdownRows = computed(() => filterRowsForTable(activeTab.value, salesBreakdownRows[activeTab.value] || [], visibleSalesBreakdownColumns.value))
const columnFilterOptionMap = computed(() => {
const out = {}
@@ -1502,17 +1868,34 @@ const columnFilterOptionMap = computed(() => {
const productTableRows = computed(() => {
const out = []
appendGroupRows(out, filteredProductRows.value, 0, [])
appendGroupRows(out, filteredProductRows.value, 0, [], groupLevels)
return out
})
function appendGroupRows (out, sourceRows, level, parentKeys) {
if (level >= groupLevels.length) {
out.push(...sourceRows)
const productGroupKeys = computed(() => {
const keys = []
collectGroupKeys(keys, filteredProductRows.value, 0, [], groupLevels)
return keys
})
const productAutoExpandKeys = computed(() => {
const keys = []
collectGroupKeys(keys, filteredProductRows.value, 0, [], activeGroupLevels.value, autoExpandThroughLevel.value)
return keys
})
const allProductGroupsExpanded = computed(() => {
const keys = productAutoExpandKeys.value
return keys.length > 0 && keys.every(key => expandedGroups.value[key] === true)
})
function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels) {
if (level >= levels.length) {
out.push(...sortProductLeafRows(sourceRows))
return
}
const groupDef = groupLevels[level]
const groupDef = levels[level]
const grouped = new Map()
for (const row of sourceRows) {
const value = normalizeGroupValue(rawProductCellValue(row, groupDef.key) || row[groupDef.fallback])
@@ -1526,12 +1909,45 @@ function appendGroupRows (out, sourceRows, level, parentKeys) {
const key = [...parentKeys, `${groupDef.key}:${value}`].join('|')
out.push(makeGroupRow(key, level, groupDef, value, groupRows))
if (isGroupExpanded(key)) {
appendGroupRows(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`])
appendGroupRows(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`], levels)
}
})
}
function collectGroupKeys (out, sourceRows, level, parentKeys, levels = groupLevels, maxLevel = levels.length - 1) {
if (level >= levels.length) return
const groupDef = levels[level]
const grouped = new Map()
for (const row of sourceRows) {
const value = normalizeGroupValue(rawProductCellValue(row, groupDef.key) || row[groupDef.fallback])
if (!grouped.has(value)) grouped.set(value, [])
grouped.get(value).push(row)
}
Array.from(grouped.entries())
.sort((a, b) => a[0].localeCompare(b[0], 'tr'))
.forEach(([value, groupRows]) => {
const key = [...parentKeys, `${groupDef.key}:${value}`].join('|')
out.push(key)
if (level < maxLevel) {
collectGroupKeys(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`], levels, maxLevel)
}
})
}
function sortProductLeafRows (sourceRows) {
return [...sourceRows].sort((a, b) => {
const colorCmp = String(a?.color_code || '').localeCompare(String(b?.color_code || ''), 'tr', { numeric: true })
if (colorCmp !== 0) return colorCmp
const yakaCmp = String(a?.yaka_kodu || '').localeCompare(String(b?.yaka_kodu || ''), 'tr', { numeric: true })
if (yakaCmp !== 0) return yakaCmp
return String(a?.item_description || '').localeCompare(String(b?.item_description || ''), 'tr', { numeric: true })
})
}
function makeGroupRow (key, level, groupDef, value, groupRows) {
const imageSource = groupRows.find(row => row?.product_code) || {}
return {
__group: true,
row_key: `group|${key}`,
@@ -1539,8 +1955,11 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
level,
group_field: groupDef.key,
group_value: value,
label: `${groupDef.label}: ${value}`,
label: value,
count: groupRows.length,
image_product_code: imageSource.product_code || '',
image_color_code: imageSource.color_code || '',
image_yaka_kodu: imageSource.yaka_kodu || '',
product_code: groupDef.key === 'product_code' ? value : distinctSummary(groupRows, 'product_code'),
color_code: groupDef.key === 'color_code' ? value : distinctSummary(groupRows, 'color_code'),
yaka_kodu: groupDef.key === 'yaka_kodu' ? value : distinctSummary(groupRows, 'yaka_kodu'),
@@ -1550,33 +1969,48 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
askili_yan: groupDef.key === 'askili_yan' ? value : distinctSummary(groupRows, 'askili_yan'),
urun_ana_grubu: groupDef.key === 'urun_ana_grubu' ? value : distinctSummary(groupRows, 'urun_ana_grubu', 'seri'),
urun_alt_grubu: groupDef.key === 'urun_alt_grubu' ? value : distinctSummary(groupRows, 'urun_alt_grubu'),
market_key: distinctSummary(groupRows, 'market_key'),
market_key: groupDef.key === 'market_key' ? value : distinctSummary(groupRows, 'market_key'),
stock_qty: sumRows(groupRows, 'stock_qty'),
sales_qty_90d: sumRows(groupRows, 'sales_qty_90d'),
sales_qty_180d: sumRows(groupRows, 'sales_qty_180d'),
sales_qty_365d: sumRows(groupRows, 'sales_qty_365d'),
sales_qty_total: sumRows(groupRows, 'sales_qty_total'),
sales_usd_90d: sumRows(groupRows, 'sales_usd_90d'),
sales_usd_180d: sumRows(groupRows, 'sales_usd_180d'),
sales_usd_365d: sumRows(groupRows, 'sales_usd_365d'),
sales_usd_total: sumRows(groupRows, 'sales_usd_total'),
gross_profit_usd_90d: sumRows(groupRows, 'gross_profit_usd_90d'),
gross_profit_usd_180d: sumRows(groupRows, 'gross_profit_usd_180d'),
gross_profit_usd_total: sumRows(groupRows, 'gross_profit_usd_total'),
market_count_90d: distinctCount(groupRows, 'market_key'),
customer_count_90d: sumRows(groupRows, 'customer_count_90d'),
market_count_total: sumRows(groupRows, 'market_count_total'),
customer_count_total: sumRows(groupRows, 'customer_count_total'),
avg_price_usd_90d: weightedAverage(groupRows, 'sales_usd_90d', 'sales_qty_90d'),
avg_price_usd_180d: weightedAverage(groupRows, 'sales_usd_180d', 'sales_qty_180d'),
avg_price_usd_365d: weightedAverage(groupRows, 'sales_usd_365d', 'sales_qty_365d'),
avg_price_usd_total: weightedAverage(groupRows, 'sales_usd_total', 'sales_qty_total'),
base_price_usd: weightedAverageOrAverage(groupRows, 'base_price_usd', 'sales_qty_90d'),
cost_price_usd: weightedAverageOrAverage(groupRows, 'cost_price_usd', 'sales_qty_90d'),
unit_profit_base_90d: weightedAverageOrAverage(groupRows, 'unit_profit_base_90d', 'sales_qty_90d'),
unit_profit_cost_90d: weightedAverageOrAverage(groupRows, 'unit_profit_cost_90d', 'sales_qty_90d'),
unit_profit_base_180d: weightedAverageOrAverage(groupRows, 'unit_profit_base_180d', 'sales_qty_180d'),
unit_profit_cost_180d: weightedAverageOrAverage(groupRows, 'unit_profit_cost_180d', 'sales_qty_180d'),
unit_profit_base_total: weightedAverageOrAverage(groupRows, 'unit_profit_base_total', 'sales_qty_total'),
unit_profit_cost_total: weightedAverageOrAverage(groupRows, 'unit_profit_cost_total', 'sales_qty_total'),
gross_margin_base_90d: marginFromRows(groupRows, 'sales_usd_90d', 'sales_qty_90d', 'base_price_usd'),
gross_margin_cost_90d: marginFromRows(groupRows, 'sales_usd_90d', 'sales_qty_90d', 'cost_price_usd'),
gross_margin_base_180d: marginFromRows(groupRows, 'sales_usd_180d', 'sales_qty_180d', 'base_price_usd'),
gross_margin_cost_180d: marginFromRows(groupRows, 'sales_usd_180d', 'sales_qty_180d', 'cost_price_usd'),
gross_margin_base_total: marginFromRows(groupRows, 'sales_usd_total', 'sales_qty_total', 'base_price_usd'),
gross_margin_cost_total: marginFromRows(groupRows, 'sales_usd_total', 'sales_qty_total', 'cost_price_usd'),
gross_margin_90d: ratio(sumRows(groupRows, 'gross_profit_usd_90d'), sumRows(groupRows, 'sales_usd_90d')),
gross_margin_180d: ratio(sumRows(groupRows, 'gross_profit_usd_180d'), sumRows(groupRows, 'sales_usd_180d')),
stock_days_90d: weightedAverageOrAverage(groupRows, 'stock_days_90d', 'stock_qty'),
stock_days_180d: weightedAverageOrAverage(groupRows, 'stock_days_180d', 'stock_qty'),
stock_days_total: weightedAverageOrAverage(groupRows, 'stock_days_total', 'stock_qty'),
sales_index_90d: averageRows(groupRows, 'sales_index_90d'),
sales_index_total: averageRows(groupRows, 'sales_index_total'),
performance_score: weightedAverageOrAverage(groupRows, 'performance_score', 'sales_qty_90d'),
performance_bucket: dominantValue(groupRows, 'performance_bucket'),
recommendation: `${formatNumber(groupRows.length, 0)} satır`
@@ -1688,12 +2122,33 @@ function toggleGroup (key) {
}
}
function toggleAllProductGroups () {
const keys = productAutoExpandKeys.value
if (allProductGroupsExpanded.value) {
expandedGroups.value = {}
return
}
expandSelectedProductGroups()
}
function expandSelectedProductGroups () {
const keys = productAutoExpandKeys.value
const next = { ...expandedGroups.value }
for (const key of keys) next[key] = true
expandedGroups.value = next
}
function groupLabelColumnName (row) {
return row?.group_field || 'product_code'
}
function groupShowsImage (row) {
return Number(row?.level ?? 0) < autoExpandThroughLevel.value
}
function groupCellClass (name) {
return ['stock_qty', 'sales_qty_90d', 'sales_qty_180d', 'sales_usd_90d', 'sales_usd_180d', 'stock_days_90d', 'stock_days_180d', 'avg_price_usd_90d', 'avg_price_usd_180d', 'base_price_usd', 'cost_price_usd', 'unit_profit_base_90d', 'unit_profit_cost_90d', 'unit_profit_base_180d', 'unit_profit_cost_180d', 'gross_profit_usd_90d', 'gross_profit_usd_180d', 'gross_margin_base_90d', 'gross_margin_cost_90d', 'gross_margin_base_180d', 'gross_margin_cost_180d', 'gross_margin_90d', 'gross_margin_180d', 'market_count_90d', 'customer_count_90d', 'sales_index_90d', 'performance_score'].includes(name)
if (name === 'image') return 'product-image-cell'
return ['stock_qty', 'sales_qty_90d', 'sales_qty_180d', 'sales_qty_365d', 'sales_qty_total', 'sales_usd_90d', 'sales_usd_180d', 'sales_usd_365d', 'sales_usd_total', 'stock_days_90d', 'stock_days_180d', 'stock_days_total', 'avg_price_usd_90d', 'avg_price_usd_180d', 'avg_price_usd_365d', 'avg_price_usd_total', 'base_price_usd', 'cost_price_usd', 'unit_profit_base_90d', 'unit_profit_cost_90d', 'unit_profit_base_180d', 'unit_profit_cost_180d', 'unit_profit_base_total', 'unit_profit_cost_total', 'gross_profit_usd_90d', 'gross_profit_usd_180d', 'gross_profit_usd_total', 'gross_margin_base_90d', 'gross_margin_cost_90d', 'gross_margin_base_180d', 'gross_margin_cost_180d', 'gross_margin_base_total', 'gross_margin_cost_total', 'gross_margin_90d', 'gross_margin_180d', 'market_count_90d', 'customer_count_90d', 'market_count_total', 'customer_count_total', 'sales_index_90d', 'sales_index_total', 'performance_score'].includes(name)
? 'text-right'
: ''
}
@@ -1707,6 +2162,7 @@ function formatGroupCell (row, name) {
function withProductMargins (row) {
return {
...row,
avg_price_usd_365d: Number(row?.sales_qty_365d || 0) > 0 ? Number(row?.sales_usd_365d || 0) / Number(row?.sales_qty_365d || 0) : 0,
gross_margin_base_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.base_price_usd),
gross_margin_cost_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.cost_price_usd),
gross_margin_base_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.base_price_usd),
@@ -1793,6 +2249,7 @@ function rawProductCellValue (row, name) {
switch (name) {
case 'urun_ilk_grubu': return row.urun_ilk_grubu || row.yas_grubu || ''
case 'urun_ana_grubu': return row.urun_ana_grubu || row.seri || ''
case 'market_key': return displayMarketName(row.market_key)
case 'performance_bucket': return bucketLabel(row.performance_bucket)
default: return row[name]
}
@@ -1803,6 +2260,7 @@ function rawColumnCellValue (row, col) {
switch (name) {
case 'urun_ilk_grubu': return row.urun_ilk_grubu || row.yas_grubu || ''
case 'urun_ana_grubu': return row.urun_ana_grubu || row.seri || ''
case 'market_key': return displayMarketName(row.market_key)
case 'performance_bucket': return bucketLabel(row.performance_bucket)
case 'is_overdue': return row.is_overdue ? 'Evet' : 'Hayır'
default:
@@ -1823,26 +2281,39 @@ function formatProductCell (row, name) {
case 'stock_qty':
case 'sales_qty_90d':
case 'sales_qty_180d':
case 'sales_qty_365d':
case 'sales_qty_total':
case 'market_count_90d':
case 'customer_count_90d':
case 'market_count_total':
case 'customer_count_total':
return formatNumber(row[name], 0)
case 'stock_days_90d':
case 'stock_days_180d':
case 'stock_days_total':
case 'sales_index_90d':
case 'sales_index_total':
case 'performance_score':
return formatNumber(row[name], name === 'performance_score' ? 1 : 2)
case 'sales_usd_90d':
case 'sales_usd_180d':
case 'sales_usd_365d':
case 'sales_usd_total':
case 'avg_price_usd_90d':
case 'avg_price_usd_180d':
case 'avg_price_usd_365d':
case 'avg_price_usd_total':
case 'base_price_usd':
case 'cost_price_usd':
case 'unit_profit_base_90d':
case 'unit_profit_cost_90d':
case 'unit_profit_base_180d':
case 'unit_profit_cost_180d':
case 'unit_profit_base_total':
case 'unit_profit_cost_total':
case 'gross_profit_usd_90d':
case 'gross_profit_usd_180d':
case 'gross_profit_usd_total':
return formatMoney(row[name], 'USD')
case 'gross_margin_base_90d':
case 'gross_margin_cost_90d':
@@ -1857,6 +2328,8 @@ function formatProductCell (row, name) {
return row.urun_ilk_grubu || row.yas_grubu || ''
case 'urun_ana_grubu':
return row.urun_ana_grubu || row.seri || ''
case 'market_key':
return displayMarketName(row.market_key)
default:
return rawProductCellValue(row, name) || ''
}
@@ -1873,6 +2346,35 @@ function normalizeRow (row) {
})
}
function performanceVariantKey (row) {
return [
String(row?.product_code || '').trim(),
String(row?.color_code || '').trim(),
String(row?.yaka_kodu || '').trim(),
displayMarketName(row?.market_key)
].join('|')
}
function generalMetricFields (row) {
return {
period_start: row?.period_start || '',
period_end: row?.period_end || '',
sales_qty_total: Number(row?.sales_qty_total || 0),
sales_usd_total: Number(row?.sales_usd_total || 0),
avg_daily_sales_total: Number(row?.avg_daily_sales_total || 0),
stock_days_total: Number(row?.stock_days_total || 0),
avg_price_usd_total: Number(row?.avg_price_usd_total || 0),
gross_profit_usd_total: Number(row?.gross_profit_usd_total || 0),
gross_margin_total: Number(row?.gross_margin_total || 0),
unit_profit_cost_total: Number(row?.unit_profit_cost_total || 0),
unit_profit_base_total: Number(row?.unit_profit_base_total || 0),
market_count_total: Number(row?.market_count_total || 0),
customer_count_total: Number(row?.customer_count_total || 0),
invoice_count_total: Number(row?.invoice_count_total || 0),
sales_index_total: Number(row?.sales_index_total || 0)
}
}
function normalizeGeneralRow (row) {
const product = String(row?.product_code || '').trim()
const color = String(row?.color_code || '').trim()
@@ -1923,6 +2425,13 @@ function normalizeOrderMarketDetailRow (row) {
}
}
function normalizeSalesBreakdownRow (row) {
return {
...row,
row_key: `sales-breakdown|${row?.breakdown || ''}|${row?.product_code || ''}|${row?.color_code || ''}|${row?.yaka_kodu || ''}|${row?.country || ''}|${row?.customer_segment || ''}|${row?.market_key || ''}|${row?.customer_code || ''}`
}
}
function orderGroupLabel (row) {
if (row?.breakdown === 'customer') {
return [row.customer_code, row.customer_name].filter(Boolean).join(' - ') || '-'
@@ -1931,12 +2440,17 @@ function orderGroupLabel (row) {
}
function productImageKey (row) {
const product = String(row?.product_code || '').trim()
const color = String(row?.color_code || '').trim()
const yaka = String(row?.yaka_kodu || '').trim()
const product = String(row?.image_product_code || row?.product_code || '').trim()
const color = String(row?.image_color_code || row?.color_code || '').trim()
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
return `${product}|${color}|${yaka}`
}
function displayMarketName (value) {
const parts = String(value || '').split('|').map(part => part.trim()).filter(Boolean)
return parts.length ? parts[parts.length - 1] : ''
}
function normalizeUploadsPath (storagePath) {
const raw = String(storagePath || '').trim()
if (!raw) return ''
@@ -1976,11 +2490,11 @@ async function fetchProductImagesForRow (row) {
return imageListByKey.value[key]
}
const code = String(row?.product_code || '').trim()
const code = String(row?.image_product_code || row?.product_code || '').trim()
if (!code) return []
const color = String(row?.color_code || '').trim()
const yaka = String(row?.yaka_kodu || '').trim()
const color = String(row?.image_color_code || row?.color_code || '').trim()
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
try {
const resp = await api.get('/product-images', {
@@ -2024,11 +2538,11 @@ async function openProductImageDialog (row) {
const urls = await fetchProductImagesForRow(row)
imageDialogUrls.value = urls
imageSlide.value = 0
imageDialogTitle.value = String(row?.product_code || '-')
imageDialogTitle.value = String(row?.image_product_code || row?.product_code || '-')
imageDialogSubtitle.value = [
row?.item_description,
row?.color_code ? `Renk: ${row.color_code}` : '',
row?.yaka_kodu ? `Yaka: ${row.yaka_kodu}` : ''
row?.image_color_code || row?.color_code,
row?.image_yaka_kodu || row?.yaka_kodu
].filter(Boolean).join(' | ')
imageDialog.value = true
}
@@ -2038,9 +2552,9 @@ async function openPerformanceDialog (row) {
performanceDialogTitle.value = String(row?.product_code || '-')
performanceDialogSubtitle.value = [
row?.item_description,
row?.color_code ? `Renk: ${row.color_code}` : '',
row?.yaka_kodu ? `Yaka: ${row.yaka_kodu}` : '',
row?.market_key ? `Piyasa: ${row.market_key}` : ''
row?.color_code,
row?.yaka_kodu,
row?.market_key
].filter(Boolean).join(' | ')
performanceDialog.value = true
detailLoading.value = true
@@ -2071,7 +2585,13 @@ async function openPerformanceDialog (row) {
async function reload () {
loading.value = true
try {
const [summaryResp, listResp, generalResp, marketResp, countryResp, customerResp] = await Promise.all([
const salesBreakdownRequests = salesBreakdownTabKeys.map(tabKey => {
return api.get('/pricing/product-performance/sales-breakdown', {
params: { limit: 800, mode: salesBreakdownModes[tabKey] },
timeout: 60000
})
})
const [summaryResp, listResp, generalResp, marketResp, countryResp, customerResp, ...salesBreakdownResp] = await Promise.all([
api.get('/pricing/product-performance/summary', { timeout: 60000 }),
api.get('/pricing/product-performance', {
params: {
@@ -2088,11 +2608,17 @@ async function reload () {
api.get('/pricing/product-performance/customers', {
params: { limit: 500, breakdown: customerBreakdown.value },
timeout: 60000
})
}),
...salesBreakdownRequests
])
summary.value = summaryResp?.data || {}
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow)
generalRows.value = (Array.isArray(generalResp?.data) ? generalResp.data : []).map(normalizeGeneralRow)
const normalizedGeneralRows = (Array.isArray(generalResp?.data) ? generalResp.data : []).map(normalizeGeneralRow)
const generalByVariant = new Map(normalizedGeneralRows.map(row => [performanceVariantKey(row), generalMetricFields(row)]))
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(row => {
const generalMetrics = generalByVariant.get(performanceVariantKey(row)) || {}
return normalizeRow({ ...row, ...generalMetrics })
})
generalRows.value = normalizedGeneralRows
marketRows.value = Array.isArray(marketResp?.data) ? marketResp.data : []
countryRows.value = (Array.isArray(countryResp?.data) ? countryResp.data : []).map(row => ({
...row,
@@ -2102,6 +2628,9 @@ async function reload () {
...row,
customer_key: `${row.breakdown || '-'}|${row.market_key || '-'}|${row.country || '-'}|${row.customer_segment || '-'}|${row.customer_code || '-'}`
}))
salesBreakdownTabKeys.forEach((tabKey, index) => {
salesBreakdownRows[tabKey] = (Array.isArray(salesBreakdownResp[index]?.data) ? salesBreakdownResp[index].data : []).map(normalizeSalesBreakdownRow)
})
pagination.value.rowsNumber = Number(listResp?.data?.total_count || 0)
if (activeTab.value === 'order_product_customers') {
await loadOrderProductCustomers(false)
@@ -2109,7 +2638,7 @@ async function reload () {
if (activeTab.value === 'order_market_details') {
await loadOrderMarketDetails(false)
}
void primeProductImages([...rows.value, ...generalRows.value])
void primeProductImages(rows.value)
} catch (err) {
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
} finally {
@@ -2180,6 +2709,7 @@ function bucketColor (value) {
case 'STOK_RISKI': return 'orange-8'
case 'FIYAT_BASKISI': return 'red-7'
case 'FIYAT_FIRSATI': return 'teal-7'
case 'MALIYET_YOK': return 'blue-grey-7'
default: return 'grey-7'
}
}
@@ -2193,6 +2723,14 @@ watch(activeTab, tab => {
}
})
watch(productTableRows, tableRows => {
if (activeTab.value !== 'products') return
const imageRows = tableRows
.filter(row => !row.__group || groupShowsImage(row))
.slice(0, 240)
void primeProductImages(imageRows)
}, { immediate: true })
onMounted(reload)
</script>
@@ -2212,6 +2750,11 @@ onMounted(reload)
margin-top: 4px;
}
.detail-level-menu {
width: 260px;
padding: 10px;
}
.performance-table {
max-height: calc(100vh - 260px);
}
@@ -2229,6 +2772,20 @@ onMounted(reload)
.performance-table :deep(.q-table td) {
border-right: 1px solid #d5dce5;
border-bottom: 1px solid #d5dce5;
vertical-align: middle;
white-space: normal;
}
.performance-table :deep(.q-table tbody td) {
max-width: 220px;
}
.performance-table :deep(.q-table tbody td span:not(.q-badge):not(.q-icon)) {
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.25;
}
.performance-table :deep(.q-table th:first-child),
@@ -2244,6 +2801,113 @@ onMounted(reload)
border-top: 1px solid #d5dce5;
}
.product-breakdown-table :deep(.q-table) {
min-width: 2050px;
table-layout: fixed;
}
.product-breakdown-table :deep(.q-table th:nth-child(-n+10)),
.product-breakdown-table :deep(.q-table td:nth-child(-n+10)) {
position: sticky;
z-index: 2;
}
.product-breakdown-table :deep(.q-table th:nth-child(-n+10)) {
z-index: 4;
background: #f8fbff;
}
.product-breakdown-table :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
background: #fff;
}
.product-breakdown-table :deep(.q-table th:nth-child(1)),
.product-breakdown-table :deep(.q-table td:nth-child(1)) {
left: 0;
width: 80px;
min-width: 80px;
max-width: 80px;
}
.product-breakdown-table :deep(.q-table th:nth-child(2)),
.product-breakdown-table :deep(.q-table td:nth-child(2)) {
left: 80px;
width: 96px;
min-width: 96px;
max-width: 96px;
}
.product-breakdown-table :deep(.q-table th:nth-child(3)),
.product-breakdown-table :deep(.q-table td:nth-child(3)) {
left: 176px;
width: 48px;
min-width: 48px;
max-width: 48px;
}
.product-breakdown-table :deep(.q-table th:nth-child(4)),
.product-breakdown-table :deep(.q-table td:nth-child(4)) {
left: 224px;
width: 48px;
min-width: 48px;
max-width: 48px;
}
.product-breakdown-table :deep(.q-table th:nth-child(5)),
.product-breakdown-table :deep(.q-table td:nth-child(5)) {
left: 272px;
width: 150px;
min-width: 150px;
max-width: 150px;
}
.product-breakdown-table :deep(.q-table th:nth-child(6)),
.product-breakdown-table :deep(.q-table td:nth-child(6)) {
left: 422px;
width: 72px;
min-width: 72px;
max-width: 72px;
}
.product-breakdown-table :deep(.q-table th:nth-child(7)),
.product-breakdown-table :deep(.q-table td:nth-child(7)) {
left: 494px;
width: 72px;
min-width: 72px;
max-width: 72px;
}
.product-breakdown-table :deep(.q-table th:nth-child(8)),
.product-breakdown-table :deep(.q-table td:nth-child(8)) {
left: 566px;
width: 92px;
min-width: 92px;
max-width: 92px;
}
.product-breakdown-table :deep(.q-table th:nth-child(9)),
.product-breakdown-table :deep(.q-table td:nth-child(9)) {
left: 658px;
width: 96px;
min-width: 96px;
max-width: 96px;
}
.product-breakdown-table :deep(.q-table th:nth-child(10)),
.product-breakdown-table :deep(.q-table td:nth-child(10)) {
left: 754px;
width: 96px;
min-width: 96px;
max-width: 96px;
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
}
.product-breakdown-table :deep(.q-table th:nth-child(11)),
.product-breakdown-table :deep(.q-table td:nth-child(11)) {
width: 180px;
min-width: 140px;
}
.filterable-header-cell {
vertical-align: middle;
}
@@ -2253,7 +2917,7 @@ onMounted(reload)
grid-template-columns: minmax(0, 1fr) 20px;
align-items: center;
gap: 2px;
min-width: 78px;
min-width: 42px;
}
.header-with-filter > span {
@@ -2305,47 +2969,47 @@ onMounted(reload)
}
.group-row td {
background: #eaf4ff;
background: color-mix(in srgb, var(--q-primary) 8%, #ffffff);
font-weight: 600;
color: #17324f;
border-color: #c3d4e6;
color: color-mix(in srgb, var(--q-primary) 55%, #1f2937);
border-color: color-mix(in srgb, var(--q-primary) 18%, #d5dce5);
white-space: nowrap;
}
.group-row-level-0 td {
background: #1565c0;
background: color-mix(in srgb, var(--q-primary) 82%, #1f2937);
color: #fff;
border-color: #0d47a1;
border-color: color-mix(in srgb, var(--q-primary) 75%, #111827);
}
.group-row-level-1 td {
background: #2f7fd0;
background: color-mix(in srgb, var(--q-primary) 68%, #ffffff);
color: #fff;
border-color: #1565c0;
border-color: color-mix(in srgb, var(--q-primary) 58%, #d5dce5);
}
.group-row-level-2 td {
background: #6da9df;
color: #0c2f52;
border-color: #3c83c7;
background: color-mix(in srgb, var(--q-primary) 36%, #ffffff);
color: color-mix(in srgb, var(--q-primary) 65%, #111827);
border-color: color-mix(in srgb, var(--q-primary) 34%, #d5dce5);
}
.group-row-level-3 td {
background: #a9cdef;
color: #12385c;
border-color: #78addb;
background: color-mix(in srgb, var(--q-primary) 22%, #ffffff);
color: color-mix(in srgb, var(--q-primary) 58%, #1f2937);
border-color: color-mix(in srgb, var(--q-primary) 24%, #d5dce5);
}
.group-row-level-4 td {
background: #d1e5f8;
color: #17324f;
border-color: #9fc5e8;
background: color-mix(in srgb, var(--q-primary) 14%, #ffffff);
color: color-mix(in srgb, var(--q-primary) 52%, #1f2937);
border-color: color-mix(in srgb, var(--q-primary) 18%, #d5dce5);
}
.group-row-level-5 td {
background: #eaf4ff;
color: #17324f;
border-color: #c3d4e6;
background: color-mix(in srgb, var(--q-primary) 8%, #ffffff);
color: color-mix(in srgb, var(--q-primary) 48%, #1f2937);
border-color: color-mix(in srgb, var(--q-primary) 14%, #d5dce5);
}
.group-cell-label {
@@ -2360,24 +3024,27 @@ onMounted(reload)
}
.product-image-cell {
width: 72px;
min-width: 72px;
width: 80px;
min-width: 80px;
}
.product-thumb-button {
width: 56px;
height: 72px;
width: 68px;
height: 92px;
border: 1px solid #d9dde3;
border-radius: 6px;
background: #fff;
padding: 2px;
padding: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.product-thumb,
.product-thumb-placeholder {
width: 50px;
height: 66px;
width: 58px;
height: 82px;
}
.product-thumb-placeholder {