Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-04 14:33:10 +03:00
parent 00626152c2
commit 7b1588d69d
11 changed files with 2065 additions and 78 deletions

View File

@@ -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 != "" {