ui: update ProductPerformanceProfitability page to enhance table views and tabs management
This commit is contained in:
@@ -21,6 +21,7 @@ func main() {
|
||||
stage := flag.String("stage", "all", "refresh stage: all, sales, stock, price, or kpi")
|
||||
resumeAfter := flag.Int("resume-after", 0, "skip this many source rows before inserting; implies skip-delete")
|
||||
skipDelete := flag.Bool("skip-delete", false, "skip deleting cache rows for the selected stage")
|
||||
productPrefix := flag.String("product-prefix", "", "optional product code prefix filter: S, O, N, X, I, or A")
|
||||
start := flag.String("start", "2022-01-01", "start date in YYYY-MM-DD")
|
||||
end := flag.String("end", "", "end date in YYYY-MM-DD; default today")
|
||||
mssqlTimeoutSec := flag.Int("mssql-timeout-sec", 43200, "MSSQL connection/read timeout in seconds for long refresh jobs")
|
||||
@@ -66,12 +67,13 @@ func main() {
|
||||
defer cancel()
|
||||
|
||||
result, err := queries.RefreshProductPerformance(ctx, pg, queries.ProductPerformanceRefreshRequest{
|
||||
Mode: *mode,
|
||||
Stage: *stage,
|
||||
ResumeAfter: *resumeAfter,
|
||||
SkipDelete: *skipDelete,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Mode: *mode,
|
||||
Stage: *stage,
|
||||
ResumeAfter: *resumeAfter,
|
||||
SkipDelete: *skipDelete,
|
||||
ProductPrefix: *productPrefix,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("refresh failed: %v", err)
|
||||
|
||||
+25
@@ -939,6 +939,31 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
"pricing", "view",
|
||||
wrapV3(routes.GetProductPerformanceSummaryHandler(pgDB)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/pricing/product-performance/general", "GET",
|
||||
"pricing", "view",
|
||||
wrapV3(routes.GetProductPerformanceGeneralHandler(pgDB)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/pricing/product-performance/orders", "GET",
|
||||
"pricing", "view",
|
||||
wrapV3(routes.GetProductPerformanceOrdersHandler(pgDB)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/pricing/product-performance/orders/groups", "GET",
|
||||
"pricing", "view",
|
||||
wrapV3(routes.GetProductPerformanceOrderGroupsHandler(pgDB)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/pricing/product-performance/orders/product-customers", "GET",
|
||||
"pricing", "view",
|
||||
wrapV3(routes.GetProductPerformanceOrderProductCustomersHandler(pgDB)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/pricing/product-performance/orders/market-details", "GET",
|
||||
"pricing", "view",
|
||||
wrapV3(routes.GetProductPerformanceOrderMarketDetailsHandler(pgDB)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/pricing/product-performance/markets", "GET",
|
||||
"pricing", "view",
|
||||
|
||||
@@ -71,6 +71,162 @@ type ProductPerformanceSummary struct {
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ProductPerformanceGeneralRow struct {
|
||||
PeriodStart string `json:"period_start"`
|
||||
PeriodEnd string `json:"period_end"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ColorCode string `json:"color_code"`
|
||||
YakaKodu string `json:"yaka_kodu"`
|
||||
ItemDescription string `json:"item_description"`
|
||||
Kategori string `json:"kategori"`
|
||||
Seri string `json:"seri"`
|
||||
YasGrubu string `json:"yas_grubu"`
|
||||
AskiliYan string `json:"askili_yan"`
|
||||
UrunIlkGrubu string `json:"urun_ilk_grubu"`
|
||||
UrunAnaGrubu string `json:"urun_ana_grubu"`
|
||||
UrunAltGrubu string `json:"urun_alt_grubu"`
|
||||
MarketKey string `json:"market_key"`
|
||||
StockQty float64 `json:"stock_qty"`
|
||||
SalesQtyTotal float64 `json:"sales_qty_total"`
|
||||
SalesUSDTotal float64 `json:"sales_usd_total"`
|
||||
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
|
||||
StockDaysTotal float64 `json:"stock_days_total"`
|
||||
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
|
||||
CostPriceUSD float64 `json:"cost_price_usd"`
|
||||
BasePriceUSD float64 `json:"base_price_usd"`
|
||||
GrossProfitUSDTotal float64 `json:"gross_profit_usd_total"`
|
||||
GrossMarginTotal float64 `json:"gross_margin_total"`
|
||||
UnitProfitCostTotal float64 `json:"unit_profit_cost_total"`
|
||||
UnitProfitBaseTotal float64 `json:"unit_profit_base_total"`
|
||||
MarketCountTotal int `json:"market_count_total"`
|
||||
CustomerCountTotal int `json:"customer_count_total"`
|
||||
InvoiceCountTotal int `json:"invoice_count_total"`
|
||||
SalesIndexTotal float64 `json:"sales_index_total"`
|
||||
PerformanceScore float64 `json:"performance_score"`
|
||||
PerformanceBucket string `json:"performance_bucket"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
FirstSaleDate string `json:"first_sale_date"`
|
||||
LastSaleDate string `json:"last_sale_date"`
|
||||
LastRefNumber string `json:"last_ref_number"`
|
||||
}
|
||||
|
||||
type ProductPerformanceOrderAnalysisRow struct {
|
||||
ProductCode string `json:"product_code"`
|
||||
ColorCode string `json:"color_code"`
|
||||
YakaKodu string `json:"yaka_kodu"`
|
||||
ItemDescription string `json:"item_description"`
|
||||
Kategori string `json:"kategori"`
|
||||
Seri string `json:"seri"`
|
||||
YasGrubu string `json:"yas_grubu"`
|
||||
AskiliYan string `json:"askili_yan"`
|
||||
UrunIlkGrubu string `json:"urun_ilk_grubu"`
|
||||
UrunAnaGrubu string `json:"urun_ana_grubu"`
|
||||
UrunAltGrubu string `json:"urun_alt_grubu"`
|
||||
MarketKey string `json:"market_key"`
|
||||
OrderQty float64 `json:"order_qty"`
|
||||
OrderUSD float64 `json:"order_usd"`
|
||||
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
|
||||
StockQty float64 `json:"stock_qty"`
|
||||
NetStockAfterOrder float64 `json:"net_stock_after_order"`
|
||||
CostPriceUSD float64 `json:"cost_price_usd"`
|
||||
BasePriceUSD float64 `json:"base_price_usd"`
|
||||
UnitProfitCostUSD float64 `json:"unit_profit_cost_usd"`
|
||||
UnitProfitBaseUSD float64 `json:"unit_profit_base_usd"`
|
||||
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
|
||||
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
|
||||
ExpectedMarginCost float64 `json:"expected_margin_cost"`
|
||||
MarketCount int `json:"market_count"`
|
||||
CustomerCount int `json:"customer_count"`
|
||||
OrderCount int `json:"order_count"`
|
||||
LineCount int `json:"line_count"`
|
||||
FirstOrderDate string `json:"first_order_date"`
|
||||
LastOrderDate string `json:"last_order_date"`
|
||||
EarliestDueDate string `json:"earliest_due_date"`
|
||||
OverdueQty float64 `json:"overdue_qty"`
|
||||
PerformanceBucket string `json:"performance_bucket"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
}
|
||||
|
||||
type ProductPerformanceOrderGroupRow struct {
|
||||
Breakdown string `json:"breakdown"`
|
||||
GroupKey string `json:"group_key"`
|
||||
MarketKey string `json:"market_key"`
|
||||
CustomerCode string `json:"customer_code"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ProductCount int `json:"product_count"`
|
||||
OrderQty float64 `json:"order_qty"`
|
||||
OrderUSD float64 `json:"order_usd"`
|
||||
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
|
||||
BaseCostUSD float64 `json:"base_cost_usd"`
|
||||
CostAmountUSD float64 `json:"cost_amount_usd"`
|
||||
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
|
||||
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
|
||||
ExpectedMarginBase float64 `json:"expected_margin_base"`
|
||||
ExpectedMarginCost float64 `json:"expected_margin_cost"`
|
||||
StockQty float64 `json:"stock_qty"`
|
||||
NetStockAfterOrder float64 `json:"net_stock_after_order"`
|
||||
MarketCount int `json:"market_count"`
|
||||
CustomerCount int `json:"customer_count"`
|
||||
OrderCount int `json:"order_count"`
|
||||
LineCount int `json:"line_count"`
|
||||
OverdueQty float64 `json:"overdue_qty"`
|
||||
PerformanceBucket string `json:"performance_bucket"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
}
|
||||
|
||||
type ProductPerformanceOrderProductCustomerRow struct {
|
||||
ProductCode string `json:"product_code"`
|
||||
ColorCode string `json:"color_code"`
|
||||
YakaKodu string `json:"yaka_kodu"`
|
||||
ItemDescription string `json:"item_description"`
|
||||
MarketKey string `json:"market_key"`
|
||||
CustomerCode string `json:"customer_code"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
OrderQty float64 `json:"order_qty"`
|
||||
OrderUSD float64 `json:"order_usd"`
|
||||
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
|
||||
StockQty float64 `json:"stock_qty"`
|
||||
NetStockAfterOrder float64 `json:"net_stock_after_order"`
|
||||
CostPriceUSD float64 `json:"cost_price_usd"`
|
||||
BasePriceUSD float64 `json:"base_price_usd"`
|
||||
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
|
||||
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
|
||||
ExpectedMarginBase float64 `json:"expected_margin_base"`
|
||||
ExpectedMarginCost float64 `json:"expected_margin_cost"`
|
||||
OrderCount int `json:"order_count"`
|
||||
LineCount int `json:"line_count"`
|
||||
OverdueQty float64 `json:"overdue_qty"`
|
||||
PerformanceBucket string `json:"performance_bucket"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
}
|
||||
|
||||
type ProductPerformanceOrderMarketDetailRow struct {
|
||||
MarketKey string `json:"market_key"`
|
||||
CustomerCode string `json:"customer_code"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
OrderNumber string `json:"order_number"`
|
||||
OrderDate string `json:"order_date"`
|
||||
DueDate string `json:"due_date"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ColorCode string `json:"color_code"`
|
||||
YakaKodu string `json:"yaka_kodu"`
|
||||
ItemDescription string `json:"item_description"`
|
||||
OrderQty float64 `json:"order_qty"`
|
||||
OrderUSD float64 `json:"order_usd"`
|
||||
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
|
||||
StockQty float64 `json:"stock_qty"`
|
||||
NetStockAfterOrder float64 `json:"net_stock_after_order"`
|
||||
CostPriceUSD float64 `json:"cost_price_usd"`
|
||||
BasePriceUSD float64 `json:"base_price_usd"`
|
||||
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
|
||||
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
|
||||
ExpectedMarginBase float64 `json:"expected_margin_base"`
|
||||
ExpectedMarginCost float64 `json:"expected_margin_cost"`
|
||||
IsOverdue bool `json:"is_overdue"`
|
||||
PerformanceBucket string `json:"performance_bucket"`
|
||||
Recommendation string `json:"recommendation"`
|
||||
}
|
||||
|
||||
type ProductPerformanceMarketRow struct {
|
||||
MarketKey string `json:"market_key"`
|
||||
Kategori string `json:"kategori"`
|
||||
|
||||
+1145
-24
@@ -7,8 +7,11 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type ProductPerformanceFilters struct {
|
||||
@@ -25,12 +28,13 @@ type ProductPerformanceFilters struct {
|
||||
}
|
||||
|
||||
type ProductPerformanceRefreshRequest struct {
|
||||
Mode string
|
||||
Stage string
|
||||
ResumeAfter int
|
||||
SkipDelete bool
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
Mode string
|
||||
Stage string
|
||||
ResumeAfter int
|
||||
SkipDelete bool
|
||||
ProductPrefix string
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
}
|
||||
|
||||
type ProductPerformanceRefreshResult struct {
|
||||
@@ -219,7 +223,8 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
|
||||
func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) {
|
||||
started := time.Now()
|
||||
stage := productPerformanceRefreshStage(req.Stage)
|
||||
log.Printf("[ProductPerformanceRefresh] start mode=%s stage=%s start=%s end=%s", req.Mode, stage, req.StartDate.Format("2006-01-02"), req.EndDate.Format("2006-01-02"))
|
||||
productPrefix := productPerformanceProductPrefix(req.ProductPrefix)
|
||||
log.Printf("[ProductPerformanceRefresh] start mode=%s stage=%s prefix=%s start=%s end=%s", req.Mode, stage, productPrefix, req.StartDate.Format("2006-01-02"), req.EndDate.Format("2006-01-02"))
|
||||
if pg == nil {
|
||||
return ProductPerformanceRefreshResult{}, fmt.Errorf("postgres db nil")
|
||||
}
|
||||
@@ -282,12 +287,18 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
|
||||
if shouldRun("sales") {
|
||||
if err := runStage("sales", func(tx *sql.Tx) error {
|
||||
log.Printf("[ProductPerformanceRefresh] delete sales cache start")
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM mk_product_performance_sales_daily WHERE sales_date BETWEEN $1 AND $2`, req.StartDate, req.EndDate); err != nil {
|
||||
deleteSQL := `DELETE FROM mk_product_performance_sales_daily WHERE sales_date BETWEEN $1 AND $2`
|
||||
deleteArgs := []any{req.StartDate, req.EndDate}
|
||||
if productPrefix != "" {
|
||||
deleteSQL += ` AND product_code ILIKE $3 || '%'`
|
||||
deleteArgs = append(deleteArgs, productPrefix)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, deleteSQL, deleteArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[ProductPerformanceRefresh] delete sales cache done")
|
||||
log.Printf("[ProductPerformanceRefresh] sales refresh start")
|
||||
rows, err := refreshProductPerformanceSales(ctx, tx, req.StartDate, req.EndDate)
|
||||
rows, err := refreshProductPerformanceSales(ctx, tx, req.StartDate, req.EndDate, productPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -303,7 +314,7 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
|
||||
|
||||
var stockRows int
|
||||
if shouldRun("stock") {
|
||||
rows, err := refreshProductPerformanceStockChunked(ctx, pg, req.StartDate, req.EndDate, started, req.SkipDelete || req.ResumeAfter > 0, req.ResumeAfter)
|
||||
rows, err := refreshProductPerformanceStockChunked(ctx, pg, req.StartDate, req.EndDate, started, req.SkipDelete || req.ResumeAfter > 0, req.ResumeAfter, productPrefix)
|
||||
if err != nil {
|
||||
return ProductPerformanceRefreshResult{}, err
|
||||
}
|
||||
@@ -315,7 +326,7 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
|
||||
if shouldRun("price") {
|
||||
if err := runStage("price", func(tx *sql.Tx) error {
|
||||
log.Printf("[ProductPerformanceRefresh] price refresh start")
|
||||
if err := refreshProductPerformancePrices(ctx, tx); err != nil {
|
||||
if err := refreshProductPerformancePrices(ctx, tx, productPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[ProductPerformanceRefresh] price refresh done elapsed=%s", time.Since(started).Round(time.Second))
|
||||
@@ -369,9 +380,19 @@ func productPerformanceRefreshStage(raw string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time) (int, error) {
|
||||
log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate)
|
||||
func productPerformanceProductPrefix(raw string) string {
|
||||
prefix := strings.ToUpper(strings.TrimSpace(raw))
|
||||
switch prefix {
|
||||
case "", "S", "O", "N", "X", "I", "A":
|
||||
return prefix
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
|
||||
log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate, productPrefix)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -432,9 +453,9 @@ DO UPDATE SET
|
||||
return count, rows.Err()
|
||||
}
|
||||
|
||||
func refreshProductPerformanceStock(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time) (int, error) {
|
||||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockSQL(), startDate, endDate)
|
||||
func refreshProductPerformanceStock(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
|
||||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockSQL(), startDate, endDate, productPrefix)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -461,7 +482,7 @@ func refreshProductPerformanceStock(ctx context.Context, tx *sql.Tx, startDate,
|
||||
return count, rows.Err()
|
||||
}
|
||||
|
||||
func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, startDate, endDate, started time.Time, skipDelete bool, resumeAfter int) (int, error) {
|
||||
func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, startDate, endDate, started time.Time, skipDelete bool, resumeAfter int, productPrefix string) (int, error) {
|
||||
if skipDelete {
|
||||
log.Printf("[ProductPerformanceRefresh] stock delete skipped resume_after=%d", resumeAfter)
|
||||
} else {
|
||||
@@ -472,7 +493,13 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star
|
||||
}
|
||||
defer deleteTx.Rollback()
|
||||
log.Printf("[ProductPerformanceRefresh] delete stock cache start")
|
||||
if _, err := deleteTx.ExecContext(ctx, `DELETE FROM mk_product_performance_stock_daily WHERE stock_date BETWEEN $1 AND $2`, startDate, endDate); err != nil {
|
||||
deleteSQL := `DELETE FROM mk_product_performance_stock_daily WHERE stock_date BETWEEN $1 AND $2`
|
||||
deleteArgs := []any{startDate, endDate}
|
||||
if productPrefix != "" {
|
||||
deleteSQL += ` AND product_code ILIKE $3 || '%'`
|
||||
deleteArgs = append(deleteArgs, productPrefix)
|
||||
}
|
||||
if _, err := deleteTx.ExecContext(ctx, deleteSQL, deleteArgs...); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log.Printf("[ProductPerformanceRefresh] delete stock cache done")
|
||||
@@ -484,8 +511,8 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star
|
||||
}
|
||||
|
||||
log.Printf("[ProductPerformanceRefresh] stock refresh start")
|
||||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockSQL(), startDate, endDate)
|
||||
log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockSQL(), startDate, endDate, productPrefix)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -582,9 +609,9 @@ DO UPDATE SET
|
||||
return err
|
||||
}
|
||||
|
||||
func refreshProductPerformancePrices(ctx context.Context, tx *sql.Tx) error {
|
||||
log.Printf("[ProductPerformanceRefresh] price mssql query start")
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformancePriceSQL())
|
||||
func refreshProductPerformancePrices(ctx context.Context, tx *sql.Tx, productPrefix string) error {
|
||||
log.Printf("[ProductPerformanceRefresh] price mssql query start prefix=%s", productPrefix)
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, productPerformancePriceSQL(), productPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -742,6 +769,965 @@ CROSS JOIN Sales90
|
||||
return s, err
|
||||
}
|
||||
|
||||
func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceGeneralRow, error) {
|
||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 500
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
WITH Bounds AS (
|
||||
SELECT
|
||||
DATE '2022-01-01' AS period_start,
|
||||
COALESCE(
|
||||
(SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily),
|
||||
(SELECT MAX(sales_date) FROM mk_product_performance_sales_daily),
|
||||
current_date
|
||||
) AS period_end
|
||||
),
|
||||
LatestStockDate AS (
|
||||
SELECT MAX(stock_date) AS stock_date
|
||||
FROM mk_product_performance_stock_daily
|
||||
),
|
||||
SalesAgg AS (
|
||||
SELECT
|
||||
product_code,
|
||||
color_code,
|
||||
yaka_kodu,
|
||||
market_key,
|
||||
MAX(item_description) AS item_description,
|
||||
MAX(kategori) AS kategori,
|
||||
MAX(seri) AS seri,
|
||||
MAX(yas_grubu) AS yas_grubu,
|
||||
MAX(askili_yan) AS askili_yan,
|
||||
MAX(urun_ilk_grubu) AS urun_ilk_grubu,
|
||||
MAX(urun_ana_grubu) AS urun_ana_grubu,
|
||||
MAX(urun_alt_grubu) AS urun_alt_grubu,
|
||||
MIN(sales_date) AS first_sale_date,
|
||||
MAX(sales_date) AS last_sale_date,
|
||||
MAX(last_ref_number) AS last_ref_number,
|
||||
COALESCE(SUM(sales_qty),0) AS sales_qty_total,
|
||||
COALESCE(SUM(sales_usd),0) AS sales_usd_total,
|
||||
COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')),0)::integer AS customer_count_total,
|
||||
COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
|
||||
FROM mk_product_performance_sales_daily, Bounds
|
||||
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
||||
GROUP BY product_code, color_code, yaka_kodu, market_key
|
||||
),
|
||||
StockAgg AS (
|
||||
SELECT
|
||||
s.product_code,
|
||||
s.color_code,
|
||||
s.yaka_kodu,
|
||||
COALESCE(SUM(s.stock_qty),0) AS stock_qty
|
||||
FROM mk_product_performance_stock_daily s
|
||||
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
||||
GROUP BY s.product_code, s.color_code, s.yaka_kodu
|
||||
),
|
||||
StockOnly AS (
|
||||
SELECT
|
||||
s.product_code,
|
||||
s.color_code,
|
||||
s.yaka_kodu,
|
||||
'STOK'::text AS market_key,
|
||||
MAX(s.item_description) AS item_description,
|
||||
MAX(s.kategori) AS kategori,
|
||||
MAX(s.seri) AS seri,
|
||||
MAX(s.yas_grubu) AS yas_grubu,
|
||||
MAX(s.askili_yan) AS askili_yan,
|
||||
MAX(s.urun_ilk_grubu) AS urun_ilk_grubu,
|
||||
MAX(s.urun_ana_grubu) AS urun_ana_grubu,
|
||||
MAX(s.urun_alt_grubu) AS urun_alt_grubu,
|
||||
NULL::date AS first_sale_date,
|
||||
NULL::date AS last_sale_date,
|
||||
''::text AS last_ref_number,
|
||||
0::numeric AS sales_qty_total,
|
||||
0::numeric AS sales_usd_total,
|
||||
0::integer AS customer_count_total,
|
||||
0::integer AS invoice_count_total
|
||||
FROM mk_product_performance_stock_daily s
|
||||
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM SalesAgg a
|
||||
WHERE a.product_code = s.product_code
|
||||
AND a.color_code = s.color_code
|
||||
AND a.yaka_kodu = s.yaka_kodu
|
||||
)
|
||||
GROUP BY s.product_code, s.color_code, s.yaka_kodu
|
||||
),
|
||||
BaseRows AS (
|
||||
SELECT * FROM SalesAgg
|
||||
UNION ALL
|
||||
SELECT * FROM StockOnly
|
||||
),
|
||||
Spread AS (
|
||||
SELECT
|
||||
product_code,
|
||||
color_code,
|
||||
yaka_kodu,
|
||||
COUNT(DISTINCT NULLIF(market_key, 'STOK'))::integer AS market_count_total,
|
||||
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all
|
||||
FROM mk_product_performance_sales_daily, Bounds
|
||||
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
|
||||
GROUP BY product_code, color_code, yaka_kodu
|
||||
),
|
||||
Scored AS (
|
||||
SELECT
|
||||
Bounds.period_start,
|
||||
Bounds.period_end,
|
||||
b.product_code,
|
||||
b.color_code,
|
||||
b.yaka_kodu,
|
||||
b.item_description,
|
||||
b.kategori,
|
||||
b.seri,
|
||||
b.yas_grubu,
|
||||
b.askili_yan,
|
||||
b.urun_ilk_grubu,
|
||||
b.urun_ana_grubu,
|
||||
b.urun_alt_grubu,
|
||||
b.market_key,
|
||||
COALESCE(st.stock_qty, 0) AS stock_qty,
|
||||
b.sales_qty_total,
|
||||
b.sales_usd_total,
|
||||
b.sales_qty_total / GREATEST(1, (Bounds.period_end - Bounds.period_start + 1)) AS avg_daily_sales_total,
|
||||
CASE
|
||||
WHEN b.sales_qty_total <= 0 THEN 9999
|
||||
ELSE COALESCE(st.stock_qty, 0) / NULLIF(b.sales_qty_total / GREATEST(1, (Bounds.period_end - Bounds.period_start + 1)), 0)
|
||||
END AS stock_days_total,
|
||||
CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE b.sales_usd_total / NULLIF(b.sales_qty_total,0) END AS avg_price_usd_total,
|
||||
COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
|
||||
COALESCE(pd.base_price_usd,0) AS base_price_usd,
|
||||
b.sales_usd_total - (b.sales_qty_total * COALESCE(pd.cost_price_usd,0)) AS gross_profit_usd_total,
|
||||
CASE WHEN b.sales_usd_total <= 0 THEN 0 ELSE (b.sales_usd_total - (b.sales_qty_total * COALESCE(pd.cost_price_usd,0))) / NULLIF(b.sales_usd_total,0) END AS gross_margin_total,
|
||||
CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.cost_price_usd,0) END AS unit_profit_cost_total,
|
||||
CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.base_price_usd,0) END AS unit_profit_base_total,
|
||||
COALESCE(sp.market_count_total,0) AS market_count_total,
|
||||
COALESCE(sp.customer_count_total_all, b.customer_count_total) AS customer_count_total,
|
||||
b.invoice_count_total,
|
||||
CASE
|
||||
WHEN AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu) <= 0 THEN 0
|
||||
ELSE b.sales_qty_total / NULLIF(AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu),0)
|
||||
END AS sales_index_total,
|
||||
b.first_sale_date,
|
||||
b.last_sale_date,
|
||||
b.last_ref_number
|
||||
FROM BaseRows b
|
||||
CROSS JOIN Bounds
|
||||
LEFT JOIN StockAgg st
|
||||
ON st.product_code = b.product_code
|
||||
AND st.color_code = b.color_code
|
||||
AND st.yaka_kodu = b.yaka_kodu
|
||||
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = b.product_code
|
||||
LEFT JOIN Spread sp
|
||||
ON sp.product_code = b.product_code
|
||||
AND sp.color_code = b.color_code
|
||||
AND sp.yaka_kodu = b.yaka_kodu
|
||||
)
|
||||
SELECT
|
||||
to_char(period_start,'YYYY-MM-DD'),
|
||||
to_char(period_end,'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_total, sales_usd_total, avg_daily_sales_total, stock_days_total, avg_price_usd_total,
|
||||
cost_price_usd, base_price_usd, gross_profit_usd_total, gross_margin_total, unit_profit_cost_total, unit_profit_base_total,
|
||||
market_count_total, customer_count_total, invoice_count_total, sales_index_total,
|
||||
ROUND((
|
||||
LEAST(35, GREATEST(0, sales_index_total) * 18)
|
||||
+ LEAST(25, GREATEST(0, gross_margin_total) * 50)
|
||||
+ LEAST(20, customer_count_total * 1.5)
|
||||
+ LEAST(10, market_count_total * 2.5)
|
||||
+ CASE
|
||||
WHEN sales_qty_total > 0 AND stock_days_total BETWEEN 20 AND 180 THEN 10
|
||||
WHEN sales_qty_total > 0 AND stock_days_total > 365 THEN -10
|
||||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN -15
|
||||
ELSE 0
|
||||
END
|
||||
)::numeric, 4) AS performance_score,
|
||||
CASE
|
||||
WHEN sales_qty_total > 0 AND stock_qty <= 0 AND sales_index_total >= 1 THEN 'STOKSUZ_TALEP'
|
||||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'STOK_RISKI'
|
||||
WHEN gross_margin_total < 0 THEN 'FIYAT_BASKISI'
|
||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'YILDIZ_URUN'
|
||||
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
|
||||
ELSE 'TAKIP'
|
||||
END AS performance_bucket,
|
||||
CASE
|
||||
WHEN sales_qty_total > 0 AND stock_qty <= 0 THEN 'Talep var, stok yok. Uretim/satin alma onceligi ver.'
|
||||
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'Satis yok, stok maliyeti tasiyor. Piyasa/fiyat aksiyonu gerekli.'
|
||||
WHEN gross_margin_total < 0 THEN 'Ciplak maliyet altinda satis var. Fiyat veya maliyet kontrol edilmeli.'
|
||||
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
|
||||
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'Karli ama yavas. Dogru piyasada satis firsati var.'
|
||||
ELSE 'Izleme ve piyasa bazli aksiyon.'
|
||||
END AS recommendation,
|
||||
COALESCE(to_char(first_sale_date,'YYYY-MM-DD'),''),
|
||||
COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''),
|
||||
COALESCE(last_ref_number,'')
|
||||
FROM Scored
|
||||
ORDER BY performance_score DESC, sales_usd_total DESC, product_code ASC, color_code ASC, yaka_kodu ASC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]models.ProductPerformanceGeneralRow, 0, limit)
|
||||
for rows.Next() {
|
||||
var r models.ProductPerformanceGeneralRow
|
||||
if err := rows.Scan(
|
||||
&r.PeriodStart, &r.PeriodEnd, &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.SalesQtyTotal, &r.SalesUSDTotal, &r.AvgDailySalesTotal, &r.StockDaysTotal, &r.AvgPriceUSDTotal,
|
||||
&r.CostPriceUSD, &r.BasePriceUSD, &r.GrossProfitUSDTotal, &r.GrossMarginTotal, &r.UnitProfitCostTotal, &r.UnitProfitBaseTotal,
|
||||
&r.MarketCountTotal, &r.CustomerCountTotal, &r.InvoiceCountTotal, &r.SalesIndexTotal, &r.PerformanceScore,
|
||||
&r.PerformanceBucket, &r.Recommendation, &r.FirstSaleDate, &r.LastSaleDate, &r.LastRefNumber,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
|
||||
if db.MssqlDB == nil {
|
||||
return nil, fmt.Errorf("mssql db nil")
|
||||
}
|
||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 500
|
||||
}
|
||||
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||
WITH OpenOrderLines AS (
|
||||
SELECT
|
||||
OrderDate = CAST(h.OrderDate AS date),
|
||||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date),
|
||||
h.OrderHeaderID,
|
||||
h.OrderNumber,
|
||||
h.CurrAccCode,
|
||||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||||
ItemDescription = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT ItemDescription
|
||||
FROM cdItemDesc WITH(NOLOCK)
|
||||
WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode
|
||||
AND cdItemDesc.ItemCode = l.ItemCode
|
||||
AND cdItemDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
Kategori = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||||
AND cdItemAttributeDesc.AttributeTypeCode = 42
|
||||
AND cdItemAttributeDesc.AttributeCode = (
|
||||
SELECT TOP 1 AttributeCode
|
||||
FROM prItemAttribute WITH(NOLOCK)
|
||||
WHERE AttributeTypeCode = 42
|
||||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||||
AND prItemAttribute.ItemCode = l.ItemCode
|
||||
)
|
||||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
Seri = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||||
AND cdItemAttributeDesc.AttributeTypeCode = 2
|
||||
AND cdItemAttributeDesc.AttributeCode = (
|
||||
SELECT TOP 1 AttributeCode
|
||||
FROM prItemAttribute WITH(NOLOCK)
|
||||
WHERE AttributeTypeCode = 2
|
||||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||||
AND prItemAttribute.ItemCode = l.ItemCode
|
||||
)
|
||||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
YasGrubu = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||||
AND cdItemAttributeDesc.AttributeTypeCode = 44
|
||||
AND cdItemAttributeDesc.AttributeCode = (
|
||||
SELECT TOP 1 AttributeCode
|
||||
FROM prItemAttribute WITH(NOLOCK)
|
||||
WHERE AttributeTypeCode = 44
|
||||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||||
AND prItemAttribute.ItemCode = l.ItemCode
|
||||
)
|
||||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
AskiliYan = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT TOP 1 ProductAtt45
|
||||
FROM ProductAttributesFilter WITH(NOLOCK)
|
||||
WHERE ProductAttributesFilter.ItemCode = l.ItemCode
|
||||
), SPACE(0))),
|
||||
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||||
AND cdItemAttributeDesc.AttributeTypeCode = 1
|
||||
AND cdItemAttributeDesc.AttributeCode = (
|
||||
SELECT TOP 1 AttributeCode
|
||||
FROM prItemAttribute WITH(NOLOCK)
|
||||
WHERE AttributeTypeCode = 1
|
||||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||||
AND prItemAttribute.ItemCode = l.ItemCode
|
||||
)
|
||||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
UrunAltGrubu = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdItemAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
|
||||
AND cdItemAttributeDesc.AttributeTypeCode = 2
|
||||
AND cdItemAttributeDesc.AttributeCode = (
|
||||
SELECT TOP 1 AttributeCode
|
||||
FROM prItemAttribute WITH(NOLOCK)
|
||||
WHERE AttributeTypeCode = 2
|
||||
AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
|
||||
AND prItemAttribute.ItemCode = l.ItemCode
|
||||
)
|
||||
AND cdItemAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
MarketKey = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
|
||||
AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
|
||||
AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
|
||||
AND cdCurrAccAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
Qty = ISNULL(l.Qty1, 0),
|
||||
AmountUSD = CASE
|
||||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||||
ELSE 0
|
||||
END
|
||||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK)
|
||||
ON l.OrderHeaderID = h.OrderHeaderID
|
||||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK)
|
||||
ON c.OrderLineID = l.OrderLineID
|
||||
AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK)
|
||||
ON caf.CurrAccTypeCode = h.CurrAccTypeCode
|
||||
AND caf.CurrAccCode = h.CurrAccCode
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 Rate
|
||||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||||
WHERE CurrencyCode = 'USD'
|
||||
AND RelationCurrencyCode = 'TRY'
|
||||
AND ExchangeTypeCode = 6
|
||||
AND Rate > 0
|
||||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||||
ORDER BY Date DESC
|
||||
) usd
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 Rate
|
||||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||||
WHERE CurrencyCode = h.DocCurrencyCode
|
||||
AND RelationCurrencyCode = 'TRY'
|
||||
AND ExchangeTypeCode = 6
|
||||
AND Rate > 0
|
||||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||||
ORDER BY Date DESC
|
||||
) cur
|
||||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||||
AND ISNULL(h.IsClosed, 0) = 0
|
||||
AND h.OrderTypeCode = 1
|
||||
AND h.ProcessCode = 'WS'
|
||||
AND ISNULL(l.IsClosed, 0) = 0
|
||||
AND l.ItemTypeCode = 1
|
||||
AND ISNULL(l.Qty1, 0) > 0
|
||||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||||
AND (
|
||||
l.ItemCode LIKE 'S%'
|
||||
OR l.ItemCode LIKE 'O%'
|
||||
OR l.ItemCode LIKE 'N%'
|
||||
OR l.ItemCode LIKE 'X%'
|
||||
OR l.ItemCode LIKE 'I%'
|
||||
OR l.ItemCode LIKE 'A%'
|
||||
)
|
||||
),
|
||||
Spread AS (
|
||||
SELECT
|
||||
ProductCode,
|
||||
ColorCode,
|
||||
YakaKodu,
|
||||
MarketCount = COUNT(DISTINCT NULLIF(MarketKey, '')),
|
||||
CustomerCount = COUNT(DISTINCT NULLIF(CurrAccCode, ''))
|
||||
FROM OpenOrderLines
|
||||
GROUP BY ProductCode, ColorCode, YakaKodu
|
||||
)
|
||||
SELECT TOP (@p1)
|
||||
o.ProductCode,
|
||||
o.ColorCode,
|
||||
o.YakaKodu,
|
||||
ItemDescription = MAX(o.ItemDescription),
|
||||
Kategori = MAX(o.Kategori),
|
||||
Seri = MAX(o.Seri),
|
||||
YasGrubu = MAX(o.YasGrubu),
|
||||
AskiliYan = MAX(o.AskiliYan),
|
||||
UrunAnaGrubu = MAX(o.UrunAnaGrubu),
|
||||
UrunAltGrubu = MAX(o.UrunAltGrubu),
|
||||
o.MarketKey,
|
||||
OrderQty = SUM(o.Qty),
|
||||
OrderUSD = SUM(o.AmountUSD),
|
||||
AvgOrderPriceUSD = CASE WHEN SUM(o.Qty) = 0 THEN 0 ELSE SUM(o.AmountUSD) / NULLIF(SUM(o.Qty), 0) END,
|
||||
MarketCount = MAX(s.MarketCount),
|
||||
CustomerCount = COUNT(DISTINCT NULLIF(o.CurrAccCode, '')),
|
||||
OrderCount = COUNT(DISTINCT o.OrderHeaderID),
|
||||
LineCount = COUNT(*),
|
||||
FirstOrderDate = CONVERT(varchar, MIN(o.OrderDate), 23),
|
||||
LastOrderDate = CONVERT(varchar, MAX(o.OrderDate), 23),
|
||||
EarliestDueDate = CONVERT(varchar, MIN(o.DueDate), 23),
|
||||
OverdueQty = SUM(CASE WHEN o.DueDate < CAST(GETDATE() AS date) THEN o.Qty ELSE 0 END)
|
||||
FROM OpenOrderLines o
|
||||
LEFT JOIN Spread s
|
||||
ON s.ProductCode = o.ProductCode
|
||||
AND s.ColorCode = o.ColorCode
|
||||
AND s.YakaKodu = o.YakaKodu
|
||||
GROUP BY o.ProductCode, o.ColorCode, o.YakaKodu, o.MarketKey
|
||||
ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.YakaKodu
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]models.ProductPerformanceOrderAnalysisRow, 0, limit)
|
||||
productCodes := make([]string, 0, limit)
|
||||
stockKeys := make([]string, 0, limit)
|
||||
seenProducts := map[string]bool{}
|
||||
seenStockKeys := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var r models.ProductPerformanceOrderAnalysisRow
|
||||
if err := rows.Scan(
|
||||
&r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan,
|
||||
&r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD,
|
||||
&r.MarketCount, &r.CustomerCount, &r.OrderCount, &r.LineCount, &r.FirstOrderDate, &r.LastOrderDate, &r.EarliestDueDate, &r.OverdueQty,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.UrunIlkGrubu = r.YasGrubu
|
||||
out = append(out, r)
|
||||
if !seenProducts[r.ProductCode] {
|
||||
seenProducts[r.ProductCode] = true
|
||||
productCodes = append(productCodes, r.ProductCode)
|
||||
}
|
||||
key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
|
||||
if !seenStockKeys[key] {
|
||||
seenStockKeys[key] = true
|
||||
stockKeys = append(stockKeys, key)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range out {
|
||||
row := &out[i]
|
||||
price := priceByProduct[row.ProductCode]
|
||||
row.CostPriceUSD = price.cost
|
||||
row.BasePriceUSD = price.base
|
||||
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||||
row.UnitProfitCostUSD = row.AvgOrderPriceUSD - row.CostPriceUSD
|
||||
row.UnitProfitBaseUSD = row.AvgOrderPriceUSD - row.BasePriceUSD
|
||||
row.ExpectedProfitCostUSD = row.UnitProfitCostUSD * row.OrderQty
|
||||
row.ExpectedProfitBaseUSD = row.UnitProfitBaseUSD * row.OrderQty
|
||||
if row.OrderUSD != 0 {
|
||||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||||
}
|
||||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderRecommendation(*row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceOrderGroupRow, error) {
|
||||
if db.MssqlDB == nil {
|
||||
return nil, fmt.Errorf("mssql db nil")
|
||||
}
|
||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mode := strings.ToLower(strings.TrimSpace(breakdown))
|
||||
if mode != "customer" {
|
||||
mode = "market"
|
||||
}
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 500
|
||||
}
|
||||
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||
WITH OpenOrderLines AS (
|
||||
SELECT
|
||||
h.OrderHeaderID,
|
||||
h.CurrAccCode,
|
||||
CustomerName = ISNULL(cad.CurrAccDescription, ''),
|
||||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||||
MarketKey = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription
|
||||
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
|
||||
AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
|
||||
AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
|
||||
AND cdCurrAccAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
Qty = ISNULL(l.Qty1, 0),
|
||||
AmountUSD = CASE
|
||||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||||
ELSE 0
|
||||
END,
|
||||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date)
|
||||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK)
|
||||
ON l.OrderHeaderID = h.OrderHeaderID
|
||||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK)
|
||||
ON c.OrderLineID = l.OrderLineID
|
||||
AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK)
|
||||
ON caf.CurrAccTypeCode = h.CurrAccTypeCode
|
||||
AND caf.CurrAccCode = h.CurrAccCode
|
||||
LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK)
|
||||
ON cad.CurrAccTypeCode = h.CurrAccTypeCode
|
||||
AND cad.CurrAccCode = h.CurrAccCode
|
||||
AND cad.LangCode = 'TR'
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 Rate
|
||||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||||
WHERE CurrencyCode = 'USD'
|
||||
AND RelationCurrencyCode = 'TRY'
|
||||
AND ExchangeTypeCode = 6
|
||||
AND Rate > 0
|
||||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||||
ORDER BY Date DESC
|
||||
) usd
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 Rate
|
||||
FROM dbo.AllExchangeRates WITH(NOLOCK)
|
||||
WHERE CurrencyCode = h.DocCurrencyCode
|
||||
AND RelationCurrencyCode = 'TRY'
|
||||
AND ExchangeTypeCode = 6
|
||||
AND Rate > 0
|
||||
AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
|
||||
ORDER BY Date DESC
|
||||
) cur
|
||||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||||
AND ISNULL(h.IsClosed, 0) = 0
|
||||
AND h.OrderTypeCode = 1
|
||||
AND h.ProcessCode = 'WS'
|
||||
AND ISNULL(l.IsClosed, 0) = 0
|
||||
AND l.ItemTypeCode = 1
|
||||
AND ISNULL(l.Qty1, 0) > 0
|
||||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||||
AND (
|
||||
l.ItemCode LIKE 'S%'
|
||||
OR l.ItemCode LIKE 'O%'
|
||||
OR l.ItemCode LIKE 'N%'
|
||||
OR l.ItemCode LIKE 'X%'
|
||||
OR l.ItemCode LIKE 'I%'
|
||||
OR l.ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
SELECT TOP (@p1)
|
||||
ProductCode,
|
||||
ColorCode,
|
||||
YakaKodu,
|
||||
MarketKey,
|
||||
CurrAccCode,
|
||||
CustomerName,
|
||||
OrderQty = SUM(Qty),
|
||||
OrderUSD = SUM(AmountUSD),
|
||||
OrderCount = COUNT(DISTINCT OrderHeaderID),
|
||||
LineCount = COUNT(*),
|
||||
OverdueQty = SUM(CASE WHEN DueDate < CAST(GETDATE() AS date) THEN Qty ELSE 0 END)
|
||||
FROM OpenOrderLines
|
||||
GROUP BY ProductCode, ColorCode, YakaKodu, MarketKey, CurrAccCode, CustomerName
|
||||
ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
||||
`, limit*4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type detail struct {
|
||||
productCode string
|
||||
colorCode string
|
||||
yakaKodu string
|
||||
marketKey string
|
||||
customerCode string
|
||||
customerName string
|
||||
orderQty float64
|
||||
orderUSD float64
|
||||
orderCount int
|
||||
lineCount int
|
||||
overdueQty float64
|
||||
}
|
||||
details := make([]detail, 0, limit*2)
|
||||
productCodes := make([]string, 0, limit)
|
||||
stockKeys := make([]string, 0, limit)
|
||||
seenProducts := map[string]bool{}
|
||||
seenStockKeys := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var d detail
|
||||
if err := rows.Scan(&d.productCode, &d.colorCode, &d.yakaKodu, &d.marketKey, &d.customerCode, &d.customerName, &d.orderQty, &d.orderUSD, &d.orderCount, &d.lineCount, &d.overdueQty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
details = append(details, d)
|
||||
if !seenProducts[d.productCode] {
|
||||
seenProducts[d.productCode] = true
|
||||
productCodes = append(productCodes, d.productCode)
|
||||
}
|
||||
key := productPerformanceVariantKey(d.productCode, d.colorCode, d.yakaKodu)
|
||||
if !seenStockKeys[key] {
|
||||
seenStockKeys[key] = true
|
||||
stockKeys = append(stockKeys, key)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grouped := map[string]*models.ProductPerformanceOrderGroupRow{}
|
||||
productSets := map[string]map[string]bool{}
|
||||
marketSets := map[string]map[string]bool{}
|
||||
customerSets := map[string]map[string]bool{}
|
||||
stockVariantSets := map[string]map[string]bool{}
|
||||
for _, d := range details {
|
||||
groupKey := strings.TrimSpace(d.marketKey)
|
||||
customerCode := ""
|
||||
customerName := ""
|
||||
if mode == "customer" {
|
||||
groupKey = strings.TrimSpace(d.customerCode)
|
||||
customerCode = strings.TrimSpace(d.customerCode)
|
||||
customerName = strings.TrimSpace(d.customerName)
|
||||
}
|
||||
if groupKey == "" {
|
||||
groupKey = "-"
|
||||
}
|
||||
row := grouped[groupKey]
|
||||
if row == nil {
|
||||
row = &models.ProductPerformanceOrderGroupRow{
|
||||
Breakdown: mode,
|
||||
GroupKey: groupKey,
|
||||
MarketKey: strings.TrimSpace(d.marketKey),
|
||||
CustomerCode: customerCode,
|
||||
CustomerName: customerName,
|
||||
}
|
||||
grouped[groupKey] = row
|
||||
productSets[groupKey] = map[string]bool{}
|
||||
marketSets[groupKey] = map[string]bool{}
|
||||
customerSets[groupKey] = map[string]bool{}
|
||||
stockVariantSets[groupKey] = map[string]bool{}
|
||||
}
|
||||
price := priceByProduct[d.productCode]
|
||||
variantKey := productPerformanceVariantKey(d.productCode, d.colorCode, d.yakaKodu)
|
||||
row.OrderQty += d.orderQty
|
||||
row.OrderUSD += d.orderUSD
|
||||
row.BaseCostUSD += d.orderQty * price.base
|
||||
row.CostAmountUSD += d.orderQty * price.cost
|
||||
if !stockVariantSets[groupKey][variantKey] {
|
||||
row.StockQty += stockByKey[variantKey]
|
||||
stockVariantSets[groupKey][variantKey] = true
|
||||
}
|
||||
row.OrderCount += d.orderCount
|
||||
row.LineCount += d.lineCount
|
||||
row.OverdueQty += d.overdueQty
|
||||
productSets[groupKey][d.productCode] = true
|
||||
if strings.TrimSpace(d.marketKey) != "" {
|
||||
marketSets[groupKey][strings.TrimSpace(d.marketKey)] = true
|
||||
}
|
||||
if strings.TrimSpace(d.customerCode) != "" {
|
||||
customerSets[groupKey][strings.TrimSpace(d.customerCode)] = true
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]models.ProductPerformanceOrderGroupRow, 0, len(grouped))
|
||||
for key, row := range grouped {
|
||||
row.ProductCount = len(productSets[key])
|
||||
row.MarketCount = len(marketSets[key])
|
||||
row.CustomerCount = len(customerSets[key])
|
||||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||||
if row.OrderQty != 0 {
|
||||
row.AvgOrderPriceUSD = row.OrderUSD / row.OrderQty
|
||||
}
|
||||
row.ExpectedProfitBaseUSD = row.OrderUSD - row.BaseCostUSD
|
||||
row.ExpectedProfitCostUSD = row.OrderUSD - row.CostAmountUSD
|
||||
if row.OrderUSD != 0 {
|
||||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||||
}
|
||||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderGroupRecommendation(*row)
|
||||
out = append(out, *row)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].ExpectedProfitCostUSD < 0 && out[j].ExpectedProfitCostUSD >= 0 {
|
||||
return true
|
||||
}
|
||||
if out[i].ExpectedProfitCostUSD >= 0 && out[j].ExpectedProfitCostUSD < 0 {
|
||||
return false
|
||||
}
|
||||
return out[i].OrderUSD > out[j].OrderUSD
|
||||
})
|
||||
if len(out) > limit {
|
||||
out = out[:limit]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListProductPerformanceOrderProductCustomers(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderProductCustomerRow, error) {
|
||||
if db.MssqlDB == nil {
|
||||
return nil, fmt.Errorf("mssql db nil")
|
||||
}
|
||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 500
|
||||
}
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||
WITH OpenOrderLines AS (
|
||||
SELECT
|
||||
h.OrderHeaderID,
|
||||
h.CurrAccCode,
|
||||
CustomerName = ISNULL(cad.CurrAccDescription, ''),
|
||||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||||
ItemDescription = dbo.HG_Temizlik(ISNULL((SELECT ItemDescription FROM cdItemDesc WITH(NOLOCK) WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode AND cdItemDesc.ItemCode = l.ItemCode AND cdItemDesc.LangCode = 'TR'), SPACE(0))),
|
||||
MarketKey = dbo.HG_Temizlik(ISNULL((
|
||||
SELECT AttributeDescription FROM cdCurrAccAttributeDesc WITH(NOLOCK)
|
||||
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
|
||||
AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
|
||||
AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
|
||||
AND cdCurrAccAttributeDesc.LangCode = 'TR'
|
||||
), SPACE(0))),
|
||||
Qty = ISNULL(l.Qty1, 0),
|
||||
AmountUSD = CASE
|
||||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||||
ELSE 0
|
||||
END,
|
||||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date)
|
||||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK) ON l.OrderHeaderID = h.OrderHeaderID
|
||||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK) ON c.OrderLineID = l.OrderLineID AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK) ON caf.CurrAccTypeCode = h.CurrAccTypeCode AND caf.CurrAccCode = h.CurrAccCode
|
||||
LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK) ON cad.CurrAccTypeCode = h.CurrAccTypeCode AND cad.CurrAccCode = h.CurrAccCode AND cad.LangCode = 'TR'
|
||||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = 'USD' AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) usd
|
||||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = h.DocCurrencyCode AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) cur
|
||||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||||
AND ISNULL(h.IsClosed, 0) = 0
|
||||
AND h.OrderTypeCode = 1
|
||||
AND h.ProcessCode = 'WS'
|
||||
AND ISNULL(l.IsClosed, 0) = 0
|
||||
AND l.ItemTypeCode = 1
|
||||
AND ISNULL(l.Qty1, 0) > 0
|
||||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||||
AND (l.ItemCode LIKE 'S%' OR l.ItemCode LIKE 'O%' OR l.ItemCode LIKE 'N%' OR l.ItemCode LIKE 'X%' OR l.ItemCode LIKE 'I%' OR l.ItemCode LIKE 'A%')
|
||||
)
|
||||
SELECT TOP (@p1)
|
||||
ProductCode, ColorCode, YakaKodu, MAX(ItemDescription), MarketKey, CurrAccCode, CustomerName,
|
||||
SUM(Qty), SUM(AmountUSD), CASE WHEN SUM(Qty)=0 THEN 0 ELSE SUM(AmountUSD)/NULLIF(SUM(Qty),0) END,
|
||||
COUNT(DISTINCT OrderHeaderID), COUNT(*), SUM(CASE WHEN DueDate < CAST(GETDATE() AS date) THEN Qty ELSE 0 END)
|
||||
FROM OpenOrderLines
|
||||
GROUP BY ProductCode, ColorCode, YakaKodu, MarketKey, CurrAccCode, CustomerName
|
||||
ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]models.ProductPerformanceOrderProductCustomerRow, 0, limit)
|
||||
productCodes := make([]string, 0, limit)
|
||||
stockKeys := make([]string, 0, limit)
|
||||
seenProducts := map[string]bool{}
|
||||
seenStockKeys := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var r models.ProductPerformanceOrderProductCustomerRow
|
||||
if err := rows.Scan(&r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.MarketKey, &r.CustomerCode, &r.CustomerName, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD, &r.OrderCount, &r.LineCount, &r.OverdueQty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
if !seenProducts[r.ProductCode] {
|
||||
seenProducts[r.ProductCode] = true
|
||||
productCodes = append(productCodes, r.ProductCode)
|
||||
}
|
||||
key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
|
||||
if !seenStockKeys[key] {
|
||||
seenStockKeys[key] = true
|
||||
stockKeys = append(stockKeys, key)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range out {
|
||||
row := &out[i]
|
||||
price := priceByProduct[row.ProductCode]
|
||||
row.CostPriceUSD = price.cost
|
||||
row.BasePriceUSD = price.base
|
||||
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||||
row.ExpectedProfitBaseUSD = row.OrderUSD - row.OrderQty*row.BasePriceUSD
|
||||
row.ExpectedProfitCostUSD = row.OrderUSD - row.OrderQty*row.CostPriceUSD
|
||||
if row.OrderUSD != 0 {
|
||||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||||
}
|
||||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderProductCustomerRecommendation(*row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListProductPerformanceOrderMarketDetails(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderMarketDetailRow, error) {
|
||||
if db.MssqlDB == nil {
|
||||
return nil, fmt.Errorf("mssql db nil")
|
||||
}
|
||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 || limit > 1500 {
|
||||
limit = 800
|
||||
}
|
||||
rows, err := db.MssqlDB.QueryContext(ctx, `
|
||||
WITH OpenOrderLines AS (
|
||||
SELECT
|
||||
MarketKey = dbo.HG_Temizlik(ISNULL((SELECT AttributeDescription FROM cdCurrAccAttributeDesc WITH(NOLOCK) WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3 AND cdCurrAccAttributeDesc.AttributeTypeCode = 1 AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01 AND cdCurrAccAttributeDesc.LangCode = 'TR'), SPACE(0))),
|
||||
h.CurrAccCode,
|
||||
CustomerName = ISNULL(cad.CurrAccDescription, ''),
|
||||
h.OrderNumber,
|
||||
OrderDate = CAST(h.OrderDate AS date),
|
||||
DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date),
|
||||
ProductCode = LTRIM(RTRIM(l.ItemCode)),
|
||||
ColorCode = LTRIM(RTRIM(ISNULL(l.ColorCode, ''))),
|
||||
YakaKodu = LTRIM(RTRIM(ISNULL(l.ItemDim2Code, ''))),
|
||||
ItemDescription = dbo.HG_Temizlik(ISNULL((SELECT ItemDescription FROM cdItemDesc WITH(NOLOCK) WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode AND cdItemDesc.ItemCode = l.ItemCode AND cdItemDesc.LangCode = 'TR'), SPACE(0))),
|
||||
Qty = ISNULL(l.Qty1, 0),
|
||||
AmountUSD = CASE
|
||||
WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
|
||||
WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
|
||||
WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
|
||||
ELSE 0
|
||||
END
|
||||
FROM dbo.trOrderHeader h WITH(NOLOCK)
|
||||
INNER JOIN dbo.trOrderLine l WITH(NOLOCK) ON l.OrderHeaderID = h.OrderHeaderID
|
||||
LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK) ON c.OrderLineID = l.OrderLineID AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
|
||||
LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK) ON caf.CurrAccTypeCode = h.CurrAccTypeCode AND caf.CurrAccCode = h.CurrAccCode
|
||||
LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK) ON cad.CurrAccTypeCode = h.CurrAccTypeCode AND cad.CurrAccCode = h.CurrAccCode AND cad.LangCode = 'TR'
|
||||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = 'USD' AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) usd
|
||||
OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = h.DocCurrencyCode AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) cur
|
||||
WHERE ISNULL(h.IsCancelOrder, 0) = 0
|
||||
AND ISNULL(h.IsClosed, 0) = 0
|
||||
AND h.OrderTypeCode = 1
|
||||
AND h.ProcessCode = 'WS'
|
||||
AND ISNULL(l.IsClosed, 0) = 0
|
||||
AND l.ItemTypeCode = 1
|
||||
AND ISNULL(l.Qty1, 0) > 0
|
||||
AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
|
||||
AND (l.ItemCode LIKE 'S%' OR l.ItemCode LIKE 'O%' OR l.ItemCode LIKE 'N%' OR l.ItemCode LIKE 'X%' OR l.ItemCode LIKE 'I%' OR l.ItemCode LIKE 'A%')
|
||||
)
|
||||
SELECT TOP (@p1)
|
||||
MarketKey, CurrAccCode, CustomerName, OrderNumber, CONVERT(varchar, OrderDate, 23), CONVERT(varchar, DueDate, 23),
|
||||
ProductCode, ColorCode, YakaKodu, MAX(ItemDescription),
|
||||
SUM(Qty), SUM(AmountUSD), CASE WHEN SUM(Qty)=0 THEN 0 ELSE SUM(AmountUSD)/NULLIF(SUM(Qty),0) END,
|
||||
CASE WHEN MIN(DueDate) < CAST(GETDATE() AS date) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END
|
||||
FROM OpenOrderLines
|
||||
GROUP BY MarketKey, CurrAccCode, CustomerName, OrderNumber, OrderDate, DueDate, ProductCode, ColorCode, YakaKodu
|
||||
ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DESC
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]models.ProductPerformanceOrderMarketDetailRow, 0, limit)
|
||||
productCodes := make([]string, 0, limit)
|
||||
stockKeys := make([]string, 0, limit)
|
||||
seenProducts := map[string]bool{}
|
||||
seenStockKeys := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var r models.ProductPerformanceOrderMarketDetailRow
|
||||
if err := rows.Scan(&r.MarketKey, &r.CustomerCode, &r.CustomerName, &r.OrderNumber, &r.OrderDate, &r.DueDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD, &r.IsOverdue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
if !seenProducts[r.ProductCode] {
|
||||
seenProducts[r.ProductCode] = true
|
||||
productCodes = append(productCodes, r.ProductCode)
|
||||
}
|
||||
key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
|
||||
if !seenStockKeys[key] {
|
||||
seenStockKeys[key] = true
|
||||
stockKeys = append(stockKeys, key)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range out {
|
||||
row := &out[i]
|
||||
price := priceByProduct[row.ProductCode]
|
||||
row.CostPriceUSD = price.cost
|
||||
row.BasePriceUSD = price.base
|
||||
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
|
||||
row.NetStockAfterOrder = row.StockQty - row.OrderQty
|
||||
row.ExpectedProfitBaseUSD = row.OrderUSD - row.OrderQty*row.BasePriceUSD
|
||||
row.ExpectedProfitCostUSD = row.OrderUSD - row.OrderQty*row.CostPriceUSD
|
||||
if row.OrderUSD != 0 {
|
||||
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
|
||||
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
|
||||
}
|
||||
row.PerformanceBucket, row.Recommendation = productPerformanceOrderMarketDetailRecommendation(*row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ListProductPerformanceMarkets(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceMarketRow, error) {
|
||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||
return nil, err
|
||||
@@ -1076,6 +2062,141 @@ func productPerformanceOrderBy(sortBy string, desc bool) string {
|
||||
return col + " " + dir + ", product_code ASC, color_code ASC, yaka_kodu ASC"
|
||||
}
|
||||
|
||||
type productPerformancePriceInfo struct {
|
||||
cost float64
|
||||
base float64
|
||||
}
|
||||
|
||||
func productPerformanceVariantKey(productCode, colorCode, yakaKodu string) string {
|
||||
return strings.TrimSpace(productCode) + "|" + strings.TrimSpace(colorCode) + "|" + strings.TrimSpace(yakaKodu)
|
||||
}
|
||||
|
||||
func productPerformancePriceLookup(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]productPerformancePriceInfo, error) {
|
||||
out := make(map[string]productPerformancePriceInfo, len(productCodes))
|
||||
if len(productCodes) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT product_code, COALESCE(cost_price_usd,0), COALESCE(base_price_usd,0)
|
||||
FROM mk_product_performance_price_dim
|
||||
WHERE product_code = ANY($1)
|
||||
`, pq.Array(productCodes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var productCode string
|
||||
var p productPerformancePriceInfo
|
||||
if err := rows.Scan(&productCode, &p.cost, &p.base); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[productCode] = p
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func productPerformanceStockLookup(ctx context.Context, pg *sql.DB, variantKeys []string) (map[string]float64, error) {
|
||||
out := make(map[string]float64, len(variantKeys))
|
||||
if len(variantKeys) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
WITH Latest AS (
|
||||
SELECT MAX(stock_date) AS stock_date
|
||||
FROM mk_product_performance_stock_daily
|
||||
)
|
||||
SELECT
|
||||
product_code || '|' || color_code || '|' || yaka_kodu AS variant_key,
|
||||
COALESCE(SUM(stock_qty),0) AS stock_qty
|
||||
FROM mk_product_performance_stock_daily
|
||||
WHERE stock_date = (SELECT stock_date FROM Latest)
|
||||
AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
|
||||
GROUP BY product_code, color_code, yaka_kodu
|
||||
`, pq.Array(variantKeys))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var key string
|
||||
var stockQty float64
|
||||
if err := rows.Scan(&key, &stockQty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[key] = stockQty
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func productPerformanceOrderRecommendation(row models.ProductPerformanceOrderAnalysisRow) (string, string) {
|
||||
switch {
|
||||
case row.OrderQty > 0 && row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||
return "STOKSUZ_TALEP", "Açık sipariş stoktan büyük. Karlı talep var; üretim/satın alma önceliği ver."
|
||||
case row.ExpectedProfitCostUSD < 0:
|
||||
return "FIYAT_BASKISI", "Açık sipariş çıplak maliyete göre zarar yazıyor. Fiyat/maliyet kontrol edilmeli."
|
||||
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder >= 0:
|
||||
return "YILDIZ_URUN", "Açık sipariş karlı ve stok karşılıyor. Teslimat korunmalı."
|
||||
case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder < 0:
|
||||
return "FIYAT_FIRSATI", "Karlı talep var ama stok yetersiz. Üretim planına alınmalı."
|
||||
case row.OverdueQty > 0:
|
||||
return "TAKIP", "Termin gecikmesi olan açık sipariş var. Operasyon takibi gerekli."
|
||||
default:
|
||||
return "TAKIP", "Sipariş, stok ve fiyat düzenli izlenmeli."
|
||||
}
|
||||
}
|
||||
|
||||
func productPerformanceOrderGroupRecommendation(row models.ProductPerformanceOrderGroupRow) (string, string) {
|
||||
switch {
|
||||
case row.ExpectedProfitCostUSD < 0:
|
||||
return "FIYAT_BASKISI", "Çıplak maliyete göre zarar yazan açık sipariş var. Fiyat/maliyet acil kontrol edilmeli."
|
||||
case row.ExpectedProfitBaseUSD < 0:
|
||||
return "FIYAT_BASKISI", "Taban maliyete göre brüt zarar var. Satış fiyatı veya iskonto kontrol edilmeli."
|
||||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||
return "STOKSUZ_TALEP", "Karlı açık talep var ama stok yetersiz. Üretim/satın alma önceliği ver."
|
||||
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
||||
return "YILDIZ_URUN", "Piyasa/müşteri açık siparişleri karlı. Teslimat ve stok korunmalı."
|
||||
case row.OverdueQty > 0:
|
||||
return "TAKIP", "Geciken açık sipariş var. Operasyon takibi gerekli."
|
||||
default:
|
||||
return "TAKIP", "Sipariş karlılığı ve stok yeterliliği izlenmeli."
|
||||
}
|
||||
}
|
||||
|
||||
func productPerformanceOrderProductCustomerRecommendation(row models.ProductPerformanceOrderProductCustomerRow) (string, string) {
|
||||
switch {
|
||||
case row.ExpectedProfitCostUSD < 0:
|
||||
return "FIYAT_BASKISI", "Bu müşteri/ürün açık siparişi çıplak maliyete göre zarar yazıyor."
|
||||
case row.ExpectedProfitBaseUSD < 0:
|
||||
return "FIYAT_BASKISI", "Bu müşteri/ürün açık siparişi taban maliyete göre brüt zarar yazıyor."
|
||||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||
return "STOKSUZ_TALEP", "Müşteride karlı talep var ama stok yetersiz."
|
||||
case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
|
||||
return "YILDIZ_URUN", "Müşteri bazında karlı açık talep var."
|
||||
case row.OverdueQty > 0:
|
||||
return "TAKIP", "Bu müşteri/ürün kırılımında geciken açık sipariş var."
|
||||
default:
|
||||
return "TAKIP", "Müşteri talebi, fiyat ve stok birlikte izlenmeli."
|
||||
}
|
||||
}
|
||||
|
||||
func productPerformanceOrderMarketDetailRecommendation(row models.ProductPerformanceOrderMarketDetailRow) (string, string) {
|
||||
switch {
|
||||
case row.ExpectedProfitCostUSD < 0:
|
||||
return "FIYAT_BASKISI", "Sipariş satırı çıplak maliyete göre zarar yazıyor."
|
||||
case row.ExpectedProfitBaseUSD < 0:
|
||||
return "FIYAT_BASKISI", "Sipariş satırı taban maliyete göre brüt zarar yazıyor."
|
||||
case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
|
||||
return "STOKSUZ_TALEP", "Karlı sipariş var ama mevcut stok siparişi karşılamıyor."
|
||||
case row.IsOverdue:
|
||||
return "TAKIP", "Termin geçmiş; teslimat aksiyonu gerekli."
|
||||
case row.ExpectedMarginCost >= 0.25:
|
||||
return "YILDIZ_URUN", "Sipariş karlı; teslimat ve stok korunmalı."
|
||||
default:
|
||||
return "TAKIP", "Sipariş fiyatı, stok ve termin izlenmeli."
|
||||
}
|
||||
}
|
||||
|
||||
func dateOnly(t time.Time) time.Time {
|
||||
y, m, d := t.Date()
|
||||
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
|
||||
|
||||
@@ -153,7 +153,18 @@ func productPerformanceSalesSQL() string {
|
||||
WHERE I.InvoiceDate >= @p1
|
||||
AND I.InvoiceDate < DATEADD(DAY, 1, @p2)
|
||||
AND I.ItemTypeCode = 1
|
||||
AND (I.ItemCode LIKE 'S%' OR I.ItemCode LIKE 'O%' OR I.ItemCode LIKE 'X%' OR I.ItemCode LIKE 'N%')
|
||||
AND (
|
||||
LEN(LTRIM(RTRIM(I.ItemCode))) = 13
|
||||
AND (
|
||||
I.ItemCode LIKE 'S%'
|
||||
OR I.ItemCode LIKE 'O%'
|
||||
OR I.ItemCode LIKE 'N%'
|
||||
OR I.ItemCode LIKE 'X%'
|
||||
OR I.ItemCode LIKE 'I%'
|
||||
OR I.ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
AND (@p3 = '' OR I.ItemCode LIKE @p3 + '%')
|
||||
AND (
|
||||
(I.CompanyCode = 1 AND I.ATAtt01 IN (1,2) AND I.ProcessCode IN ('WS','R'))
|
||||
OR (I.CompanyCode = 4 AND I.ProcessCode = 'R')
|
||||
@@ -228,7 +239,18 @@ Raw AS (
|
||||
INNER JOIN ActiveWarehouses W
|
||||
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
|
||||
WHERE S.ItemTypeCode = 1
|
||||
AND (S.ItemCode LIKE 'S%' OR S.ItemCode LIKE 'O%' OR S.ItemCode LIKE 'X%' OR S.ItemCode LIKE 'N%')
|
||||
AND (
|
||||
LEN(LTRIM(RTRIM(S.ItemCode))) = 13
|
||||
AND (
|
||||
S.ItemCode LIKE 'S%'
|
||||
OR S.ItemCode LIKE 'O%'
|
||||
OR S.ItemCode LIKE 'N%'
|
||||
OR S.ItemCode LIKE 'X%'
|
||||
OR S.ItemCode LIKE 'I%'
|
||||
OR S.ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
AND (@p3 = '' OR S.ItemCode LIKE @p3 + '%')
|
||||
AND S.DocumentDate < DATEADD(DAY, 1, @p2)
|
||||
GROUP BY
|
||||
S.ItemCode,
|
||||
@@ -334,12 +356,34 @@ func productPerformancePriceSQL() string {
|
||||
SELECT DISTINCT ItemCode
|
||||
FROM trPriceListLine WITH(NOLOCK)
|
||||
WHERE ItemTypeCode = 1
|
||||
AND (ItemCode LIKE 'S%' OR ItemCode LIKE 'O%' OR ItemCode LIKE 'X%' OR ItemCode LIKE 'N%')
|
||||
AND (
|
||||
LEN(LTRIM(RTRIM(ItemCode))) = 13
|
||||
AND (
|
||||
ItemCode LIKE 'S%'
|
||||
OR ItemCode LIKE 'O%'
|
||||
OR ItemCode LIKE 'N%'
|
||||
OR ItemCode LIKE 'X%'
|
||||
OR ItemCode LIKE 'I%'
|
||||
OR ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
AND (@p1 = '' OR ItemCode LIKE @p1 + '%')
|
||||
UNION
|
||||
SELECT DISTINCT ItemCode
|
||||
FROM prItemBasePrice WITH(NOLOCK)
|
||||
WHERE ItemTypeCode = 1
|
||||
AND (ItemCode LIKE 'S%' OR ItemCode LIKE 'O%' OR ItemCode LIKE 'X%' OR ItemCode LIKE 'N%')
|
||||
AND (
|
||||
LEN(LTRIM(RTRIM(ItemCode))) = 13
|
||||
AND (
|
||||
ItemCode LIKE 'S%'
|
||||
OR ItemCode LIKE 'O%'
|
||||
OR ItemCode LIKE 'N%'
|
||||
OR ItemCode LIKE 'X%'
|
||||
OR ItemCode LIKE 'I%'
|
||||
OR ItemCode LIKE 'A%'
|
||||
)
|
||||
)
|
||||
AND (@p1 = '' OR ItemCode LIKE @p1 + '%')
|
||||
),
|
||||
LatestPrice AS (
|
||||
SELECT
|
||||
|
||||
@@ -102,6 +102,91 @@ func GetProductPerformanceSummaryHandler(pg *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func GetProductPerformanceGeneralHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 500))
|
||||
if err != nil {
|
||||
http.Error(w, "genel urun performans KPI alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
}
|
||||
|
||||
func GetProductPerformanceOrdersHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 500))
|
||||
if err != nil {
|
||||
http.Error(w, "siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
}
|
||||
|
||||
func GetProductPerformanceOrderGroupsHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := queries.ListProductPerformanceOrderGroups(
|
||||
ctx,
|
||||
pg,
|
||||
strings.TrimSpace(r.URL.Query().Get("breakdown")),
|
||||
intQuery(r, "limit", 500),
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "siparis grup analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
}
|
||||
|
||||
func GetProductPerformanceOrderProductCustomersHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 500))
|
||||
if err != nil {
|
||||
http.Error(w, "urun musteri siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
}
|
||||
|
||||
func GetProductPerformanceOrderMarketDetailsHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
traceID := utils.TraceIDFromRequest(r)
|
||||
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 800))
|
||||
if err != nil {
|
||||
http.Error(w, "piyasa siparis detay analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
}
|
||||
|
||||
func GetProductPerformanceMarketsHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
@@ -156,12 +241,13 @@ func GetProductPerformanceCustomersHandler(pg *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
type productPerformanceRefreshPayload struct {
|
||||
Mode string `json:"mode"`
|
||||
Stage string `json:"stage"`
|
||||
ResumeAfter int `json:"resume_after"`
|
||||
SkipDelete bool `json:"skip_delete"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Mode string `json:"mode"`
|
||||
Stage string `json:"stage"`
|
||||
ResumeAfter int `json:"resume_after"`
|
||||
SkipDelete bool `json:"skip_delete"`
|
||||
ProductPrefix string `json:"product_prefix"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
}
|
||||
|
||||
func PostProductPerformanceRefreshHandler(pg *sql.DB) http.HandlerFunc {
|
||||
@@ -181,12 +267,13 @@ func PostProductPerformanceRefreshHandler(pg *sql.DB) http.HandlerFunc {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Hour)
|
||||
defer cancel()
|
||||
result, err := queries.RefreshProductPerformance(ctx, pg, queries.ProductPerformanceRefreshRequest{
|
||||
Mode: payload.Mode,
|
||||
Stage: payload.Stage,
|
||||
ResumeAfter: payload.ResumeAfter,
|
||||
SkipDelete: payload.SkipDelete,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Mode: payload.Mode,
|
||||
Stage: payload.Stage,
|
||||
ResumeAfter: payload.ResumeAfter,
|
||||
SkipDelete: payload.SkipDelete,
|
||||
ProductPrefix: payload.ProductPrefix,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "urun performans refresh hatasi: "+err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -356,7 +356,7 @@ const menuItems = [
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Ürün Fiyatlandırma',
|
||||
label: 'Ürün Fiyatlandırma ve Analiz',
|
||||
icon: 'sell',
|
||||
|
||||
children: [
|
||||
|
||||
@@ -32,38 +32,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-card flat bordered class="q-mb-sm">
|
||||
<q-card-section class="row q-col-gutter-sm items-end">
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input v-model="filters.q" dense outlined clearable label="Ürün / açıklama ara" @keyup.enter="reload" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input v-model="filters.product_code" dense outlined clearable label="Ürün kodu" @keyup.enter="reload" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input v-model="filters.kategori" dense outlined clearable label="Kategori" @keyup.enter="reload" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input v-model="filters.seri" dense outlined clearable label="Ürün Ana Grubu" @keyup.enter="reload" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-select
|
||||
v-model="filters.bucket"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
emit-value
|
||||
map-options
|
||||
label="Durum"
|
||||
:options="bucketOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-1">
|
||||
<q-btn class="full-width" color="secondary" outline icon="search" label="Ara" @click="reload" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
dense
|
||||
@@ -73,6 +41,9 @@
|
||||
align="left"
|
||||
>
|
||||
<q-tab name="products" icon="inventory_2" label="Ürün KPI" />
|
||||
<q-tab name="general" icon="analytics" label="KPI Genel" />
|
||||
<q-tab name="order_product_customers" icon="assignment_ind" label="Ürün > Müşteri Sipariş" />
|
||||
<q-tab name="order_market_details" icon="account_tree" label="Piyasa > Sipariş Detay" />
|
||||
<q-tab name="idle" icon="warning" label="Atıl Stok / Maliyet" />
|
||||
<q-tab name="markets" icon="query_stats" label="Piyasa Grup Kıyası" />
|
||||
<q-tab name="countries" icon="public" label="Ülke / Segment" />
|
||||
@@ -80,7 +51,121 @@
|
||||
</q-tabs>
|
||||
|
||||
<q-table
|
||||
v-if="activeTab === 'products'"
|
||||
v-if="activeTab === 'general'"
|
||||
flat
|
||||
bordered
|
||||
row-key="row_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="generalRows"
|
||||
:columns="generalColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score', descending: true }"
|
||||
>
|
||||
<template #body-cell-image="props">
|
||||
<q-td :props="props" class="product-image-cell">
|
||||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||||
<q-img
|
||||
v-if="getCachedProductImageUrl(props.row)"
|
||||
:src="getCachedProductImageUrl(props.row)"
|
||||
fit="contain"
|
||||
class="product-thumb"
|
||||
no-spinner
|
||||
/>
|
||||
<div v-else class="product-thumb-placeholder">
|
||||
<q-icon name="image" size="22px" />
|
||||
</div>
|
||||
<q-tooltip>Foto</q-tooltip>
|
||||
</button>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-performance_bucket="props">
|
||||
<q-td :props="props">
|
||||
<q-badge :color="bucketColor(props.row.performance_bucket)">
|
||||
{{ bucketLabel(props.row.performance_bucket) }}
|
||||
</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-table
|
||||
v-else-if="activeTab === 'order_product_customers'"
|
||||
flat
|
||||
bordered
|
||||
row-key="row_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="orderProductCustomerRows"
|
||||
:columns="orderProductCustomerColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_usd', descending: true }"
|
||||
>
|
||||
<template #body-cell-image="props">
|
||||
<q-td :props="props" class="product-image-cell">
|
||||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||||
<q-img
|
||||
v-if="getCachedProductImageUrl(props.row)"
|
||||
:src="getCachedProductImageUrl(props.row)"
|
||||
fit="contain"
|
||||
class="product-thumb"
|
||||
no-spinner
|
||||
/>
|
||||
<div v-else class="product-thumb-placeholder">
|
||||
<q-icon name="image" size="22px" />
|
||||
</div>
|
||||
<q-tooltip>Foto</q-tooltip>
|
||||
</button>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-performance_bucket="props">
|
||||
<q-td :props="props">
|
||||
<q-badge :color="bucketColor(props.row.performance_bucket)">
|
||||
{{ bucketLabel(props.row.performance_bucket) }}
|
||||
</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-net_stock_after_order="props">
|
||||
<q-td :props="props" class="text-right" :class="Number(props.row.net_stock_after_order || 0) < 0 ? 'text-negative text-weight-bold' : ''">
|
||||
{{ formatNumber(props.row.net_stock_after_order, 0) }}
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-expected_profit_cost_usd="props">
|
||||
<q-td :props="props" class="text-right" :class="Number(props.row.expected_profit_cost_usd || 0) < 0 ? 'text-negative text-weight-bold' : 'text-positive'">
|
||||
{{ formatMoney(props.row.expected_profit_cost_usd, 'USD') }}
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-table
|
||||
v-else-if="activeTab === 'order_market_details'"
|
||||
flat
|
||||
bordered
|
||||
row-key="row_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="orderMarketDetailRows"
|
||||
:columns="orderMarketDetailColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_date', descending: true }"
|
||||
>
|
||||
<template #body-cell-performance_bucket="props">
|
||||
<q-td :props="props">
|
||||
<q-badge :color="bucketColor(props.row.performance_bucket)">
|
||||
{{ bucketLabel(props.row.performance_bucket) }}
|
||||
</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-net_stock_after_order="props">
|
||||
<q-td :props="props" class="text-right" :class="Number(props.row.net_stock_after_order || 0) < 0 ? 'text-negative text-weight-bold' : ''">
|
||||
{{ formatNumber(props.row.net_stock_after_order, 0) }}
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-expected_profit_cost_usd="props">
|
||||
<q-td :props="props" class="text-right" :class="Number(props.row.expected_profit_cost_usd || 0) < 0 ? 'text-negative text-weight-bold' : 'text-positive'">
|
||||
{{ formatMoney(props.row.expected_profit_cost_usd, 'USD') }}
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-table
|
||||
v-else-if="activeTab === 'products'"
|
||||
flat
|
||||
bordered
|
||||
row-key="row_key"
|
||||
@@ -99,15 +184,22 @@
|
||||
</q-tr>
|
||||
<q-tr class="table-filter-row">
|
||||
<q-th v-for="col in props.cols" :key="`filter-${col.name}`">
|
||||
<q-input
|
||||
<q-select
|
||||
v-if="isColumnFilterable(col.name)"
|
||||
v-model="columnFilters[col.name]"
|
||||
:options="columnFilterOptions(col.name)"
|
||||
dense
|
||||
borderless
|
||||
clearable
|
||||
debounce="250"
|
||||
multiple
|
||||
use-chips
|
||||
emit-value
|
||||
map-options
|
||||
options-dense
|
||||
class="header-filter"
|
||||
placeholder="Filtre"
|
||||
popup-content-class="header-filter-popup"
|
||||
placeholder="Seç"
|
||||
@click.stop
|
||||
/>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
@@ -135,7 +227,10 @@
|
||||
Ort.Satış {{ formatMoney(props.row.avg_price_usd_90d, 'USD') }} ·
|
||||
Taban {{ formatMoney(props.row.avg_base_price_usd, 'USD') }} ·
|
||||
Çıplak {{ formatMoney(props.row.avg_cost_price_usd, 'USD') }} ·
|
||||
Marj {{ formatPercent(props.row.avg_gross_margin_90d) }}
|
||||
90G K/Z {{ formatMoney(props.row.gross_profit_usd_90d, 'USD') }} ·
|
||||
90G Marj {{ formatPercent(props.row.gross_margin_90d) }} ·
|
||||
180G K/Z {{ formatMoney(props.row.gross_profit_usd_180d, 'USD') }} ·
|
||||
180G Marj {{ formatPercent(props.row.gross_margin_180d) }}
|
||||
</span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
@@ -400,7 +495,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { formatMoney, formatNumber, formatPercent } from 'src/utils/formatters'
|
||||
@@ -412,6 +507,16 @@ const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const rows = ref([])
|
||||
const generalRows = ref([])
|
||||
const orderAnalysisRows = ref([])
|
||||
const orderMarketRows = ref([])
|
||||
const orderCustomerRows = ref([])
|
||||
const orderProductCustomerRows = ref([])
|
||||
const orderMarketDetailRows = ref([])
|
||||
const orderAnalysisLoaded = ref(false)
|
||||
const orderProductCustomersLoaded = ref(false)
|
||||
const orderMarketDetailsLoaded = ref(false)
|
||||
const orderAnalysisMode = ref('product')
|
||||
const marketRows = ref([])
|
||||
const countryRows = ref([])
|
||||
const customerRows = ref([])
|
||||
@@ -433,13 +538,6 @@ const detailLoading = ref(false)
|
||||
const detailSalesRows = ref([])
|
||||
const detailStockSizes = ref([])
|
||||
|
||||
const filters = reactive({
|
||||
q: '',
|
||||
product_code: '',
|
||||
kategori: '',
|
||||
seri: '',
|
||||
bucket: ''
|
||||
})
|
||||
const columnFilters = reactive({})
|
||||
const expandedGroups = ref({})
|
||||
|
||||
@@ -510,6 +608,8 @@ const columns = [
|
||||
{ name: 'unit_profit_cost_90d', label: '90G P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_base_180d', label: '180G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_cost_180d', label: '180G P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_usd_90d', label: '90G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_usd_180d', label: '180G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_90d', label: 'Marj', field: 'gross_margin_90d', align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_180d', label: '180G Marj', field: 'gross_margin_180d', align: 'right', sortable: true },
|
||||
{ name: 'market_count_90d', label: '90G Piyasa', field: row => formatNumber(row.market_count_90d, 0), align: 'right', sortable: true },
|
||||
@@ -520,6 +620,155 @@ const columns = [
|
||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||
]
|
||||
|
||||
const generalColumns = [
|
||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||||
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
|
||||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
|
||||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
|
||||
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
|
||||
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
|
||||
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left', sortable: true },
|
||||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left', sortable: true },
|
||||
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
|
||||
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
|
||||
{ name: 'period_start', label: 'Başlangıç', field: 'period_start', align: 'left', sortable: true },
|
||||
{ name: 'period_end', label: 'Bitiş', field: 'period_end', align: 'left', sortable: true },
|
||||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_total', label: 'Toplam Satış', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_total', label: 'Toplam USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'avg_daily_sales_total', label: 'Günlük Ort.', field: row => formatNumber(row.avg_daily_sales_total, 3), align: 'right', sortable: true },
|
||||
{ name: 'stock_days_total', label: 'Genel Stok Gün', field: row => formatNumber(row.stock_days_total, 1), align: 'right', sortable: true },
|
||||
{ name: 'avg_price_usd_total', label: 'Genel Ort. USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_base_total', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_total, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_cost_total', label: 'P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_total, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_usd_total', label: 'Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_total, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_total', label: 'Genel Marj', field: row => formatPercent(row.gross_margin_total), align: 'right', sortable: true },
|
||||
{ name: 'market_count_total', label: 'Toplam Piyasa', field: row => formatNumber(row.market_count_total, 0), align: 'right', sortable: true },
|
||||
{ name: 'customer_count_total', label: 'Toplam Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
||||
{ name: 'invoice_count_total', label: 'Fatura', field: row => formatNumber(row.invoice_count_total, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_index_total', label: 'Genel Endeks', field: row => formatNumber(row.sales_index_total, 2), align: 'right', sortable: true },
|
||||
{ name: 'performance_score', label: 'Genel Skor', field: row => formatNumber(row.performance_score, 1), align: 'right', sortable: true },
|
||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||||
{ name: 'first_sale_date', label: 'İlk Satış', field: 'first_sale_date', align: 'left', sortable: true },
|
||||
{ name: 'last_sale_date', label: 'Son Satış', field: 'last_sale_date', align: 'left', sortable: true },
|
||||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||||
]
|
||||
|
||||
const orderAnalysisColumns = [
|
||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||||
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
|
||||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
|
||||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
|
||||
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
|
||||
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => row.urun_ilk_grubu || row.yas_grubu || '', align: 'left', sortable: true },
|
||||
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left', sortable: true },
|
||||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => row.urun_ana_grubu || row.seri || '', align: 'left', sortable: true },
|
||||
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
|
||||
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
|
||||
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'order_usd', label: 'Sipariş USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'avg_order_price_usd', label: 'Ort. Sipariş USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||||
{ name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_base_usd', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_cost_usd', label: 'P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_base_usd', label: 'Taban Toplam K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_cost_usd', label: 'Çıplak Toplam K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||||
{ name: 'expected_margin_cost', label: 'Beklenen Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||||
{ name: 'market_count', label: 'Piyasa', field: row => formatNumber(row.market_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'customer_count', label: 'Müşteri', field: row => formatNumber(row.customer_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'order_count', label: 'Sipariş', field: row => formatNumber(row.order_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'line_count', label: 'Satır', field: row => formatNumber(row.line_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'earliest_due_date', label: 'İlk Termin', field: 'earliest_due_date', align: 'left', sortable: true },
|
||||
{ name: 'overdue_qty', label: 'Geciken Adet', field: row => formatNumber(row.overdue_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||||
]
|
||||
|
||||
const orderGroupColumns = [
|
||||
{ name: 'group_key', label: 'Kırılım', field: row => orderGroupLabel(row), align: 'left', sortable: true },
|
||||
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
|
||||
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
|
||||
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
|
||||
{ name: 'product_count', label: 'Ürün', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'order_usd', label: 'Sipariş USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'base_cost_usd', label: 'Taban Maliyet USD', field: row => formatMoney(row.base_cost_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'cost_amount_usd', label: 'Çıplak Maliyet USD', field: row => formatMoney(row.cost_amount_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_cost_usd', label: 'Yakl. Net K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||||
{ name: 'expected_margin_base', label: 'Brüt Marj', field: row => formatPercent(row.expected_margin_base), align: 'right', sortable: true },
|
||||
{ name: 'expected_margin_cost', label: 'Yakl. Net Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||||
{ name: 'market_count', label: 'Piyasa', field: row => formatNumber(row.market_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'customer_count', label: 'Müşteri', field: row => formatNumber(row.customer_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'order_count', label: 'Sipariş', field: row => formatNumber(row.order_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'line_count', label: 'Satır', field: row => formatNumber(row.line_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'overdue_qty', label: 'Geciken Adet', field: row => formatNumber(row.overdue_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||||
]
|
||||
|
||||
const orderProductCustomerColumns = [
|
||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||||
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
|
||||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
|
||||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
|
||||
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
|
||||
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
|
||||
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
|
||||
{ name: 'order_qty', label: 'Açık Sipariş', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'order_usd', label: 'Sipariş USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||||
{ name: 'base_price_usd', label: 'Taban USD', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'cost_price_usd', label: 'Çıplak USD', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_cost_usd', label: 'Yakl. Net K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||||
{ name: 'expected_margin_base', label: 'Brüt Marj', field: row => formatPercent(row.expected_margin_base), align: 'right', sortable: true },
|
||||
{ name: 'expected_margin_cost', label: 'Yakl. Net Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||||
{ name: 'order_count', label: 'Sipariş', field: row => formatNumber(row.order_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'line_count', label: 'Satır', field: row => formatNumber(row.line_count, 0), align: 'right', sortable: true },
|
||||
{ name: 'overdue_qty', label: 'Geciken Adet', field: row => formatNumber(row.overdue_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||||
]
|
||||
|
||||
const orderMarketDetailColumns = [
|
||||
{ name: 'market_key', label: 'Piyasa', field: 'market_key', align: 'left', sortable: true },
|
||||
{ name: 'customer_code', label: 'Müşteri Kodu', field: 'customer_code', align: 'left', sortable: true },
|
||||
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
|
||||
{ name: 'order_number', label: 'Sipariş No', field: 'order_number', align: 'left', sortable: true },
|
||||
{ name: 'order_date', label: 'Sipariş Tarihi', field: 'order_date', align: 'left', sortable: true },
|
||||
{ name: 'due_date', label: 'Termin', field: 'due_date', align: 'left', sortable: true },
|
||||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||||
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
|
||||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left', sortable: true },
|
||||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left', sortable: true },
|
||||
{ name: 'order_qty', label: 'Adet', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'order_usd', label: 'USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'expected_profit_cost_usd', label: 'Yakl. Net K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||||
{ name: 'expected_margin_base', label: 'Brüt Marj', field: row => formatPercent(row.expected_margin_base), align: 'right', sortable: true },
|
||||
{ name: 'expected_margin_cost', label: 'Yakl. Net Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||||
{ name: 'is_overdue', label: 'Gecikti', field: row => row.is_overdue ? 'Evet' : 'Hayır', align: 'left', sortable: true },
|
||||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||||
]
|
||||
|
||||
const idleColumns = [
|
||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||||
@@ -612,6 +861,17 @@ const idleRows = computed(() => rows.value
|
||||
))
|
||||
.sort((a, b) => Number(b.idle_cost_usd || 0) - Number(a.idle_cost_usd || 0)))
|
||||
|
||||
const visibleOrderAnalysisRows = computed(() => {
|
||||
if (orderAnalysisMode.value === 'market') return orderMarketRows.value
|
||||
if (orderAnalysisMode.value === 'customer') return orderCustomerRows.value
|
||||
return orderAnalysisRows.value
|
||||
})
|
||||
|
||||
const visibleOrderAnalysisColumns = computed(() => {
|
||||
if (orderAnalysisMode.value === 'market' || orderAnalysisMode.value === 'customer') return orderGroupColumns
|
||||
return orderAnalysisColumns
|
||||
})
|
||||
|
||||
const performanceCards = computed(() => {
|
||||
const row = performanceDialogRow.value || {}
|
||||
return [
|
||||
@@ -642,8 +902,30 @@ const filterableColumnNames = new Set([
|
||||
'urun_ana_grubu',
|
||||
'urun_alt_grubu',
|
||||
'market_key',
|
||||
'performance_bucket',
|
||||
'recommendation'
|
||||
'stock_qty',
|
||||
'sales_qty_90d',
|
||||
'sales_qty_180d',
|
||||
'sales_usd_90d',
|
||||
'sales_usd_180d',
|
||||
'stock_days_90d',
|
||||
'stock_days_180d',
|
||||
'avg_price_usd_90d',
|
||||
'avg_price_usd_180d',
|
||||
'base_price_usd',
|
||||
'cost_price_usd',
|
||||
'unit_profit_base_90d',
|
||||
'unit_profit_cost_90d',
|
||||
'unit_profit_base_180d',
|
||||
'unit_profit_cost_180d',
|
||||
'gross_profit_usd_90d',
|
||||
'gross_profit_usd_180d',
|
||||
'gross_margin_90d',
|
||||
'gross_margin_180d',
|
||||
'market_count_90d',
|
||||
'customer_count_90d',
|
||||
'sales_index_90d',
|
||||
'performance_score',
|
||||
'performance_bucket'
|
||||
])
|
||||
|
||||
const groupLevels = [
|
||||
@@ -659,12 +941,33 @@ const groupLevels = [
|
||||
const filteredProductRows = computed(() => rows.value.filter(row => {
|
||||
return columns.every(col => {
|
||||
if (!isColumnFilterable(col.name)) return true
|
||||
const needle = String(columnFilters[col.name] || '').trim().toLocaleLowerCase('tr-TR')
|
||||
if (!needle) return true
|
||||
return String(rawProductCellValue(row, col.name) || '').toLocaleLowerCase('tr-TR').includes(needle)
|
||||
const selected = Array.isArray(columnFilters[col.name]) ? columnFilters[col.name] : []
|
||||
if (!selected.length) return true
|
||||
return selected.includes(columnFilterValue(row, col.name))
|
||||
})
|
||||
}))
|
||||
|
||||
const columnFilterOptionMap = computed(() => {
|
||||
const out = {}
|
||||
for (const col of columns) {
|
||||
if (!isColumnFilterable(col.name)) continue
|
||||
const seen = new Map()
|
||||
for (const row of rows.value) {
|
||||
const value = columnFilterValue(row, col.name)
|
||||
if (!value) continue
|
||||
if (!seen.has(value)) {
|
||||
seen.set(value, {
|
||||
label: columnFilterLabel(row, col.name),
|
||||
value
|
||||
})
|
||||
}
|
||||
}
|
||||
out[col.name] = Array.from(seen.values())
|
||||
.sort((a, b) => String(a.label).localeCompare(String(b.label), 'tr', { numeric: true }))
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
const productTableRows = computed(() => {
|
||||
const out = []
|
||||
appendGroupRows(out, filteredProductRows.value, 0, [])
|
||||
@@ -709,13 +1012,16 @@ function makeGroupRow (key, level, label, groupRows) {
|
||||
sales_qty_180d: sumRows(groupRows, 'sales_qty_180d'),
|
||||
sales_usd_90d: sumRows(groupRows, 'sales_usd_90d'),
|
||||
sales_usd_180d: sumRows(groupRows, 'sales_usd_180d'),
|
||||
gross_profit_usd_90d: sumRows(groupRows, 'gross_profit_usd_90d'),
|
||||
gross_profit_usd_180d: sumRows(groupRows, 'gross_profit_usd_180d'),
|
||||
market_count_90d: distinctCount(groupRows, 'market_key'),
|
||||
customer_count_90d: sumRows(groupRows, 'customer_count_90d'),
|
||||
avg_price_usd_90d: weightedAverage(groupRows, 'sales_usd_90d', 'sales_qty_90d'),
|
||||
avg_price_usd_180d: weightedAverage(groupRows, 'sales_usd_180d', 'sales_qty_180d'),
|
||||
avg_base_price_usd: averageRows(groupRows, 'base_price_usd'),
|
||||
avg_cost_price_usd: averageRows(groupRows, 'cost_price_usd'),
|
||||
avg_gross_margin_90d: averageRows(groupRows, 'gross_margin_90d')
|
||||
gross_margin_90d: ratio(sumRows(groupRows, 'gross_profit_usd_90d'), sumRows(groupRows, 'sales_usd_90d')),
|
||||
gross_margin_180d: ratio(sumRows(groupRows, 'gross_profit_usd_180d'), sumRows(groupRows, 'sales_usd_180d'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,6 +1038,12 @@ function distinctCount (sourceRows, field) {
|
||||
return values.size
|
||||
}
|
||||
|
||||
function ratio (amount, base) {
|
||||
const divisor = Number(base || 0)
|
||||
if (!divisor) return 0
|
||||
return Number(amount || 0) / divisor
|
||||
}
|
||||
|
||||
function averageRows (sourceRows, field) {
|
||||
const values = sourceRows.map(row => Number(row[field] || 0)).filter(value => Number.isFinite(value) && value !== 0)
|
||||
if (!values.length) return 0
|
||||
@@ -764,6 +1076,18 @@ function isColumnFilterable (name) {
|
||||
return filterableColumnNames.has(name)
|
||||
}
|
||||
|
||||
function columnFilterOptions (name) {
|
||||
return columnFilterOptionMap.value[name] || []
|
||||
}
|
||||
|
||||
function columnFilterValue (row, name) {
|
||||
return String(rawProductCellValue(row, name) ?? '').trim()
|
||||
}
|
||||
|
||||
function columnFilterLabel (row, name) {
|
||||
return formatProductCell(row, name) || columnFilterValue(row, name)
|
||||
}
|
||||
|
||||
function rawProductCellValue (row, name) {
|
||||
switch (name) {
|
||||
case 'urun_ilk_grubu': return row.urun_ilk_grubu || row.yas_grubu || ''
|
||||
@@ -796,6 +1120,8 @@ function formatProductCell (row, name) {
|
||||
case 'unit_profit_cost_90d':
|
||||
case 'unit_profit_base_180d':
|
||||
case 'unit_profit_cost_180d':
|
||||
case 'gross_profit_usd_90d':
|
||||
case 'gross_profit_usd_180d':
|
||||
return formatMoney(row[name], 'USD')
|
||||
case 'gross_margin_90d':
|
||||
case 'gross_margin_180d':
|
||||
@@ -820,6 +1146,63 @@ function normalizeRow (row) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGeneralRow (row) {
|
||||
const product = String(row?.product_code || '').trim()
|
||||
const color = String(row?.color_code || '').trim()
|
||||
const yaka = String(row?.yaka_kodu || '').trim()
|
||||
const market = String(row?.market_key || '').trim()
|
||||
return {
|
||||
...row,
|
||||
row_key: `general|${product}|${color}|${yaka}|${market}`
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderAnalysisRow (row) {
|
||||
const product = String(row?.product_code || '').trim()
|
||||
const color = String(row?.color_code || '').trim()
|
||||
const yaka = String(row?.yaka_kodu || '').trim()
|
||||
const market = String(row?.market_key || '').trim()
|
||||
return {
|
||||
...row,
|
||||
row_key: `orders|${product}|${color}|${yaka}|${market}`
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderGroupRow (row) {
|
||||
const key = String(row?.group_key || '').trim()
|
||||
const mode = String(row?.breakdown || '').trim()
|
||||
return {
|
||||
...row,
|
||||
row_key: `order-group|${mode}|${key}|${row?.market_key || ''}|${row?.customer_code || ''}`
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderProductCustomerRow (row) {
|
||||
const product = String(row?.product_code || '').trim()
|
||||
const color = String(row?.color_code || '').trim()
|
||||
const yaka = String(row?.yaka_kodu || '').trim()
|
||||
const market = String(row?.market_key || '').trim()
|
||||
const customer = String(row?.customer_code || '').trim()
|
||||
return {
|
||||
...row,
|
||||
row_key: `order-product-customer|${product}|${color}|${yaka}|${market}|${customer}`
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderMarketDetailRow (row) {
|
||||
return {
|
||||
...row,
|
||||
row_key: `order-market-detail|${row?.market_key || ''}|${row?.customer_code || ''}|${row?.order_number || ''}|${row?.product_code || ''}|${row?.color_code || ''}|${row?.yaka_kodu || ''}`
|
||||
}
|
||||
}
|
||||
|
||||
function orderGroupLabel (row) {
|
||||
if (row?.breakdown === 'customer') {
|
||||
return [row.customer_code, row.customer_name].filter(Boolean).join(' - ') || '-'
|
||||
}
|
||||
return row?.group_key || row?.market_key || '-'
|
||||
}
|
||||
|
||||
function productImageKey (row) {
|
||||
const product = String(row?.product_code || '').trim()
|
||||
const color = String(row?.color_code || '').trim()
|
||||
@@ -961,22 +1344,18 @@ async function openPerformanceDialog (row) {
|
||||
async function reload () {
|
||||
loading.value = true
|
||||
try {
|
||||
const [summaryResp, listResp, marketResp, countryResp, customerResp] = await Promise.all([
|
||||
const [summaryResp, listResp, generalResp, marketResp, countryResp, customerResp] = await Promise.all([
|
||||
api.get('/pricing/product-performance/summary', { timeout: 60000 }),
|
||||
api.get('/pricing/product-performance', {
|
||||
params: {
|
||||
page: pagination.value.page,
|
||||
limit: pagination.value.rowsPerPage,
|
||||
q: filters.q || undefined,
|
||||
product_code: filters.product_code || undefined,
|
||||
kategori: filters.kategori || undefined,
|
||||
seri: filters.seri || undefined,
|
||||
bucket: filters.bucket || undefined,
|
||||
sort_by: pagination.value.sortBy || undefined,
|
||||
descending: pagination.value.descending ? 'true' : 'false'
|
||||
},
|
||||
timeout: 60000
|
||||
}),
|
||||
api.get('/pricing/product-performance/general', { params: { limit: 500 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/markets', { params: { limit: 200 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/countries', { params: { limit: 200 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/customers', {
|
||||
@@ -986,6 +1365,7 @@ async function reload () {
|
||||
])
|
||||
summary.value = summaryResp?.data || {}
|
||||
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow)
|
||||
generalRows.value = (Array.isArray(generalResp?.data) ? generalResp.data : []).map(normalizeGeneralRow)
|
||||
marketRows.value = Array.isArray(marketResp?.data) ? marketResp.data : []
|
||||
countryRows.value = (Array.isArray(countryResp?.data) ? countryResp.data : []).map(row => ({
|
||||
...row,
|
||||
@@ -996,7 +1376,13 @@ async function reload () {
|
||||
customer_key: `${row.breakdown || '-'}|${row.market_key || '-'}|${row.country || '-'}|${row.customer_segment || '-'}|${row.customer_code || '-'}`
|
||||
}))
|
||||
pagination.value.rowsNumber = Number(listResp?.data?.total_count || 0)
|
||||
void primeProductImages(rows.value)
|
||||
if (activeTab.value === 'order_product_customers') {
|
||||
await loadOrderProductCustomers(false)
|
||||
}
|
||||
if (activeTab.value === 'order_market_details') {
|
||||
await loadOrderMarketDetails(false)
|
||||
}
|
||||
void primeProductImages([...rows.value, ...generalRows.value])
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
|
||||
} finally {
|
||||
@@ -1004,6 +1390,53 @@ async function reload () {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrderProductCustomers (showLoading = true) {
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
const resp = await api.get('/pricing/product-performance/orders/product-customers', { params: { limit: 500 }, timeout: 90000 })
|
||||
orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
|
||||
orderProductCustomersLoaded.value = true
|
||||
void primeProductImages(orderProductCustomerRows.value)
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün müşteri sipariş analizi alınamadı' })
|
||||
} finally {
|
||||
if (showLoading) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrderMarketDetails (showLoading = true) {
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
const resp = await api.get('/pricing/product-performance/orders/market-details', { params: { limit: 800 }, timeout: 90000 })
|
||||
orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
|
||||
orderMarketDetailsLoaded.value = true
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Piyasa sipariş detay analizi alınamadı' })
|
||||
} finally {
|
||||
if (showLoading) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrderAnalysis (showLoading = true) {
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
const [productResp, marketResp, customerResp] = await Promise.all([
|
||||
api.get('/pricing/product-performance/orders', { params: { limit: 500 }, timeout: 90000 }),
|
||||
api.get('/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'market' }, timeout: 90000 }),
|
||||
api.get('/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'customer' }, timeout: 90000 })
|
||||
])
|
||||
orderAnalysisRows.value = (Array.isArray(productResp?.data) ? productResp.data : []).map(normalizeOrderAnalysisRow)
|
||||
orderMarketRows.value = (Array.isArray(marketResp?.data) ? marketResp.data : []).map(normalizeOrderGroupRow)
|
||||
orderCustomerRows.value = (Array.isArray(customerResp?.data) ? customerResp.data : []).map(normalizeOrderGroupRow)
|
||||
orderAnalysisLoaded.value = true
|
||||
void primeProductImages(orderAnalysisRows.value)
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Sipariş analiz verisi alınamadı' })
|
||||
} finally {
|
||||
if (showLoading) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runDelta () {
|
||||
refreshing.value = true
|
||||
try {
|
||||
@@ -1040,6 +1473,15 @@ function bucketColor (value) {
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeTab, tab => {
|
||||
if (tab === 'order_product_customers' && !orderProductCustomersLoaded.value) {
|
||||
void loadOrderProductCustomers(true)
|
||||
}
|
||||
if (tab === 'order_market_details' && !orderMarketDetailsLoaded.value) {
|
||||
void loadOrderMarketDetails(true)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(reload)
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user