Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user