package routes import ( "bssapp-backend/auth" "bssapp-backend/db" "bssapp-backend/models" "bssapp-backend/queries" "bssapp-backend/utils" "context" "database/sql" "encoding/json" "log" "net/http" "strconv" "strings" "time" ) func logProductionHasCostDetailEditorOptionItemDiagnostics(ctx context.Context, uretimDB *sql.DB, search string, limit int) { logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.detail-editor-options.diagnostics", "search", search, "limit", limit, ) expectedColumns := []string{ "nStokID", "sKodu", "sAciklama", "sModel", "sBirimCinsi1", "IsBlocked", } probes := []struct { name string sqlText string }{ { name: "projection:nStokID", sqlText: `SELECT TOP 1 RTRIM(CONVERT(VARCHAR(32), ISNULL(S.nStokID, 0))) FROM dbo.tbStok S`, }, { name: "projection:sKodu", sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) FROM dbo.tbStok S`, }, { name: "projection:sAciklama", sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) FROM dbo.tbStok S`, }, { name: "projection:sModel", sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) FROM dbo.tbStok S`, }, { name: "projection:sBirimCinsi1", sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(S.sBirimCinsi1, '')))) FROM dbo.tbStok S`, }, { name: "projection:IsBlocked", sqlText: `SELECT TOP 1 RTRIM(CONVERT(VARCHAR(32), ISNULL(S.IsBlocked, 0))) FROM dbo.tbStok S`, }, { name: "filter:model_like_count", sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE '_.%'`, }, { name: "filter:isblocked_count", sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE ISNULL(S.IsBlocked, 0) = 0`, }, { name: "filter:final_count", sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE ISNULL(S.IsBlocked, 0) = 0 AND LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE '_.%'`, }, } log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics start search=%q limit=%d", search, limit) for _, columnName := range expectedColumns { var exists int err := uretimDB.QueryRowContext( ctx, `SELECT COUNT(1) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'tbStok' AND COLUMN_NAME = @p1`, columnName, ).Scan(&exists) if err != nil { logger.Error("query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item diagnostics column check error column=%s err=%v", columnName, err) continue } log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics column=%s exists=%t", columnName, exists > 0) } for _, probe := range probes { var sample sql.NullString err := uretimDB.QueryRowContext(ctx, probe.sqlText).Scan(&sample) if err != nil { log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item diagnostics probe=%s err=%v", probe.name, err) continue } log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics probe=%s sample=%q", probe.name, strings.TrimSpace(sample.String)) } log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics end search=%q limit=%d", search, limit) } // GET /api/pricing/production-product-costing/no-cost-products func GetProductionNoCostProductsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } search := strings.TrimSpace(r.URL.Query().Get("search")) fromDate := strings.TrimSpace(r.URL.Query().Get("from_date")) rows, err := queries.GetProductionNoCostProducts(r.Context(), uretimDB, fromDate, search) if err != nil { log.Printf("❌ [ProductionNoCost] query error: %v", err) http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) return } defer rows.Close() list := make([]models.ProductionNoCostProductRow, 0, 200) for rows.Next() { var item models.ProductionNoCostProductRow if err := rows.Scan( &item.UretimSekli, &item.UrtSiparisNo, &item.IslemTarihi, &item.FirmaKodu, &item.FirmaAdi, &item.SonIsEmriVeren, &item.MMiktarG, &item.MModelKodu, &item.Kodu, &item.ModelAdi, &item.SKullaniciAdi, &item.SKullaniciGunc, ); err != nil { log.Printf("⚠️ [ProductionNoCost] scan error: %v", err) continue } list = append(list, item) } if err := rows.Err(); err != nil { log.Printf("⚠️ [ProductionNoCost] rows error: %v", err) http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(list) } // GET /api/pricing/production-product-costing/has-cost-products func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } search := strings.TrimSpace(r.URL.Query().Get("search")) offset := parsePositiveIntOrDefault(r.URL.Query().Get("offset"), 0) limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 300) if limit > 1000 { limit = 1000 } rows, err := queries.GetProductionHasCostProducts(r.Context(), uretimDB, search, offset, limit) if err != nil { log.Printf("❌ [ProductionHasCost] query error: %v", err) http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) return } defer rows.Close() list := make([]models.ProductionHasCostProductRow, 0, 200) for rows.Next() { var item models.ProductionHasCostProductRow if err := rows.Scan( &item.UretimSekli, &item.NOnMLNo, &item.UrunKodu, &item.UrunAdi, &item.Tarihi, &item.DteKayitTarihi, &item.SKullaniciAdi, &item.LTutarTL, &item.LTutarUSD, &item.LTutarEURO, &item.DteGuncellemeTarihi, &item.SGuncellemeKullaniciAdi, &item.NUrtReceteID, &item.SAciklama, &item.SonSiparisTarihi, &item.MaliyetDurumu, ); err != nil { log.Printf("⚠️ [ProductionHasCost] scan error: %v", err) continue } list = append(list, item) } if err := rows.Err(); err != nil { log.Printf("⚠️ [ProductionHasCost] rows error: %v", err) http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(list) } // GET /api/pricing/production-product-costing/has-cost-history func GetProductionHasCostHistoryHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu")) if productCode == "" { http.Error(w, "urun_kodu zorunlu", http.StatusBadRequest) return } rows, err := queries.GetProductionHasCostHistoryByProductCode(r.Context(), uretimDB, productCode) if err != nil { log.Printf("❌ [ProductionHasCostHistory] query error: %v", err) http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) return } defer rows.Close() list := make([]models.ProductionHasCostHistoryRow, 0, 100) for rows.Next() { var item models.ProductionHasCostHistoryRow if err := rows.Scan( &item.NOnMLNo, &item.UrunKodu, &item.UrunAdi, &item.Tarihi, &item.SKullaniciAdi, &item.LTutarUSD, &item.LTutarTL, &item.LTutarEURO, &item.SDovizCinsi, &item.LTutarDoviz, &item.DteGuncellemeTarihi, &item.SGuncellemeKullaniciAdi, &item.NUrtReceteID, &item.SAciklama, ); err != nil { log.Printf("⚠️ [ProductionHasCostHistory] scan error: %v", err) continue } list = append(list, item) } if err := rows.Err(); err != nil { log.Printf("⚠️ [ProductionHasCostHistory] rows error: %v", err) http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(list) } // GET /api/pricing/production-product-costing/has-cost-detail-groups func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } detailSource := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("detail_source"))) recipeCode := strings.TrimSpace(r.URL.Query().Get("recete_kodu")) productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu")) traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.detail-groups", "detail_source", detailSource, "urun_kodu", productCode, "recete_kodu", recipeCode, ) if detailSource == "no-cost" || recipeCode != "" { if recipeCode == "" { logger.Warn("request invalid", "reason", "missing recete_kodu") http.Error(w, "recete_kodu zorunlu", http.StatusBadRequest) return } logger.Info("request start") rows, err := queries.GetProductionNoCostDetailRowsByRecipeCode(ctx, uretimDB, recipeCode, productCode) if err != nil { logger.Error("query error", "err", err) log.Printf("❌ [ProductionNoCostDetailGroups] query error: %v", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() groups := make([]models.ProductionHasCostDetailGroup, 0, 16) groupIndexByName := map[string]int{} scannedRows := 0 scanErrors := 0 for rows.Next() { var ( groupName string groupTotal float64 groupTotalUSD float64 fiyatGirilen sql.NullFloat64 fiyatDoviz sql.NullString maliyeteDahil sql.NullBool cmPriceTypeID sql.NullInt64 item models.ProductionHasCostDetailGroupItem ) if err := rows.Scan( &groupName, &groupTotal, &groupTotalUSD, &item.NOnMLNo, &item.NOnMLDetNo, &item.NHammaddeTuruNo, &item.SKodu, &item.SAciklama, &item.SRenk, &item.SBeden, &item.SAciklama2, &item.LMiktar, &item.LFiyat, &item.LTutar, &item.SFiyatTipi, &item.SDovizCinsi, &item.LDovizKuru, &item.LDovizFiyati, &fiyatGirilen, &fiyatDoviz, &maliyeteDahil, &cmPriceTypeID, &item.USDTutar, &item.EURTutar, &item.GBPTutar, &item.SBirim, &item.SHammaddeTuruAdi, &item.SParcaAdi, ); err != nil { scanErrors += 1 logger.Warn("scan error", "scan_index", scannedRows+scanErrors+1, "err", err) log.Printf("⚠️ [ProductionNoCostDetailGroups] scan error: %v", err) continue } scannedRows += 1 if fiyatGirilen.Valid { item.FiyatGirilen = new(float64) *item.FiyatGirilen = fiyatGirilen.Float64 } if fiyatDoviz.Valid { item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String) } item.MaliyeteDahil = !maliyeteDahil.Valid || maliyeteDahil.Bool if cmPriceTypeID.Valid { value := int(cmPriceTypeID.Int64) item.CMPriceTypeID = &value } idx, ok := groupIndexByName[groupName] if !ok { groups = append(groups, models.ProductionHasCostDetailGroup{ SAciklama3: groupName, TotalTutar: groupTotal, TotalUSDTutar: groupTotalUSD, Items: make([]models.ProductionHasCostDetailGroupItem, 0, 8), }) idx = len(groups) - 1 groupIndexByName[groupName] = idx } groups[idx].Items = append(groups[idx].Items, item) } if err := rows.Err(); err != nil { logger.Error("rows error", "err", err) log.Printf("⚠️ [ProductionNoCostDetailGroups] rows error: %v", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "group_count", len(groups), "row_count", scannedRows, "scan_errors", scanErrors) log.Printf("[ProductionNoCostDetailGroups] done recete_kodu=%s groups=%d", recipeCode, len(groups)) _ = json.NewEncoder(w).Encode(groups) return } rawOnMLNo := strings.TrimSpace(r.URL.Query().Get("n_onml_no")) nOnMLNo, err := strconv.Atoi(rawOnMLNo) if err != nil || nOnMLNo <= 0 { logger.Warn("request invalid", "reason", "invalid n_onml_no", "raw_n_onml_no", rawOnMLNo) http.Error(w, "n_onml_no zorunlu ve pozitif sayi olmali", http.StatusBadRequest) return } logger = logger.With("n_onml_no", nOnMLNo) logger.Info("request start") log.Printf("[ProductionHasCostDetailGroups] start n_onml_no=%d", nOnMLNo) rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo) if err != nil { logger.Error("query error", "err", err) log.Printf("❌ [ProductionHasCostDetailGroups] query error: %v", err) http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) return } defer rows.Close() groups := make([]models.ProductionHasCostDetailGroup, 0, 16) groupIndexByName := map[string]int{} scannedRows := 0 scanErrors := 0 for rows.Next() { var ( groupName string groupTotal float64 groupTotalUSD float64 fiyatGirilen sql.NullFloat64 fiyatDoviz sql.NullString maliyeteDahil sql.NullBool cmPriceTypeID sql.NullInt64 item models.ProductionHasCostDetailGroupItem ) if err := rows.Scan( &groupName, &groupTotal, &groupTotalUSD, &item.NOnMLNo, &item.NOnMLDetNo, &item.NHammaddeTuruNo, &item.SKodu, &item.SAciklama, &item.SRenk, &item.SBeden, &item.SAciklama2, &item.LMiktar, &item.LFiyat, &item.LTutar, &item.SFiyatTipi, &item.SDovizCinsi, &item.LDovizKuru, &item.LDovizFiyati, &fiyatGirilen, &fiyatDoviz, &maliyeteDahil, &cmPriceTypeID, &item.USDTutar, &item.EURTutar, &item.GBPTutar, &item.SBirim, &item.SHammaddeTuruAdi, &item.SParcaAdi, ); err != nil { log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err) continue } scannedRows += 1 if fiyatGirilen.Valid { item.FiyatGirilen = new(float64) *item.FiyatGirilen = fiyatGirilen.Float64 } if fiyatDoviz.Valid { item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String) } item.MaliyeteDahil = maliyeteDahil.Valid && maliyeteDahil.Bool if cmPriceTypeID.Valid { value := int(cmPriceTypeID.Int64) item.CMPriceTypeID = &value } idx, ok := groupIndexByName[groupName] if !ok { groups = append(groups, models.ProductionHasCostDetailGroup{ SAciklama3: groupName, TotalTutar: groupTotal, TotalUSDTutar: groupTotalUSD, Items: make([]models.ProductionHasCostDetailGroupItem, 0, 24), }) idx = len(groups) - 1 groupIndexByName[groupName] = idx } groups[idx].Items = append(groups[idx].Items, item) } if err := rows.Err(); err != nil { log.Printf("⚠️ [ProductionHasCostDetailGroups] rows error: %v", err) http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError) return } logger.Info("request done", "group_count", len(groups), "row_count", scannedRows, "scan_errors", scanErrors) log.Printf("[ProductionHasCostDetailGroups] done n_onml_no=%d groups=%d rows=%d scan_errors=%d", nOnMLNo, len(groups), scannedRows, scanErrors) _ = json.NewEncoder(w).Encode(groups) } // GET /api/pricing/production-product-costing/has-cost-detail-header func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } mssqlDB := db.GetDB() detailSource := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("detail_source"))) recipeCode := strings.TrimSpace(r.URL.Query().Get("recete_kodu")) productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu")) traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.detail-header", "detail_source", detailSource, "urun_kodu", productCode, "recete_kodu", recipeCode, ) if detailSource == "no-cost" || recipeCode != "" { if recipeCode == "" { logger.Warn("request invalid", "reason", "missing recete_kodu") http.Error(w, "recete_kodu zorunlu", http.StatusBadRequest) return } logger.Info("request start") row, err := queries.GetProductionNoCostDetailHeaderByRecipeCode(ctx, uretimDB, recipeCode, productCode) if err != nil { logger.Error("query prepare error", "err", err) log.Printf("❌ [ProductionNoCostDetailHeader] query prepare error: %v", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } var item models.ProductionHasCostDetailHeader if err := row.Scan( &item.UretimiYapanFirma, &item.SonIsEmriVeren, &item.NOnMLNo, &item.UrunKodu, &item.UrunAdi, &item.UretimSekliID, &item.UretimSekli, &item.DteKayitTarihi, &item.SKullaniciAdi, &item.LTutarTL, &item.LTutarUSD, &item.LTutarEURO, &item.LTutarGBP, &item.SDovizCinsi, &item.LTutarDoviz, &item.DteGuncellemeTarihi, &item.SGuncellemeKullaniciAdi, &item.NUrtReceteID, ); err != nil { logger.Warn("scan or not found", "err", err) if err == sql.ErrNoRows { logger.Warn("row not found") http.Error(w, "Kayit bulunamadi", http.StatusNotFound) return } log.Printf("❌ [ProductionNoCostDetailHeader] scan error: %v", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "n_urt_recete_id", item.NUrtReceteID, "urun_kodu", item.UrunKodu) if mssqlDB != nil { ana, alt, err := queries.GetProductAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu) if err != nil { logger.Warn("product group query error", "err", err) } else { item.UrunAnaGrubu = ana item.UrunAltGrubu = alt } } _ = json.NewEncoder(w).Encode(item) return } rawOnMLNo := strings.TrimSpace(r.URL.Query().Get("n_onml_no")) nOnMLNo, err := strconv.Atoi(rawOnMLNo) if err != nil || nOnMLNo <= 0 { logger.Warn("request invalid", "reason", "invalid n_onml_no", "raw_n_onml_no", rawOnMLNo) http.Error(w, "n_onml_no zorunlu ve pozitif sayi olmali", http.StatusBadRequest) return } logger = logger.With("n_onml_no", nOnMLNo) logger.Info("request start") row, err := queries.GetProductionHasCostDetailHeaderByOnMLNo(ctx, uretimDB, nOnMLNo) if err != nil { logger.Error("query prepare error", "err", err) log.Printf("❌ [ProductionHasCostDetailHeader] query prepare error: %v", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } var item models.ProductionHasCostDetailHeader if err := row.Scan( &item.UretimiYapanFirma, &item.SonIsEmriVeren, &item.NOnMLNo, &item.UrunKodu, &item.UrunAdi, &item.UretimSekliID, &item.UretimSekli, &item.DteKayitTarihi, &item.SKullaniciAdi, &item.LTutarTL, &item.LTutarUSD, &item.LTutarEURO, &item.LTutarGBP, &item.SDovizCinsi, &item.LTutarDoviz, &item.DteGuncellemeTarihi, &item.SGuncellemeKullaniciAdi, &item.NUrtReceteID, ); err != nil { logger.Warn("scan or not found", "err", err) if err == sql.ErrNoRows { http.Error(w, "Kayit bulunamadi", http.StatusNotFound) return } log.Printf("❌ [ProductionHasCostDetailHeader] scan error: %v", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "n_onml_no", item.NOnMLNo, "urun_kodu", item.UrunKodu, "n_urt_recete_id", item.NUrtReceteID) if mssqlDB != nil { ana, alt, err := queries.GetProductAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu) if err != nil { logger.Warn("product group query error", "err", err) } else { item.UrunAnaGrubu = ana item.UrunAltGrubu = alt } } _ = json.NewEncoder(w).Encode(item) } // GET /api/pricing/production-product-costing/production-types func GetProductionTypesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.production-types") logger.Info("request start") rows, err := queries.GetProductionTypes(ctx, uretimDB) if err != nil { logger.Error("query error", "err", err) log.Printf("❌ [ProductionTypes] query error: %v", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() var types []models.ProductionType for rows.Next() { var t models.ProductionType if err := rows.Scan(&t.ID, &t.Aciklama); err != nil { logger.Warn("scan error", "err", err) log.Printf("⚠️ [ProductionTypes] scan error: %v", err) continue } types = append(types, t) } if err := rows.Err(); err != nil { logger.Error("rows error", "err", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "row_count", len(types)) _ = json.NewEncoder(w).Encode(types) } // GET /api/pricing/production-product-costing/detail-editor-options func GetProductionHasCostDetailEditorOptionsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) kind := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("kind"))) search := strings.TrimSpace(r.URL.Query().Get("search")) limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 100) if limit > 200 { limit = 200 } logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.detail-editor-options", "kind", kind, "search", search, "limit", limit, ) logger.Info("request start") switch kind { case "hammadde": uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } rows, err := queries.GetProductionHasCostDetailHammaddeTypeOptions(ctx, uretimDB, search, limit) if err != nil { logger.Error("hammadde query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde query error: %v", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() if columns, columnErr := rows.Columns(); columnErr != nil { log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item columns read error search=%q limit=%d err=%v", search, limit, columnErr) } else { log.Printf("[ProductionHasCostDetailEditorOptions] item columns=%v", columns) } list := make([]models.ProductionHasCostDetailEditorOption, 0, limit) for rows.Next() { var item models.ProductionHasCostDetailEditorOption if err := rows.Scan(&item.NHammaddeTuruNo, &item.SHammaddeTuruAdi, &item.SAciklama3); err != nil { logger.Warn("hammadde scan error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] hammadde scan error: %v", err) continue } item.Kind = "hammadde" item.Value = item.NHammaddeTuruNo item.Label = strings.TrimSpace(item.NHammaddeTuruNo + " - " + item.SHammaddeTuruAdi) item.SParcaAdi = item.SAciklama3 list = append(list, item) } if err := rows.Err(); err != nil { logger.Error("hammadde rows error", "err", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "kind", "hammadde", "row_count", len(list)) _ = json.NewEncoder(w).Encode(list) return case "item": uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } log.Printf("[ProductionHasCostDetailEditorOptions] item start search=%q limit=%d", search, limit) rows, err := queries.GetProductionHasCostDetailItemOptions(ctx, uretimDB, search, limit) if err != nil { logger.Error("item query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item query error search=%q limit=%d err=%v", search, limit, err) logProductionHasCostDetailEditorOptionItemDiagnostics(ctx, uretimDB, search, limit) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() if columns, columnErr := rows.Columns(); columnErr != nil { log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item columns read error search=%q limit=%d err=%v", search, limit, columnErr) } else { log.Printf("[ProductionHasCostDetailEditorOptions] item columns=%v", columns) } list := make([]models.ProductionHasCostDetailEditorOption, 0, limit) scanErrors := 0 for rows.Next() { var item models.ProductionHasCostDetailEditorOption if err := rows.Scan(&item.NStokID, &item.SKodu, &item.SAciklama, &item.SModel, &item.SBirim); err != nil { scanErrors += 1 logger.Warn("item scan error", "scan_index", len(list)+scanErrors, "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item scan error scan_index=%d err=%v", len(list)+scanErrors, err) continue } item.Kind = "item" item.Value = item.SKodu item.Label = strings.TrimSpace(item.SKodu + " - " + item.SAciklama) list = append(list, item) } if err := rows.Err(); err != nil { logger.Error("item rows error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item rows error search=%q limit=%d err=%v", search, limit, err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "kind", "item", "row_count", len(list), "scan_errors", scanErrors) log.Printf("[ProductionHasCostDetailEditorOptions] item done search=%q limit=%d row_count=%d scan_errors=%d", search, limit, len(list), scanErrors) _ = json.NewEncoder(w).Encode(list) return case "color": uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } modelCode := strings.TrimSpace(r.URL.Query().Get("model_code")) if modelCode == "" { _ = json.NewEncoder(w).Encode([]models.ProductionHasCostDetailEditorOption{}) return } rows, err := queries.GetProductionHasCostDetailColorOptions(ctx, uretimDB, modelCode, search, limit) if err != nil { logger.Error("color query error", "model_code", modelCode, "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] color query error: %v", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() list := make([]models.ProductionHasCostDetailEditorOption, 0, limit) for rows.Next() { var item models.ProductionHasCostDetailEditorOption if err := rows.Scan(&item.ColorCode, &item.ColorDescription); err != nil { logger.Warn("color scan error", "model_code", modelCode, "err", err) log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] color scan error: %v", err) continue } item.Kind = "color" item.Value = item.ColorCode item.Label = strings.TrimSpace(item.ColorCode + " - " + item.ColorDescription) list = append(list, item) } if err := rows.Err(); err != nil { logger.Error("color rows error", "model_code", modelCode, "err", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "kind", "color", "model_code", modelCode, "row_count", len(list)) _ = json.NewEncoder(w).Encode(list) return } logger.Warn("request invalid", "reason", "invalid kind") http.Error(w, "kind hammadde, item veya color olmali", http.StatusBadRequest) } // GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates func GetProductionHasCostDetailExchangeRatesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") mssqlDB := db.GetDB() if mssqlDB == nil { http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.exchange-rates") rawCostDate := strings.TrimSpace(r.URL.Query().Get("maliyet_tarihi")) costDate := "" if rawCostDate != "" { parsedDate, err := time.Parse("2006-01-02", rawCostDate) if err != nil { logger.Warn("request invalid", "reason", "invalid maliyet_tarihi", "maliyet_tarihi", rawCostDate) http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest) return } costDate = parsedDate.Format("2006-01-02") } logger.Info("request start", "maliyet_tarihi", costDate) log.Printf("[ProductionHasCostDetailExchangeRates] start maliyet_tarihi=%s", costDate) row, err := queries.GetProductionHasCostDetailExchangeRatesByDate(ctx, mssqlDB, costDate) if err != nil { logger.Error("query prepare error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailExchangeRates] query prepare error: %v", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } item := models.ProductionHasCostDetailExchangeRates{ TRYRate: 1, } if err := row.Scan( &item.RateDate, &item.USDRate, &item.EURRate, &item.GBPRate, ); err != nil { if err == sql.ErrNoRows { item.RateDate = costDate logger.Info("request done", "rate_date", item.RateDate, "fallback", true) _ = json.NewEncoder(w).Encode(item) return } log.Printf("⚠️ [ProductionHasCostDetailExchangeRates] scan error: %v", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "rate_date", item.RateDate, "usd_rate", item.USDRate, "eur_rate", item.EURRate, "gbp_rate", item.GBPRate) log.Printf("[ProductionHasCostDetailExchangeRates] done maliyet_tarihi=%s rate_date=%s usd=%.4f eur=%.4f", costDate, item.RateDate, item.USDRate, item.EURRate) _ = json.NewEncoder(w).Encode(item) } // POST /api/pricing/production-product-costing/has-cost-detail-bulk-prices func PostProductionHasCostDetailBulkPricesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") mssqlDB := db.GetDB() if mssqlDB == nil { http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.bulk-prices") var req models.ProductionHasCostDetailBulkPriceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { logger.Warn("request invalid", "reason", "invalid request body", "err", err) http.Error(w, "Gecersiz istek govdesi", http.StatusBadRequest) return } costDate := strings.TrimSpace(req.MaliyetTarihi) itemsCount := len(req.Items) responseChan := make(chan *models.ProductionHasCostDetailBulkPriceRow, itemsCount) logger.Info("request start", "n_onml_no", strings.TrimSpace(req.NOnMLNo), "urun_kodu", strings.TrimSpace(req.UrunKodu), "maliyet_tarihi", costDate, "item_count", itemsCount, ) log.Printf("[ProductionHasCostDetailBulkPrices] start n_onml_no=%s urun_kodu=%s maliyet_tarihi=%s item_count=%d", strings.TrimSpace(req.NOnMLNo), strings.TrimSpace(req.UrunKodu), costDate, itemsCount) for _, item := range req.Items { go func(item models.ProductionHasCostDetailPriceLookupItem) { sKodu := normalizeLookupValue(item.SKodu) if sKodu == "" { responseChan <- nil return } colorCode := firstNonEmptyString( normalizeLookupValue(item.ColorCode), normalizeLookupValue(item.SRenk), ) itemDim1Code := firstNonEmptyString( normalizeLookupValue(item.ItemDim1Code), ) row, err := queries.GetProductionHasCostLatestPurchasePriceForItem( ctx, mssqlDB, sKodu, colorCode, itemDim1Code, costDate, ) if err != nil { logger.Warn("item lookup error", "s_kodu", sKodu, "color_code", colorCode, "item_dim1_code", itemDim1Code, "err", err) responseChan <- nil return } var result models.ProductionHasCostDetailBulkPriceRow if err := row.Scan( &result.PriceType, &result.Tarih, &result.FaturaKodu, &result.MasrafKodu, &result.MasrafDetay, &result.ColorCode, &result.ColorDescription, &result.ItemDim1Code, &result.ItemDim1Description, &result.FiyatGirilen, &result.FiyatDoviz, ); err != nil { logger.Warn("item scan error", "s_kodu", sKodu, "color_code", colorCode, "item_dim1_code", itemDim1Code, "err", err) responseChan <- nil return } result.RowKey = strings.TrimSpace(item.RowKey) result.NOnMLDetNo = strings.TrimSpace(item.NOnMLDetNo) result.NHammaddeTuruNo = strings.TrimSpace(item.NHammaddeTuruNo) result.SKodu = sKodu if strings.TrimSpace(result.ColorCode) == "" { result.ColorCode = colorCode } if strings.TrimSpace(result.ItemDim1Code) == "" { result.ItemDim1Code = itemDim1Code } responseChan <- &result }(item) } response := make([]models.ProductionHasCostDetailBulkPriceRow, 0, itemsCount) for i := 0; i < itemsCount; i++ { res := <-responseChan if res != nil { response = append(response, *res) } } logger.Info("request done", "item_count", itemsCount, "matched_count", len(response)) log.Printf("[ProductionHasCostDetailBulkPrices] done item_count=%d matched=%d", itemsCount, len(response)) _ = json.NewEncoder(w).Encode(map[string]any{ "items": response, }) } // GET /api/pricing/production-product-costing/has-cost-detail-line-history func GetProductionHasCostDetailLineHistoryHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() mssqlDB := db.GetDB() if uretimDB == nil && mssqlDB == nil { http.Error(w, "Veritabani baglantilari aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.line-history") rawOnMLNo := strings.TrimSpace(r.URL.Query().Get("n_onml_no")) currentOnMLNo, _ := strconv.Atoi(rawOnMLNo) nHammaddeTuruNo := strings.TrimSpace(r.URL.Query().Get("n_hammadde_turu_no")) sKodu := normalizeLookupValue(r.URL.Query().Get("s_kodu")) colorCode := firstNonEmptyString( normalizeLookupValue(r.URL.Query().Get("color_code")), normalizeLookupValue(r.URL.Query().Get("s_renk")), ) costDate := strings.TrimSpace(r.URL.Query().Get("maliyet_tarihi")) if sKodu == "" { logger.Warn("request invalid", "reason", "missing s_kodu") http.Error(w, "s_kodu zorunlu", http.StatusBadRequest) return } logger.Info("request start", "n_onml_no", currentOnMLNo, "n_hammadde_turu_no", nHammaddeTuruNo, "s_kodu", sKodu, "color_code", colorCode, "maliyet_tarihi", costDate) log.Printf("[ProductionHasCostDetailLineHistory] start n_onml_no=%d n_hammadde_turu_no=%s s_kodu=%s color=%s maliyet_tarihi=%s", currentOnMLNo, nHammaddeTuruNo, sKodu, colorCode, costDate) const LINE_HISTORY_ROW_LIMIT = 500 response := models.ProductionHasCostDetailLineHistoryResponse{ PurchaseRows: make([]models.ProductionHasCostDetailPurchaseHistoryRow, 0, LINE_HISTORY_ROW_LIMIT), RecipeRows: make([]models.ProductionHasCostDetailRecipeHistoryRow, 0, LINE_HISTORY_ROW_LIMIT), } allowLegacyAutoFallback := true if mssqlDB != nil { rows, err := queries.GetProductionHasCostPurchaseHistoryByExpenseCode( ctx, mssqlDB, sKodu, costDate, LINE_HISTORY_ROW_LIMIT, ) if err != nil { logger.Warn("purchase query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase query error: %v", err) } else { defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailPurchaseHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.Tarih, &item.FaturaKodu, &item.FirmaKodu, &item.FirmaAciklama, &item.MasrafKodu, &item.MasrafDetay, &item.ColorCode, &item.ColorDescription, &item.ItemDim1Code, &item.ItemDim1Description, &item.Miktar, &item.BIRIM, &item.EvrakFiyat, &item.EvrakTutar, &item.EvrakDoviz, ); err != nil { logger.Warn("purchase scan error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase scan error: %v", err) continue } response.PurchaseRows = append(response.PurchaseRows, item) } if err := rows.Err(); err != nil { logger.Warn("purchase rows error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase rows error: %v", err) } } } if allowLegacyAutoFallback && mssqlDB != nil && len(response.PurchaseRows) == 0 { similarPrefix := queries.BuildProductionHasCostSimilarCodePrefix(sKodu) if similarPrefix != "" { logger.Info("purchase fallback start", "s_kodu", sKodu, "similar_prefix", similarPrefix, "maliyet_tarihi", costDate) rows, err := queries.GetProductionHasCostPurchaseHistoryByCodePrefix( ctx, mssqlDB, similarPrefix, costDate, LINE_HISTORY_ROW_LIMIT, ) if err != nil { logger.Warn("purchase fallback query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase fallback query error: %v", err) } else { defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailPurchaseHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.Tarih, &item.FaturaKodu, &item.FirmaKodu, &item.FirmaAciklama, &item.MasrafKodu, &item.MasrafDetay, &item.ColorCode, &item.ColorDescription, &item.ItemDim1Code, &item.ItemDim1Description, &item.Miktar, &item.BIRIM, &item.EvrakFiyat, &item.EvrakTutar, &item.EvrakDoviz, ); err != nil { logger.Warn("purchase fallback scan error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase fallback scan error: %v", err) continue } response.PurchaseRows = append(response.PurchaseRows, item) } if err := rows.Err(); err != nil { logger.Warn("purchase fallback rows error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] purchase fallback rows error: %v", err) } } } } if uretimDB != nil { rows, err := queries.GetProductionHasCostRecipeHistoryByExpenseCode( ctx, uretimDB, currentOnMLNo, sKodu, colorCode, costDate, LINE_HISTORY_ROW_LIMIT, ) if err != nil { logger.Warn("recipe query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe query error: %v", err) } else { defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailRecipeHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.DteIslemTarihi, &item.NOnMLNo, &item.FirmaKodu, &item.FirmaAciklama, &item.SKodu, &item.SAciklama, &item.SRenk, &item.LMiktar, &item.SBirim, &item.LDovizFiyati, &item.LDovizTutari, &item.USD, &item.DUMMY, ); err != nil { logger.Warn("recipe scan error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe scan error: %v", err) continue } response.RecipeRows = append(response.RecipeRows, item) } if err := rows.Err(); err != nil { logger.Warn("recipe rows error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe rows error: %v", err) } } } if allowLegacyAutoFallback && uretimDB != nil && len(response.RecipeRows) == 0 { similarPrefix := queries.BuildProductionHasCostSimilarCodePrefix(sKodu) if similarPrefix != "" { logger.Info("recipe fallback prefix start", "s_kodu", sKodu, "similar_prefix", similarPrefix, "maliyet_tarihi", costDate) rows, err := queries.GetProductionHasCostOnMLHistoryByCodePrefix( ctx, uretimDB, similarPrefix, costDate, LINE_HISTORY_ROW_LIMIT, ) if err != nil { logger.Warn("recipe fallback prefix query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback prefix query error: %v", err) } else { defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailRecipeHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.DteIslemTarihi, &item.NOnMLNo, &item.FirmaKodu, &item.FirmaAciklama, &item.SKodu, &item.SAciklama, &item.SRenk, &item.LMiktar, &item.SBirim, &item.LDovizFiyati, &item.LDovizTutari, &item.USD, &item.DUMMY, ); err != nil { logger.Warn("recipe fallback prefix scan error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback prefix scan error: %v", err) continue } response.RecipeRows = append(response.RecipeRows, item) } if err := rows.Err(); err != nil { logger.Warn("recipe fallback prefix rows error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback prefix rows error: %v", err) } } } } if allowLegacyAutoFallback && uretimDB != nil && len(response.RecipeRows) == 0 && nHammaddeTuruNo != "" { logger.Info("recipe fallback hammadde-turu start", "n_hammadde_turu_no", nHammaddeTuruNo, "maliyet_tarihi", costDate) rows, err := queries.GetProductionHasCostOnMLHistoryByHammaddeTuruNo( ctx, uretimDB, nHammaddeTuruNo, costDate, LINE_HISTORY_ROW_LIMIT, ) if err != nil { logger.Warn("recipe fallback hammadde-turu query error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback hammadde-turu query error: %v", err) } else { defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailRecipeHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.DteIslemTarihi, &item.NOnMLNo, &item.FirmaKodu, &item.FirmaAciklama, &item.SKodu, &item.SAciklama, &item.SRenk, &item.LMiktar, &item.SBirim, &item.LDovizFiyati, &item.LDovizTutari, &item.USD, &item.DUMMY, ); err != nil { logger.Warn("recipe fallback hammadde-turu scan error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback hammadde-turu scan error: %v", err) continue } response.RecipeRows = append(response.RecipeRows, item) } if err := rows.Err(); err != nil { logger.Warn("recipe fallback hammadde-turu rows error", "err", err) log.Printf("⚠️ [ProductionHasCostDetailLineHistory] recipe fallback hammadde-turu rows error: %v", err) } } } logger.Info("request done", "s_kodu", sKodu, "purchase_count", len(response.PurchaseRows), "recipe_count", len(response.RecipeRows)) log.Printf("[ProductionHasCostDetailLineHistory] done s_kodu=%s purchase_rows=%d recipe_rows=%d", sKodu, len(response.PurchaseRows), len(response.RecipeRows)) _ = json.NewEncoder(w).Encode(response) } // GET /api/pricing/production-product-costing/has-cost-detail-similar-history func GetProductionHasCostDetailSimilarHistoryHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") mssqlDB := db.GetDB() uretimDB := db.GetUretimDB() if mssqlDB == nil || uretimDB == nil { http.Error(w, "Veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.similar-history") q := r.URL.Query() nHammaddeTuruNo := strings.TrimSpace(q.Get("n_hammadde_turu_no")) sKodu := normalizeLookupValue(q.Get("s_kodu")) costDate := strings.TrimSpace(q.Get("maliyet_tarihi")) limit := parsePositiveIntOrDefault(q.Get("limit"), 500) searchMode := strings.ToLower(strings.TrimSpace(q.Get("search_mode"))) if searchMode == "" || searchMode == "exact" { searchMode = "prefix" } similarPrefix := queries.BuildProductionHasCostSimilarCodePrefix(sKodu) if nHammaddeTuruNo == "" && sKodu == "" { http.Error(w, "n_hammadde_turu_no veya s_kodu parametresi zorunludur", http.StatusBadRequest) return } logger.Info("request start", "search_mode", searchMode, "n_hammadde_turu_no", nHammaddeTuruNo, "s_kodu", sKodu, "similar_prefix", similarPrefix, "maliyet_tarihi", costDate, "limit", limit) response := models.ProductionHasCostDetailLineHistoryResponse{ PurchaseRows: make([]models.ProductionHasCostDetailPurchaseHistoryRow, 0, limit), RecipeRows: make([]models.ProductionHasCostDetailRecipeHistoryRow, 0, limit), } purchaseMatchStage := searchMode recipeMatchStage := searchMode allowRecipeAutoFallback := false if searchMode == "alternative" { purchaseMatchStage = "skipped" if nHammaddeTuruNo != "" && uretimDB != nil { rows, err := queries.GetProductionHasCostOnMLHistoryByHammaddeTuruNo(ctx, uretimDB, nHammaddeTuruNo, costDate, limit) if err != nil { logger.Warn("alternative onml query error", "err", err) http.Error(w, "Sorgu calistirilirken hata olustu", http.StatusInternalServerError) return } defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailRecipeHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.DteIslemTarihi, &item.NOnMLNo, &item.FirmaKodu, &item.FirmaAciklama, &item.SKodu, &item.SAciklama, &item.SRenk, &item.LMiktar, &item.SBirim, &item.LDovizFiyati, &item.LDovizTutari, &item.USD, &item.DUMMY, ); err != nil { logger.Warn("alternative onml scan error", "err", err) continue } response.RecipeRows = append(response.RecipeRows, item) } if err := rows.Err(); err != nil { logger.Warn("alternative onml rows error", "err", err) } } if len(response.PurchaseRows) == 0 { purchaseMatchStage = "empty" } if len(response.RecipeRows) == 0 { recipeMatchStage = "empty" } logger.Info("request done", "search_mode", searchMode, "purchase_match_stage", purchaseMatchStage, "recipe_match_stage", recipeMatchStage, "similar_prefix", similarPrefix, "purchase_count", len(response.PurchaseRows), "recipe_count", len(response.RecipeRows), ) _ = json.NewEncoder(w).Encode(map[string]any{ "purchaseRows": response.PurchaseRows, "recipeRows": response.RecipeRows, "purchase_match_stage": purchaseMatchStage, "recipe_match_stage": recipeMatchStage, "similar_code_prefix": similarPrefix, "search_mode": searchMode, }) return } if similarPrefix != "" { if mssqlDB != nil { rows, err := queries.GetProductionHasCostPurchaseHistoryByCodePrefix(ctx, mssqlDB, similarPrefix, costDate, limit) if err != nil { logger.Warn("prefix purchase query error", "err", err) } else { defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailPurchaseHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.Tarih, &item.FaturaKodu, &item.FirmaKodu, &item.FirmaAciklama, &item.MasrafKodu, &item.MasrafDetay, &item.ColorCode, &item.ColorDescription, &item.ItemDim1Code, &item.ItemDim1Description, &item.Miktar, &item.BIRIM, &item.EvrakFiyat, &item.EvrakTutar, &item.EvrakDoviz, ); err != nil { logger.Warn("prefix purchase scan error", "err", err) continue } response.PurchaseRows = append(response.PurchaseRows, item) } if err := rows.Err(); err != nil { logger.Warn("prefix purchase rows error", "err", err) } } } if uretimDB != nil { rows, err := queries.GetProductionHasCostOnMLHistoryByCodePrefix(ctx, uretimDB, similarPrefix, costDate, limit) if err != nil { logger.Warn("prefix onml query error", "err", err) } else { defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailRecipeHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.DteIslemTarihi, &item.NOnMLNo, &item.FirmaKodu, &item.FirmaAciklama, &item.SKodu, &item.SAciklama, &item.SRenk, &item.LMiktar, &item.SBirim, &item.LDovizFiyati, &item.LDovizTutari, &item.USD, &item.DUMMY, ); err != nil { logger.Warn("prefix onml scan error", "err", err) continue } response.RecipeRows = append(response.RecipeRows, item) } if err := rows.Err(); err != nil { logger.Warn("prefix onml rows error", "err", err) } } } } if allowRecipeAutoFallback && len(response.RecipeRows) == 0 && nHammaddeTuruNo != "" && uretimDB != nil { recipeMatchStage = "hammadde-turu-fallback" rows, err := queries.GetProductionHasCostOnMLHistoryByHammaddeTuruNo(ctx, uretimDB, nHammaddeTuruNo, costDate, limit) if err != nil { logger.Warn("fallback onml query error", "err", err) http.Error(w, "Sorgu calistirilirken hata olustu", http.StatusInternalServerError) return } defer rows.Close() for rows.Next() { var item models.ProductionHasCostDetailRecipeHistoryRow if err := rows.Scan( &item.SourceType, &item.PriceType, &item.DteIslemTarihi, &item.NOnMLNo, &item.FirmaKodu, &item.FirmaAciklama, &item.SKodu, &item.SAciklama, &item.SRenk, &item.LMiktar, &item.SBirim, &item.LDovizFiyati, &item.LDovizTutari, &item.USD, &item.DUMMY, ); err != nil { logger.Warn("fallback onml scan error", "err", err) continue } response.RecipeRows = append(response.RecipeRows, item) } if err := rows.Err(); err != nil { logger.Warn("fallback onml rows error", "err", err) } } if len(response.PurchaseRows) == 0 { purchaseMatchStage = "empty" } if len(response.RecipeRows) == 0 { recipeMatchStage = "empty" } logger.Info("request done", "search_mode", searchMode, "purchase_match_stage", purchaseMatchStage, "recipe_match_stage", recipeMatchStage, "similar_prefix", similarPrefix, "purchase_count", len(response.PurchaseRows), "recipe_count", len(response.RecipeRows), ) _ = json.NewEncoder(w).Encode(map[string]any{ "purchaseRows": response.PurchaseRows, "recipeRows": response.RecipeRows, "purchase_match_stage": purchaseMatchStage, "recipe_match_stage": recipeMatchStage, "similar_code_prefix": similarPrefix, "search_mode": searchMode, }) } func parsePositiveIntOrDefault(raw string, fallback int) int { v, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil || v < 0 { return fallback } return v } func normalizeLookupValue(raw string) string { return strings.ToUpper(strings.TrimSpace(raw)) } func firstNonEmptyString(values ...string) string { for _, value := range values { value = strings.TrimSpace(value) if value != "" { return value } } return "" } // ============================================================ // MT BOLUM MAPPING (URETIM DB) // ============================================================ // GET /api/pricing/production-product-costing/options/urun-ana-grup func GetProductionProductCostingUrunAnaGrupOptionsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") mssqlDB := db.GetDB() if mssqlDB == nil { http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) search := strings.TrimSpace(r.URL.Query().Get("search")) limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 50) if limit > 500 { limit = 500 } logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.options.urun-ana-grup", "search", search, "limit", limit, ) logger.Info("request start") rows, err := queries.GetProductionProductCostingAnaGrupOptions(ctx, mssqlDB, search, limit) if err != nil { logger.Error("query error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() out := make([]models.ProductionProductCostingLookupOption, 0, limit) for rows.Next() { var v string if err := rows.Scan(&v); err != nil { logger.Warn("scan error", "err", err) continue } v = strings.TrimSpace(v) if v == "" { continue } out = append(out, models.ProductionProductCostingLookupOption{Value: v, Label: v}) } if err := rows.Err(); err != nil { logger.Error("rows error", "err", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "row_count", len(out)) _ = json.NewEncoder(w).Encode(out) } // GET /api/pricing/production-product-costing/options/urun-alt-grup func GetProductionProductCostingUrunAltGrupOptionsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") mssqlDB := db.GetDB() if mssqlDB == nil { http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) urunAnaGrubu := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu")) search := strings.TrimSpace(r.URL.Query().Get("search")) limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 50) if limit > 500 { limit = 500 } logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.options.urun-alt-grup", "urun_ana_grubu", urunAnaGrubu, "search", search, "limit", limit, ) logger.Info("request start") rows, err := queries.GetProductionProductCostingAltGrupOptions(ctx, mssqlDB, urunAnaGrubu, search, limit) if err != nil { logger.Error("query error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() out := make([]models.ProductionProductCostingLookupOption, 0, limit) for rows.Next() { var v string if err := rows.Scan(&v); err != nil { logger.Warn("scan error", "err", err) continue } v = strings.TrimSpace(v) if v == "" { continue } out = append(out, models.ProductionProductCostingLookupOption{Value: v, Label: v}) } if err := rows.Err(); err != nil { logger.Error("rows error", "err", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "row_count", len(out)) _ = json.NewEncoder(w).Encode(out) } // GET /api/pricing/production-product-costing/options/urun-ana-alt-combos func GetProductionProductCostingUrunAnaAltCombosHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") mssqlDB := db.GetDB() if mssqlDB == nil { http.Error(w, "MSSQL veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) search := strings.TrimSpace(r.URL.Query().Get("search")) limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 2000) if limit > 5000 { limit = 5000 } logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.options.urun-ana-alt-combos", "search", search, "limit", limit, ) logger.Info("request start") rows, err := queries.GetProductionProductCostingAnaAltComboRows(ctx, mssqlDB, search, limit) if err != nil { // Keep the response generic, but log the underlying SQL driver error for diagnostics. logger.Error("query error", "err", err, "search", search, "limit", limit, "trace_id", traceID) log.Printf("❌ [ProductionProductCostingAnaAltCombos] query error trace_id=%s search=%q limit=%d err=%v", traceID, search, limit, err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() out := make([]models.ProductionProductCostingAnaAltComboRow, 0, 1024) for rows.Next() { var item models.ProductionProductCostingAnaAltComboRow if err := rows.Scan(&item.UrunIlkGrubu, &item.UrunAnaGrubu, &item.UrunAltGrubu); err != nil { logger.Warn("scan error", "err", err) continue } item.UrunIlkGrubu = strings.TrimSpace(item.UrunIlkGrubu) item.UrunAnaGrubu = strings.TrimSpace(item.UrunAnaGrubu) item.UrunAltGrubu = strings.TrimSpace(item.UrunAltGrubu) if item.UrunIlkGrubu == "" || item.UrunAnaGrubu == "" || item.UrunAltGrubu == "" { continue } out = append(out, item) } if err := rows.Err(); err != nil { logger.Error("rows error", "err", err, "trace_id", traceID) log.Printf("⚠️ [ProductionProductCostingAnaAltCombos] rows error trace_id=%s err=%v", traceID, err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "row_count", len(out)) _ = json.NewEncoder(w).Encode(out) } // GET /api/pricing/production-product-costing/options/mtbolum func GetProductionProductCostingMTBolumOptionsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) search := strings.TrimSpace(r.URL.Query().Get("search")) limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 50) if limit > 500 { limit = 500 } logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.options.mtbolum", "search", search, "limit", limit, ) logger.Info("request start") rows, err := queries.GetProductionProductCostingMTBolumOptions(ctx, uretimDB, search, limit) if err != nil { logger.Error("query error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() out := make([]models.ProductionProductCostingLookupOption, 0, limit) for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { logger.Warn("scan error", "err", err) continue } label := strings.TrimSpace(strconv.Itoa(id) + " - " + strings.TrimSpace(name)) out = append(out, models.ProductionProductCostingLookupOption{ Value: strconv.Itoa(id), Label: label, }) } if err := rows.Err(); err != nil { logger.Error("rows error", "err", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "row_count", len(out)) _ = json.NewEncoder(w).Encode(out) } // GET /api/pricing/production-product-costing/maliyet-parca-eslestirme func GetProductionProductCostingParcaMappingsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) urunIlkGrubu := strings.TrimSpace(r.URL.Query().Get("urun_ilk_grubu")) urunAnaGrubu := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu")) urunAltGrubu := strings.TrimSpace(r.URL.Query().Get("urun_alt_grubu")) nUrtMTBolumID := parsePositiveIntOrDefault(r.URL.Query().Get("n_urt_mt_bolum_id"), 0) rawOnlyActive := strings.TrimSpace(r.URL.Query().Get("only_active")) var onlyActive *bool = nil if rawOnlyActive != "" { v := rawOnlyActive == "1" || strings.EqualFold(rawOnlyActive, "true") onlyActive = &v } logger := utils.SlogFromContext(ctx).With( "handler", "production-product-costing.maliyet-parca-eslestirme.list", "urun_ilk_grubu", urunIlkGrubu, "urun_ana_grubu", urunAnaGrubu, "urun_alt_grubu", urunAltGrubu, "n_urt_mt_bolum_id", nUrtMTBolumID, "only_active", rawOnlyActive, ) logger.Info("request start") rows, err := queries.ListProductionProductCostingParcaMappings(ctx, uretimDB, urunIlkGrubu, urunAnaGrubu, urunAltGrubu, nUrtMTBolumID, onlyActive) if err != nil { logger.Error("query error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() out := make([]models.ProductionProductCostingParcaMappingRow, 0, 200) for rows.Next() { var row models.ProductionProductCostingParcaMappingRow var bAktif sql.NullBool var hammaddeCsv sql.NullString if err := rows.Scan( &row.ID, &row.UrunIlkGrubu, &row.UrunAnaGrubu, &row.UrunAltGrubu, &row.NUrtMTBolumID, &row.ParcaBolumAdi, &hammaddeCsv, &bAktif, &row.DteIslem, &row.SKullaniciAdi, ); err != nil { logger.Warn("scan error", "err", err) continue } row.BAktif = bAktif.Valid && bAktif.Bool row.NHammaddeTurleri = make([]string, 0, 8) if hammaddeCsv.Valid { for _, part := range strings.Split(hammaddeCsv.String, ",") { part = strings.TrimSpace(part) if part != "" { row.NHammaddeTurleri = append(row.NHammaddeTurleri, part) } } } out = append(out, row) } if err := rows.Err(); err != nil { logger.Error("rows error", "err", err) http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } logger.Info("request done", "row_count", len(out)) _ = json.NewEncoder(w).Encode(out) } // POST /api/pricing/production-product-costing/maliyet-parca-eslestirme/upsert func PostProductionProductCostingParcaMappingUpsertHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } claims, _ := auth.GetClaimsFromContext(r.Context()) user := "" if claims != nil { user = strings.TrimSpace(claims.Username) } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.maliyet-parca-eslestirme.upsert") logger.Info("request start") var req models.ProductionProductCostingParcaMappingUpsertRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { logger.Warn("invalid json", "err", err) http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } req.UrunAnaGrubu = strings.TrimSpace(req.UrunAnaGrubu) req.UrunAltGrubu = strings.TrimSpace(req.UrunAltGrubu) req.UrunIlkGrubu = strings.TrimSpace(req.UrunIlkGrubu) if req.UrunIlkGrubu == "" || req.UrunAnaGrubu == "" || req.UrunAltGrubu == "" || req.NUrtMTBolumID <= 0 { http.Error(w, "urunIlkGrubu, urunAnaGrubu, urunAltGrubu ve nUrtMTBolumID zorunlu", http.StatusBadRequest) return } id, err := queries.UpsertProductionProductCostingParcaMapping(ctx, uretimDB, req.UrunIlkGrubu, req.UrunAnaGrubu, req.UrunAltGrubu, req.NUrtMTBolumID, req.NHammaddeTurleri, req.BAktif, user) if err != nil { logger.Error("exec error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } logger.Info("request done", "id", id, "user", user, "bAktif", req.BAktif) _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "id": id}) } type productionProductCostingSetActiveRequest struct { ID int `json:"id"` BAktif bool `json:"bAktif"` } // POST /api/pricing/production-product-costing/maliyet-parca-eslestirme/set-active func PostProductionProductCostingParcaMappingSetActiveHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } claims, _ := auth.GetClaimsFromContext(r.Context()) user := "" if claims != nil { user = strings.TrimSpace(claims.Username) } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.maliyet-parca-eslestirme.set-active") logger.Info("request start") var req productionProductCostingSetActiveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { logger.Warn("invalid json", "err", err) http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } if req.ID <= 0 { http.Error(w, "id zorunlu", http.StatusBadRequest) return } if err := queries.SetProductionProductCostingParcaMappingActive(ctx, uretimDB, req.ID, req.BAktif, user); err != nil { logger.Error("exec error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } logger.Info("request done", "id", req.ID, "bAktif", req.BAktif, "user", user) _ = json.NewEncoder(w).Encode(map[string]any{"ok": true}) } // DELETE /api/pricing/production-product-costing/maliyet-parca-eslestirme?id=123 func DeleteProductionProductCostingParcaMappingHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") uretimDB := db.GetUretimDB() if uretimDB == nil { http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) return } traceID := utils.TraceIDFromRequest(r) ctx := utils.ContextWithTraceID(r.Context(), traceID) logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.maliyet-parca-eslestirme.delete") logger.Info("request start") id := parsePositiveIntOrDefault(r.URL.Query().Get("id"), 0) if id <= 0 { http.Error(w, "id zorunlu", http.StatusBadRequest) return } if err := queries.DeleteProductionProductCostingParcaMapping(ctx, uretimDB, id); err != nil { logger.Error("exec error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } logger.Info("request done", "id", id) _ = json.NewEncoder(w).Encode(map[string]any{"ok": true}) }