ui: update ProductPerformanceProfitability page to enhance table views and tabs management
This commit is contained in:
@@ -670,8 +670,10 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
limit := f.Limit
|
limit := f.Limit
|
||||||
if limit <= 0 || limit > 500 {
|
if limit <= 0 {
|
||||||
limit = 100
|
limit = 100
|
||||||
|
} else if limit > 50000 {
|
||||||
|
limit = 50000
|
||||||
}
|
}
|
||||||
page := f.Page
|
page := f.Page
|
||||||
if page <= 0 {
|
if page <= 0 {
|
||||||
@@ -741,6 +743,25 @@ KPI AS (
|
|||||||
FROM mk_product_performance_kpi_daily
|
FROM mk_product_performance_kpi_daily
|
||||||
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
||||||
),
|
),
|
||||||
|
VariantStock AS (
|
||||||
|
SELECT
|
||||||
|
product_code,
|
||||||
|
color_code,
|
||||||
|
yaka_kodu,
|
||||||
|
MAX(stock_qty) AS stock_qty,
|
||||||
|
MAX(cost_price_usd) AS cost_price_usd,
|
||||||
|
BOOL_OR(performance_bucket IN ('STOK_RISKI','TAKIP')) AS has_risk_bucket,
|
||||||
|
SUM(COALESCE(sales_qty_90d,0)) AS sales_qty_90d
|
||||||
|
FROM KPI
|
||||||
|
GROUP BY product_code, color_code, yaka_kodu
|
||||||
|
),
|
||||||
|
StockTotals AS (
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(stock_qty),0) AS total_stock,
|
||||||
|
COALESCE(SUM(stock_qty * cost_price_usd),0) AS stock_cost_usd,
|
||||||
|
COALESCE(SUM(CASE WHEN has_risk_bucket AND COALESCE(sales_qty_90d,0)=0 THEN stock_qty * cost_price_usd ELSE 0 END),0) AS risk_cost_usd
|
||||||
|
FROM VariantStock
|
||||||
|
),
|
||||||
Sales90 AS (
|
Sales90 AS (
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT market_key) AS market_count_90d,
|
COUNT(DISTINCT market_key) AS market_count_90d,
|
||||||
@@ -754,9 +775,9 @@ SELECT
|
|||||||
COUNT(*) FILTER (WHERE performance_bucket='YILDIZ_URUN'),
|
COUNT(*) FILTER (WHERE performance_bucket='YILDIZ_URUN'),
|
||||||
COUNT(*) FILTER (WHERE performance_bucket='STOK_RISKI'),
|
COUNT(*) FILTER (WHERE performance_bucket='STOK_RISKI'),
|
||||||
COUNT(*) FILTER (WHERE performance_bucket='STOKSUZ_TALEP'),
|
COUNT(*) FILTER (WHERE performance_bucket='STOKSUZ_TALEP'),
|
||||||
COALESCE(SUM(stock_qty),0),
|
COALESCE(MAX(StockTotals.total_stock),0),
|
||||||
COALESCE(SUM(stock_qty * cost_price_usd),0),
|
COALESCE(MAX(StockTotals.stock_cost_usd),0),
|
||||||
COALESCE(SUM(CASE WHEN performance_bucket IN ('STOK_RISKI','TAKIP') AND COALESCE(sales_qty_90d,0)=0 THEN stock_qty * cost_price_usd ELSE 0 END),0),
|
COALESCE(MAX(StockTotals.risk_cost_usd),0),
|
||||||
COALESCE(SUM(sales_qty_90d),0),
|
COALESCE(SUM(sales_qty_90d),0),
|
||||||
COALESCE(SUM(sales_usd_90d),0),
|
COALESCE(SUM(sales_usd_90d),0),
|
||||||
COALESCE(SUM(gross_profit_usd_90d),0),
|
COALESCE(SUM(gross_profit_usd_90d),0),
|
||||||
@@ -765,6 +786,7 @@ SELECT
|
|||||||
COALESCE(to_char(MAX(updated_at),'YYYY-MM-DD HH24:MI:SS'),'')
|
COALESCE(to_char(MAX(updated_at),'YYYY-MM-DD HH24:MI:SS'),'')
|
||||||
FROM KPI
|
FROM KPI
|
||||||
CROSS JOIN Sales90
|
CROSS JOIN Sales90
|
||||||
|
CROSS JOIN StockTotals
|
||||||
`).Scan(&s.KpiDate, &s.TotalRows, &s.StarCount, &s.StockRisk, &s.NoStockDemand, &s.TotalStock, &s.StockCostUSD, &s.RiskCostUSD, &s.SalesQty90, &s.SalesUSD90, &s.GrossProfit90, &s.MarketCount90, &s.CustomerCount90, &s.UpdatedAt)
|
`).Scan(&s.KpiDate, &s.TotalRows, &s.StarCount, &s.StockRisk, &s.NoStockDemand, &s.TotalStock, &s.StockCostUSD, &s.RiskCostUSD, &s.SalesQty90, &s.SalesUSD90, &s.GrossProfit90, &s.MarketCount90, &s.CustomerCount90, &s.UpdatedAt)
|
||||||
return s, err
|
return s, err
|
||||||
}
|
}
|
||||||
@@ -2007,10 +2029,22 @@ func ListProductPerformanceStockSizes(ctx context.Context, productCode, colorCod
|
|||||||
return nil, fmt.Errorf("mssql db nil")
|
return nil, fmt.Errorf("mssql db nil")
|
||||||
}
|
}
|
||||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||||
|
;WITH ActiveWarehouses AS (
|
||||||
|
SELECT WarehouseCode
|
||||||
|
FROM (VALUES
|
||||||
|
('1-0-12'),('1.01.2014'),('1.02.2005'),('1.02.2004'),('1-0-43'),
|
||||||
|
('4.02.2001'),('1-0-55'),('1.01.2003'),('1-0-21'),('1-0-2'),
|
||||||
|
('1.01.2004'),('1-0-49'),('1-0-37'),('1-0-29'),('1-0-28'),
|
||||||
|
('1-0-10'),('100'),('1.02.2006'),('1-0-42'),('1.01.2002'),
|
||||||
|
('1-0-52')
|
||||||
|
) W(WarehouseCode)
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
SizeCode = LTRIM(RTRIM(ISNULL(S.ItemDim1Code, ''))),
|
SizeCode = LTRIM(RTRIM(ISNULL(S.ItemDim1Code, ''))),
|
||||||
StockQty = SUM(S.InventoryQty1)
|
StockQty = SUM(S.InventoryQty1)
|
||||||
FROM StockWithCost S WITH(NOLOCK)
|
FROM StockWithCost S WITH(NOLOCK)
|
||||||
|
INNER JOIN ActiveWarehouses W
|
||||||
|
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
|
||||||
WHERE S.ItemTypeCode = 1
|
WHERE S.ItemTypeCode = 1
|
||||||
AND LTRIM(RTRIM(S.ItemCode)) = @p1
|
AND LTRIM(RTRIM(S.ItemCode)) = @p1
|
||||||
AND LTRIM(RTRIM(ISNULL(S.ColorCode, ''))) = @p2
|
AND LTRIM(RTRIM(ISNULL(S.ColorCode, ''))) = @p2
|
||||||
@@ -2068,39 +2102,43 @@ func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
|
|||||||
|
|
||||||
func productPerformanceOrderBy(sortBy string, desc bool) string {
|
func productPerformanceOrderBy(sortBy string, desc bool) string {
|
||||||
allowed := map[string]string{
|
allowed := map[string]string{
|
||||||
"product_code": "product_code",
|
"product_code": "product_code",
|
||||||
"color_code": "color_code",
|
"color_code": "color_code",
|
||||||
"yaka_kodu": "yaka_kodu",
|
"yaka_kodu": "yaka_kodu",
|
||||||
"item_description": "item_description",
|
"item_description": "item_description",
|
||||||
"kategori": "kategori",
|
"kategori": "kategori",
|
||||||
"urun_ilk_grubu": "urun_ilk_grubu",
|
"urun_ilk_grubu": "urun_ilk_grubu",
|
||||||
"askili_yan": "askili_yan",
|
"askili_yan": "askili_yan",
|
||||||
"urun_ana_grubu": "urun_ana_grubu",
|
"urun_ana_grubu": "urun_ana_grubu",
|
||||||
"urun_alt_grubu": "urun_alt_grubu",
|
"urun_alt_grubu": "urun_alt_grubu",
|
||||||
"market_key": "market_key",
|
"market_key": "market_key",
|
||||||
"stock_qty": "stock_qty",
|
"stock_qty": "stock_qty",
|
||||||
"sales_qty_90d": "sales_qty_90d",
|
"sales_qty_90d": "sales_qty_90d",
|
||||||
"sales_qty_180d": "sales_qty_180d",
|
"sales_qty_180d": "sales_qty_180d",
|
||||||
"sales_usd_90d": "sales_usd_90d",
|
"sales_usd_90d": "sales_usd_90d",
|
||||||
"sales_usd_180d": "sales_usd_180d",
|
"sales_usd_180d": "sales_usd_180d",
|
||||||
"stock_days_90d": "stock_days_90d",
|
"stock_days_90d": "stock_days_90d",
|
||||||
"stock_days_180d": "stock_days_180d",
|
"stock_days_180d": "stock_days_180d",
|
||||||
"avg_price_usd_90d": "avg_price_usd_90d",
|
"avg_price_usd_90d": "avg_price_usd_90d",
|
||||||
"avg_price_usd_180d": "avg_price_usd_180d",
|
"avg_price_usd_180d": "avg_price_usd_180d",
|
||||||
"base_price_usd": "base_price_usd",
|
"base_price_usd": "base_price_usd",
|
||||||
"cost_price_usd": "cost_price_usd",
|
"cost_price_usd": "cost_price_usd",
|
||||||
"unit_profit_base_90d": "unit_profit_base_90d",
|
"unit_profit_base_90d": "unit_profit_base_90d",
|
||||||
"unit_profit_cost_90d": "unit_profit_cost_90d",
|
"unit_profit_cost_90d": "unit_profit_cost_90d",
|
||||||
"unit_profit_base_180d": "unit_profit_base_180d",
|
"unit_profit_base_180d": "unit_profit_base_180d",
|
||||||
"unit_profit_cost_180d": "unit_profit_cost_180d",
|
"unit_profit_cost_180d": "unit_profit_cost_180d",
|
||||||
"gross_margin_90d": "gross_margin_90d",
|
"gross_margin_base_90d": "(CASE WHEN COALESCE(sales_usd_90d,0) <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_90d,0) END)",
|
||||||
"gross_margin_180d": "gross_margin_180d",
|
"gross_margin_cost_90d": "gross_margin_90d",
|
||||||
"market_count_90d": "market_count_90d",
|
"gross_margin_base_180d": "(CASE WHEN COALESCE(sales_usd_180d,0) <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_180d,0) END)",
|
||||||
"customer_count_90d": "customer_count_90d",
|
"gross_margin_cost_180d": "gross_margin_180d",
|
||||||
"sales_index_90d": "sales_index_90d",
|
"gross_margin_90d": "gross_margin_90d",
|
||||||
"performance_score": "performance_score",
|
"gross_margin_180d": "gross_margin_180d",
|
||||||
"performance_bucket": "performance_bucket",
|
"market_count_90d": "market_count_90d",
|
||||||
"last_sale_date": "last_sale_date",
|
"customer_count_90d": "customer_count_90d",
|
||||||
|
"sales_index_90d": "sales_index_90d",
|
||||||
|
"performance_score": "performance_score",
|
||||||
|
"performance_bucket": "performance_bucket",
|
||||||
|
"last_sale_date": "last_sale_date",
|
||||||
}
|
}
|
||||||
col := allowed[strings.TrimSpace(sortBy)]
|
col := allowed[strings.TrimSpace(sortBy)]
|
||||||
if col == "" {
|
if col == "" {
|
||||||
|
|||||||
@@ -216,10 +216,11 @@ func productPerformanceStockSQL() string {
|
|||||||
;WITH ActiveWarehouses AS (
|
;WITH ActiveWarehouses AS (
|
||||||
SELECT WarehouseCode
|
SELECT WarehouseCode
|
||||||
FROM (VALUES
|
FROM (VALUES
|
||||||
('1-0-14'),('1-0-10'),('1-0-8'),('1-2-5'),('1-2-4'),('1-0-12'),
|
('1-0-12'),('1.01.2014'),('1.02.2005'),('1.02.2004'),('1-0-43'),
|
||||||
('100'),('1-0-28'),('1-0-24'),('1-2-6'),('1-1-14'),('1-0-2'),
|
('4.02.2001'),('1-0-55'),('1.01.2003'),('1-0-21'),('1-0-2'),
|
||||||
('1-0-52'),('1-1-2'),('1-0-21'),('1-1-3'),('1-0-33'),('101'),
|
('1.01.2004'),('1-0-49'),('1-0-37'),('1-0-29'),('1-0-28'),
|
||||||
('1-014'),('1-0-49'),('1-0-36')
|
('1-0-10'),('100'),('1.02.2006'),('1-0-42'),('1.01.2002'),
|
||||||
|
('1-0-52')
|
||||||
) W(WarehouseCode)
|
) W(WarehouseCode)
|
||||||
),
|
),
|
||||||
Raw AS (
|
Raw AS (
|
||||||
|
|||||||
@@ -449,32 +449,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body="props">
|
<template #body="props">
|
||||||
<q-tr v-if="props.row.__group" :props="props" class="group-row">
|
<q-tr
|
||||||
<q-td :colspan="columns.length" :style="{ paddingLeft: `${8 + props.row.level * 18}px` }">
|
v-if="props.row.__group"
|
||||||
<q-btn
|
:props="props"
|
||||||
flat
|
:class="['group-row', `group-row-level-${Math.min(Number(props.row.level || 0), 5)}`]"
|
||||||
dense
|
>
|
||||||
round
|
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
|
||||||
size="sm"
|
<div
|
||||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
v-if="col.name === groupLabelColumnName(props.row)"
|
||||||
@click.stop="toggleGroup(props.row.key)"
|
class="group-cell-label"
|
||||||
/>
|
:style="{ paddingLeft: `${props.row.level * 18}px` }"
|
||||||
<span class="group-title">{{ props.row.label }}</span>
|
>
|
||||||
<span class="group-meta">
|
<q-btn
|
||||||
{{ formatNumber(props.row.count, 0) }} satır ·
|
flat
|
||||||
Stok {{ formatNumber(props.row.stock_qty, 0) }} ·
|
dense
|
||||||
90G {{ formatNumber(props.row.sales_qty_90d, 0) }} ·
|
round
|
||||||
180G {{ formatNumber(props.row.sales_qty_180d, 0) }} ·
|
size="sm"
|
||||||
Piyasa {{ formatNumber(props.row.market_count_90d, 0) }} ·
|
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||||
Müşteri {{ formatNumber(props.row.customer_count_90d, 0) }} ·
|
@click.stop="toggleGroup(props.row.key)"
|
||||||
Ort.Satış {{ formatMoney(props.row.avg_price_usd_90d, 'USD') }} ·
|
/>
|
||||||
Taban {{ formatMoney(props.row.avg_base_price_usd, 'USD') }} ·
|
<span class="group-title">{{ props.row.label }}</span>
|
||||||
Çıplak {{ formatMoney(props.row.avg_cost_price_usd, 'USD') }} ·
|
</div>
|
||||||
90G K/Z {{ formatMoney(props.row.gross_profit_usd_90d, 'USD') }} ·
|
<span v-else>{{ formatGroupCell(props.row, col.name) }}</span>
|
||||||
90G Marj {{ formatPercent(props.row.gross_margin_90d) }} ·
|
|
||||||
180G K/Z {{ formatMoney(props.row.gross_profit_usd_180d, 'USD') }} ·
|
|
||||||
180G Marj {{ formatPercent(props.row.gross_margin_180d) }}
|
|
||||||
</span>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
<q-tr v-else :props="props" class="cursor-pointer" @click="openPerformanceDialog(props.row)">
|
<q-tr v-else :props="props" class="cursor-pointer" @click="openPerformanceDialog(props.row)">
|
||||||
@@ -1059,10 +1055,11 @@ const detailStockSizes = ref([])
|
|||||||
const columnFilters = reactive({})
|
const columnFilters = reactive({})
|
||||||
const columnFilterSearch = reactive({})
|
const columnFilterSearch = reactive({})
|
||||||
const expandedGroups = ref({})
|
const expandedGroups = ref({})
|
||||||
|
const productKpiFetchLimit = 50000
|
||||||
|
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
page: 1,
|
page: 1,
|
||||||
rowsPerPage: 500,
|
rowsPerPage: 100,
|
||||||
rowsNumber: 0,
|
rowsNumber: 0,
|
||||||
sortBy: 'performance_score',
|
sortBy: 'performance_score',
|
||||||
descending: true
|
descending: true
|
||||||
@@ -1129,8 +1126,10 @@ const columns = [
|
|||||||
{ 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_cost_180d', label: '180G P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_180d, '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_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_180d', label: '180G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'gross_margin_90d', label: 'Marj', field: 'gross_margin_90d', 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_180d', label: '180G Marj', field: 'gross_margin_180d', 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: 'market_count_90d', label: '90G Piyasa', field: row => formatNumber(row.market_count_90d, 0), 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: 'customer_count_90d', label: '90G Müşteri', field: row => formatNumber(row.customer_count_90d, 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_90d', label: 'Piyasa End.', field: row => formatNumber(row.sales_index_90d, 2), align: 'right', sortable: true },
|
||||||
@@ -1164,7 +1163,8 @@ const generalColumns = [
|
|||||||
{ name: 'unit_profit_base_total', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'unit_profit_base_total', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_total, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'unit_profit_cost_total', label: 'P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'unit_profit_cost_total', label: 'P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_total, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'gross_profit_usd_total', label: 'Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_total, 'USD'), align: 'right', sortable: true },
|
{ name: 'gross_profit_usd_total', label: 'Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_total, 'USD'), align: 'right', sortable: true },
|
||||||
{ name: 'gross_margin_total', label: 'Genel Marj', field: row => formatPercent(row.gross_margin_total), 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_total', label: 'Toplam Piyasa', field: row => formatNumber(row.market_count_total, 0), align: 'right', sortable: true },
|
{ name: 'market_count_total', label: 'Toplam Piyasa', field: row => formatNumber(row.market_count_total, 0), align: 'right', sortable: true },
|
||||||
{ name: 'customer_count_total', label: 'Toplam Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
{ name: 'customer_count_total', label: 'Toplam Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
||||||
{ name: 'invoice_count_total', label: 'Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
|
{ name: 'invoice_count_total', label: 'Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
|
||||||
@@ -1368,17 +1368,52 @@ const summaryCards = computed(() => [
|
|||||||
{ key: 'risk_cost', label: 'Risk Maliyeti', value: formatMoney(summary.value.risk_cost_usd, 'USD') }
|
{ key: 'risk_cost', label: 'Risk Maliyeti', value: formatMoney(summary.value.risk_cost_usd, 'USD') }
|
||||||
])
|
])
|
||||||
|
|
||||||
const idleRows = computed(() => rows.value
|
const idleRows = computed(() => {
|
||||||
.map(row => ({
|
const variants = new Map()
|
||||||
...row,
|
for (const row of rows.value) {
|
||||||
idle_cost_usd: Number(row.stock_qty || 0) * Number(row.cost_price_usd || 0)
|
const key = `${row.product_code || ''}|${row.color_code || ''}|${row.yaka_kodu || ''}`
|
||||||
}))
|
const current = variants.get(key)
|
||||||
.filter(row => Number(row.stock_qty || 0) > 0 && (
|
if (!current) {
|
||||||
row.performance_bucket === 'STOK_RISKI' ||
|
variants.set(key, {
|
||||||
Number(row.sales_qty_90d || 0) === 0 ||
|
...row,
|
||||||
Number(row.stock_days_90d || 0) > 180
|
market_key: row.market_key && row.market_key !== 'STOK' ? row.market_key : '',
|
||||||
))
|
sales_qty_90d: Number(row.sales_qty_90d || 0),
|
||||||
.sort((a, b) => Number(b.idle_cost_usd || 0) - Number(a.idle_cost_usd || 0)))
|
sales_usd_90d: Number(row.sales_usd_90d || 0),
|
||||||
|
gross_profit_usd_90d: Number(row.gross_profit_usd_90d || 0),
|
||||||
|
customer_count_90d: Number(row.customer_count_90d || 0),
|
||||||
|
market_count_90d: row.market_key && row.market_key !== 'STOK' ? 1 : 0,
|
||||||
|
has_stock_risk: row.performance_bucket === 'STOK_RISKI'
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
current.market_key = distinctTextSummary(current.market_key, row.market_key)
|
||||||
|
current.sales_qty_90d += Number(row.sales_qty_90d || 0)
|
||||||
|
current.sales_usd_90d += Number(row.sales_usd_90d || 0)
|
||||||
|
current.gross_profit_usd_90d += Number(row.gross_profit_usd_90d || 0)
|
||||||
|
current.customer_count_90d += Number(row.customer_count_90d || 0)
|
||||||
|
current.market_count_90d += row.market_key && row.market_key !== 'STOK' ? 1 : 0
|
||||||
|
current.has_stock_risk = current.has_stock_risk || row.performance_bucket === 'STOK_RISKI'
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(variants.values())
|
||||||
|
.map(row => {
|
||||||
|
const stockQty = Number(row.stock_qty || 0)
|
||||||
|
const salesQty90 = Number(row.sales_qty_90d || 0)
|
||||||
|
const stockDays90 = salesQty90 > 0 ? stockQty / (salesQty90 / 90) : 0
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
stock_days_90d: stockDays90,
|
||||||
|
idle_cost_usd: stockQty * Number(row.cost_price_usd || 0),
|
||||||
|
performance_bucket: row.has_stock_risk ? 'STOK_RISKI' : row.performance_bucket
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(row => Number(row.stock_qty || 0) > 0 && (
|
||||||
|
row.performance_bucket === 'STOK_RISKI' ||
|
||||||
|
Number(row.sales_qty_90d || 0) === 0 ||
|
||||||
|
Number(row.stock_days_90d || 0) > 180
|
||||||
|
))
|
||||||
|
.sort((a, b) => Number(b.idle_cost_usd || 0) - Number(a.idle_cost_usd || 0))
|
||||||
|
})
|
||||||
|
|
||||||
const visibleOrderAnalysisRows = computed(() => {
|
const visibleOrderAnalysisRows = computed(() => {
|
||||||
if (orderAnalysisMode.value === 'market') return orderMarketRows.value
|
if (orderAnalysisMode.value === 'market') return orderMarketRows.value
|
||||||
@@ -1405,7 +1440,8 @@ const performanceCards = computed(() => {
|
|||||||
{ key: 'cost', label: 'Çıplak Maliyet', value: formatMoney(row.cost_price_usd, 'USD') },
|
{ key: 'cost', label: 'Çıplak Maliyet', value: formatMoney(row.cost_price_usd, 'USD') },
|
||||||
{ key: 'profitBase', label: 'P.Başı Taban K/Z', value: formatMoney(row.unit_profit_base_90d, 'USD') },
|
{ key: 'profitBase', label: 'P.Başı Taban K/Z', value: formatMoney(row.unit_profit_base_90d, 'USD') },
|
||||||
{ key: 'profitCost', label: 'P.Başı Çıplak K/Z', value: formatMoney(row.unit_profit_cost_90d, 'USD') },
|
{ key: 'profitCost', label: 'P.Başı Çıplak K/Z', value: formatMoney(row.unit_profit_cost_90d, 'USD') },
|
||||||
{ key: 'margin', label: '90G Marj', value: formatPercent(row.gross_margin_90d) },
|
{ key: 'marginBase', label: '90G Taban Marj', value: formatPercent(row.gross_margin_base_90d) },
|
||||||
|
{ key: 'marginCost', label: '90G Çıplak Marj', value: formatPercent(row.gross_margin_cost_90d) },
|
||||||
{ key: 'bucket', label: 'Durum', value: bucketLabel(row.performance_bucket) }
|
{ key: 'bucket', label: 'Durum', value: bucketLabel(row.performance_bucket) }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -1488,21 +1524,33 @@ function appendGroupRows (out, sourceRows, level, parentKeys) {
|
|||||||
.sort((a, b) => a[0].localeCompare(b[0], 'tr'))
|
.sort((a, b) => a[0].localeCompare(b[0], 'tr'))
|
||||||
.forEach(([value, groupRows]) => {
|
.forEach(([value, groupRows]) => {
|
||||||
const key = [...parentKeys, `${groupDef.key}:${value}`].join('|')
|
const key = [...parentKeys, `${groupDef.key}:${value}`].join('|')
|
||||||
out.push(makeGroupRow(key, level, `${groupDef.label}: ${value}`, groupRows))
|
out.push(makeGroupRow(key, level, groupDef, value, groupRows))
|
||||||
if (isGroupExpanded(key)) {
|
if (isGroupExpanded(key)) {
|
||||||
appendGroupRows(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`])
|
appendGroupRows(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGroupRow (key, level, label, groupRows) {
|
function makeGroupRow (key, level, groupDef, value, groupRows) {
|
||||||
return {
|
return {
|
||||||
__group: true,
|
__group: true,
|
||||||
row_key: `group|${key}`,
|
row_key: `group|${key}`,
|
||||||
key,
|
key,
|
||||||
level,
|
level,
|
||||||
label,
|
group_field: groupDef.key,
|
||||||
|
group_value: value,
|
||||||
|
label: `${groupDef.label}: ${value}`,
|
||||||
count: groupRows.length,
|
count: groupRows.length,
|
||||||
|
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'),
|
||||||
|
item_description: distinctSummary(groupRows, 'item_description'),
|
||||||
|
kategori: groupDef.key === 'kategori' ? value : distinctSummary(groupRows, 'kategori'),
|
||||||
|
urun_ilk_grubu: groupDef.key === 'urun_ilk_grubu' ? value : distinctSummary(groupRows, 'urun_ilk_grubu', 'yas_grubu'),
|
||||||
|
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'),
|
||||||
stock_qty: sumRows(groupRows, 'stock_qty'),
|
stock_qty: sumRows(groupRows, 'stock_qty'),
|
||||||
sales_qty_90d: sumRows(groupRows, 'sales_qty_90d'),
|
sales_qty_90d: sumRows(groupRows, 'sales_qty_90d'),
|
||||||
sales_qty_180d: sumRows(groupRows, 'sales_qty_180d'),
|
sales_qty_180d: sumRows(groupRows, 'sales_qty_180d'),
|
||||||
@@ -1514,10 +1562,24 @@ function makeGroupRow (key, level, label, groupRows) {
|
|||||||
customer_count_90d: sumRows(groupRows, 'customer_count_90d'),
|
customer_count_90d: sumRows(groupRows, 'customer_count_90d'),
|
||||||
avg_price_usd_90d: weightedAverage(groupRows, 'sales_usd_90d', 'sales_qty_90d'),
|
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_180d: weightedAverage(groupRows, 'sales_usd_180d', 'sales_qty_180d'),
|
||||||
avg_base_price_usd: averageRows(groupRows, 'base_price_usd'),
|
base_price_usd: weightedAverageOrAverage(groupRows, 'base_price_usd', 'sales_qty_90d'),
|
||||||
avg_cost_price_usd: averageRows(groupRows, 'cost_price_usd'),
|
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'),
|
||||||
|
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_90d: ratio(sumRows(groupRows, 'gross_profit_usd_90d'), sumRows(groupRows, 'sales_usd_90d')),
|
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'))
|
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'),
|
||||||
|
sales_index_90d: averageRows(groupRows, 'sales_index_90d'),
|
||||||
|
performance_score: weightedAverageOrAverage(groupRows, 'performance_score', 'sales_qty_90d'),
|
||||||
|
performance_bucket: dominantValue(groupRows, 'performance_bucket'),
|
||||||
|
recommendation: `${formatNumber(groupRows.length, 0)} satır`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1534,6 +1596,35 @@ function distinctCount (sourceRows, field) {
|
|||||||
return values.size
|
return values.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function distinctSummary (sourceRows, field, fallbackField) {
|
||||||
|
const values = new Set()
|
||||||
|
for (const row of sourceRows) {
|
||||||
|
const value = String(row[field] || (fallbackField ? row[fallbackField] : '') || '').trim()
|
||||||
|
if (value && value !== '-') values.add(value)
|
||||||
|
if (values.size > 1) return `${values.size} farklı`
|
||||||
|
}
|
||||||
|
return Array.from(values)[0] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function distinctTextSummary (current, next) {
|
||||||
|
const left = String(current || '').trim()
|
||||||
|
const right = String(next || '').trim()
|
||||||
|
if (!right || right === 'STOK') return left
|
||||||
|
if (!left) return right
|
||||||
|
if (left === right || left === 'Çoklu') return left
|
||||||
|
return 'Çoklu'
|
||||||
|
}
|
||||||
|
|
||||||
|
function dominantValue (sourceRows, field) {
|
||||||
|
const counts = new Map()
|
||||||
|
for (const row of sourceRows) {
|
||||||
|
const value = String(row[field] || '').trim()
|
||||||
|
if (!value) continue
|
||||||
|
counts.set(value, (counts.get(value) || 0) + 1)
|
||||||
|
}
|
||||||
|
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] || ''
|
||||||
|
}
|
||||||
|
|
||||||
function ratio (amount, base) {
|
function ratio (amount, base) {
|
||||||
const divisor = Number(base || 0)
|
const divisor = Number(base || 0)
|
||||||
if (!divisor) return 0
|
if (!divisor) return 0
|
||||||
@@ -1552,6 +1643,35 @@ function weightedAverage (sourceRows, amountField, qtyField) {
|
|||||||
return sumRows(sourceRows, amountField) / qty
|
return sumRows(sourceRows, amountField) / qty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function weightedAverageOrAverage (sourceRows, valueField, qtyField) {
|
||||||
|
const weightedRows = sourceRows
|
||||||
|
.map(row => ({
|
||||||
|
value: Number(row[valueField] || 0),
|
||||||
|
qty: Number(row[qtyField] || 0)
|
||||||
|
}))
|
||||||
|
.filter(row => Number.isFinite(row.value) && Number.isFinite(row.qty) && row.qty > 0)
|
||||||
|
const qty = weightedRows.reduce((sum, row) => sum + row.qty, 0)
|
||||||
|
if (qty > 0) {
|
||||||
|
return weightedRows.reduce((sum, row) => sum + row.value * row.qty, 0) / qty
|
||||||
|
}
|
||||||
|
return averageRows(sourceRows, valueField)
|
||||||
|
}
|
||||||
|
|
||||||
|
function marginFromSalesCost (salesUSD, qty, unitCost) {
|
||||||
|
const sales = Number(salesUSD || 0)
|
||||||
|
if (!sales) return 0
|
||||||
|
return (sales - (Number(qty || 0) * Number(unitCost || 0))) / sales
|
||||||
|
}
|
||||||
|
|
||||||
|
function marginFromRows (sourceRows, salesField, qtyField, unitCostField) {
|
||||||
|
const sales = sumRows(sourceRows, salesField)
|
||||||
|
if (!sales) return 0
|
||||||
|
const costAmount = sourceRows.reduce((sum, row) => {
|
||||||
|
return sum + Number(row[qtyField] || 0) * Number(row[unitCostField] || 0)
|
||||||
|
}, 0)
|
||||||
|
return (sales - costAmount) / sales
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeGroupValue (value) {
|
function normalizeGroupValue (value) {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
return text || '-'
|
return text || '-'
|
||||||
@@ -1568,6 +1688,40 @@ function toggleGroup (key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupLabelColumnName (row) {
|
||||||
|
return row?.group_field || 'product_code'
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
? 'text-right'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGroupCell (row, name) {
|
||||||
|
if (name === 'image') return ''
|
||||||
|
if (name === 'recommendation') return row.recommendation || ''
|
||||||
|
return formatProductCell(row, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function withProductMargins (row) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
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),
|
||||||
|
gross_margin_cost_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.cost_price_usd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withGeneralMargins (row) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
gross_margin_base_total: marginFromSalesCost(row?.sales_usd_total, row?.sales_qty_total, row?.base_price_usd),
|
||||||
|
gross_margin_cost_total: marginFromSalesCost(row?.sales_usd_total, row?.sales_qty_total, row?.cost_price_usd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function filterKey (tableKey, name) {
|
function filterKey (tableKey, name) {
|
||||||
return `${tableKey}:${name}`
|
return `${tableKey}:${name}`
|
||||||
}
|
}
|
||||||
@@ -1690,6 +1844,12 @@ function formatProductCell (row, name) {
|
|||||||
case 'gross_profit_usd_90d':
|
case 'gross_profit_usd_90d':
|
||||||
case 'gross_profit_usd_180d':
|
case 'gross_profit_usd_180d':
|
||||||
return formatMoney(row[name], 'USD')
|
return formatMoney(row[name], 'USD')
|
||||||
|
case 'gross_margin_base_90d':
|
||||||
|
case 'gross_margin_cost_90d':
|
||||||
|
case 'gross_margin_base_180d':
|
||||||
|
case 'gross_margin_cost_180d':
|
||||||
|
case 'gross_margin_base_total':
|
||||||
|
case 'gross_margin_cost_total':
|
||||||
case 'gross_margin_90d':
|
case 'gross_margin_90d':
|
||||||
case 'gross_margin_180d':
|
case 'gross_margin_180d':
|
||||||
return formatPercent(row[name])
|
return formatPercent(row[name])
|
||||||
@@ -1707,10 +1867,10 @@ function normalizeRow (row) {
|
|||||||
const color = String(row?.color_code || '').trim()
|
const color = String(row?.color_code || '').trim()
|
||||||
const yaka = String(row?.yaka_kodu || '').trim()
|
const yaka = String(row?.yaka_kodu || '').trim()
|
||||||
const market = String(row?.market_key || '').trim()
|
const market = String(row?.market_key || '').trim()
|
||||||
return {
|
return withProductMargins({
|
||||||
...row,
|
...row,
|
||||||
row_key: `${product}|${color}|${yaka}|${market}`
|
row_key: `${product}|${color}|${yaka}|${market}`
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeGeneralRow (row) {
|
function normalizeGeneralRow (row) {
|
||||||
@@ -1718,10 +1878,10 @@ function normalizeGeneralRow (row) {
|
|||||||
const color = String(row?.color_code || '').trim()
|
const color = String(row?.color_code || '').trim()
|
||||||
const yaka = String(row?.yaka_kodu || '').trim()
|
const yaka = String(row?.yaka_kodu || '').trim()
|
||||||
const market = String(row?.market_key || '').trim()
|
const market = String(row?.market_key || '').trim()
|
||||||
return {
|
return withGeneralMargins({
|
||||||
...row,
|
...row,
|
||||||
row_key: `general|${product}|${color}|${yaka}|${market}`
|
row_key: `general|${product}|${color}|${yaka}|${market}`
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeOrderAnalysisRow (row) {
|
function normalizeOrderAnalysisRow (row) {
|
||||||
@@ -1915,8 +2075,8 @@ async function reload () {
|
|||||||
api.get('/pricing/product-performance/summary', { timeout: 60000 }),
|
api.get('/pricing/product-performance/summary', { timeout: 60000 }),
|
||||||
api.get('/pricing/product-performance', {
|
api.get('/pricing/product-performance', {
|
||||||
params: {
|
params: {
|
||||||
page: pagination.value.page,
|
page: 1,
|
||||||
limit: pagination.value.rowsPerPage,
|
limit: productKpiFetchLimit,
|
||||||
sort_by: pagination.value.sortBy || undefined,
|
sort_by: pagination.value.sortBy || undefined,
|
||||||
descending: pagination.value.descending ? 'true' : 'false'
|
descending: pagination.value.descending ? 'true' : 'false'
|
||||||
},
|
},
|
||||||
@@ -2060,11 +2220,28 @@ onMounted(reload)
|
|||||||
max-height: calc(100vh - 320px);
|
max-height: calc(100vh - 320px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.performance-table :deep(.q-table) {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance-table :deep(.q-table th),
|
||||||
|
.performance-table :deep(.q-table td) {
|
||||||
|
border-right: 1px solid #d5dce5;
|
||||||
|
border-bottom: 1px solid #d5dce5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance-table :deep(.q-table th:first-child),
|
||||||
|
.performance-table :deep(.q-table td:first-child) {
|
||||||
|
border-left: 1px solid #d5dce5;
|
||||||
|
}
|
||||||
|
|
||||||
.performance-table :deep(th) {
|
.performance-table :deep(th) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: #fff;
|
background: #f8fbff;
|
||||||
|
border-top: 1px solid #d5dce5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterable-header-cell {
|
.filterable-header-cell {
|
||||||
@@ -2128,18 +2305,58 @@ onMounted(reload)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.group-row td {
|
.group-row td {
|
||||||
background: #eef2f6;
|
background: #eaf4ff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #17324f;
|
||||||
|
border-color: #c3d4e6;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row-level-0 td {
|
||||||
|
background: #1565c0;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #0d47a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row-level-1 td {
|
||||||
|
background: #2f7fd0;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row-level-2 td {
|
||||||
|
background: #6da9df;
|
||||||
|
color: #0c2f52;
|
||||||
|
border-color: #3c83c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row-level-3 td {
|
||||||
|
background: #a9cdef;
|
||||||
|
color: #12385c;
|
||||||
|
border-color: #78addb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row-level-4 td {
|
||||||
|
background: #d1e5f8;
|
||||||
|
color: #17324f;
|
||||||
|
border-color: #9fc5e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row-level-5 td {
|
||||||
|
background: #eaf4ff;
|
||||||
|
color: #17324f;
|
||||||
|
border-color: #c3d4e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-cell-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-title {
|
.group-title {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
white-space: normal;
|
||||||
|
|
||||||
.group-meta {
|
|
||||||
margin-left: 12px;
|
|
||||||
color: #5f6b7a;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image-cell {
|
.product-image-cell {
|
||||||
|
|||||||
Reference in New Issue
Block a user