Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-06 00:53:30 +03:00
parent 19a42e0551
commit a0da3f2bed
2 changed files with 513 additions and 203 deletions
+326 -140
View File
@@ -1072,6 +1072,9 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
return 0, err
}
log.Printf("[ProductPerformanceRefresh] grouped snapshot definitions ready count=%d", len(defs))
if err := deleteStaleProductPerformanceGroupedSnapshots(ctx, pg, productPerformanceSnapshotKey("grouped", "product_detail")+":%"); err != nil {
return 0, err
}
total := 0
for i, def := range defs {
started := time.Now()
@@ -1107,7 +1110,23 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
return total, nil
}
func deleteStaleProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB, reportKeyLike string) error {
reportKeyLike = strings.TrimSpace(reportKeyLike)
if reportKeyLike == "" {
return nil
}
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key LIKE $1`, reportKeyLike); err != nil {
return err
}
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot_meta WHERE report_key LIKE $1`, reportKeyLike); err != nil {
return err
}
return nil
}
func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
_ = ctx
_ = pg
defs := []productPerformanceGroupedSnapshotDefinition{
{Mode: "products", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
{Mode: "products", Levels: []string{"urun_ana_grubu"}},
@@ -1118,30 +1137,6 @@ func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.D
{Mode: "order_product_customers", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}},
{Mode: "order_market_details", Levels: []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
}
productRows, ok, err := loadProductPerformanceSnapshotMapRows(ctx, pg, productPerformanceSnapshotKey("products"), 50000)
if err != nil {
return nil, err
}
if ok {
mainGroups := make([]string, 0)
seen := map[string]bool{}
for _, row := range productRows {
mainGroup := productPerformanceGroupedFilterValue(row, "urun_ana_grubu")
if mainGroup == "" || seen[mainGroup] {
continue
}
seen[mainGroup] = true
mainGroups = append(mainGroups, mainGroup)
}
sort.Strings(mainGroups)
for _, mainGroup := range mainGroups {
defs = append(defs, productPerformanceGroupedSnapshotDefinition{
Mode: "product_detail",
Levels: []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"},
MainGroup: mainGroup,
})
}
}
return defs, nil
}
@@ -3920,6 +3915,7 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
out := make([]map[string]any, 0, len(sourceRows))
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
if len(out) > 0 || !productPerformanceLiveFallbackEnabled() {
return out, nil
}
@@ -4038,6 +4034,9 @@ func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB,
}
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
hasFilters := len(effectiveFilters) > 0
if hasFilters {
return nil, false, nil
}
hasManualExpansion := len(req.ExpandedKeys) > 0
query := `
SELECT payload
@@ -4098,7 +4097,6 @@ SELECT EXISTS (
return nil, false, nil
}
}
out = filterProductPerformancePreparedGroupedRows(out, effectiveFilters)
out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
return out, true, nil
@@ -4988,9 +4986,9 @@ func productPerformanceGroupedFilterValue(row map[string]any, field string) stri
}
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
if rows, ok, err := productPerformanceGroupedSnapshotRows(ctx, pg, mode, limit); err != nil {
if rows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, mode, limit); err != nil {
return nil, err
} else if ok {
} else if rows != nil {
return rows, nil
}
@@ -5036,6 +5034,7 @@ func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql
}
if mode == "products" || mode == "product_detail" {
rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
rows = mergeProductPerformanceSalesSpreadKeys(ctx, pg, rows)
}
return rows, nil
}
@@ -5076,6 +5075,88 @@ func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.
return rows
}
func mergeProductPerformanceSalesSpreadKeys(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
if len(rows) == 0 {
return rows
}
const query = `
WITH Latest AS (
SELECT COALESCE(
(SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily),
(SELECT MAX(sales_date) FROM mk_product_performance_sales_daily),
current_date
)::date AS kpi_date
),
Spread AS (
SELECT
s.product_code,
s.color_code,
s.yaka_kodu,
s.market_key,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_90d,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_180d,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_365d,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_total
FROM mk_product_performance_sales_daily s
CROSS JOIN Latest
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
AND upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
GROUP BY s.product_code, s.color_code, s.yaka_kodu, s.market_key
)
SELECT jsonb_build_object(
'product_code', product_code,
'color_code', color_code,
'yaka_kodu', yaka_kodu,
'market_key', market_key,
'__customer_keys_90d', customer_keys_90d,
'__customer_keys_180d', customer_keys_180d,
'__customer_keys_365d', customer_keys_365d,
'__customer_keys_total', customer_keys_total
)
FROM Spread`
spreadRows, err := queryProductPerformanceJSONRows(ctx, pg, query)
if err != nil {
log.Printf("[ProductPerformanceRefresh] product sales spread keys skipped err=%v", err)
return rows
}
byKey := make(map[string]map[string]any, len(spreadRows))
for _, row := range spreadRows {
key := productPerformanceMapMarketVariantKey(row)
if key != "" {
byKey[key] = row
}
}
for _, row := range rows {
spread := byKey[productPerformanceMapMarketVariantKey(row)]
if spread == nil {
continue
}
for _, suffix := range productPerformancePeriodSuffixes() {
field := "__customer_keys_" + suffix
if value, ok := spread[field]; ok {
row[field] = value
}
}
}
return rows
}
func productPerformanceMapMarketVariantKey(row map[string]any) string {
productCode := normalizeProductPerformanceProductCode(stringFromMap(row, "product_code"))
if productCode == "" {
@@ -5181,23 +5262,21 @@ type productPerformanceGroupedSnapshotAvgState struct {
}
type productPerformanceGroupedSnapshotNode struct {
Key string
Level int
Field string
Value string
Row map[string]any
Count int
Children map[string]*productPerformanceGroupedSnapshotNode
ChildOrder []string
StockMetricSeen map[string]map[string]bool
IdleSeen map[string]bool
Avg map[string]*productPerformanceGroupedSnapshotAvgState
BucketCounts map[string]int
Image map[string]any
Market90Seen map[string]bool
MarketTotalSeen map[string]bool
Customer90Seen map[string]bool
CustomerTotalSeen map[string]bool
Key string
Level int
Field string
Value string
Row map[string]any
Count int
Children map[string]*productPerformanceGroupedSnapshotNode
ChildOrder []string
StockMetricSeen map[string]map[string]bool
IdleSeen map[string]bool
Avg map[string]*productPerformanceGroupedSnapshotAvgState
BucketCounts map[string]int
Image map[string]any
MarketSeen map[string]map[string]bool
CustomerSeen map[string]map[string]bool
}
func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, levels []string, mode string) []map[string]any {
@@ -5253,7 +5332,7 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
n.BucketCounts[bucket]++
}
for key, value := range row {
if key == "row_key" || key == "key" || isProductPerformanceMarginField(key) {
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) || isProductPerformanceMarginField(key) {
continue
}
switch {
@@ -5324,34 +5403,35 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctVariantMetric(row map
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" {
if floatFromMap(row, "sales_usd_90d") > 0 {
if n.Market90Seen == nil {
n.Market90Seen = map[string]bool{}
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if n.MarketSeen == nil {
n.MarketSeen = map[string]map[string]bool{}
}
if n.MarketSeen[suffix] == nil {
n.MarketSeen[suffix] = map[string]bool{}
}
n.MarketSeen[suffix][market] = true
}
n.Market90Seen[market] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
if n.MarketTotalSeen == nil {
n.MarketTotalSeen = map[string]bool{}
}
n.MarketTotalSeen[market] = true
}
}
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
if customer != "" && customer != "-" {
if floatFromMap(row, "sales_usd_90d") > 0 {
if n.Customer90Seen == nil {
n.Customer90Seen = map[string]bool{}
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if n.CustomerSeen == nil {
n.CustomerSeen = map[string]map[string]bool{}
}
if n.CustomerSeen[suffix] == nil {
n.CustomerSeen[suffix] = map[string]bool{}
}
n.CustomerSeen[suffix][customer] = true
}
n.Customer90Seen[customer] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
if n.CustomerTotalSeen == nil {
n.CustomerTotalSeen = map[string]bool{}
}
n.CustomerTotalSeen[customer] = true
}
}
for _, suffix := range productPerformancePeriodSuffixes() {
n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
}
}
func appendProductPerformanceGroupedSnapshotNodes(out *[]map[string]any, nodes map[string]*productPerformanceGroupedSnapshotNode, order []string) {
@@ -5387,7 +5467,7 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
row[key] = state.Sum / state.Count
}
}
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
applyProductPerformanceDistinctSpread(row, n.MarketSeen, n.CustomerSeen)
deriveProductPerformanceGroupMetrics(row, n.Field)
if bucket := n.dominantBucket(); bucket != "" {
row["performance_bucket"] = bucket
@@ -5508,14 +5588,12 @@ func clearProductPerformanceGroupDimensions(row map[string]any, groupField, grou
func aggregateProductPerformanceRows(rows []map[string]any, groupField string) map[string]any {
out := map[string]any{}
market90Seen := map[string]bool{}
marketTotalSeen := map[string]bool{}
customer90Seen := map[string]bool{}
customerTotalSeen := map[string]bool{}
marketSeen := map[string]map[string]bool{}
customerSeen := map[string]map[string]bool{}
for _, row := range rows {
addProductPerformanceDistinctSpread(row, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
addProductPerformanceDistinctSpread(row, marketSeen, customerSeen)
for key, value := range row {
if key == "row_key" || key == "key" {
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) {
continue
}
if isProductPerformanceMarginField(key) {
@@ -5541,6 +5619,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
for _, row := range rows {
for key := range row {
if isProductPerformanceInternalGroupField(key) {
continue
}
if isProductPerformanceMarginField(key) {
continue
}
@@ -5549,62 +5630,119 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
}
}
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
applyProductPerformanceDistinctSpread(out, marketSeen, customerSeen)
deriveProductPerformanceGroupMetrics(out, groupField)
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
return out
}
func addProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
func addProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" {
if floatFromMap(row, "sales_usd_90d") > 0 {
market90Seen[market] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
marketTotalSeen[market] = true
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if marketSeen[suffix] == nil {
marketSeen[suffix] = map[string]bool{}
}
marketSeen[suffix][market] = true
}
}
}
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
if customer != "" && customer != "-" {
if floatFromMap(row, "sales_usd_90d") > 0 {
customer90Seen[customer] = true
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if customerSeen[suffix] == nil {
customerSeen[suffix] = map[string]bool{}
}
customerSeen[suffix][customer] = true
}
}
if floatFromMap(row, "sales_usd_total") > 0 {
customerTotalSeen[customer] = true
}
for _, suffix := range productPerformancePeriodSuffixes() {
customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
}
}
func applyProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
for _, suffix := range productPerformancePeriodSuffixes() {
marketField := "market_count_" + suffix
customerField := "customer_count_" + suffix
if seen := marketSeen[suffix]; len(seen) > 0 {
row[marketField] = len(seen)
}
if seen := customerSeen[suffix]; len(seen) > 0 {
row[customerField] = len(seen)
}
if floatFromMap(row, "sales_usd_"+suffix) > 0 && (suffix == "90d" || suffix == "total") {
if intFromMap(row, marketField) == 0 {
row[marketField] = 1
}
if intFromMap(row, customerField) == 0 {
if suffix == "total" {
row[customerField] = maxInt(1, intFromMap(row, "customer_count_90d"))
} else {
row[customerField] = 1
}
}
}
}
}
func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
if len(market90Seen) > 0 {
row["market_count_90d"] = len(market90Seen)
func productPerformanceAddSeenStrings(seen map[string]map[string]bool, suffix string, values []string) map[string]map[string]bool {
if len(values) == 0 {
return seen
}
if len(marketTotalSeen) > 0 {
row["market_count_total"] = len(marketTotalSeen)
if seen == nil {
seen = map[string]map[string]bool{}
}
if len(customer90Seen) > 0 {
row["customer_count_90d"] = len(customer90Seen)
if seen[suffix] == nil {
seen[suffix] = map[string]bool{}
}
if len(customerTotalSeen) > 0 {
row["customer_count_total"] = len(customerTotalSeen)
}
if floatFromMap(row, "sales_usd_90d") > 0 {
if intFromMap(row, "market_count_90d") == 0 {
row["market_count_90d"] = 1
}
if intFromMap(row, "customer_count_90d") == 0 {
row["customer_count_90d"] = 1
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || value == "-" {
continue
}
seen[suffix][value] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
if intFromMap(row, "market_count_total") == 0 {
row["market_count_total"] = maxInt(1, intFromMap(row, "market_count_90d"))
}
if intFromMap(row, "customer_count_total") == 0 {
row["customer_count_total"] = maxInt(1, intFromMap(row, "customer_count_90d"))
}
return seen
}
func productPerformanceStringSliceFromMap(row map[string]any, field string) []string {
value, ok := row[field]
if !ok || value == nil {
return nil
}
switch v := value.(type) {
case []string:
return v
case []any:
out := make([]string, 0, len(v))
for _, item := range v {
text := strings.TrimSpace(fmt.Sprint(item))
if text != "" {
out = append(out, text)
}
}
return out
case string:
text := strings.TrimSpace(v)
if text == "" {
return nil
}
var parsed []string
if strings.HasPrefix(text, "[") && json.Unmarshal([]byte(text), &parsed) == nil {
return parsed
}
return []string{text}
default:
return nil
}
}
func isProductPerformanceInternalGroupField(field string) bool {
return strings.HasPrefix(field, "__")
}
func productPerformanceGroupRecommendation(row map[string]any, groupField string, count int) string {
@@ -5711,7 +5849,7 @@ func productPerformanceMapVariantKey(row map[string]any) string {
func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
normalizeProductPerformanceCostFields(out)
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
for _, suffix := range productPerformancePeriodSuffixes() {
sales := floatFromMap(out, "sales_usd_"+suffix)
qty := floatFromMap(out, "sales_qty_"+suffix)
stockQty := floatFromMap(out, "stock_qty")
@@ -5719,6 +5857,19 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
if turnoverBase <= 0 {
turnoverBase = stockQty
}
days := productPerformancePeriodDays(out, suffix)
avgDaily := floatFromMap(out, "avg_daily_sales_"+suffix)
if days > 0 {
avgDaily = qty / days
out["avg_daily_sales_"+suffix] = avgDaily
}
if avgDaily > 0 && turnoverBase > 0 {
out["stock_days_"+suffix] = turnoverBase / avgDaily
} else if qty <= 0 && stockQty > 0 {
out["stock_days_"+suffix] = 9999
} else if _, ok := out["stock_days_"+suffix]; !ok {
out["stock_days_"+suffix] = 0
}
if turnoverBase > 0 {
out["stock_turnover_"+suffix] = qty / turnoverBase
} else {
@@ -5775,37 +5926,15 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
}
if _, ok := out["customer_score_90d"]; !ok {
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
}
if _, ok := out["customer_score_180d"]; !ok {
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
}
if _, ok := out["customer_score_365d"]; !ok {
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
}
if _, ok := out["customer_score_total"]; !ok {
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
}
if _, ok := out["performance_score_90d"]; !ok {
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
}
if _, ok := out["performance_score_180d"]; !ok {
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
}
if _, ok := out["performance_score_365d"]; !ok {
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
}
if _, ok := out["performance_score_total"]; !ok {
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
}
if _, ok := out["performance_score"]; !ok {
if score, exists := productPerformanceOptionalFloat(out, "performance_score_90d"); exists {
out["performance_score"] = score
} else {
out["performance_score"] = productPerformanceGroupScore(out, groupField)
}
}
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
out["performance_score"] = out["performance_score_90d"]
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
score := productPerformanceOrderGroupScore(out)
out["performance_score_90d"] = score
@@ -5832,11 +5961,56 @@ func normalizeProductPerformanceCostFields(row map[string]any) {
}
}
normalize("cost_price_usd", "base_price_usd")
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
for _, suffix := range productPerformancePeriodSuffixes() {
normalize("cost_price_usd_"+suffix, "base_price_usd_"+suffix)
}
}
func productPerformancePeriodSuffixes() []string {
return []string{"90d", "180d", "365d", "total"}
}
func productPerformancePeriodDays(row map[string]any, suffix string) float64 {
switch suffix {
case "90d":
return 90
case "180d":
return 180
case "365d":
return 365
case "total":
start := parseProductPerformanceDate(stringFromMap(row, "period_start"))
if start.IsZero() {
start = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
}
end := parseProductPerformanceDate(stringFromMap(row, "period_end"))
if end.IsZero() {
end = parseProductPerformanceDate(stringFromMap(row, "kpi_date"))
}
if end.IsZero() || end.Before(start) {
return 0
}
return end.Sub(start).Hours()/24 + 1
default:
return 0
}
}
func parseProductPerformanceDate(value string) time.Time {
value = strings.TrimSpace(value)
if value == "" {
return time.Time{}
}
if len(value) >= len("2006-01-02") {
value = value[:len("2006-01-02")]
}
t, err := time.Parse("2006-01-02", value)
if err != nil {
return time.Time{}
}
return t
}
func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
value, ok := row[field]
if !ok {
@@ -6172,7 +6346,16 @@ func mapGroupValue(row map[string]any, field string) string {
}
func shouldSumProductPerformanceField(field string) bool {
return strings.HasSuffix(field, "_qty") ||
return strings.HasPrefix(field, "sales_qty_") ||
strings.HasPrefix(field, "sales_usd_") ||
strings.HasPrefix(field, "avg_daily_sales_") ||
strings.HasPrefix(field, "gross_profit_") ||
strings.HasPrefix(field, "invoice_count_") ||
strings.HasPrefix(field, "market_count_") ||
strings.HasPrefix(field, "customer_count_") ||
strings.HasPrefix(field, "product_group_count_") ||
strings.HasPrefix(field, "product_count_") ||
strings.HasSuffix(field, "_qty") ||
strings.HasSuffix(field, "_usd") ||
strings.HasSuffix(field, "_count") ||
strings.HasSuffix(field, "_value_usd") ||
@@ -6189,6 +6372,9 @@ func shouldSumProductPerformanceField(field string) bool {
}
func shouldAverageProductPerformanceField(field string) bool {
if strings.HasPrefix(field, "avg_daily_sales_") {
return false
}
return strings.HasPrefix(field, "avg_") ||
strings.HasPrefix(field, "unit_") ||
strings.HasPrefix(field, "base_price") ||
+187 -63
View File
@@ -35,6 +35,15 @@
:disable="pageBusy"
class="period-selector bg-white q-px-xs q-py-none"
/>
<q-btn
dense
outline
color="primary"
icon="view_column"
:label="detailColumnsHidden ? 'Detay Kolonları Göster' : 'Detay Kolonları Gizle'"
:disable="pageBusy"
@click="detailColumnsHidden = !detailColumnsHidden"
/>
<q-btn-dropdown
v-model="detailLevelMenuOpen"
split
@@ -90,20 +99,6 @@
</button>
</div>
<div v-if="activeTab === 'product_detail'" class="main-group-bar q-mb-xs">
<button
v-for="option in detailMainGroupOptions"
:key="option.value"
type="button"
:class="['main-group-button', { active: selectedDetailMainGroup === option.value }]"
:disabled="pageBusy"
@click="selectedDetailMainGroup = option.value"
>
<span>{{ option.label }}</span>
<small>{{ formatNumber(option.stock_qty, 0) }} stok</small>
</button>
</div>
<div ref="topScrollbarRef" class="performance-top-scrollbar q-mb-xs">
<div ref="topScrollbarInnerRef" class="performance-top-scrollbar-inner"></div>
</div>
@@ -113,9 +108,14 @@
flat
bordered
row-key="row_key"
class="performance-table sticky-dim-table sticky-dim-7 bg-white"
:class="[
'performance-table',
'sticky-dim-table',
detailColumnsHidden ? 'sticky-dim-4' : 'sticky-dim-7',
'bg-white'
]"
:rows="filteredGeneralRows"
:columns="generalColumns"
:columns="visibleGeneralColumns"
:loading="activeTableLoading"
v-model:pagination="tablePagination.general"
virtual-scroll
@@ -560,6 +560,7 @@
'performance-table',
'product-breakdown-table',
'bg-white',
{ 'compact-detail-columns': detailColumnsHidden },
activeTab === 'product_detail' ? 'product-detail-table' : 'product-summary-table'
]"
:rows="displayProductKpiTableRows"
@@ -1603,6 +1604,7 @@ const loadingNow = ref(0)
const loadingStage = ref('')
const generalRowsLoaded = ref(false)
const detailLevelMenuOpen = ref(false)
const detailColumnsHidden = ref(false)
const topScrollbarRef = ref(null)
const topScrollbarInnerRef = ref(null)
let backendGroupedTimer = null
@@ -1675,7 +1677,6 @@ const productPerformanceExcelExportFilterFields = new Set([
])
const performanceTabs = [
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
{ name: 'product_detail', icon: 'category', label: 'Detay KPI' },
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk/Yaka > Piyasa > Müşteri' },
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
@@ -2024,7 +2025,32 @@ const productColumns = computed(() => orderedMetricColumns(columns.filter(visibl
const productDetailColumns = computed(() => orderedMetricColumns(columns
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
.filter(visiblePeriodColumn)))
const activeProductColumns = computed(() => activeTab.value === 'product_detail' ? productDetailColumns.value : productColumns.value)
const detailColumnNames = new Set([
'item_description',
'kategori',
'askili_yan',
'urun_ilk_grubu',
'urun_ana_grubu',
'urun_alt_grubu',
'period_start',
'period_end',
'country',
'customer_segment',
'customer_code',
'customer_name',
'first_sale_date',
'last_sale_date',
'last_ref_number'
])
function applyDetailColumnVisibility (sourceColumns) {
if (!detailColumnsHidden.value) return sourceColumns
return sourceColumns.filter(col => !detailColumnNames.has(col.name))
}
const visibleProductColumns = computed(() => applyDetailColumnVisibility(productColumns.value))
const visibleProductDetailColumns = computed(() => applyDetailColumnVisibility(productDetailColumns.value))
const activeProductColumns = computed(() => activeTab.value === 'product_detail' ? visibleProductDetailColumns.value : visibleProductColumns.value)
const generalColumns = [
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
@@ -2066,6 +2092,8 @@ const generalColumns = [
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
]
const visibleGeneralColumns = computed(() => applyDetailColumnVisibility(generalColumns))
const orderAnalysisColumns = [
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
@@ -2513,7 +2541,6 @@ function ensureColorYakaColumn (targetColumns) {
].forEach(target => ensureColumns(target, customerPeriodScoreColumns))
;[
generalColumns,
orderAnalysisColumns,
orderGroupColumns,
orderProductCustomerColumns,
@@ -3052,7 +3079,7 @@ function filterSourceRowsForTab (tabKey, fallbackRows) {
}
function backendGroupedFilterOptionParams (tabKey) {
const columns = columnsForTableKey(tabKey)
const columns = filterColumnsForTableKey(tabKey)
const fields = columns
.map(col => col?.name || '')
.filter(name => backendGroupedFilterFields.has(name))
@@ -3132,7 +3159,7 @@ function backendFilterOptionLabel (name, value) {
function backendGroupedFilterState (tabKey) {
const out = {}
const columns = columnsForTableKey(tabKey)
const columns = filterColumnsForTableKey(tabKey)
for (const col of columns) {
const name = col?.name || ''
if (!isColumnFilterable(name)) continue
@@ -3152,7 +3179,7 @@ function shouldUseBackendGroupedRows (tabKey) {
function tableFilterSource (tableKey) {
if (backendGroupedSupportedTab(tableKey)) {
return { rows: backendGroupedRows.value[tableKey] || [], columns: columnsForTableKey(tableKey) }
return { rows: backendGroupedRows.value[tableKey] || [], columns: filterColumnsForTableKey(tableKey) }
}
if (tableKey === 'general') return { rows: generalRows.value, columns: generalColumns }
if (tableKey === 'markets') return { rows: marketRows.value, columns: marketColumns }
@@ -3162,6 +3189,20 @@ function tableFilterSource (tableKey) {
}
function columnsForTableKey (tabKey) {
if (tabKey === 'products') return visibleProductColumns.value
if (tabKey === 'product_detail') return visibleProductDetailColumns.value
if (tabKey === 'general') return visibleGeneralColumns.value
if (tabKey === 'order_product_customers') return orderProductCustomerColumns
if (tabKey === 'order_market_details') return orderMarketDetailColumns
if (tabKey === 'idle') return idleColumns
if (tabKey === 'markets') return marketColumns
if (tabKey === 'countries') return countryColumns
if (tabKey === 'customers') return customerColumns
if (salesBreakdownTabKeys.includes(tabKey)) return visibleSalesBreakdownColumns.value
return productColumns.value
}
function filterColumnsForTableKey (tabKey) {
if (tabKey === 'products') return productColumns.value
if (tabKey === 'product_detail') return productDetailColumns.value
if (tabKey === 'general') return generalColumns
@@ -3466,10 +3507,12 @@ function aggregateGroupFields (sourceRows, groupField = '') {
}
function shouldSumField (field) {
return /(_qty|_usd|_count|_value_usd|line_count|invoice_count|order_count|product_count|market_count|customer_count|overdue_qty|stock_qty|net_stock_after_order|idle_cost_usd)$/i.test(field)
return /^(sales_qty_|sales_usd_|avg_daily_sales_|gross_profit_|invoice_count_|market_count_|customer_count_|product_group_count_|product_count_)/i.test(field) ||
/(_qty|_usd|_count|_value_usd|line_count|invoice_count|order_count|product_count|market_count|customer_count|overdue_qty|stock_qty|net_stock_after_order|idle_cost_usd)$/i.test(field)
}
function shouldAverageField (field) {
if (/^avg_daily_sales_/i.test(field)) return false
return /^(avg_|unit_|base_price|cost_price|gross_margin|expected_margin|sales_index|performance_score|customer_score|stock_days|stock_turnover)/i.test(field) ||
/(_price_usd|_margin|_index|_days)$/i.test(field)
}
@@ -3491,6 +3534,12 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
const stockQty = Number(out.stock_qty || 0)
const avgStock = Number(out[`avg_stock_${suffix}`] || 0)
const turnoverBase = avgStock > 0 ? avgStock : stockQty
const days = productPerformancePeriodDays(out, suffix)
const avgDaily = days > 0 ? qty / days : Number(out[`avg_daily_sales_${suffix}`] || 0)
if (days > 0) out[`avg_daily_sales_${suffix}`] = avgDaily
if (avgDaily > 0 && turnoverBase > 0) out[`stock_days_${suffix}`] = turnoverBase / avgDaily
else if (qty <= 0 && stockQty > 0) out[`stock_days_${suffix}`] = 9999
else if (!Object.prototype.hasOwnProperty.call(out, `stock_days_${suffix}`)) out[`stock_days_${suffix}`] = 0
out[`stock_turnover_${suffix}`] = turnoverBase > 0 ? qty / turnoverBase : 0
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
const { costPrice, basePrice } = periodCostPair(out, suffix)
@@ -3530,19 +3579,33 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
if (sourceRows.some(row => row.performance_bucket)) {
out.performance_bucket = dominantValue(sourceRows, 'performance_bucket')
}
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_90d')) out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d'))
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_180d')) out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d'))
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_365d')) out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d'))
if (!Object.prototype.hasOwnProperty.call(out, 'customer_score_total')) out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_90d')) out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_180d')) out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_365d')) out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score_total')) out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score')) {
out.performance_score = Object.prototype.hasOwnProperty.call(out, 'performance_score_90d')
? Number(out.performance_score_90d || 0)
: groupPerformanceScore(out, groupField)
}
out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d'))
out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d'))
out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d'))
out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total'))
out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d'))
out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
out.performance_score = Number(out.performance_score_90d || 0)
}
function productPerformancePeriodDays (row, suffix) {
if (suffix === '90d') return 90
if (suffix === '180d') return 180
if (suffix === '365d') return 365
if (suffix !== 'total') return 0
const start = parseProductPerformanceDate(row?.period_start) || new Date(Date.UTC(2022, 0, 1))
const end = parseProductPerformanceDate(row?.period_end) || parseProductPerformanceDate(row?.kpi_date)
if (!start || !end || end < start) return 0
return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1
}
function parseProductPerformanceDate (value) {
const text = String(value || '').trim().slice(0, 10)
if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return null
const [year, month, day] = text.split('-').map(Number)
return new Date(Date.UTC(year, month - 1, day))
}
function groupPerformanceScore (row, groupField = '') {
@@ -5204,7 +5267,7 @@ function productPerformanceExcelExportTableKey () {
function buildProductPerformanceExcelExportFilters () {
const tableKey = productPerformanceExcelExportTableKey()
const out = {}
for (const col of columnsForTableKey(tableKey) || []) {
for (const col of filterColumnsForTableKey(tableKey) || []) {
const name = String(col?.name || '').trim()
if (!productPerformanceExcelExportFilterFields.has(name)) continue
const selected = selectedColumnFilters(tableKey, name)
@@ -5986,95 +6049,95 @@ onBeforeUnmount(() => {
table-layout: fixed;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(-n+10)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(-n+10)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(-n+10)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(-n+10)) {
position: sticky;
z-index: 2;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(-n+10)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(-n+10)) {
z-index: 30;
background: #f8fbff;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+10)) {
background: #fff;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(1)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(1)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(1)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(1)) {
left: 0;
width: 190px;
min-width: 190px;
max-width: 190px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(2)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(2)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(2)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(2)) {
left: 190px;
width: 140px;
min-width: 140px;
max-width: 140px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(3)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(3)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(3)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(3)) {
left: 330px;
width: 110px;
min-width: 110px;
max-width: 110px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(4)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(4)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(4)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(4)) {
left: 440px;
width: 150px;
min-width: 150px;
max-width: 150px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(5)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(5)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(5)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(5)) {
left: 590px;
width: 170px;
min-width: 170px;
max-width: 170px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(6)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(6)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(6)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(6)) {
left: 760px;
width: 150px;
min-width: 150px;
max-width: 150px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(7)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(7)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(7)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(7)) {
left: 910px;
width: 130px;
min-width: 130px;
max-width: 130px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(8)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(8)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(8)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(8)) {
left: 1040px;
width: 90px;
min-width: 90px;
max-width: 90px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(9)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(9)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(9)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(9)) {
left: 1130px;
width: 80px;
min-width: 80px;
max-width: 80px;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(10)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(10)) {
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(10)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(10)) {
left: 1210px;
width: 130px;
min-width: 130px;
@@ -6082,6 +6145,58 @@ onBeforeUnmount(() => {
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
}
.product-breakdown-table.compact-detail-columns :deep(.q-table) {
min-width: 2450px;
}
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(-n+4)),
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(-n+4)) {
position: sticky;
z-index: 2;
}
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(-n+4)) {
z-index: 30;
background: #f8fbff;
}
.product-breakdown-table.compact-detail-columns :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+4)) {
background: #fff;
}
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(1)),
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(1)) {
left: 0;
width: 86px;
min-width: 86px;
max-width: 86px;
}
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(2)),
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(2)) {
left: 86px;
width: 150px;
min-width: 150px;
max-width: 150px;
}
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(3)),
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(3)) {
left: 236px;
width: 112px;
min-width: 112px;
max-width: 112px;
}
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(4)),
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(4)) {
left: 348px;
width: 132px;
min-width: 132px;
max-width: 132px;
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
}
.product-breakdown-table.product-detail-table :deep(.q-table) {
min-width: 2620px;
}
@@ -6198,10 +6313,14 @@ onBeforeUnmount(() => {
font-variant-numeric: tabular-nums;
}
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th:nth-child(n+11):not(.text-right)),
.product-breakdown-table:not(.product-detail-table) :deep(.q-table td:nth-child(n+11):not(.text-right)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table th:nth-child(n+11):not(.text-right)),
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(n+11):not(.text-right)),
.product-breakdown-table.compact-detail-columns :deep(.q-table th:nth-child(n+5):not(.text-right)),
.product-breakdown-table.compact-detail-columns :deep(.q-table td:nth-child(n+5):not(.text-right)),
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(n+4):not(.text-right)),
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(n+4):not(.text-right)),
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(n+5):not(.text-right)),
.sticky-dim-table.sticky-dim-4 :deep(.q-table td:nth-child(n+5):not(.text-right)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(n+6):not(.text-right)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(n+6):not(.text-right)),
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(n+7):not(.text-right)),
@@ -6281,6 +6400,8 @@ onBeforeUnmount(() => {
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(-n+3)),
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(-n+4)),
.sticky-dim-table.sticky-dim-4 :deep(.q-table td:nth-child(-n+4)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(-n+5)),
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
@@ -6300,6 +6421,7 @@ onBeforeUnmount(() => {
}
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(-n+4)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(-n+7)),
@@ -6317,6 +6439,8 @@ onBeforeUnmount(() => {
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(3)),
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(3)),
.sticky-dim-table.sticky-dim-4 :deep(.q-table th:nth-child(4)),
.sticky-dim-table.sticky-dim-4 :deep(.q-table td:nth-child(4)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(5)),
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(5)),
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(6)),