Fix product performance grouped JSON build
This commit is contained in:
+326
-140
@@ -1072,6 +1072,9 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
log.Printf("[ProductPerformanceRefresh] grouped snapshot definitions ready count=%d", len(defs))
|
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
|
total := 0
|
||||||
for i, def := range defs {
|
for i, def := range defs {
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
@@ -1107,7 +1110,23 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
|
|||||||
return total, nil
|
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) {
|
func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = pg
|
||||||
defs := []productPerformanceGroupedSnapshotDefinition{
|
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_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
|
||||||
{Mode: "products", Levels: []string{"urun_ana_grubu"}},
|
{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_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"}},
|
{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
|
return defs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3920,6 +3915,7 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
|
|||||||
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
|
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
|
||||||
out := make([]map[string]any, 0, len(sourceRows))
|
out := make([]map[string]any, 0, len(sourceRows))
|
||||||
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
|
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() {
|
if len(out) > 0 || !productPerformanceLiveFallbackEnabled() {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -4038,6 +4034,9 @@ func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB,
|
|||||||
}
|
}
|
||||||
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
|
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
|
||||||
hasFilters := len(effectiveFilters) > 0
|
hasFilters := len(effectiveFilters) > 0
|
||||||
|
if hasFilters {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
hasManualExpansion := len(req.ExpandedKeys) > 0
|
hasManualExpansion := len(req.ExpandedKeys) > 0
|
||||||
query := `
|
query := `
|
||||||
SELECT payload
|
SELECT payload
|
||||||
@@ -4098,7 +4097,6 @@ SELECT EXISTS (
|
|||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out = filterProductPerformancePreparedGroupedRows(out, effectiveFilters)
|
|
||||||
out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
|
||||||
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
|
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
|
||||||
return out, true, nil
|
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) {
|
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
|
return nil, err
|
||||||
} else if ok {
|
} else if rows != nil {
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5036,6 +5034,7 @@ func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql
|
|||||||
}
|
}
|
||||||
if mode == "products" || mode == "product_detail" {
|
if mode == "products" || mode == "product_detail" {
|
||||||
rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
|
rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
|
||||||
|
rows = mergeProductPerformanceSalesSpreadKeys(ctx, pg, rows)
|
||||||
}
|
}
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
@@ -5076,6 +5075,88 @@ func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.
|
|||||||
return rows
|
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 {
|
func productPerformanceMapMarketVariantKey(row map[string]any) string {
|
||||||
productCode := normalizeProductPerformanceProductCode(stringFromMap(row, "product_code"))
|
productCode := normalizeProductPerformanceProductCode(stringFromMap(row, "product_code"))
|
||||||
if productCode == "" {
|
if productCode == "" {
|
||||||
@@ -5181,23 +5262,21 @@ type productPerformanceGroupedSnapshotAvgState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type productPerformanceGroupedSnapshotNode struct {
|
type productPerformanceGroupedSnapshotNode struct {
|
||||||
Key string
|
Key string
|
||||||
Level int
|
Level int
|
||||||
Field string
|
Field string
|
||||||
Value string
|
Value string
|
||||||
Row map[string]any
|
Row map[string]any
|
||||||
Count int
|
Count int
|
||||||
Children map[string]*productPerformanceGroupedSnapshotNode
|
Children map[string]*productPerformanceGroupedSnapshotNode
|
||||||
ChildOrder []string
|
ChildOrder []string
|
||||||
StockMetricSeen map[string]map[string]bool
|
StockMetricSeen map[string]map[string]bool
|
||||||
IdleSeen map[string]bool
|
IdleSeen map[string]bool
|
||||||
Avg map[string]*productPerformanceGroupedSnapshotAvgState
|
Avg map[string]*productPerformanceGroupedSnapshotAvgState
|
||||||
BucketCounts map[string]int
|
BucketCounts map[string]int
|
||||||
Image map[string]any
|
Image map[string]any
|
||||||
Market90Seen map[string]bool
|
MarketSeen map[string]map[string]bool
|
||||||
MarketTotalSeen map[string]bool
|
CustomerSeen map[string]map[string]bool
|
||||||
Customer90Seen map[string]bool
|
|
||||||
CustomerTotalSeen map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, levels []string, mode string) []map[string]any {
|
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]++
|
n.BucketCounts[bucket]++
|
||||||
}
|
}
|
||||||
for key, value := range row {
|
for key, value := range row {
|
||||||
if key == "row_key" || key == "key" || isProductPerformanceMarginField(key) {
|
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) || isProductPerformanceMarginField(key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
@@ -5324,34 +5403,35 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctVariantMetric(row map
|
|||||||
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
|
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
|
||||||
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
||||||
if market != "" && market != "STOK" {
|
if market != "" && market != "STOK" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
if n.Market90Seen == nil {
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
|
||||||
n.Market90Seen = map[string]bool{}
|
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"))
|
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
||||||
if customer != "" && customer != "-" {
|
if customer != "" && customer != "-" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
if n.Customer90Seen == nil {
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
|
||||||
n.Customer90Seen = map[string]bool{}
|
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) {
|
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
|
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)
|
deriveProductPerformanceGroupMetrics(row, n.Field)
|
||||||
if bucket := n.dominantBucket(); bucket != "" {
|
if bucket := n.dominantBucket(); bucket != "" {
|
||||||
row["performance_bucket"] = 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 {
|
func aggregateProductPerformanceRows(rows []map[string]any, groupField string) map[string]any {
|
||||||
out := map[string]any{}
|
out := map[string]any{}
|
||||||
market90Seen := map[string]bool{}
|
marketSeen := map[string]map[string]bool{}
|
||||||
marketTotalSeen := map[string]bool{}
|
customerSeen := map[string]map[string]bool{}
|
||||||
customer90Seen := map[string]bool{}
|
|
||||||
customerTotalSeen := map[string]bool{}
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
addProductPerformanceDistinctSpread(row, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
|
addProductPerformanceDistinctSpread(row, marketSeen, customerSeen)
|
||||||
for key, value := range row {
|
for key, value := range row {
|
||||||
if key == "row_key" || key == "key" {
|
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isProductPerformanceMarginField(key) {
|
if isProductPerformanceMarginField(key) {
|
||||||
@@ -5541,6 +5619,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
|
|||||||
}
|
}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
for key := range row {
|
for key := range row {
|
||||||
|
if isProductPerformanceInternalGroupField(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if isProductPerformanceMarginField(key) {
|
if isProductPerformanceMarginField(key) {
|
||||||
continue
|
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)
|
deriveProductPerformanceGroupMetrics(out, groupField)
|
||||||
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
|
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
|
||||||
return out
|
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"))
|
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
|
||||||
if market != "" && market != "STOK" {
|
if market != "" && market != "STOK" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
market90Seen[market] = true
|
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
|
||||||
}
|
if marketSeen[suffix] == nil {
|
||||||
if floatFromMap(row, "sales_usd_total") > 0 {
|
marketSeen[suffix] = map[string]bool{}
|
||||||
marketTotalSeen[market] = true
|
}
|
||||||
|
marketSeen[suffix][market] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
|
||||||
if customer != "" && customer != "-" {
|
if customer != "" && customer != "-" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
customer90Seen[customer] = true
|
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) {
|
func productPerformanceAddSeenStrings(seen map[string]map[string]bool, suffix string, values []string) map[string]map[string]bool {
|
||||||
if len(market90Seen) > 0 {
|
if len(values) == 0 {
|
||||||
row["market_count_90d"] = len(market90Seen)
|
return seen
|
||||||
}
|
}
|
||||||
if len(marketTotalSeen) > 0 {
|
if seen == nil {
|
||||||
row["market_count_total"] = len(marketTotalSeen)
|
seen = map[string]map[string]bool{}
|
||||||
}
|
}
|
||||||
if len(customer90Seen) > 0 {
|
if seen[suffix] == nil {
|
||||||
row["customer_count_90d"] = len(customer90Seen)
|
seen[suffix] = map[string]bool{}
|
||||||
}
|
}
|
||||||
if len(customerTotalSeen) > 0 {
|
for _, value := range values {
|
||||||
row["customer_count_total"] = len(customerTotalSeen)
|
value = strings.TrimSpace(value)
|
||||||
}
|
if value == "" || value == "-" {
|
||||||
if floatFromMap(row, "sales_usd_90d") > 0 {
|
continue
|
||||||
if intFromMap(row, "market_count_90d") == 0 {
|
|
||||||
row["market_count_90d"] = 1
|
|
||||||
}
|
|
||||||
if intFromMap(row, "customer_count_90d") == 0 {
|
|
||||||
row["customer_count_90d"] = 1
|
|
||||||
}
|
}
|
||||||
|
seen[suffix][value] = true
|
||||||
}
|
}
|
||||||
if floatFromMap(row, "sales_usd_total") > 0 {
|
return seen
|
||||||
if intFromMap(row, "market_count_total") == 0 {
|
}
|
||||||
row["market_count_total"] = maxInt(1, intFromMap(row, "market_count_90d"))
|
|
||||||
}
|
func productPerformanceStringSliceFromMap(row map[string]any, field string) []string {
|
||||||
if intFromMap(row, "customer_count_total") == 0 {
|
value, ok := row[field]
|
||||||
row["customer_count_total"] = maxInt(1, intFromMap(row, "customer_count_90d"))
|
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 {
|
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) {
|
func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
|
||||||
normalizeProductPerformanceCostFields(out)
|
normalizeProductPerformanceCostFields(out)
|
||||||
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
|
for _, suffix := range productPerformancePeriodSuffixes() {
|
||||||
sales := floatFromMap(out, "sales_usd_"+suffix)
|
sales := floatFromMap(out, "sales_usd_"+suffix)
|
||||||
qty := floatFromMap(out, "sales_qty_"+suffix)
|
qty := floatFromMap(out, "sales_qty_"+suffix)
|
||||||
stockQty := floatFromMap(out, "stock_qty")
|
stockQty := floatFromMap(out, "stock_qty")
|
||||||
@@ -5719,6 +5857,19 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|||||||
if turnoverBase <= 0 {
|
if turnoverBase <= 0 {
|
||||||
turnoverBase = stockQty
|
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 {
|
if turnoverBase > 0 {
|
||||||
out["stock_turnover_"+suffix] = qty / turnoverBase
|
out["stock_turnover_"+suffix] = qty / turnoverBase
|
||||||
} else {
|
} else {
|
||||||
@@ -5775,37 +5926,15 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
|
|||||||
if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
|
if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
|
||||||
out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
|
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"))
|
||||||
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
|
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
|
||||||
}
|
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
|
||||||
if _, ok := out["customer_score_180d"]; !ok {
|
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
|
||||||
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
|
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
|
||||||
}
|
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
|
||||||
if _, ok := out["customer_score_365d"]; !ok {
|
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
|
||||||
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
|
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
|
||||||
}
|
out["performance_score"] = out["performance_score_90d"]
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
|
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
|
||||||
score := productPerformanceOrderGroupScore(out)
|
score := productPerformanceOrderGroupScore(out)
|
||||||
out["performance_score_90d"] = score
|
out["performance_score_90d"] = score
|
||||||
@@ -5832,11 +5961,56 @@ func normalizeProductPerformanceCostFields(row map[string]any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
normalize("cost_price_usd", "base_price_usd")
|
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)
|
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) {
|
func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
|
||||||
value, ok := row[field]
|
value, ok := row[field]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -6172,7 +6346,16 @@ func mapGroupValue(row map[string]any, field string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldSumProductPerformanceField(field string) bool {
|
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, "_usd") ||
|
||||||
strings.HasSuffix(field, "_count") ||
|
strings.HasSuffix(field, "_count") ||
|
||||||
strings.HasSuffix(field, "_value_usd") ||
|
strings.HasSuffix(field, "_value_usd") ||
|
||||||
@@ -6189,6 +6372,9 @@ func shouldSumProductPerformanceField(field string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldAverageProductPerformanceField(field string) bool {
|
func shouldAverageProductPerformanceField(field string) bool {
|
||||||
|
if strings.HasPrefix(field, "avg_daily_sales_") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return strings.HasPrefix(field, "avg_") ||
|
return strings.HasPrefix(field, "avg_") ||
|
||||||
strings.HasPrefix(field, "unit_") ||
|
strings.HasPrefix(field, "unit_") ||
|
||||||
strings.HasPrefix(field, "base_price") ||
|
strings.HasPrefix(field, "base_price") ||
|
||||||
|
|||||||
@@ -35,6 +35,15 @@
|
|||||||
:disable="pageBusy"
|
:disable="pageBusy"
|
||||||
class="period-selector bg-white q-px-xs q-py-none"
|
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
|
<q-btn-dropdown
|
||||||
v-model="detailLevelMenuOpen"
|
v-model="detailLevelMenuOpen"
|
||||||
split
|
split
|
||||||
@@ -90,20 +99,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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="topScrollbarRef" class="performance-top-scrollbar q-mb-xs">
|
||||||
<div ref="topScrollbarInnerRef" class="performance-top-scrollbar-inner"></div>
|
<div ref="topScrollbarInnerRef" class="performance-top-scrollbar-inner"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,9 +108,14 @@
|
|||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
row-key="row_key"
|
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"
|
:rows="filteredGeneralRows"
|
||||||
:columns="generalColumns"
|
:columns="visibleGeneralColumns"
|
||||||
:loading="activeTableLoading"
|
:loading="activeTableLoading"
|
||||||
v-model:pagination="tablePagination.general"
|
v-model:pagination="tablePagination.general"
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
@@ -560,6 +560,7 @@
|
|||||||
'performance-table',
|
'performance-table',
|
||||||
'product-breakdown-table',
|
'product-breakdown-table',
|
||||||
'bg-white',
|
'bg-white',
|
||||||
|
{ 'compact-detail-columns': detailColumnsHidden },
|
||||||
activeTab === 'product_detail' ? 'product-detail-table' : 'product-summary-table'
|
activeTab === 'product_detail' ? 'product-detail-table' : 'product-summary-table'
|
||||||
]"
|
]"
|
||||||
:rows="displayProductKpiTableRows"
|
:rows="displayProductKpiTableRows"
|
||||||
@@ -1603,6 +1604,7 @@ const loadingNow = ref(0)
|
|||||||
const loadingStage = ref('')
|
const loadingStage = ref('')
|
||||||
const generalRowsLoaded = ref(false)
|
const generalRowsLoaded = ref(false)
|
||||||
const detailLevelMenuOpen = ref(false)
|
const detailLevelMenuOpen = ref(false)
|
||||||
|
const detailColumnsHidden = ref(false)
|
||||||
const topScrollbarRef = ref(null)
|
const topScrollbarRef = ref(null)
|
||||||
const topScrollbarInnerRef = ref(null)
|
const topScrollbarInnerRef = ref(null)
|
||||||
let backendGroupedTimer = null
|
let backendGroupedTimer = null
|
||||||
@@ -1675,7 +1677,6 @@ const productPerformanceExcelExportFilterFields = new Set([
|
|||||||
])
|
])
|
||||||
const performanceTabs = [
|
const performanceTabs = [
|
||||||
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
{ 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: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk/Yaka > Piyasa > Müşteri' },
|
||||||
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
{ 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' },
|
{ 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
|
const productDetailColumns = computed(() => orderedMetricColumns(columns
|
||||||
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
|
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
|
||||||
.filter(visiblePeriodColumn)))
|
.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 = [
|
const generalColumns = [
|
||||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||||
@@ -2066,6 +2092,8 @@ const generalColumns = [
|
|||||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const visibleGeneralColumns = computed(() => applyDetailColumnVisibility(generalColumns))
|
||||||
|
|
||||||
const orderAnalysisColumns = [
|
const orderAnalysisColumns = [
|
||||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
{ 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))
|
].forEach(target => ensureColumns(target, customerPeriodScoreColumns))
|
||||||
|
|
||||||
;[
|
;[
|
||||||
generalColumns,
|
|
||||||
orderAnalysisColumns,
|
orderAnalysisColumns,
|
||||||
orderGroupColumns,
|
orderGroupColumns,
|
||||||
orderProductCustomerColumns,
|
orderProductCustomerColumns,
|
||||||
@@ -3052,7 +3079,7 @@ function filterSourceRowsForTab (tabKey, fallbackRows) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backendGroupedFilterOptionParams (tabKey) {
|
function backendGroupedFilterOptionParams (tabKey) {
|
||||||
const columns = columnsForTableKey(tabKey)
|
const columns = filterColumnsForTableKey(tabKey)
|
||||||
const fields = columns
|
const fields = columns
|
||||||
.map(col => col?.name || '')
|
.map(col => col?.name || '')
|
||||||
.filter(name => backendGroupedFilterFields.has(name))
|
.filter(name => backendGroupedFilterFields.has(name))
|
||||||
@@ -3132,7 +3159,7 @@ function backendFilterOptionLabel (name, value) {
|
|||||||
|
|
||||||
function backendGroupedFilterState (tabKey) {
|
function backendGroupedFilterState (tabKey) {
|
||||||
const out = {}
|
const out = {}
|
||||||
const columns = columnsForTableKey(tabKey)
|
const columns = filterColumnsForTableKey(tabKey)
|
||||||
for (const col of columns) {
|
for (const col of columns) {
|
||||||
const name = col?.name || ''
|
const name = col?.name || ''
|
||||||
if (!isColumnFilterable(name)) continue
|
if (!isColumnFilterable(name)) continue
|
||||||
@@ -3152,7 +3179,7 @@ function shouldUseBackendGroupedRows (tabKey) {
|
|||||||
|
|
||||||
function tableFilterSource (tableKey) {
|
function tableFilterSource (tableKey) {
|
||||||
if (backendGroupedSupportedTab(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 === 'general') return { rows: generalRows.value, columns: generalColumns }
|
||||||
if (tableKey === 'markets') return { rows: marketRows.value, columns: marketColumns }
|
if (tableKey === 'markets') return { rows: marketRows.value, columns: marketColumns }
|
||||||
@@ -3162,6 +3189,20 @@ function tableFilterSource (tableKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function columnsForTableKey (tabKey) {
|
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 === 'products') return productColumns.value
|
||||||
if (tabKey === 'product_detail') return productDetailColumns.value
|
if (tabKey === 'product_detail') return productDetailColumns.value
|
||||||
if (tabKey === 'general') return generalColumns
|
if (tabKey === 'general') return generalColumns
|
||||||
@@ -3466,10 +3507,12 @@ function aggregateGroupFields (sourceRows, groupField = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldSumField (field) {
|
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) {
|
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) ||
|
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)
|
/(_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 stockQty = Number(out.stock_qty || 0)
|
||||||
const avgStock = Number(out[`avg_stock_${suffix}`] || 0)
|
const avgStock = Number(out[`avg_stock_${suffix}`] || 0)
|
||||||
const turnoverBase = avgStock > 0 ? avgStock : stockQty
|
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
|
out[`stock_turnover_${suffix}`] = turnoverBase > 0 ? qty / turnoverBase : 0
|
||||||
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
|
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
|
||||||
const { costPrice, basePrice } = periodCostPair(out, suffix)
|
const { costPrice, basePrice } = periodCostPair(out, suffix)
|
||||||
@@ -3530,19 +3579,33 @@ function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
|||||||
if (sourceRows.some(row => row.performance_bucket)) {
|
if (sourceRows.some(row => row.performance_bucket)) {
|
||||||
out.performance_bucket = dominantValue(sourceRows, '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'))
|
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'))
|
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'))
|
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'))
|
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'))
|
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'))
|
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'))
|
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'))
|
out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
|
||||||
if (!Object.prototype.hasOwnProperty.call(out, 'performance_score')) {
|
out.performance_score = Number(out.performance_score_90d || 0)
|
||||||
out.performance_score = Object.prototype.hasOwnProperty.call(out, 'performance_score_90d')
|
}
|
||||||
? Number(out.performance_score_90d || 0)
|
|
||||||
: groupPerformanceScore(out, groupField)
|
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 = '') {
|
function groupPerformanceScore (row, groupField = '') {
|
||||||
@@ -5204,7 +5267,7 @@ function productPerformanceExcelExportTableKey () {
|
|||||||
function buildProductPerformanceExcelExportFilters () {
|
function buildProductPerformanceExcelExportFilters () {
|
||||||
const tableKey = productPerformanceExcelExportTableKey()
|
const tableKey = productPerformanceExcelExportTableKey()
|
||||||
const out = {}
|
const out = {}
|
||||||
for (const col of columnsForTableKey(tableKey) || []) {
|
for (const col of filterColumnsForTableKey(tableKey) || []) {
|
||||||
const name = String(col?.name || '').trim()
|
const name = String(col?.name || '').trim()
|
||||||
if (!productPerformanceExcelExportFilterFields.has(name)) continue
|
if (!productPerformanceExcelExportFilterFields.has(name)) continue
|
||||||
const selected = selectedColumnFilters(tableKey, name)
|
const selected = selectedColumnFilters(tableKey, name)
|
||||||
@@ -5986,95 +6049,95 @@ onBeforeUnmount(() => {
|
|||||||
table-layout: fixed;
|
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):not(.compact-detail-columns) :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 td:nth-child(-n+10)) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
z-index: 2;
|
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;
|
z-index: 30;
|
||||||
background: #f8fbff;
|
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;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-breakdown-table:not(.product-detail-table) :deep(.q-table th: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) :deep(.q-table td:nth-child(1)) {
|
.product-breakdown-table:not(.product-detail-table):not(.compact-detail-columns) :deep(.q-table td:nth-child(1)) {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 190px;
|
width: 190px;
|
||||||
min-width: 190px;
|
min-width: 190px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(2)) {
|
||||||
left: 190px;
|
left: 190px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(3)) {
|
||||||
left: 330px;
|
left: 330px;
|
||||||
width: 110px;
|
width: 110px;
|
||||||
min-width: 110px;
|
min-width: 110px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(4)) {
|
||||||
left: 440px;
|
left: 440px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(5)) {
|
||||||
left: 590px;
|
left: 590px;
|
||||||
width: 170px;
|
width: 170px;
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(6)) {
|
||||||
left: 760px;
|
left: 760px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(7)) {
|
||||||
left: 910px;
|
left: 910px;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(8)) {
|
||||||
left: 1040px;
|
left: 1040px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(9)) {
|
||||||
left: 1130px;
|
left: 1130px;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
max-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):not(.compact-detail-columns) :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 td:nth-child(10)) {
|
||||||
left: 1210px;
|
left: 1210px;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
@@ -6082,6 +6145,58 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
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) {
|
.product-breakdown-table.product-detail-table :deep(.q-table) {
|
||||||
min-width: 2620px;
|
min-width: 2620px;
|
||||||
}
|
}
|
||||||
@@ -6198,10 +6313,14 @@ onBeforeUnmount(() => {
|
|||||||
font-variant-numeric: tabular-nums;
|
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):not(.compact-detail-columns) :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 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 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-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 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-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)),
|
.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 th:nth-child(-n+3)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td: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 th:nth-child(-n+5)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td: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)),
|
.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-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-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-6 :deep(.q-table th:nth-child(-n+6)),
|
||||||
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(-n+7)),
|
.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 th:nth-child(3)),
|
||||||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td: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 th:nth-child(5)),
|
||||||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td: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)),
|
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(6)),
|
||||||
|
|||||||
Reference in New Issue
Block a user