package routes import ( "bssapp-backend/queries" "bssapp-backend/utils" "context" "database/sql" "encoding/json" "log" "net/http" "strconv" "strings" "time" ) func GetProductPerformanceHandler(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), 180*time.Second) defer cancel() f := queries.ProductPerformanceFilters{ Search: strings.TrimSpace(r.URL.Query().Get("q")), ProductCode: strings.TrimSpace(r.URL.Query().Get("product_code")), MarketKey: strings.TrimSpace(r.URL.Query().Get("market_key")), Kategori: strings.TrimSpace(r.URL.Query().Get("kategori")), Seri: strings.TrimSpace(r.URL.Query().Get("seri")), Bucket: strings.TrimSpace(r.URL.Query().Get("bucket")), SortBy: strings.TrimSpace(r.URL.Query().Get("sort_by")), Descending: boolQuery(r, "descending", true), Limit: intQuery(r, "limit", 50000), Page: intQuery(r, "page", 1), } rows, total, err := queries.ListProductPerformance(ctx, pg, f) if err != nil { http.Error(w, "urun performans listesi alinamadi: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("X-Total-Count", strconv.Itoa(total)) _ = json.NewEncoder(w).Encode(map[string]any{ "rows": rows, "total_count": total, "page": f.Page, "limit": f.Limit, }) } } func GetProductPerformanceSalesDetailsHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() rows, err := queries.ListProductPerformanceSalesDetails( ctx, pg, strings.TrimSpace(r.URL.Query().Get("product_code")), strings.TrimSpace(r.URL.Query().Get("color_code")), strings.TrimSpace(r.URL.Query().Get("yaka_kodu")), intQuery(r, "limit", 100), ) if err != nil { http.Error(w, "urun satis detayi alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } } func GetProductPerformanceStockSizesHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() rows, err := queries.ListProductPerformanceStockSizes( ctx, strings.TrimSpace(r.URL.Query().Get("product_code")), strings.TrimSpace(r.URL.Query().Get("color_code")), strings.TrimSpace(r.URL.Query().Get("yaka_kodu")), ) if err != nil { http.Error(w, "urun beden stok detayi alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } } func GetProductPerformanceSummaryHandler(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), 180*time.Second) defer cancel() summary, err := queries.GetProductPerformanceSummary(ctx, pg) if err != nil { http.Error(w, "urun performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(summary) } } 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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 50000)) 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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 50000)) 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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceOrderGroups( ctx, pg, strings.TrimSpace(r.URL.Query().Get("breakdown")), intQuery(r, "limit", 50000), ) 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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 50000)) 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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 50000)) 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") traceID := utils.TraceIDFromRequest(r) ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 50000)) if err != nil { http.Error(w, "piyasa performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } } func GetProductPerformanceCountriesHandler(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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 50000)) if err != nil { http.Error(w, "ulke performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } } func GetProductPerformanceCustomersHandler(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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceCustomers( ctx, pg, strings.TrimSpace(r.URL.Query().Get("breakdown")), intQuery(r, "limit", 50000), ) if err != nil { http.Error(w, "musteri performans kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } } func GetProductPerformanceSalesBreakdownHandler(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), 180*time.Second) defer cancel() rows, err := queries.ListProductPerformanceSalesBreakdown( ctx, pg, strings.TrimSpace(r.URL.Query().Get("mode")), intQuery(r, "limit", 50000), ) if err != nil { http.Error(w, "satis kpi kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } } func GetProductPerformanceGroupedHandler(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), 180*time.Second) defer cancel() mode := strings.TrimSpace(r.URL.Query().Get("mode")) mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu")) sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by")) descending := boolQuery(r, "descending", true) groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels")) limit := intQuery(r, "limit", 50000) expanded := map[string]bool{} filters := map[string][]string{} expandThroughLevel := -1 if rawExpandLevel := strings.TrimSpace(r.URL.Query().Get("expand_through_level")); rawExpandLevel != "" { if parsed, err := strconv.Atoi(rawExpandLevel); err == nil { expandThroughLevel = parsed } } const maxExpandedGroupKeys = 1000 addExpandedKey := func(key string) { key = strings.TrimSpace(key) if key != "" && len(expanded) < maxExpandedGroupKeys { expanded[key] = true } } if r.Method == http.MethodPost { var body struct { Mode string `json:"mode"` GroupLevels []string `json:"group_levels"` ExpandedKeys []string `json:"expanded_keys"` ExpandThroughLevel *int `json:"expand_through_level"` Limit int `json:"limit"` MainGroup string `json:"urun_ana_grubu"` Filters map[string][]string `json:"filters"` SortBy string `json:"sort_by"` Descending *bool `json:"descending"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest) return } if strings.TrimSpace(body.Mode) != "" { mode = strings.TrimSpace(body.Mode) } if strings.TrimSpace(body.MainGroup) != "" { mainGroup = strings.TrimSpace(body.MainGroup) } if len(body.GroupLevels) > 0 { groupLevels = body.GroupLevels } if body.Limit > 0 { limit = body.Limit } for _, key := range body.ExpandedKeys { addExpandedKey(key) } if body.ExpandThroughLevel != nil { expandThroughLevel = *body.ExpandThroughLevel } if len(body.Filters) > 0 { filters = body.Filters } if strings.TrimSpace(body.SortBy) != "" { sortBy = strings.TrimSpace(body.SortBy) } if body.Descending != nil { descending = *body.Descending } } else { for _, key := range strings.Split(r.URL.Query().Get("expanded_keys"), ",") { addExpandedKey(key) } } rows, err := queries.ListProductPerformanceGrouped(ctx, pg, queries.ProductPerformanceGroupedRequest{ Mode: mode, GroupLevels: groupLevels, ExpandedKeys: expanded, ExpandThroughLevel: expandThroughLevel, Limit: limit, MainGroup: mainGroup, Filters: filters, SortBy: sortBy, Descending: descending, }) if err != nil { log.Printf("[ProductPerformance][grouped] failed trace_id=%s mode=%s levels=%v expand_through_level=%d filters=%d: %v", traceID, mode, groupLevels, expandThroughLevel, len(filters), err) http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } } func GetProductPerformanceGroupedFilterOptionsHandler(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() mode := strings.TrimSpace(r.URL.Query().Get("mode")) mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu")) groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels")) fields := splitCSVQuery(r.URL.Query().Get("fields")) limit := intQuery(r, "limit", 5000) if r.Method == http.MethodPost { var body struct { Mode string `json:"mode"` GroupLevels []string `json:"group_levels"` MainGroup string `json:"urun_ana_grubu"` Fields []string `json:"fields"` Limit int `json:"limit"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "gecersiz filtre secenek istegi: "+err.Error(), http.StatusBadRequest) return } if strings.TrimSpace(body.Mode) != "" { mode = strings.TrimSpace(body.Mode) } if strings.TrimSpace(body.MainGroup) != "" { mainGroup = strings.TrimSpace(body.MainGroup) } if len(body.GroupLevels) > 0 { groupLevels = body.GroupLevels } if len(body.Fields) > 0 { fields = body.Fields } if body.Limit > 0 { limit = body.Limit } } options, err := queries.ListProductPerformanceGroupedFilterOptions(ctx, pg, queries.ProductPerformanceGroupedFilterOptionsRequest{ Mode: mode, GroupLevels: groupLevels, MainGroup: mainGroup, Fields: fields, Limit: limit, }) if err != nil { log.Printf("[ProductPerformance][grouped-filter-options] failed trace_id=%s mode=%s levels=%v fields=%d: %v", traceID, mode, groupLevels, len(fields), err) http.Error(w, "urun performans filtre secenekleri alinamadi: "+err.Error(), http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(options) } } func splitCSVQuery(value string) []string { parts := strings.Split(value, ",") out := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part != "" { out = append(out, part) } } return out } func intQuery(r *http.Request, key string, fallback int) int { raw := strings.TrimSpace(r.URL.Query().Get(key)) if raw == "" { return fallback } n, err := strconv.Atoi(raw) if err != nil || n <= 0 { return fallback } return n } func boolQuery(r *http.Request, key string, fallback bool) bool { raw := strings.TrimSpace(strings.ToLower(r.URL.Query().Get(key))) if raw == "" { return fallback } return raw == "1" || raw == "true" || raw == "yes" || raw == "on" }