diff --git a/svc/main.go b/svc/main.go index 1a7ae81..2c11fea 100644 --- a/svc/main.go +++ b/svc/main.go @@ -579,6 +579,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "finance", "export", wrapV3(routes.ExportPDFHandler(mssql)), ) + bindV3(r, pgDB, + "/api/export-pdf", "POST", + "finance", "export", + wrapV3(routes.ExportPDFHandler(mssql)), + ) bindV3(r, pgDB, "/api/exportstamentheaderreport-pdf", "GET", @@ -795,6 +800,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "pricing", "view", wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)), ) + bindV3(r, pgDB, + "/api/pricing/products/export-all", "GET", + "pricing", "view", + wrapV3(http.HandlerFunc(routes.ExportAllProductPricingHandler)), + ) bindV3(r, pgDB, "/api/pricing/brand-classification/lookups", "GET", "pricing", "view", @@ -840,6 +850,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "pricing", "view", wrapV3(routes.GetPricingParameterRulesHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/pricing/pricing-rules/export-all", "GET", + "pricing", "view", + wrapV3(routes.ExportPricingRulesHandler(pgDB)), + ) bindV3(r, pgDB, "/api/pricing/production-product-costing/no-cost-products", "GET", "costing", "view", diff --git a/svc/queries/product_pricing.go b/svc/queries/product_pricing.go index 1aaac7c..ece69f1 100644 --- a/svc/queries/product_pricing.go +++ b/svc/queries/product_pricing.go @@ -36,6 +36,583 @@ type ProductPricingPage struct { Limit int } +func GetAllProductPricingRows(ctx context.Context, chunkSize int, filters ProductPricingFilters, sortBy string, descending bool) ([]models.ProductPricing, error) { + if chunkSize <= 0 || chunkSize > 1000 { + chunkSize = 1000 + } + + paramIndex := 1 + args := make([]any, 0, 64) + nextParam := func() string { + name := "@p" + strconv.Itoa(paramIndex) + paramIndex++ + return name + } + whereParts := []string{ + "ProductAtt42 IN ('SERI', 'AKSESUAR')", + "IsBlocked = 0", + "LEN(LTRIM(RTRIM(ProductCode))) = 13", + } + addInFilter := func(expr string, values []string) { + clean := make([]string, 0, len(values)) + for _, v := range values { + v = strings.TrimSpace(v) + if v == "" { + continue + } + clean = append(clean, v) + } + if len(clean) == 0 { + return + } + ors := make([]string, 0, len(clean)) + for _, v := range clean { + p := nextParam() + ors = append(ors, expr+" = "+p) + args = append(args, v) + } + whereParts = append(whereParts, "("+strings.Join(ors, " OR ")+")") + } + brandGroupExpr := `CASE ABS(CHECKSUM(LTRIM(RTRIM(ProductCode)))) % 3 + WHEN 0 THEN 'MARKA GRUBU A' + WHEN 1 THEN 'MARKA GRUBU B' + ELSE 'MARKA GRUBU C' + END` + addInFilter("LTRIM(RTRIM(ProductCode))", filters.ProductCode) + addInFilter(brandGroupExpr, filters.BrandGroup) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '')", filters.AskiliYan) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '')", filters.Kategori) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '')", filters.UrunIlkGrubu) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '')", filters.UrunAnaGrubu) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '')", filters.UrunAltGrubu) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '')", filters.Icerik) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '')", filters.Karisim) + addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '')", filters.Marka) + if q := strings.TrimSpace(filters.Search); q != "" { + p := nextParam() + args = append(args, "%"+q+"%") + whereParts = append(whereParts, "("+strings.Join([]string{ + "LTRIM(RTRIM(ProductCode)) LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') LIKE " + p, + "COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') LIKE " + p, + }, " OR ")+")") + } + whereSQL := strings.Join(whereParts, " AND ") + + sortBy = strings.TrimSpace(sortBy) + orderDir := "DESC" + if !descending { + orderDir = "ASC" + } + orderExpr := "CAST(ROUND(ISNULL(sb.InventoryQty1, 0) - ISNULL(pb.PickingQty1, 0) - ISNULL(rb.ReserveQty1, 0) - ISNULL(db.DispOrderQty1, 0), 2) AS DECIMAL(18, 2))" + if sortBy == "productCode" { + orderExpr = "rc.ProductCode" + orderDir = "ASC" + } + + baseQuery := ` + IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes; + IF OBJECT_ID('tempdb..#stock_base') IS NOT NULL DROP TABLE #stock_base; + IF OBJECT_ID('tempdb..#pick_base') IS NOT NULL DROP TABLE #pick_base; + IF OBJECT_ID('tempdb..#reserve_base') IS NOT NULL DROP TABLE #reserve_base; + IF OBJECT_ID('tempdb..#disp_base') IS NOT NULL DROP TABLE #disp_base; + + SELECT + f.ProductCode, + MAX(f.BrandGroupSec) AS BrandGroupSec, + MAX(f.AskiliYan) AS AskiliYan, + MAX(f.Kategori) AS Kategori, + MAX(f.UrunIlkGrubu) AS UrunIlkGrubu, + MAX(f.UrunAnaGrubu) AS UrunAnaGrubu, + MAX(f.UrunAltGrubu) AS UrunAltGrubu, + MAX(f.Icerik) AS Icerik, + MAX(f.Karisim) AS Karisim, + MAX(f.Marka) AS Marka, + MAX(f.BrandCode) AS BrandCode + INTO #req_codes + FROM ( + SELECT + LTRIM(RTRIM(ProductCode)) AS ProductCode, + ` + brandGroupExpr + ` AS BrandGroupSec, + COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan, + COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori, + COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik, + COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim, + COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka, + COALESCE(LTRIM(RTRIM(ProductAtt10)), '') AS BrandCode + FROM ProductFilterWithDescription('TR') + WHERE ` + whereSQL + ` + ) f + GROUP BY f.ProductCode; + + CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode); + + SELECT + LTRIM(RTRIM(s.ItemCode)) AS ItemCode, + SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 + INTO #stock_base + FROM trStock s WITH(NOLOCK) + INNER JOIN #req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) + WHERE s.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(s.ItemCode)); + + CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode); + + SELECT + LTRIM(RTRIM(p.ItemCode)) AS ItemCode, + SUM(p.Qty1) AS PickingQty1 + INTO #pick_base + FROM PickingStates p + INNER JOIN #req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) + WHERE p.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(p.ItemCode)); + + SELECT + LTRIM(RTRIM(r.ItemCode)) AS ItemCode, + SUM(r.Qty1) AS ReserveQty1 + INTO #reserve_base + FROM ReserveStates r + INNER JOIN #req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(r.ItemCode)) + WHERE r.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(r.ItemCode)); + + SELECT + LTRIM(RTRIM(d.ItemCode)) AS ItemCode, + SUM(d.Qty1) AS DispOrderQty1 + INTO #disp_base + FROM DispOrderStates d + INNER JOIN #req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(d.ItemCode)) + WHERE d.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13 + GROUP BY LTRIM(RTRIM(d.ItemCode)); + + SELECT + rc.ProductCode, + rc.BrandGroupSec, + rc.AskiliYan, + rc.Kategori, + rc.UrunIlkGrubu, + rc.UrunAnaGrubu, + rc.UrunAltGrubu, + rc.Icerik, + rc.Karisim, + rc.Marka, + rc.BrandCode, + CAST(ROUND( + ISNULL(sb.InventoryQty1, 0) + - ISNULL(pb.PickingQty1, 0) + - ISNULL(rb.ReserveQty1, 0) + - ISNULL(db.DispOrderQty1, 0) + , 2) AS DECIMAL(18, 2)) AS StockQty + FROM #req_codes rc + LEFT JOIN #stock_base sb + ON sb.ItemCode = rc.ProductCode + LEFT JOIN #pick_base pb + ON pb.ItemCode = rc.ProductCode + LEFT JOIN #reserve_base rb + ON rb.ItemCode = rc.ProductCode + LEFT JOIN #disp_base db + ON db.ItemCode = rc.ProductCode + ORDER BY + ` + orderExpr + ` ` + orderDir + `, + rc.ProductCode ASC; + ` + + rows, err := db.MssqlDB.QueryContext(ctx, baseQuery, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := make([]models.ProductPricing, 0, 2048) + for rows.Next() { + var item models.ProductPricing + if err := rows.Scan( + &item.ProductCode, + &item.BrandGroupSec, + &item.AskiliYan, + &item.Kategori, + &item.UrunIlkGrubu, + &item.UrunAnaGrubu, + &item.UrunAltGrubu, + &item.Icerik, + &item.Karisim, + &item.Marka, + &item.BrandCode, + &item.StockQty, + ); err != nil { + return nil, err + } + out = append(out, item) + } + if err := rows.Err(); err != nil { + return nil, err + } + if len(out) == 0 { + return out, nil + } + + if err := enrichAllProductPricingRows(ctx, out, chunkSize); err != nil { + return nil, err + } + + return out, nil +} + +func enrichAllProductPricingRows(ctx context.Context, out []models.ProductPricing, chunkSize int) error { + if len(out) == 0 { + return nil + } + if chunkSize <= 0 || chunkSize > 1000 { + chunkSize = 1000 + } + + indexByCode := make(map[string]int, len(out)) + codes := make([]string, 0, len(out)) + for i := range out { + code := strings.TrimSpace(out[i].ProductCode) + if code == "" { + continue + } + indexByCode[code] = i + codes = append(codes, code) + } + + for _, chunk := range chunkStringSlice(codes, chunkSize) { + valueRows := make([]string, 0, len(chunk)) + metricArgs := make([]any, 0, len(chunk)) + for i, code := range chunk { + paramName := "@p" + strconv.Itoa(i+1) + valueRows = append(valueRows, "("+paramName+")") + metricArgs = append(metricArgs, code) + } + + metricsQuery := ` + WITH req_codes AS ( + SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode + FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode) + WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0 + ), + latest_pricelist_line AS ( + SELECT + LTRIM(RTRIM(p.ItemCode)) AS ItemCode, + LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode, + CAST(p.Price AS DECIMAL(18, 2)) AS Price, + ROW_NUMBER() OVER ( + PARTITION BY LTRIM(RTRIM(p.ItemCode)), LTRIM(RTRIM(p.DocCurrencyCode)) + ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC + ) AS rn + FROM dbo.trPriceListLine p WITH(NOLOCK) + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) + WHERE p.ItemTypeCode = 1 + AND ISNULL(p.IsDisabled, 0) = 0 + AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY') + AND ( + (LTRIM(RTRIM(p.DocCurrencyCode)) = 'USD' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-USD') + OR (LTRIM(RTRIM(p.DocCurrencyCode)) = 'TRY' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-TRY') + ) + AND p.Price IS NOT NULL + AND p.Price > 0 + ), + base_prices AS ( + SELECT + ItemCode, + MAX(CASE WHEN DocCurrencyCode = 'USD' THEN Price END) AS BasePriceUsd, + MAX(CASE WHEN DocCurrencyCode = 'TRY' THEN Price END) AS BasePriceTry + FROM latest_pricelist_line + WHERE rn = 1 + GROUP BY ItemCode + ), + latest_base_price AS ( + SELECT + LTRIM(RTRIM(b.ItemCode)) AS ItemCode, + CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice, + CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate, + ROW_NUMBER() OVER ( + PARTITION BY LTRIM(RTRIM(b.ItemCode)) + ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC + ) AS rn + FROM prItemBasePrice b + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(b.ItemCode)) + WHERE b.ItemTypeCode = 1 + AND b.BasePriceCode = 1 + AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' + ), + stock_entry_dates AS ( + SELECT + LTRIM(RTRIM(s.ItemCode)) AS ItemCode, + CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate + FROM trStock s WITH(NOLOCK) + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) + WHERE s.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 + AND s.In_Qty1 > 0 + AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP' + AND LTRIM(RTRIM(s.WarehouseCode)) IN ( + '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', + '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', + '1-0-33','101','1-014','1-0-49','1-0-36' + ) + GROUP BY LTRIM(RTRIM(s.ItemCode)) + ) + SELECT + rc.ProductCode, + COALESCE(lp.CostPrice, 0) AS CostPrice, + COALESCE(bp.BasePriceUsd, 0) AS BasePriceUsd, + COALESCE(bp.BasePriceTry, 0) AS BasePriceTry, + COALESCE(se.StockEntryDate, '') AS StockEntryDate, + COALESCE(lp.LastPricingDate, '') AS LastPricingDate + FROM req_codes rc + LEFT JOIN latest_base_price lp + ON lp.ItemCode = rc.ProductCode + AND lp.rn = 1 + LEFT JOIN base_prices bp + ON bp.ItemCode = rc.ProductCode + LEFT JOIN stock_entry_dates se + ON se.ItemCode = rc.ProductCode; + ` + + metricRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...) + if err != nil { + return fmt.Errorf("metrics query failed: %w", err) + } + for metricRows.Next() { + var code string + var costPrice, basePriceUsd, basePriceTry float64 + var stockEntryDate, lastPricingDate string + if err := metricRows.Scan(&code, &costPrice, &basePriceUsd, &basePriceTry, &stockEntryDate, &lastPricingDate); err != nil { + _ = metricRows.Close() + return err + } + if idx, ok := indexByCode[strings.TrimSpace(code)]; ok { + out[idx].CostPrice = costPrice + out[idx].BasePriceUsd = basePriceUsd + out[idx].BasePriceTry = basePriceTry + out[idx].StockEntryDate = stockEntryDate + out[idx].LastPricingDate = lastPricingDate + } + } + if err := metricRows.Err(); err != nil { + _ = metricRows.Close() + return err + } + _ = metricRows.Close() + + if uretimDB := db.GetUretimDB(); uretimDB != nil { + costingQuery := ` + WITH req_codes AS ( + SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode + FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode) + WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0 + ) + SELECT + LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu, + CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate + FROM dbo.spUrtOnMLMas m WITH(NOLOCK) + INNER JOIN req_codes rc + ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu)) + GROUP BY LTRIM(RTRIM(m.UrunKodu)); + ` + costRows, err := uretimDB.QueryContext(ctx, costingQuery, metricArgs...) + if err == nil { + for costRows.Next() { + var code, d string + if err := costRows.Scan(&code, &d); err != nil { + _ = costRows.Close() + return err + } + if idx, ok := indexByCode[strings.TrimSpace(code)]; ok && strings.TrimSpace(d) != "" { + out[idx].LastCostingDate = strings.TrimSpace(d) + } + } + if err := costRows.Err(); err != nil { + _ = costRows.Close() + return err + } + _ = costRows.Close() + } + } + + if pg := db.PgDB; pg != nil { + tierSQL := ` + WITH ranked AS ( + SELECT + mmitem.code AS code, + sdprc.sdprcgrp_id AS grp, + sdprc.crn AS crn, + COALESCE(sdprc.prc, 0) AS prc, + ROW_NUMBER() OVER ( + PARTITION BY mmitem.code, sdprc.crn, sdprc.sdprcgrp_id + ORDER BY sdprc.zlins_dttm DESC + ) AS rn + FROM sdprc + JOIN mmitem ON mmitem.id = sdprc.mmitem_id + WHERE mmitem.code = ANY($1) + AND sdprc.sdprcgrp_id BETWEEN 1 AND 6 + AND sdprc.crn IN ('USD', 'EUR', 'TRY') + AND sdprc.prc IS NOT NULL + AND sdprc.prc > 0 + ) + SELECT code, grp, crn, prc + FROM ranked + WHERE rn = 1; + ` + pgRows, err := pg.QueryContext(ctx, tierSQL, pq.Array(chunk)) + if err == nil { + for pgRows.Next() { + var code, crn string + var grp int + var prc float64 + if err := pgRows.Scan(&code, &grp, &crn, &prc); err != nil { + _ = pgRows.Close() + return err + } + idx, ok := indexByCode[strings.TrimSpace(code)] + if !ok { + continue + } + switch strings.ToUpper(strings.TrimSpace(crn)) { + case "USD": + switch grp { + case 1: + out[idx].USD1 = prc + case 2: + out[idx].USD2 = prc + case 3: + out[idx].USD3 = prc + case 4: + out[idx].USD4 = prc + case 5: + out[idx].USD5 = prc + case 6: + out[idx].USD6 = prc + } + case "EUR": + switch grp { + case 1: + out[idx].EUR1 = prc + case 2: + out[idx].EUR2 = prc + case 3: + out[idx].EUR3 = prc + case 4: + out[idx].EUR4 = prc + case 5: + out[idx].EUR5 = prc + case 6: + out[idx].EUR6 = prc + } + case "TRY": + switch grp { + case 1: + out[idx].TRY1 = prc + case 2: + out[idx].TRY2 = prc + case 3: + out[idx].TRY3 = prc + case 4: + out[idx].TRY4 = prc + case 5: + out[idx].TRY5 = prc + case 6: + out[idx].TRY6 = prc + } + } + } + if err := pgRows.Err(); err != nil { + _ = pgRows.Close() + return err + } + _ = pgRows.Close() + } + } + } + + if pg := db.PgDB; pg != nil { + brandCodes := make([]string, 0, len(out)) + seen := make(map[string]struct{}, len(out)) + for _, it := range out { + code := strings.TrimSpace(it.BrandCode) + if code == "" { + continue + } + if _, ok := seen[code]; ok { + continue + } + seen[code] = struct{}{} + brandCodes = append(brandCodes, code) + } + for _, chunk := range chunkStringSlice(brandCodes, chunkSize) { + rows, err := pg.QueryContext(ctx, ` +SELECT + m.brand_code, + COALESCE(g.title, '') AS grp_title +FROM mk_brandgrpmatch m +JOIN mk_brandgrp g ON g.id = m.grp_id +WHERE m.brand_code = ANY($1) +`, pq.Array(chunk)) + if err != nil { + continue + } + grpByBrand := make(map[string]string, len(chunk)) + for rows.Next() { + var code, title string + if err := rows.Scan(&code, &title); err != nil { + _ = rows.Close() + return err + } + grpByBrand[strings.TrimSpace(code)] = strings.TrimSpace(title) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return err + } + _ = rows.Close() + for i := range out { + if title, ok := grpByBrand[strings.TrimSpace(out[i].BrandCode)]; ok { + out[i].BrandGroupSec = title + } + } + } + } + + return nil +} + +func chunkStringSlice(values []string, size int) [][]string { + if size <= 0 { + size = 1000 + } + out := make([][]string, 0, (len(values)+size-1)/size) + for start := 0; start < len(values); start += size { + end := start + size + if end > len(values) { + end = len(values) + } + out = append(out, values[start:end]) + } + return out +} + func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) { result := ProductPricingPage{ Rows: []models.ProductPricing{}, diff --git a/svc/routes/pricing_rules.go b/svc/routes/pricing_rules.go index 89efe3a..65f4162 100644 --- a/svc/routes/pricing_rules.go +++ b/svc/routes/pricing_rules.go @@ -5,9 +5,12 @@ import ( "bssapp-backend/utils" "database/sql" "encoding/json" + "fmt" "net/http" + "sort" "strconv" "strings" + "time" ) // Step-1/2 scope (distinct+cascade) comes from the PostgreSQL parameter cache. @@ -132,6 +135,28 @@ func GetPricingParameterRulesHandler(pg *sql.DB) http.HandlerFunc { } } +func ExportPricingRulesHandler(pg *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + traceID := utils.TraceIDFromRequest(r) + ctx := utils.ContextWithTraceID(r.Context(), traceID) + + rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r)) + if err != nil { + http.Error(w, "pricing parameter rules export error", http.StatusInternalServerError) + return + } + + rows = filterPricingRuleExportRows(rows, r) + sortPricingRuleExportRows(rows, strings.TrimSpace(r.URL.Query().Get("sort_by")), strings.TrimSpace(r.URL.Query().Get("desc")) != "0") + + filename := fmt.Sprintf("pricing_rules_all_%s.csv", time.Now().Format("2006-01-02")) + w.Header().Set("Content-Type", "text/csv; charset=utf-8") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) + _, _ = w.Write([]byte("\uFEFF")) + _, _ = w.Write([]byte(buildPricingRuleCSV(rows))) + } +} + func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFilters { return queries.PricingRuleOptionFilters{ AskiliYan: splitCSV(r.URL.Query().Get("askili_yan")), @@ -146,6 +171,266 @@ func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFil } } +func filterPricingRuleExportRows(rows []queries.PricingParameterRuleRow, r *http.Request) []queries.PricingParameterRuleRow { + rangeFilter := func(prefix string) (*float64, *float64) { + parse := func(raw string) *float64 { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64) + if err != nil { + return nil + } + return &v + } + return parse(r.URL.Query().Get(prefix + "_min")), parse(r.URL.Query().Get(prefix + "_max")) + } + + fields := []string{ + "try_base", "try1", "try2", "try3", "try4", "try5", "try6", "try_step", + "usd_base", "usd1", "usd2", "usd3", "usd4", "usd5", "usd6", "usd_step", + "eur_base", "eur1", "eur2", "eur3", "eur4", "eur5", "eur6", "eur_step", + } + minMap := map[string]*float64{} + maxMap := map[string]*float64{} + for _, field := range fields { + minMap[field], maxMap[field] = rangeFilter(field) + } + + out := make([]queries.PricingParameterRuleRow, 0, len(rows)) + for _, row := range rows { + ok := true + for _, field := range fields { + value := pricingRuleNumericValue(row, field) + if minMap[field] != nil && value < *minMap[field] { + ok = false + break + } + if maxMap[field] != nil && value > *maxMap[field] { + ok = false + break + } + } + if ok { + out = append(out, row) + } + } + return out +} + +func pricingRuleNumericValue(row queries.PricingParameterRuleRow, field string) float64 { + if row.Rule == nil { + return 0 + } + switch field { + case "try_base": + return row.Rule.TryBase + case "try1": + return row.Rule.Try1 + case "try2": + return row.Rule.Try2 + case "try3": + return row.Rule.Try3 + case "try4": + return row.Rule.Try4 + case "try5": + return row.Rule.Try5 + case "try6": + return row.Rule.Try6 + case "try_step": + return row.Rule.TryStep + case "usd_base": + return row.Rule.UsdBase + case "usd1": + return row.Rule.Usd1 + case "usd2": + return row.Rule.Usd2 + case "usd3": + return row.Rule.Usd3 + case "usd4": + return row.Rule.Usd4 + case "usd5": + return row.Rule.Usd5 + case "usd6": + return row.Rule.Usd6 + case "usd_step": + return row.Rule.UsdStep + case "eur_base": + return row.Rule.EurBase + case "eur1": + return row.Rule.Eur1 + case "eur2": + return row.Rule.Eur2 + case "eur3": + return row.Rule.Eur3 + case "eur4": + return row.Rule.Eur4 + case "eur5": + return row.Rule.Eur5 + case "eur6": + return row.Rule.Eur6 + case "eur_step": + return row.Rule.EurStep + default: + return 0 + } +} + +func sortPricingRuleExportRows(rows []queries.PricingParameterRuleRow, sortBy string, desc bool) { + sortBy = strings.TrimSpace(sortBy) + if sortBy == "" { + return + } + sort.SliceStable(rows, func(i, j int) bool { + li, lj := rows[i], rows[j] + switch sortBy { + case "has_rule": + if desc { + return boolRank(li.HasRule) > boolRank(lj.HasRule) + } + return boolRank(li.HasRule) < boolRank(lj.HasRule) + case "is_active": + liActive, ljActive := false, false + if li.Rule != nil { + liActive = li.Rule.IsActive + } + if lj.Rule != nil { + ljActive = lj.Rule.IsActive + } + if desc { + return boolRank(liActive) > boolRank(ljActive) + } + return boolRank(liActive) < boolRank(ljActive) + case "askili_yan", "kategori", "urun_ilk_grubu", "urun_ana_grubu", "urun_alt_grubu", "icerik", "marka", "brand_code", "brand_group": + vi := pricingRuleStringValue(li, sortBy) + vj := pricingRuleStringValue(lj, sortBy) + if desc { + return strings.Compare(vi, vj) > 0 + } + return strings.Compare(vi, vj) < 0 + default: + vi := pricingRuleNumericValue(li, sortBy) + vj := pricingRuleNumericValue(lj, sortBy) + if desc { + return vi > vj + } + return vi < vj + } + }) +} + +func boolRank(v bool) int { + if v { + return 1 + } + return 0 +} + +func pricingRuleStringValue(row queries.PricingParameterRuleRow, field string) string { + switch field { + case "askili_yan": + return row.AskiliYan + case "kategori": + return row.Kategori + case "urun_ilk_grubu": + return row.UrunIlkGrubu + case "urun_ana_grubu": + return row.UrunAnaGrubu + case "urun_alt_grubu": + return row.UrunAltGrubu + case "icerik": + return row.Icerik + case "marka": + return row.Marka + case "brand_code": + return row.BrandCode + case "brand_group": + return row.BrandGroupSec + default: + return "" + } +} + +func buildPricingRuleCSV(rows []queries.PricingParameterRuleRow) string { + headers := []string{ + "DURUM", "AKTIF", "ASKILI YAN", "KATEGORI", "URUN ILK GRUBU", "URUN ANA GRUBU", "URUN ALT GRUBU", + "ICERIK", "MARKA", "BRAND CODE", "MARKA GRUBU", + "TRY YUVARLAMA", "TRY TABAN", "TRY 1", "TRY 2", "TRY 3", "TRY 4", "TRY 5", "TRY 6", + "USD YUVARLAMA", "USD TABAN", "USD 1", "USD 2", "USD 3", "USD 4", "USD 5", "USD 6", + "EUR YUVARLAMA", "EUR TABAN", "EUR 1", "EUR 2", "EUR 3", "EUR 4", "EUR 5", "EUR 6", + } + var b strings.Builder + for i, h := range headers { + b.WriteString(csvEscapeValue(h)) + if i == len(headers)-1 { + b.WriteString("\n") + } else { + b.WriteString(";") + } + } + for _, row := range rows { + active := "Pasif" + if row.Rule == nil || row.Rule.IsActive { + active = "Aktif" + } + values := []string{ + map[bool]string{true: "Tanimli", false: "Yeni"}[row.HasRule], + active, + row.AskiliYan, + row.Kategori, + row.UrunIlkGrubu, + row.UrunAnaGrubu, + row.UrunAltGrubu, + row.Icerik, + row.Marka, + row.BrandCode, + row.BrandGroupSec, + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_step")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_base")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try1")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try2")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try3")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try4")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try5")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try6")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_step")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_base")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd1")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd2")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd3")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd4")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd5")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd6")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_step")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_base")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur1")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur2")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur3")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur4")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur5")), + fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur6")), + } + for i, value := range values { + b.WriteString(csvEscapeValue(value)) + if i == len(values)-1 { + b.WriteString("\n") + } else { + b.WriteString(";") + } + } + } + return b.String() +} + +func csvEscapeValue(value string) string { + text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ") + if strings.Contains(text, ";") || strings.Contains(text, "\"") { + text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"` + } + return text +} + func splitCSV(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/svc/routes/product_pricing.go b/svc/routes/product_pricing.go index c535159..c0483d8 100644 --- a/svc/routes/product_pricing.go +++ b/svc/routes/product_pricing.go @@ -2,10 +2,12 @@ package routes import ( "bssapp-backend/auth" + "bssapp-backend/models" "bssapp-backend/queries" "context" "encoding/json" "errors" + "fmt" "log" "net/http" "strconv" @@ -211,6 +213,392 @@ func GetProductPricingFilterOptionsHandler(w http.ResponseWriter, r *http.Reques _ = json.NewEncoder(w).Encode(resp) } +// GET /api/pricing/products/export-all +func ExportAllProductPricingHandler(w http.ResponseWriter, r *http.Request) { + started := time.Now() + traceID := buildPricingTraceID(r) + w.Header().Set("X-Trace-ID", traceID) + + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + log.Printf("[ProductPricingExport] trace=%s unauthorized method=%s path=%s", traceID, r.Method, r.URL.Path) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), 170*time.Second) + defer cancel() + + filters := parseProductPricingFilters(r) + if len(filters.UrunAnaGrubu) > 3 { + http.Error(w, "Urun Ana Grubu en fazla 3 secilebilir", http.StatusBadRequest) + return + } + + sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by")) + desc := true + if raw := strings.TrimSpace(r.URL.Query().Get("desc")); raw != "" { + if raw == "0" || strings.EqualFold(raw, "false") { + desc = false + } + } + + rows, err := queries.GetAllProductPricingRows(ctx, 1000, filters, sortBy, desc) + if err != nil { + if isPricingTimeoutLike(err, ctx.Err()) { + http.Error(w, "Urun fiyatlandirma export zaman asimina ugradi", http.StatusGatewayTimeout) + return + } + http.Error(w, "Urun fiyatlandirma export alinamadi: "+err.Error(), http.StatusInternalServerError) + return + } + + rows = filterProductPricingExportRows(rows, parseProductPricingExportFilters(r)) + currencies := parseExportCurrencies(r.URL.Query().Get("currencies")) + content := buildProductPricingExportCSV(rows, currencies) + filename := fmt.Sprintf("product_pricing_all_%s.csv", time.Now().Format("2006-01-02")) + + w.Header().Set("Content-Type", "text/csv; charset=utf-8") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) + _, _ = w.Write([]byte("\uFEFF")) + _, _ = w.Write([]byte(content)) + + log.Printf( + "[ProductPricingExport] trace=%s success user=%s id=%d rows=%d duration_ms=%d", + traceID, + claims.Username, + claims.ID, + len(rows), + time.Since(started).Milliseconds(), + ) +} + +type productPricingExportFilters struct { + BrandGroupSelection []string + ValueFilters map[string][]string + StockQtyMin *float64 + StockQtyMax *float64 + StockEntryFrom string + StockEntryTo string + LastPricingFrom string + LastPricingTo string +} + +func parseProductPricingFilters(r *http.Request) queries.ProductPricingFilters { + return queries.ProductPricingFilters{ + Search: strings.TrimSpace(r.URL.Query().Get("q")), + ProductCode: splitCSVParam(r.URL.Query().Get("product_code")), + BrandGroup: splitCSVParam(r.URL.Query().Get("brand_group_selection")), + AskiliYan: splitCSVParam(r.URL.Query().Get("askili_yan")), + Kategori: splitCSVParam(r.URL.Query().Get("kategori")), + UrunIlkGrubu: splitCSVParam(r.URL.Query().Get("urun_ilk_grubu")), + UrunAnaGrubu: splitCSVParam(r.URL.Query().Get("urun_ana_grubu")), + UrunAltGrubu: splitCSVParam(r.URL.Query().Get("urun_alt_grubu")), + Icerik: splitCSVParam(r.URL.Query().Get("icerik")), + Karisim: splitCSVParam(r.URL.Query().Get("karisim")), + Marka: splitCSVParam(r.URL.Query().Get("marka")), + } +} + +func parseProductPricingExportFilters(r *http.Request) productPricingExportFilters { + parseFloatPtr := func(raw string) *float64 { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64) + if err != nil { + return nil + } + return &v + } + + return productPricingExportFilters{ + BrandGroupSelection: splitCSVParam(r.URL.Query().Get("brand_group_selection_local")), + ValueFilters: map[string][]string{ + "costPrice": splitCSVParam(r.URL.Query().Get("vf_costPrice")), + "expenseForBasePrice": splitCSVParam(r.URL.Query().Get("vf_expenseForBasePrice")), + "basePriceUsd": splitCSVParam(r.URL.Query().Get("vf_basePriceUsd")), + "basePriceTry": splitCSVParam(r.URL.Query().Get("vf_basePriceTry")), + "usd1": splitCSVParam(r.URL.Query().Get("vf_usd1")), + "usd2": splitCSVParam(r.URL.Query().Get("vf_usd2")), + "usd3": splitCSVParam(r.URL.Query().Get("vf_usd3")), + "usd4": splitCSVParam(r.URL.Query().Get("vf_usd4")), + "usd5": splitCSVParam(r.URL.Query().Get("vf_usd5")), + "usd6": splitCSVParam(r.URL.Query().Get("vf_usd6")), + "eur1": splitCSVParam(r.URL.Query().Get("vf_eur1")), + "eur2": splitCSVParam(r.URL.Query().Get("vf_eur2")), + "eur3": splitCSVParam(r.URL.Query().Get("vf_eur3")), + "eur4": splitCSVParam(r.URL.Query().Get("vf_eur4")), + "eur5": splitCSVParam(r.URL.Query().Get("vf_eur5")), + "eur6": splitCSVParam(r.URL.Query().Get("vf_eur6")), + "try1": splitCSVParam(r.URL.Query().Get("vf_try1")), + "try2": splitCSVParam(r.URL.Query().Get("vf_try2")), + "try3": splitCSVParam(r.URL.Query().Get("vf_try3")), + "try4": splitCSVParam(r.URL.Query().Get("vf_try4")), + "try5": splitCSVParam(r.URL.Query().Get("vf_try5")), + "try6": splitCSVParam(r.URL.Query().Get("vf_try6")), + }, + StockQtyMin: parseFloatPtr(r.URL.Query().Get("stock_qty_min")), + StockQtyMax: parseFloatPtr(r.URL.Query().Get("stock_qty_max")), + StockEntryFrom: strings.TrimSpace(r.URL.Query().Get("stock_entry_from")), + StockEntryTo: strings.TrimSpace(r.URL.Query().Get("stock_entry_to")), + LastPricingFrom: strings.TrimSpace(r.URL.Query().Get("last_pricing_from")), + LastPricingTo: strings.TrimSpace(r.URL.Query().Get("last_pricing_to")), + } +} + +func filterProductPricingExportRows(rows []models.ProductPricing, f productPricingExportFilters) []models.ProductPricing { + if len(rows) == 0 { + return rows + } + + out := make([]models.ProductPricing, 0, len(rows)) + for _, row := range rows { + if len(f.BrandGroupSelection) > 0 && !containsText(f.BrandGroupSelection, row.BrandGroupSec) { + continue + } + if !matchesProductPricingValueFilters(row, f.ValueFilters) { + continue + } + if f.StockQtyMin != nil && row.StockQty < *f.StockQtyMin { + continue + } + if f.StockQtyMax != nil && row.StockQty > *f.StockQtyMax { + continue + } + if !matchesDateRangeString(row.StockEntryDate, f.StockEntryFrom, f.StockEntryTo) { + continue + } + if !matchesDateRangeString(row.LastPricingDate, f.LastPricingFrom, f.LastPricingTo) { + continue + } + out = append(out, row) + } + return out +} + +func parseExportCurrencies(raw string) []string { + requested := splitCSVParam(raw) + if len(requested) == 0 { + return []string{"USD", "EUR", "TRY"} + } + out := make([]string, 0, 3) + seen := map[string]bool{} + for _, item := range requested { + cur := strings.ToUpper(strings.TrimSpace(item)) + if cur != "USD" && cur != "EUR" && cur != "TRY" { + continue + } + if seen[cur] { + continue + } + seen[cur] = true + out = append(out, cur) + } + if len(out) == 0 { + return []string{"USD", "EUR", "TRY"} + } + return out +} + +func matchesProductPricingValueFilters(row models.ProductPricing, filters map[string][]string) bool { + if len(filters) == 0 { + return true + } + for field, selected := range filters { + if len(selected) == 0 { + continue + } + if !containsText(selected, productPricingValueKey(row, field)) { + return false + } + } + return true +} + +func productPricingValueKey(row models.ProductPricing, field string) string { + value := 0.0 + switch field { + case "costPrice": + value = row.CostPrice + case "expenseForBasePrice": + value = 0 + case "basePriceUsd": + value = row.BasePriceUsd + case "basePriceTry": + value = row.BasePriceTry + case "usd1": + value = row.USD1 + case "usd2": + value = row.USD2 + case "usd3": + value = row.USD3 + case "usd4": + value = row.USD4 + case "usd5": + value = row.USD5 + case "usd6": + value = row.USD6 + case "eur1": + value = row.EUR1 + case "eur2": + value = row.EUR2 + case "eur3": + value = row.EUR3 + case "eur4": + value = row.EUR4 + case "eur5": + value = row.EUR5 + case "eur6": + value = row.EUR6 + case "try1": + value = row.TRY1 + case "try2": + value = row.TRY2 + case "try3": + value = row.TRY3 + case "try4": + value = row.TRY4 + case "try5": + value = row.TRY5 + case "try6": + value = row.TRY6 + } + return fmt.Sprintf("%.2f", value) +} + +func matchesDateRangeString(value string, from string, to string) bool { + value = strings.TrimSpace(value) + from = strings.TrimSpace(from) + to = strings.TrimSpace(to) + if from == "" && to == "" { + return true + } + if value == "" { + return false + } + if from != "" && value < from { + return false + } + if to != "" && value > to { + return false + } + return true +} + +func containsText(list []string, value string) bool { + value = strings.TrimSpace(value) + for _, item := range list { + if strings.TrimSpace(item) == value { + return true + } + } + return false +} + +func csvEscape(value string) string { + text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ") + if strings.Contains(text, ";") || strings.Contains(text, "\"") { + text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"` + } + return text +} + +func csvFloat(value float64) string { + return fmt.Sprintf("%.2f", value) +} + +func buildProductPricingExportCSV(rows []models.ProductPricing, currencies []string) string { + var b strings.Builder + + headers := []string{ + "MARKA GRUBU SECIMI", + "MARKA", + "URUN KODU", + "STOK ADET", + "STOK GIRIS TARIHI", + "SON MALIYETLENDIRME", + "SON FIYATLANDIRMA TARIHI", + "ASKILI YAN", + "KATEGORI", + "URUN ILK GRUBU", + "URUN ANA GRUBU", + "URUN ALT GRUBU", + "ICERIK", + "KARISIM", + "MALIYET FIYATI", + "TABAN FIYAT MASRAF", + "TABAN USD", + "TABAN TRY", + } + for _, h := range headers { + b.WriteString(csvEscape(h)) + b.WriteString(";") + } + for idx, cur := range currencies { + for tier := 1; tier <= 6; tier++ { + b.WriteString(csvEscape(fmt.Sprintf("%s %d", cur, tier))) + if idx == len(currencies)-1 && tier == 6 { + b.WriteString("\n") + } else { + b.WriteString(";") + } + } + } + + for _, row := range rows { + base := []string{ + row.BrandGroupSec, + row.Marka, + row.ProductCode, + csvFloat(row.StockQty), + row.StockEntryDate, + row.LastCostingDate, + row.LastPricingDate, + row.AskiliYan, + row.Kategori, + row.UrunIlkGrubu, + row.UrunAnaGrubu, + row.UrunAltGrubu, + row.Icerik, + row.Karisim, + csvFloat(row.CostPrice), + csvFloat(0), + csvFloat(row.BasePriceUsd), + csvFloat(row.BasePriceTry), + } + for _, item := range base { + b.WriteString(csvEscape(item)) + b.WriteString(";") + } + for idx, cur := range currencies { + values := productPricingCurrencyValues(row, cur) + for tierIdx, value := range values { + b.WriteString(csvEscape(csvFloat(value))) + if idx == len(currencies)-1 && tierIdx == len(values)-1 { + b.WriteString("\n") + } else { + b.WriteString(";") + } + } + } + } + + return b.String() +} + +func productPricingCurrencyValues(row models.ProductPricing, currency string) []float64 { + switch currency { + case "USD": + return []float64{row.USD1, row.USD2, row.USD3, row.USD4, row.USD5, row.USD6} + case "EUR": + return []float64{row.EUR1, row.EUR2, row.EUR3, row.EUR4, row.EUR5, row.EUR6} + default: + return []float64{row.TRY1, row.TRY2, row.TRY3, row.TRY4, row.TRY5, row.TRY6} + } +} + func buildPricingTraceID(r *http.Request) string { if r != nil { if id := strings.TrimSpace(r.Header.Get("X-Request-ID")); id != "" { diff --git a/svc/routes/statements_pdf.go b/svc/routes/statements_pdf.go index 2901f23..844ab85 100644 --- a/svc/routes/statements_pdf.go +++ b/svc/routes/statements_pdf.go @@ -8,6 +8,7 @@ import ( "bssapp-backend/queries" "bytes" "database/sql" + "encoding/json" "fmt" "log" "net/http" @@ -19,6 +20,28 @@ import ( "github.com/jung-kurt/gofpdf" ) +type statementPDFHeaderRow struct { + CariKod string `json:"cari_kod"` + CariIsim string `json:"cari_isim"` + BelgeTarihi string `json:"belge_tarihi"` + VadeTarihi string `json:"vade_tarihi"` + BelgeNo string `json:"belge_no"` + IslemTipi string `json:"islem_tipi"` + Aciklama string `json:"aciklama"` + ParaBirimi string `json:"para_birimi"` + Borc float64 `json:"borc"` + Alacak float64 `json:"alacak"` + Bakiye float64 `json:"bakiye"` +} + +type statementPDFPayload struct { + AccountCode string `json:"account_code"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + LangCode string `json:"lang_code"` + Rows []statementPDFHeaderRow `json:"rows"` +} + /* ============================ SABİTLER ============================ */ // A4 Landscape (mm) @@ -468,11 +491,25 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc { log.Printf("▶️ ExportPDFHandler: account=%s start=%s end=%s parislemler=%v", accountCode, startDate, endDate, parislemler) - // 1) Header verileri - headers, belgeNos, err := queries.GetStatementsPDF(r.Context(), accountCode, startDate, endDate, langCode, parislemler) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + var ( + headers []models.StatementHeader + belgeNos []string + err error + ) + + if strings.EqualFold(r.Method, http.MethodPost) { + accountCode, startDate, endDate, langCode, headers, err = parseStatementPDFPayload(r, langCode) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + belgeNos = queriesCollectBelgeNos(headers) + } else { + headers, belgeNos, err = queries.GetStatementsPDF(r.Context(), accountCode, startDate, endDate, langCode, parislemler) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos)) @@ -506,10 +543,7 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc { for _, k := range order { sort.SliceStable(groups[k].rows, func(i, j int) bool { ri, rj := groups[k].rows[i], groups[k].rows[j] - if ri.BelgeTarihi == rj.BelgeTarihi { - return ri.BelgeNo < rj.BelgeNo - } - return ri.BelgeTarihi < rj.BelgeTarihi + return parseStatementHeaderDate(ri.BelgeTarihi).Before(parseStatementHeaderDate(rj.BelgeTarihi)) }) if n := len(groups[k].rows); n > 0 { groups[k].sonBakiye = groups[k].rows[n-1].Bakiye @@ -648,6 +682,64 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc { } } +func parseStatementPDFPayload(r *http.Request, fallbackLang string) (string, string, string, string, []models.StatementHeader, error) { + var payload statementPDFPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + return "", "", "", "", nil, fmt.Errorf("invalid statement pdf payload: %w", err) + } + + langCode := i18n.ResolveLangCode(payload.LangCode, fallbackLang) + headers := make([]models.StatementHeader, 0, len(payload.Rows)) + for _, row := range payload.Rows { + headers = append(headers, models.StatementHeader{ + CariKod: row.CariKod, + CariIsim: row.CariIsim, + BelgeTarihi: row.BelgeTarihi, + VadeTarihi: row.VadeTarihi, + BelgeNo: row.BelgeNo, + IslemTipi: row.IslemTipi, + Aciklama: row.Aciklama, + ParaBirimi: row.ParaBirimi, + Borc: row.Borc, + Alacak: row.Alacak, + Bakiye: row.Bakiye, + }) + } + + return payload.AccountCode, payload.StartDate, payload.EndDate, langCode, headers, nil +} + +func parseStatementHeaderDate(value string) time.Time { + value = strings.TrimSpace(value) + if value == "" { + return time.Time{} + } + if t, err := time.Parse("2006-01-02", value); err == nil { + return t + } + if t, err := time.Parse(time.RFC3339, value); err == nil { + return t + } + return time.Time{} +} + +func queriesCollectBelgeNos(headers []models.StatementHeader) []string { + seen := make(map[string]struct{}, len(headers)) + out := make([]string, 0, len(headers)) + for _, h := range headers { + no := strings.TrimSpace(h.BelgeNo) + if no == "" || no == "Baslangic_devir" { + continue + } + if _, ok := seen[no]; ok { + continue + } + seen[no] = struct{}{} + out = append(out, no) + } + return out +} + /* NOTLAR: - Header artık dinamik yüksekliğe sahip (drawPageHeader -> contentTopY döner). diff --git a/ui/quasar.config.js.temporary.compiled.1780404176831.mjs b/ui/quasar.config.js.temporary.compiled.1780488176351.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1780404176831.mjs rename to ui/quasar.config.js.temporary.compiled.1780488176351.mjs diff --git a/ui/src/pages/AccountAgingStatement.vue b/ui/src/pages/AccountAgingStatement.vue index 5868bb3..f578e2c 100644 --- a/ui/src/pages/AccountAgingStatement.vue +++ b/ui/src/pages/AccountAgingStatement.vue @@ -395,20 +395,6 @@ function toggleFiltersCollapsed() { filtersCollapsed.value = !filtersCollapsed.value } -function buildExportParams() { - return { - accountcode: String(selectedCari.value || '').trim(), - cari_search: String(selectedCari.value || '').trim(), - enddate: dateTo.value, - selected_date: dateTo.value, - parislemler: selectedMonType.value, - exclude_zero_12: '0', - exclude_zero_13: '0', - sort_by: String(masterPagination.value?.sortBy || ''), - sort_desc: masterPagination.value?.descending ? '1' : '0' - } -} - async function downloadAgingScreenPDF() { if (!canExportFinance.value) { $q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' }) @@ -420,7 +406,17 @@ async function downloadAgingScreenPDF() { } try { - const blob = await download('/finance/account-aging-statement/export-screen-pdf', buildExportParams()) + const blob = await download('/finance/account-aging-statement/export-screen-pdf', { + accountcode: String(selectedCari.value || '').trim(), + cari_search: String(selectedCari.value || '').trim(), + enddate: dateTo.value, + selected_date: dateTo.value, + parislemler: selectedMonType.value, + exclude_zero_12: '0', + exclude_zero_13: '0', + sort_by: String(masterPagination.value?.sortBy || ''), + sort_desc: masterPagination.value?.descending ? '1' : '0' + }) const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' })) window.open(pdfUrl, '_blank') } catch (err) { diff --git a/ui/src/pages/PricingRules.vue b/ui/src/pages/PricingRules.vue index df22db7..4f329c5 100644 --- a/ui/src/pages/PricingRules.vue +++ b/ui/src/pages/PricingRules.vue @@ -3,9 +3,6 @@
Fiyat Carpani Kurallari
-
- MSSQL urun kombinasyonlari ve bu kombinasyonlara bagli para birimi bazli fiyat kurallari. -
- {{ filteredRows.length }} / {{ rows.length }} kombinasyon gosteriliyor. Degistirilen satirlar otomatik secilir. + Satir: {{ filteredRows.length }} / {{ rows.length }} | Degisen: {{ selectedDirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ selectedCount }}
-
+
+
+ + Kopya secimi + + + Kaydetme secimi + +
+ + + + +
@@ -75,10 +119,17 @@ +
+ + Kopya +
{{ col.label }} +
+ + + {{ copyRoleLabel(props.row) }} + +
@@ -219,7 +293,7 @@