package routes import ( "bssapp-backend/db" "bssapp-backend/queries" "context" "database/sql" "encoding/json" "fmt" "net/http" "sort" "strconv" "strings" "time" "github.com/gorilla/mux" "github.com/lib/pq" ) type productSeriesDefinition struct { ID int64 `json:"id"` Code string `json:"code"` Title string `json:"title"` IsActive bool `json:"is_active"` ParentFilter string `json:"parent_filter"` SortOrder int `json:"sort_order"` Notes string `json:"notes"` } type productSeriesStockRawRow struct { ProductCode string ProductDescription string ColorCode string ColorTitle string Dim3Code string SizeCode string Qty float64 UrunAnaGrubu string UrunAltGrubu string Marka string DropVal string Fit string UrunIcerigi string Kategori string } type productSeriesMappingRow struct { RowKey string `json:"row_key"` ProductCode string `json:"product_code"` ProductDescription string `json:"product_description"` ColorCode string `json:"color_code"` ColorTitle string `json:"color_title"` Dim3Code string `json:"dim3_code"` UrunAnaGrubu string `json:"urun_ana_grubu"` UrunAltGrubu string `json:"urun_alt_grubu"` Marka string `json:"marka"` DropVal string `json:"drop_val"` Fit string `json:"fit"` UrunIcerigi string `json:"urun_icerigi"` Kategori string `json:"kategori"` SizeQty map[string]float64 `json:"size_qty"` TotalQty float64 `json:"total_qty"` SeriesIDs []int64 `json:"series_ids"` Series []productSeriesDefinition `json:"series"` MmitemID int64 `json:"mmitem_id"` Dim1ID int64 `json:"dim1_id"` Dim3ID int64 `json:"dim3_id"` MappingReady bool `json:"mapping_ready"` MappingWarning string `json:"mapping_warning"` } type productSeriesMappingsResponse struct { Rows []productSeriesMappingRow `json:"rows"` SizeColumns []string `json:"size_columns"` Definitions []productSeriesDefinition `json:"definitions"` } func GetProductSeriesDefinitionsHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() defs, err := listProductSeriesDefinitions(ctx, pg, false) if err != nil { http.Error(w, "Seri tanimlari alinamadi: "+err.Error(), http.StatusInternalServerError) return } writeJSON(w, defs) } } func PostProductSeriesDefinitionHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req productSeriesDefinition if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz istek", http.StatusBadRequest) return } req.Code = strings.TrimSpace(req.Code) req.Title = strings.TrimSpace(req.Title) if req.Code == "" { http.Error(w, "Seri kodu zorunludur", http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() err := pg.QueryRowContext(ctx, ` INSERT INTO dfgrp (code, title, is_active, typ, master, parent_filter, sort_order, is_required, notes) VALUES ($1, $2, COALESCE($3, TRUE), 'opt', 'zbggseri', $4, $5, FALSE, $6) RETURNING id `, req.Code, req.Title, req.IsActive, req.ParentFilter, req.SortOrder, req.Notes).Scan(&req.ID) if err != nil { http.Error(w, "Seri tanimi eklenemedi: "+err.Error(), http.StatusInternalServerError) return } writeJSON(w, req) } } func PutProductSeriesDefinitionHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) if id <= 0 { http.Error(w, "Gecersiz id", http.StatusBadRequest) return } var req productSeriesDefinition if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz istek", http.StatusBadRequest) return } req.Code = strings.TrimSpace(req.Code) req.Title = strings.TrimSpace(req.Title) if req.Code == "" { http.Error(w, "Seri kodu zorunludur", http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() res, err := pg.ExecContext(ctx, ` UPDATE dfgrp SET code=$2, title=$3, is_active=$4, parent_filter=$5, sort_order=$6, notes=$7 WHERE id=$1 AND master='zbggseri' `, id, req.Code, req.Title, req.IsActive, req.ParentFilter, req.SortOrder, req.Notes) if err != nil { http.Error(w, "Seri tanimi guncellenemedi: "+err.Error(), http.StatusInternalServerError) return } if n, _ := res.RowsAffected(); n == 0 { http.Error(w, "Seri tanimi bulunamadi", http.StatusNotFound) return } req.ID = id writeJSON(w, req) } } func DeleteProductSeriesDefinitionHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) if id <= 0 { http.Error(w, "Gecersiz id", http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() if _, err := pg.ExecContext(ctx, `UPDATE dfgrp SET is_active=FALSE WHERE id=$1 AND master='zbggseri'`, id); err != nil { http.Error(w, "Seri tanimi silinemedi: "+err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]any{"ok": true}) } } func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { f := readStockAttrFilters(r) limit := 0 if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" { if n, err := strconv.Atoi(raw); err == nil && n >= 0 { limit = n } } search := firstNonEmpty(r.URL.Query().Get("q"), r.URL.Query().Get("code"), r.URL.Query().Get("product_code")) ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second) defer cancel() msRows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductSeriesStockRowsQuery, f.kategori, f.urunAnaGrubu, joinFilterValues(f.urunAltGrubu), joinFilterValues(f.renk), joinFilterValues(f.renk2), joinFilterValues(f.urunIcerigi), joinFilterValues(f.fit), joinFilterValues(f.drop), joinFilterValues(f.beden), strconv.Itoa(limit), search, ) if err != nil { http.Error(w, "Stok seri listesi alinamadi: "+err.Error(), http.StatusInternalServerError) return } defer msRows.Close() grouped := map[string]*productSeriesMappingRow{} codeSet := map[string]struct{}{} colorSet := map[string]struct{}{} dim3Set := map[string]struct{}{} sizeSet := map[string]struct{}{} for msRows.Next() { var raw productSeriesStockRawRow if err := msRows.Scan( &raw.ProductCode, &raw.ProductDescription, &raw.ColorCode, &raw.ColorTitle, &raw.Dim3Code, &raw.SizeCode, &raw.Qty, &raw.UrunAnaGrubu, &raw.UrunAltGrubu, &raw.Marka, &raw.DropVal, &raw.Fit, &raw.UrunIcerigi, &raw.Kategori, ); err != nil { http.Error(w, "Stok satiri okunamadi: "+err.Error(), http.StatusInternalServerError) return } raw.ProductCode = strings.TrimSpace(raw.ProductCode) raw.ColorCode = strings.TrimSpace(raw.ColorCode) raw.Dim3Code = strings.TrimSpace(raw.Dim3Code) raw.SizeCode = strings.TrimSpace(raw.SizeCode) key := productSeriesKey(raw.ProductCode, raw.ColorCode, raw.Dim3Code) row := grouped[key] if row == nil { row = &productSeriesMappingRow{ RowKey: key, ProductCode: raw.ProductCode, ProductDescription: strings.TrimSpace(raw.ProductDescription), ColorCode: raw.ColorCode, ColorTitle: strings.TrimSpace(raw.ColorTitle), Dim3Code: raw.Dim3Code, UrunAnaGrubu: strings.TrimSpace(raw.UrunAnaGrubu), UrunAltGrubu: strings.TrimSpace(raw.UrunAltGrubu), Marka: strings.TrimSpace(raw.Marka), DropVal: strings.TrimSpace(raw.DropVal), Fit: strings.TrimSpace(raw.Fit), UrunIcerigi: strings.TrimSpace(raw.UrunIcerigi), Kategori: strings.TrimSpace(raw.Kategori), SizeQty: map[string]float64{}, SeriesIDs: []int64{}, Series: []productSeriesDefinition{}, } grouped[key] = row codeSet[raw.ProductCode] = struct{}{} colorSet[raw.ColorCode] = struct{}{} if raw.Dim3Code != "" { dim3Set[raw.Dim3Code] = struct{}{} } } if raw.SizeCode != "" { row.SizeQty[raw.SizeCode] = raw.Qty sizeSet[raw.SizeCode] = struct{}{} } row.TotalQty += raw.Qty } if err := msRows.Err(); err != nil { http.Error(w, "Stok satirlari okunamadi: "+err.Error(), http.StatusInternalServerError) return } defs, err := listProductSeriesDefinitions(ctx, pg, true) if err != nil { http.Error(w, "Seri tanimlari alinamadi: "+err.Error(), http.StatusInternalServerError) return } defByID := make(map[int64]productSeriesDefinition, len(defs)) for _, d := range defs { defByID[d.ID] = d } codes := setToSortedSlice(codeSet) mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes) dim1ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval1", setToSortedSlice(colorSet)) dim3ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval3", setToSortedSlice(dim3Set)) existing, _ := loadProductSeriesAssignments(ctx, pg, codes) out := make([]productSeriesMappingRow, 0, len(grouped)) for _, row := range grouped { row.MmitemID = mmitemByCode[row.ProductCode] row.Dim1ID = dim1ByToken[row.ColorCode] if row.Dim3Code != "" { row.Dim3ID = dim3ByToken[row.Dim3Code] } row.MappingReady = row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0) if !row.MappingReady { row.MappingWarning = "PG urun veya varyant token eslesmesi bulunamadi" } assignKey := assignmentKey(row.ProductCode, row.Dim1ID, row.Dim3ID) for _, id := range existing[assignKey] { if d, ok := defByID[id]; ok { row.SeriesIDs = append(row.SeriesIDs, id) row.Series = append(row.Series, d) } } sort.Slice(row.Series, func(i, j int) bool { return row.Series[i].Code < row.Series[j].Code }) sort.Slice(row.SeriesIDs, func(i, j int) bool { return row.SeriesIDs[i] < row.SeriesIDs[j] }) out = append(out, *row) } productTotals := productSeriesTotalQtyByCode(out) sort.Slice(out, func(i, j int) bool { totalI := productTotals[out[i].ProductCode] totalJ := productTotals[out[j].ProductCode] if totalI != totalJ { return totalI > totalJ } if out[i].ProductCode != out[j].ProductCode { return out[i].ProductCode < out[j].ProductCode } if out[i].ColorCode != out[j].ColorCode { return out[i].ColorCode < out[j].ColorCode } return out[i].Dim3Code < out[j].Dim3Code }) writeJSON(w, productSeriesMappingsResponse{ Rows: out, SizeColumns: sortSizeColumns(setToSortedSlice(sizeSet)), Definitions: defs, }) } } func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]float64 { out := make(map[string]float64, len(rows)) for _, row := range rows { out[row.ProductCode] += row.TotalQty } return out } type saveProductSeriesMappingsRequest struct { Items []struct { ProductCode string `json:"product_code"` ColorCode string `json:"color_code"` Dim3Code string `json:"dim3_code"` SeriesIDs []int64 `json:"series_ids"` } `json:"items"` } func PostProductSeriesMappingsSaveHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req saveProductSeriesMappingsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz istek", http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() tx, err := pg.BeginTx(ctx, nil) if err != nil { http.Error(w, "Transaction baslatilamadi", http.StatusInternalServerError) return } defer tx.Rollback() saved := 0 for _, item := range req.Items { code := strings.TrimSpace(item.ProductCode) color := strings.TrimSpace(item.ColorCode) dim3Token := strings.TrimSpace(item.Dim3Code) if code == "" || color == "" { continue } mmitemID, err := resolveMmitemIDTx(ctx, tx, code) if err != nil || mmitemID <= 0 { http.Error(w, "PG urun bulunamadi: "+code, http.StatusBadRequest) return } dim1ID, err := resolveDimTokenIDTx(ctx, tx, "dimval1", color) if err != nil || dim1ID <= 0 { http.Error(w, "Renk token eslesmesi bulunamadi: "+color, http.StatusBadRequest) return } var dim3ID sql.NullInt64 if dim3Token != "" { id, err := resolveDimTokenIDTx(ctx, tx, "dimval3", dim3Token) if err != nil || id <= 0 { http.Error(w, "Dim3 token eslesmesi bulunamadi: "+dim3Token, http.StatusBadRequest) return } dim3ID = sql.NullInt64{Int64: id, Valid: true} } if _, err := tx.ExecContext(ctx, ` DELETE FROM zbggseri WHERE mmitem_id=$1 AND dim1=$2 AND (($3::bigint IS NULL AND dim3 IS NULL) OR dim3=$3::bigint) `, mmitemID, dim1ID, nullableInt64Arg(dim3ID)); err != nil { http.Error(w, "Seri eslesmesi temizlenemedi: "+err.Error(), http.StatusInternalServerError) return } seen := map[int64]struct{}{} for _, seriesID := range item.SeriesIDs { if seriesID <= 0 { continue } if _, ok := seen[seriesID]; ok { continue } seen[seriesID] = struct{}{} if _, err := tx.ExecContext(ctx, ` INSERT INTO zbggseri (mmitem_id, dim1, seri_id, dim3) VALUES ($1, $2, $3, $4) `, mmitemID, dim1ID, seriesID, nullableInt64Arg(dim3ID)); err != nil { http.Error(w, "Seri eslesmesi kaydedilemedi: "+err.Error(), http.StatusInternalServerError) return } } saved++ } if err := tx.Commit(); err != nil { http.Error(w, "Seri eslesmeleri kaydedilemedi: "+err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]any{"saved": saved}) } } func listProductSeriesDefinitions(ctx context.Context, pg *sql.DB, activeOnly bool) ([]productSeriesDefinition, error) { whereActive := "" if activeOnly { whereActive = " AND is_active=TRUE" } rows, err := pg.QueryContext(ctx, ` SELECT id, COALESCE(code,''), COALESCE(title,''), COALESCE(is_active, TRUE), COALESCE(parent_filter,''), COALESCE(sort_order, 0), COALESCE(notes,'') FROM dfgrp WHERE master='zbggseri'`+whereActive+` ORDER BY sort_order, CASE WHEN code ~ '^[0-9]+$' THEN code::int ELSE NULL END NULLS LAST, code, id `) if err != nil { return nil, err } defer rows.Close() out := []productSeriesDefinition{} for rows.Next() { var item productSeriesDefinition if err := rows.Scan(&item.ID, &item.Code, &item.Title, &item.IsActive, &item.ParentFilter, &item.SortOrder, &item.Notes); err != nil { return nil, err } out = append(out, item) } return out, rows.Err() } func loadMmitemIDs(ctx context.Context, pg *sql.DB, codes []string) (map[string]int64, error) { out := map[string]int64{} if len(codes) == 0 { return out, nil } rows, err := pg.QueryContext(ctx, `SELECT code, id FROM mmitem WHERE code = ANY($1)`, pq.Array(codes)) if err != nil { return out, err } defer rows.Close() for rows.Next() { var code string var id int64 if err := rows.Scan(&code, &id); err != nil { return out, err } out[strings.TrimSpace(code)] = id } return out, rows.Err() } func loadDimTokenIDs(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) { out := map[string]int64{} if len(tokens) == 0 { return out, nil } rows, err := pg.QueryContext(ctx, ` SELECT token, dim_id FROM mk_dim_token_map WHERE dim_column=$1 AND token = ANY($2) `, column, pq.Array(tokens)) if err != nil { return out, err } defer rows.Close() for rows.Next() { var token string var id int64 if err := rows.Scan(&token, &id); err != nil { return out, err } out[strings.TrimSpace(token)] = id } return out, rows.Err() } func loadProductSeriesAssignments(ctx context.Context, pg *sql.DB, codes []string) (map[string][]int64, error) { out := map[string][]int64{} if len(codes) == 0 { return out, nil } rows, err := pg.QueryContext(ctx, ` SELECT m.code, z.dim1, COALESCE(z.dim3, 0), z.seri_id FROM zbggseri z JOIN mmitem m ON m.id = z.mmitem_id WHERE m.code = ANY($1) ORDER BY m.code, z.dim1, z.dim3, z.seri_id `, pq.Array(codes)) if err != nil { return out, err } defer rows.Close() for rows.Next() { var code string var dim1, dim3, seriesID int64 if err := rows.Scan(&code, &dim1, &dim3, &seriesID); err != nil { return out, err } key := assignmentKey(strings.TrimSpace(code), dim1, dim3) out[key] = append(out[key], seriesID) } return out, rows.Err() } func resolveMmitemIDTx(ctx context.Context, tx *sql.Tx, code string) (int64, error) { var id int64 err := tx.QueryRowContext(ctx, `SELECT id FROM mmitem WHERE code=$1`, code).Scan(&id) return id, err } func resolveDimTokenIDTx(ctx context.Context, tx *sql.Tx, column string, token string) (int64, error) { var id int64 err := tx.QueryRowContext(ctx, `SELECT dim_id FROM mk_dim_token_map WHERE dim_column=$1 AND token=$2`, column, token).Scan(&id) return id, err } func nullableInt64Arg(v sql.NullInt64) any { if !v.Valid { return nil } return v.Int64 } func productSeriesKey(productCode, colorCode, dim3Code string) string { return strings.TrimSpace(productCode) + "|" + strings.TrimSpace(colorCode) + "|" + strings.TrimSpace(dim3Code) } func assignmentKey(productCode string, dim1ID int64, dim3ID int64) string { return fmt.Sprintf("%s|%d|%d", strings.TrimSpace(productCode), dim1ID, dim3ID) } func setToSortedSlice(set map[string]struct{}) []string { out := make([]string, 0, len(set)) for v := range set { v = strings.TrimSpace(v) if v != "" { out = append(out, v) } } sort.Strings(out) return out } func sortSizeColumns(values []string) []string { sort.Slice(values, func(i, j int) bool { ai, ae := strconv.Atoi(values[i]) bi, be := strconv.Atoi(values[j]) if ae == nil && be == nil { return ai < bi } return values[i] < values[j] }) return values } func writeJSON(w http.ResponseWriter, payload any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(payload) }