ui: add B2B olmayan stok (orphans) page
This commit is contained in:
@@ -18,6 +18,8 @@ type ProductPerformanceFilters struct {
|
||||
Kategori string
|
||||
Seri string
|
||||
Bucket string
|
||||
SortBy string
|
||||
Descending bool
|
||||
Limit int
|
||||
Page int
|
||||
}
|
||||
@@ -149,19 +151,31 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
||||
stock_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_qty_30d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_qty_90d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_qty_180d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_qty_365d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_qty_730d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_usd_30d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_usd_90d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
sales_usd_365d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
avg_daily_sales_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
stock_days_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
avg_price_usd_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
avg_price_usd_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
cost_price_usd NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
base_price_usd NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
base_price_try NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
gross_profit_usd_90d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
gross_profit_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0,
|
||||
gross_margin_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
gross_margin_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
unit_profit_cost_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
unit_profit_cost_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
market_count_90d INTEGER NOT NULL DEFAULT 0,
|
||||
customer_count_90d INTEGER NOT NULL DEFAULT 0,
|
||||
sales_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
price_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
|
||||
@@ -181,6 +195,18 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_ilk_grubu TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_qty_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_price_usd_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS gross_profit_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS gross_margin_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_cost_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_cost_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`,
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
if _, err := pg.Exec(stmt); err != nil {
|
||||
@@ -631,20 +657,22 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
|
||||
return nil, 0, err
|
||||
}
|
||||
args = append(args, limit, (page-1)*limit)
|
||||
orderBy := productPerformanceOrderBy(f.SortBy, f.Descending)
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT
|
||||
to_char(kpi_date,'YYYY-MM-DD'), product_code, color_code, yaka_kodu, item_description,
|
||||
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
|
||||
sales_qty_30d, sales_qty_90d, sales_qty_365d, sales_qty_730d,
|
||||
sales_usd_30d, sales_usd_90d, sales_usd_365d, avg_daily_sales_90d,
|
||||
stock_days_90d, avg_price_usd_90d, cost_price_usd, base_price_usd, base_price_try,
|
||||
gross_profit_usd_90d, gross_margin_90d, customer_count_90d, sales_index_90d,
|
||||
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d,
|
||||
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, avg_daily_sales_90d, avg_daily_sales_180d,
|
||||
stock_days_90d, stock_days_180d, avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
|
||||
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
|
||||
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d, sales_index_90d,
|
||||
price_index_90d, margin_index_90d, performance_score, performance_bucket,
|
||||
recommendation, COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''), last_ref_number,
|
||||
to_char(updated_at,'YYYY-MM-DD HH24:MI:SS')
|
||||
FROM mk_product_performance_kpi_daily
|
||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)`+where+`
|
||||
ORDER BY performance_score DESC, sales_qty_90d DESC, product_code
|
||||
ORDER BY `+orderBy+`
|
||||
LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -656,10 +684,11 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
|
||||
if err := rows.Scan(
|
||||
&r.KpiDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
|
||||
&r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.StockQty,
|
||||
&r.SalesQty30, &r.SalesQty90, &r.SalesQty365, &r.SalesQty730,
|
||||
&r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD365, &r.AvgDailySales90,
|
||||
&r.StockDays90, &r.AvgPriceUSD90, &r.CostPriceUSD, &r.BasePriceUSD, &r.BasePriceTRY,
|
||||
&r.GrossProfitUSD90, &r.GrossMargin90, &r.CustomerCount90, &r.SalesIndex90,
|
||||
&r.SalesQty30, &r.SalesQty90, &r.SalesQty180, &r.SalesQty365, &r.SalesQty730,
|
||||
&r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD180, &r.SalesUSD365, &r.AvgDailySales90, &r.AvgDailySales180,
|
||||
&r.StockDays90, &r.StockDays180, &r.AvgPriceUSD90, &r.AvgPriceUSD180, &r.CostPriceUSD, &r.BasePriceUSD, &r.BasePriceTRY,
|
||||
&r.GrossProfitUSD90, &r.GrossProfitUSD180, &r.GrossMargin90, &r.GrossMargin180,
|
||||
&r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180, &r.MarketCount90, &r.CustomerCount90, &r.SalesIndex90,
|
||||
&r.PriceIndex90, &r.MarginIndex90, &r.PerformanceScore, &r.PerformanceBucket,
|
||||
&r.Recommendation, &r.LastSaleDate, &r.LastRefNumber, &r.UpdatedAt,
|
||||
); err != nil {
|
||||
@@ -676,6 +705,22 @@ func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.Produ
|
||||
}
|
||||
var s models.ProductPerformanceSummary
|
||||
err := pg.QueryRowContext(ctx, `
|
||||
WITH Latest AS (
|
||||
SELECT MAX(kpi_date) AS kpi_date
|
||||
FROM mk_product_performance_kpi_daily
|
||||
),
|
||||
KPI AS (
|
||||
SELECT *
|
||||
FROM mk_product_performance_kpi_daily
|
||||
WHERE kpi_date = (SELECT kpi_date FROM Latest)
|
||||
),
|
||||
Sales90 AS (
|
||||
SELECT
|
||||
COUNT(DISTINCT market_key) AS market_count_90d,
|
||||
COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
|
||||
FROM mk_product_performance_sales_daily
|
||||
WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
|
||||
)
|
||||
SELECT
|
||||
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
|
||||
COUNT(*),
|
||||
@@ -688,10 +733,12 @@ SELECT
|
||||
COALESCE(SUM(sales_qty_90d),0),
|
||||
COALESCE(SUM(sales_usd_90d),0),
|
||||
COALESCE(SUM(gross_profit_usd_90d),0),
|
||||
COALESCE(MAX(Sales90.market_count_90d),0)::integer,
|
||||
COALESCE(MAX(Sales90.customer_count_90d),0)::integer,
|
||||
COALESCE(to_char(MAX(updated_at),'YYYY-MM-DD HH24:MI:SS'),'')
|
||||
FROM mk_product_performance_kpi_daily
|
||||
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
|
||||
`).Scan(&s.KpiDate, &s.TotalRows, &s.StarCount, &s.StockRisk, &s.NoStockDemand, &s.TotalStock, &s.StockCostUSD, &s.RiskCostUSD, &s.SalesQty90, &s.SalesUSD90, &s.GrossProfit90, &s.UpdatedAt)
|
||||
FROM KPI
|
||||
CROSS JOIN Sales90
|
||||
`).Scan(&s.KpiDate, &s.TotalRows, &s.StarCount, &s.StockRisk, &s.NoStockDemand, &s.TotalStock, &s.StockCostUSD, &s.RiskCostUSD, &s.SalesQty90, &s.SalesUSD90, &s.GrossProfit90, &s.MarketCount90, &s.CustomerCount90, &s.UpdatedAt)
|
||||
return s, err
|
||||
}
|
||||
|
||||
@@ -879,6 +926,76 @@ LIMIT $1
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func ListProductPerformanceSalesDetails(ctx context.Context, pg *sql.DB, productCode, colorCode, yakaKodu string, limit int) ([]models.ProductPerformanceSalesDetailRow, error) {
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 100
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT
|
||||
to_char(sales_date,'YYYY-MM-DD') AS sales_date,
|
||||
last_ref_number,
|
||||
market_key,
|
||||
customer_country,
|
||||
customer_segment,
|
||||
customer_code,
|
||||
customer_name,
|
||||
sales_qty,
|
||||
sales_usd,
|
||||
avg_price_usd
|
||||
FROM mk_product_performance_sales_daily
|
||||
WHERE product_code=$1
|
||||
AND color_code=$2
|
||||
AND yaka_kodu=$3
|
||||
ORDER BY sales_date DESC, sales_usd DESC
|
||||
LIMIT $4
|
||||
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]models.ProductPerformanceSalesDetailRow, 0, limit)
|
||||
for rows.Next() {
|
||||
var r models.ProductPerformanceSalesDetailRow
|
||||
if err := rows.Scan(&r.SalesDate, &r.RefNumber, &r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName, &r.SalesQty, &r.SalesUSD, &r.AvgPriceUSD); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func ListProductPerformanceStockSizes(ctx context.Context, productCode, colorCode, yakaKodu string) ([]models.ProductPerformanceStockSizeRow, error) {
|
||||
if db.MssqlDB == nil {
|
||||
return nil, fmt.Errorf("mssql db nil")
|
||||
}
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||
SELECT
|
||||
SizeCode = LTRIM(RTRIM(ISNULL(S.ItemDim1Code, ''))),
|
||||
StockQty = SUM(S.InventoryQty1)
|
||||
FROM StockWithCost S WITH(NOLOCK)
|
||||
WHERE S.ItemTypeCode = 1
|
||||
AND LTRIM(RTRIM(S.ItemCode)) = @p1
|
||||
AND LTRIM(RTRIM(ISNULL(S.ColorCode, ''))) = @p2
|
||||
AND LTRIM(RTRIM(ISNULL(S.ItemDim2Code, ''))) = @p3
|
||||
GROUP BY LTRIM(RTRIM(ISNULL(S.ItemDim1Code, '')))
|
||||
HAVING SUM(S.InventoryQty1) <> 0
|
||||
ORDER BY LTRIM(RTRIM(ISNULL(S.ItemDim1Code, '')))
|
||||
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]models.ProductPerformanceStockSizeRow, 0, 32)
|
||||
for rows.Next() {
|
||||
var r models.ProductPerformanceStockSizeRow
|
||||
if err := rows.Scan(&r.SizeCode, &r.StockQty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
|
||||
parts := make([]string, 0, 6)
|
||||
args := make([]any, 0, 6)
|
||||
@@ -911,6 +1028,54 @@ func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
|
||||
return strings.Join(parts, ""), args
|
||||
}
|
||||
|
||||
func productPerformanceOrderBy(sortBy string, desc bool) string {
|
||||
allowed := map[string]string{
|
||||
"product_code": "product_code",
|
||||
"color_code": "color_code",
|
||||
"yaka_kodu": "yaka_kodu",
|
||||
"item_description": "item_description",
|
||||
"kategori": "kategori",
|
||||
"urun_ilk_grubu": "urun_ilk_grubu",
|
||||
"askili_yan": "askili_yan",
|
||||
"urun_ana_grubu": "urun_ana_grubu",
|
||||
"urun_alt_grubu": "urun_alt_grubu",
|
||||
"market_key": "market_key",
|
||||
"stock_qty": "stock_qty",
|
||||
"sales_qty_90d": "sales_qty_90d",
|
||||
"sales_qty_180d": "sales_qty_180d",
|
||||
"sales_usd_90d": "sales_usd_90d",
|
||||
"sales_usd_180d": "sales_usd_180d",
|
||||
"stock_days_90d": "stock_days_90d",
|
||||
"stock_days_180d": "stock_days_180d",
|
||||
"avg_price_usd_90d": "avg_price_usd_90d",
|
||||
"avg_price_usd_180d": "avg_price_usd_180d",
|
||||
"base_price_usd": "base_price_usd",
|
||||
"cost_price_usd": "cost_price_usd",
|
||||
"unit_profit_base_90d": "unit_profit_base_90d",
|
||||
"unit_profit_cost_90d": "unit_profit_cost_90d",
|
||||
"unit_profit_base_180d": "unit_profit_base_180d",
|
||||
"unit_profit_cost_180d": "unit_profit_cost_180d",
|
||||
"gross_margin_90d": "gross_margin_90d",
|
||||
"gross_margin_180d": "gross_margin_180d",
|
||||
"market_count_90d": "market_count_90d",
|
||||
"customer_count_90d": "customer_count_90d",
|
||||
"sales_index_90d": "sales_index_90d",
|
||||
"performance_score": "performance_score",
|
||||
"performance_bucket": "performance_bucket",
|
||||
"last_sale_date": "last_sale_date",
|
||||
}
|
||||
col := allowed[strings.TrimSpace(sortBy)]
|
||||
if col == "" {
|
||||
col = "performance_score"
|
||||
desc = true
|
||||
}
|
||||
dir := "ASC"
|
||||
if desc {
|
||||
dir = "DESC"
|
||||
}
|
||||
return col + " " + dir + ", product_code ASC, color_code ASC, yaka_kodu ASC"
|
||||
}
|
||||
|
||||
func dateOnly(t time.Time) time.Time {
|
||||
y, m, d := t.Date()
|
||||
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
|
||||
|
||||
Reference in New Issue
Block a user