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 nOnMLNoStr string nOnMLDetNoStr string hNoStr string fiyatGirilen sql.NullFloat64 fiyatDoviz sql.NullString maliyeteDahil sql.NullBool cmPriceTypeID sql.NullInt64 item models.ProductionHasCostDetailGroupItem ) if err := rows.Scan( &groupName, &groupTotal, &groupTotalUSD, &nOnMLNoStr, &nOnMLDetNoStr, &hNoStr, &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 // Model fields are strings; keep as-is to avoid type mismatches. item.NOnMLNo = strings.TrimSpace(nOnMLNoStr) item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr) item.NHammaddeTuruNo = strings.TrimSpace(hNoStr) 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 nOnMLNoStr string nOnMLDetNoStr string hNoStr string fiyatGirilen sql.NullFloat64 fiyatDoviz sql.NullString maliyeteDahil sql.NullBool cmPriceTypeID sql.NullInt64 item models.ProductionHasCostDetailGroupItem ) if err := rows.Scan( &groupName, &groupTotal, &groupTotalUSD, &nOnMLNoStr, &nOnMLDetNoStr, &hNoStr, &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++ logger.Warn("scan error", "scan_index", scannedRows+scanErrors, "err", err) log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err) continue } scannedRows++ // Model fields are strings; keep as-is to avoid type mismatches. item.NOnMLNo = strings.TrimSpace(nOnMLNoStr) item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr) item.NHammaddeTuruNo = strings.TrimSpace(hNoStr) 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.FirmaKodu, &item.NFirmaID, &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 { ilk, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu) if err != nil { logger.Warn("product group query error", "err", err) } else { item.UrunIlkGrubu = ilk 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.FirmaKodu, &item.NFirmaID, &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 { ilk, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, item.UrunKodu) if err != nil { logger.Warn("product group query error", "err", err) } else { item.UrunIlkGrubu = ilk 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, &item.MTUrtMTBolumID, &item.SParcaAdi); 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) 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/default-quantities func GetProductionProductCostingDefaultQuantitiesHandler(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"), 500) // Always list only active hammadde turleri. rows, err := queries.ListProductionProductCostingDefaultQtyRows(ctx, uretimDB, search, limit) if err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer rows.Close() out := make([]models.ProductionProductCostingDefaultQtyRow, 0, 256) for rows.Next() { var item models.ProductionProductCostingDefaultQtyRow if err := rows.Scan(&item.NHammaddeTuruNo, &item.SAciklama, &item.LDefaultMiktar, &item.DteCalcTarihi, &item.BAktif); err != nil { http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } out = append(out, item) } if err := rows.Err(); err != nil { http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(out) } // POST /api/pricing/production-product-costing/default-quantities/upsert func PostProductionProductCostingDefaultQuantitiesUpsertHandler(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) var req models.ProductionProductCostingDefaultQtyUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } if req.NHammaddeTuruNo <= 0 { http.Error(w, "nHammaddeTuruNo zorunlu", http.StatusBadRequest) return } if req.LDefaultMiktar <= 0 { http.Error(w, "lDefaultMiktar pozitif olmali", http.StatusBadRequest) return } if err := queries.UpsertProductionProductCostingDefaultQtyRow(ctx, uretimDB, req.NHammaddeTuruNo, req.LDefaultMiktar, req.BAktif); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]any{"ok": true}) } // POST /api/pricing/production-product-costing/default-quantities/update-bulk func PostProductionProductCostingDefaultQuantitiesBulkUpdateHandler(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) var req models.ProductionProductCostingDefaultQtyBulkUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } if len(req.Items) == 0 { _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "updated": 0}) return } if len(req.Items) > 5000 { http.Error(w, "cok fazla satir", http.StatusBadRequest) return } tx, err := uretimDB.BeginTx(ctx, nil) if err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer func() { _ = tx.Rollback() }() updated := 0 for _, item := range req.Items { if item.NHammaddeTuruNo <= 0 { http.Error(w, "nHammaddeTuruNo zorunlu", http.StatusBadRequest) return } if item.LDefaultMiktar <= 0 { http.Error(w, "lDefaultMiktar pozitif olmali", http.StatusBadRequest) return } activeVal := -1 if item.BAktif != nil { if *item.BAktif { activeVal = 1 } else { activeVal = 0 } } sqlText := ` UPDATE dbo.mk_MaliyetParcaEslestirme_vmiktarlar SET lDefaultMiktar = @p2, dteCalcTarihi = GETDATE(), bAktif = CASE WHEN @p3 < 0 THEN ISNULL(bAktif, 1) ELSE @p3 END WHERE nHammaddeTuruNo = @p1; SELECT @@ROWCOUNT; ` var affected int if err := tx.QueryRowContext(ctx, sqlText, item.NHammaddeTuruNo, item.LDefaultMiktar, activeVal).Scan(&affected); err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } if affected == 0 { http.Error(w, "satir bulunamadi: "+strconv.Itoa(item.NHammaddeTuruNo), http.StatusBadRequest) return } updated++ } if err := tx.Commit(); err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "updated": updated}) } // POST /api/pricing/production-product-costing/default-quantities/calc-avg func PostProductionProductCostingDefaultQuantitiesCalcAvgHandler(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) var req models.ProductionProductCostingDefaultQtyCalcRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } if req.NHammaddeTuruNo <= 0 { http.Error(w, "nHammaddeTuruNo zorunlu", http.StatusBadRequest) return } if req.TopN <= 0 { req.TopN = 10 } avg, cnt, err := queries.CalcProductionProductCostingDefaultQtyFromLastOnML(ctx, uretimDB, req.NHammaddeTuruNo, req.TopN) if err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(models.ProductionProductCostingDefaultQtyCalcResponse{ NHammaddeTuruNo: req.NHammaddeTuruNo, LDefaultMiktar: avg, NSampleCount: cnt, }) } // POST /api/pricing/production-product-costing/default-quantities/lookup func PostProductionProductCostingDefaultQuantitiesLookupHandler(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) var req models.ProductionProductCostingDefaultQtyLookupRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } rows, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos) if err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(rows)) for _, r0 := range rows { out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{ NHammaddeTuruNo: r0.No, SAciklama: r0.Aciklama, LDefaultMiktar: r0.Qty, }) } _ = json.NewEncoder(w).Encode(out) } // POST /api/pricing/production-product-costing/has-cost-detail/last-detail func PostProductionProductCostingHasCostDetailLastDetailHandler(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) var req models.ProductionProductCostingLastOnMLDetLookupRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } rows, err := queries.LookupLastOnMLMasDetByHammaddeNos(ctx, uretimDB, req.NHammaddeTuruNos, req.BeforeDate, req.ExcludeOnMLNo, req.NFirmaID, req.OnlyICode, 1) if err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } // POST /api/pricing/production-product-costing/options/hammadde-by-nos func PostProductionProductCostingOptionsHammaddeByNosHandler(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) var req models.ProductionProductCostingHammaddeByNosRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } rows, err := queries.GetProductionHammaddeByNos(ctx, uretimDB, req.NHammaddeTuruNos) if err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(rows) } // POST /api/pricing/production-product-costing/default-quantities/refresh func PostProductionProductCostingDefaultQuantitiesRefreshHandler(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) topN := parsePositiveIntOrDefault(r.URL.Query().Get("top_n"), 10) if err := queries.RefreshProductionProductCostingDefaultQty(ctx, uretimDB, topN); err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "top_n": topN}) } // POST /api/pricing/production-product-costing/onml/save func PostProductionProductCostingOnMLSaveHandler(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() 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.onml.save") var req models.ProductionProductCostingOnMLSaveRequest 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.DetailSource = strings.ToLower(strings.TrimSpace(req.DetailSource)) req.Header.UrunKodu = strings.TrimSpace(req.Header.UrunKodu) req.Header.UrunAdi = strings.TrimSpace(req.Header.UrunAdi) req.Header.MaliyetTarihi = strings.TrimSpace(req.Header.MaliyetTarihi) req.Header.SAciklama = strings.TrimSpace(req.Header.SAciklama) req.Header.FirmaKodu = strings.TrimSpace(req.Header.FirmaKodu) if req.Header.UrunKodu == "" || req.Header.MaliyetTarihi == "" { logger.Warn("validation failed: missing required header fields", "trace_id", traceID, "urun_kodu", req.Header.UrunKodu, "maliyet_tarihi", req.Header.MaliyetTarihi, ) http.Error(w, "urun_kodu ve maliyet_tarihi zorunlu", http.StatusBadRequest) return } // Resolve exchange rates (for price conversions). usdRate := 0.0 eurRate := 0.0 gbpRate := 0.0 if mssqlDB != nil { row, err := queries.GetProductionHasCostDetailExchangeRatesByDate(ctx, mssqlDB, req.Header.MaliyetTarihi) if err != nil { logger.Error("exchange rate query error", "err", err) http.Error(w, "Kur bilgisi alinamadi", http.StatusInternalServerError) return } var rateDate string if err := row.Scan(&rateDate, &usdRate, &eurRate, &gbpRate); err != nil { logger.Error("exchange rate scan error", "err", err) http.Error(w, "Kur bilgisi alinamadi", http.StatusInternalServerError) return } } if usdRate <= 0 { usdRate = 1 } if eurRate <= 0 { eurRate = 1 } if gbpRate <= 0 { gbpRate = 1 } // Resolve firma id firmaID := req.Header.NFirmaID if firmaID <= 0 && req.Header.FirmaKodu != "" { id, err := queries.LookupFirmaIDByKodu(ctx, uretimDB, req.Header.FirmaKodu) if err != nil { logger.Error("firma lookup error", "err", err) http.Error(w, "Firma bulunamadi", http.StatusBadRequest) return } firmaID = id } if firmaID <= 0 { logger.Warn("validation failed: missing firma", "trace_id", traceID, "n_firma_id", req.Header.NFirmaID, "firma_kodu", req.Header.FirmaKodu, ) http.Error(w, "Firma secilmeden kaydedilemez (n_firma_id / firma_kodu)", http.StatusBadRequest) return } // Resolve or generate nOnMLNo nOnMLNo := req.Header.NOnMLNo if nOnMLNo <= 0 { next, err := queries.GetNextOnMLNoFrom100k(ctx, uretimDB) if err != nil { logger.Error("next onmlno error", "err", err) http.Error(w, "Yeni nOnMLNo uretilemedi", http.StatusInternalServerError) return } nOnMLNo = next } // Resolve / create nMamulTuruNo from product groups (UrunAna + UrunAlt) nMamulTuruNo := 1 // Compute totals from incoming rows (TRY/USD/EUR) totalTRY := 0.0 totalUSD := 0.0 totalEUR := 0.0 for _, r := range req.Detail.Upserts { qty := r.LMiktar if qty < 0 { qty = 0 } // Convert input price to TRY (unit) cur := strings.ToUpper(strings.TrimSpace(r.FiyatDoviz)) in := r.FiyatGirilen unitTRY := in switch cur { case "USD": unitTRY = in * usdRate case "EUR": unitTRY = in * eurRate case "GBP": unitTRY = in * gbpRate case "TRY", "TL", "": unitTRY = in default: unitTRY = in } unitUSD := unitTRY / usdRate unitEUR := unitTRY / eurRate totalTRY += unitTRY * qty totalUSD += unitUSD * qty totalEUR += unitEUR * qty } // Parse Tarihi tarihi, err := time.Parse("2006-01-02", req.Header.MaliyetTarihi) if err != nil { logger.Warn("validation failed: maliyet_tarihi parse error", "trace_id", traceID, "maliyet_tarihi", req.Header.MaliyetTarihi, "err", err, ) http.Error(w, "maliyet_tarihi YYYY-MM-DD formatinda olmali", http.StatusBadRequest) return } tx, err := uretimDB.BeginTx(ctx, nil) if err != nil { logger.Error("tx begin error", "err", err) http.Error(w, "Islem baslatilamadi", http.StatusInternalServerError) return } committed := false logger.Info("tx begin", "trace_id", traceID, "n_onml_no", nOnMLNo, "urun_kodu", req.Header.UrunKodu, "maliyet_tarihi", req.Header.MaliyetTarihi, ) defer func() { if committed { return } if err := tx.Rollback(); err == nil { logger.Info("tx rollback", "trace_id", traceID, "n_onml_no", nOnMLNo) } else { logger.Warn("tx rollback failed", "trace_id", traceID, "n_onml_no", nOnMLNo, "err", err) } }() // Determine mamul turu inside same tx (to keep create atomic) mamulLabel := "" if mssqlDB != nil { _, ana, alt, err := queries.GetProductIlkAnaAltGrupByUrunKodu(ctx, mssqlDB, req.Header.UrunKodu) if err == nil { ana = strings.TrimSpace(ana) alt = strings.TrimSpace(alt) if alt == "" || alt == "-" { mamulLabel = ana } else { // matches inserted convention: "ANA-ALT" (no spaces) mamulLabel = strings.TrimSpace(ana + "-" + alt) } } } if strings.TrimSpace(mamulLabel) != "" && mamulLabel != "-" { mt, err := queries.GetOnMLMamulTuruNoByAciklama(ctx, tx, mamulLabel) if err != nil { logger.Error("mamul turu resolve error", "err", err) http.Error(w, "Mamul turu olusturulamadi", http.StatusInternalServerError) return } if mt <= 0 { logger.Warn("validation failed: mamul turu not found", "trace_id", traceID, "n_onml_no", nOnMLNo, "mamul_label", mamulLabel) http.Error(w, "Mamul turu bulunamadi: "+mamulLabel, http.StatusBadRequest) return } nMamulTuruNo = mt } else { nMamulTuruNo = 1 } var receteID sql.NullInt64 if req.Header.NUrtReceteID > 0 { receteID = sql.NullInt64{Int64: int64(req.Header.NUrtReceteID), Valid: true} } var uretimSekliID sql.NullInt64 if req.Header.UretimSekliID > 0 { uretimSekliID = sql.NullInt64{Int64: int64(req.Header.UretimSekliID), Valid: true} } var sAciklama sql.NullString if req.Header.SAciklama != "" { sAciklama = sql.NullString{String: req.Header.SAciklama, Valid: true} } logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "upsert_header") if err := queries.UpsertOnMLHeader(tx, ctx, queries.OnMLHeaderUpsertArgs{ NOnMLNo: nOnMLNo, UrunKodu: req.Header.UrunKodu, UrunAdi: req.Header.UrunAdi, Tarihi: tarihi, NMamulTuruNo: nMamulTuruNo, NUrtReceteID: receteID, UretimSekliID: uretimSekliID, SAciklama: sAciklama, NFirmaID: firmaID, SUser: user, LTutarTL: totalTRY, LTutarUSD: totalUSD, LTutarEURO: totalEUR, SDovizCinsi: "USD", LTutarDoviz: totalUSD, }); err != nil { logger.Error("header upsert error", "err", err) http.Error(w, "Header kaydedilemedi", http.StatusInternalServerError) return } // Deletes logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_deletes", "count", len(req.Detail.Deletes)) for _, d := range req.Detail.Deletes { if d.NOnMLDetNo <= 0 { continue } if _, err := tx.ExecContext(ctx, `DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo=@p1 AND nOnMLDetNo=@p2`, nOnMLNo, d.NOnMLDetNo); err != nil { logger.Error("detail delete error", "err", err) http.Error(w, "Detay silinemedi", http.StatusInternalServerError) return } } // Upserts logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "detail_upserts", "count", len(req.Detail.Upserts)) for _, row := range req.Detail.Upserts { if row.NOnMLDetNo <= 0 { continue } if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" { // FALLBACK: If nHammaddeTuruNo is missing but sKodu is present, default to 1 (General/Labor) // to avoid blocking the user, especially for labor items. if strings.TrimSpace(row.SKodu) != "" && row.NHammaddeTuruNo <= 0 { logger.Warn("n_hammadde_turu_no <= 0, falling back to 1", "trace_id", traceID, "n_onml_no", nOnMLNo, "n_onml_det_no", row.NOnMLDetNo, "s_kodu", strings.TrimSpace(row.SKodu), ) row.NHammaddeTuruNo = 1 } else { logger.Warn("validation failed: missing required detail fields (s_kodu empty)", "trace_id", traceID, "n_onml_no", nOnMLNo, "n_onml_det_no", row.NOnMLDetNo, "n_hammadde_turu_no", row.NHammaddeTuruNo, "s_kodu", strings.TrimSpace(row.SKodu), ) http.Error(w, "Detay satirinda s_kodu zorunlu", http.StatusBadRequest) return } } qty := row.LMiktar if qty < 0 { qty = 0 } cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz)) in := row.FiyatGirilen unitTRY := in switch cur { case "USD": unitTRY = in * usdRate case "EUR": unitTRY = in * eurRate case "GBP": unitTRY = in * gbpRate case "TRY", "TL", "": unitTRY = in default: unitTRY = in } unitUSD := unitTRY / usdRate lTutar := unitTRY * qty lDovizTutari := unitUSD * qty // Debug log for price resolution logger.Info("price debug", "s_kodu", strings.TrimSpace(row.SKodu), "qty", qty, "fiyat_girilen", row.FiyatGirilen, "fiyat_doviz", strings.TrimSpace(row.FiyatDoviz), "unitTRY", unitTRY, "lTutar", lTutar, "lDovizTutari", lDovizTutari, ) // Resolve stock type id from tbStok by sKodu (exact), then fallback to model-based match. // Note: In this DB, stock type is stored as tbStok.nStokTipi but spUrtOnMLMasDet expects nStokTipiID (int). rawSKodu := strings.TrimSpace(row.SKodu) logger.Info("resolving stock type", "s_kodu", rawSKodu) var nStokTipiID int err := tx.QueryRowContext(ctx, ` SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID FROM dbo.tbStok S WITH (NOLOCK) WHERE ISNULL(S.IsBlocked, 0) = 0 AND ( REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') OR LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1 OR @p1 LIKE LTRIM(RTRIM(ISNULL(S.sModel,''))) + '%' ) ORDER BY CASE WHEN REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') THEN 0 WHEN LTRIM(RTRIM(ISNULL(S.sModel,''))) = @p1 THEN 1 ELSE 2 END, S.dteKayitTarihi DESC, S.nStokID DESC `, rawSKodu).Scan(&nStokTipiID) if err != nil { if err == sql.ErrNoRows { // FALLBACK: If stock item not found in tbStok at all, default to 1. logger.Warn("stok tipi not found in tbStok, falling back to 1", "trace_id", traceID, "n_onml_no", nOnMLNo, "n_onml_det_no", row.NOnMLDetNo, "s_kodu", rawSKodu, ) nStokTipiID = 1 } else { logger.Error("stok tipi lookup error", "err", err) http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError) return } } logger.Info("stock type resolved", "s_kodu", rawSKodu, "n_stok_tipi_id", nStokTipiID) if nStokTipiID <= 0 { // FALLBACK: If stock type is missing or 0 in tbStok, default to 1 (usually 'Raw Material' or 'General'). // This prevents blocking the save process for items not fully configured in tbStok. logger.Warn("stok tipi <= 0, falling back to 1", "trace_id", traceID, "n_onml_no", nOnMLNo, "n_onml_det_no", row.NOnMLDetNo, "s_kodu", rawSKodu, "n_stok_tipi_id_orig", nStokTipiID, ) nStokTipiID = 1 } // Dummy/system-mapped required fields: const bOtoFiyat = 0 const lFireOrani = 0 const nTutarGirisTipiNo = 1 const sFiyatTipi = "ML" const nKurTipiNo = 6 const lEkMasraf = 0 const lMiktar2 = 0 const lEkFiyat = 0 // MERGE by (nOnMLNo, nOnMLDetNo) mergeSQL := ` MERGE dbo.spUrtOnMLMasDet AS T USING (SELECT @p1 AS nOnMLNo, @p2 AS nOnMLDetNo) AS S ON (T.nOnMLNo = S.nOnMLNo AND T.nOnMLDetNo = S.nOnMLDetNo) WHEN MATCHED THEN UPDATE SET nHammaddeTuruNo = @p3, sKodu = @p4, sAciklama = @p5, sRenk = @p6, sBeden = NULLIF(@p7,''), sAciklama2 = NULLIF(@p8,''), lMiktar = @p9, lFiyat = @p10, lTutar = @p11, bOtoFiyat = @p12, lFireOrani = @p13, nTutarGirisTipiNo = @p14, sFiyatTipi = @p15, sDovizCinsi = @p16, nKurTipiNo = @p17, lDovizKuru = @p18, lDovizFiyati = @p19, sBirim = @p20, lDovizTutari = @p21, lEkMasraf = @p22, lMiktar2 = @p23, nStokTipiID = @p24, lEkFiyat = @p25, nUrtMTBolumID = @p26, fiyat_girilen = NULLIF(@p27, 0), fiyat_doviz = NULLIF(@p28,''), Maliyete_dahil = @p29, cm_price_type_id = @p30, sKullaniciAdiDeg = @p31, dteIslemTarihiDeg = GETDATE(), sAciklama3 = NULLIF(@p32, '') WHEN NOT MATCHED THEN INSERT ( nOnMLNo,nOnMLDetNo,nHammaddeTuruNo,sKodu,sAciklama,sRenk,lMiktar,lFiyat,lTutar, bOtoFiyat,lFireOrani,nTutarGirisTipiNo,sFiyatTipi,sDovizCinsi,nKurTipiNo,lDovizKuru,lDovizFiyati, sBirim,sAciklama2,lDovizTutari,lEkMasraf,sKullaniciAdi,dteIslemTarihi,sBeden,sAciklama3,lMiktar2,nStokTipiID, lEkFiyat,nUrtMTBolumID,fiyat_girilen,fiyat_doviz,Maliyete_dahil,cm_price_type_id ) VALUES ( @p1,@p2,@p3,@p4,@p5,@p6,@p9,@p10,@p11, @p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19, @p20,NULLIF(@p8,''),@p21,@p22,@p31,GETDATE(),NULLIF(@p7,''),NULLIF(@p32, ''),@p23,@p24, @p25,@p26,NULLIF(@p27,0),NULLIF(@p28,''),@p29,@p30 ); ` if _, err := tx.ExecContext( ctx, mergeSQL, nOnMLNo, row.NOnMLDetNo, row.NHammaddeTuruNo, strings.TrimSpace(row.SKodu), strings.TrimSpace(row.SAciklama), strings.TrimSpace(row.SRenk), strings.TrimSpace(row.SBeden), strings.TrimSpace(row.SAciklama2), qty, unitTRY, lTutar, bOtoFiyat, lFireOrani, nTutarGirisTipiNo, sFiyatTipi, "USD", nKurTipiNo, usdRate, unitUSD, strings.TrimSpace(row.SBirim), lDovizTutari, lEkMasraf, lMiktar2, nStokTipiID, lEkFiyat, row.NUrtMTBolumID, row.FiyatGirilen, strings.TrimSpace(row.FiyatDoviz), row.MaliyeteDahil, row.CMPriceTypeID, user, strings.TrimSpace(row.SAciklama3), // p32: sAciklama3 (Grup Adi) ); err != nil { logger.Error("detail merge error", "trace_id", traceID, "n_onml_no", nOnMLNo, "n_onml_det_no", row.NOnMLDetNo, "n_hammadde_turu_no", row.NHammaddeTuruNo, "n_urt_mt_bolum_id", row.NUrtMTBolumID, "s_kodu", strings.TrimSpace(row.SKodu), "fiyat_girilen", row.FiyatGirilen, "fiyat_doviz", strings.TrimSpace(row.FiyatDoviz), "l_miktar", qty, "n_stok_tipi_id", nStokTipiID, "err", err, ) http.Error(w, "Detay kaydedilemedi", http.StatusInternalServerError) return } } // ============================================================ // Recipe sync (URETIM): ensure recipe contains all OnML hammadde rows // so future no-cost loads don't keep showing them as missing. // Table observed in queries: dbo.spUrtRecMBolumMik (nUrtMBolumID stores nHammaddeTuruNo). // ============================================================ if req.Header.NUrtReceteID > 0 { receteID := req.Header.NUrtReceteID logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "recipe_sync", "n_urt_recete_id", receteID) // Determine next available recipe detail id (nUrtRecMBolumID) nextRecDetID := 0 _ = tx.QueryRowContext(ctx, ` SELECT ISNULL(MAX(RMik.nUrtRecMBolumID), 0) + 1 FROM dbo.spUrtRecMBolumMik RMik WITH (UPDLOCK, HOLDLOCK) WHERE RMik.nUrtReceteID = @p1 `, receteID).Scan(&nextRecDetID) if nextRecDetID <= 0 { nextRecDetID = 1 } for _, row := range req.Detail.Upserts { hNo := row.NHammaddeTuruNo if hNo <= 0 { continue } // 1. FILTER: CM1/CM2 (Labor/Service) rows must NOT be written back into recipe tables. // We check the group label (sAciklama3) from the row itself. g := strings.ToUpper(strings.TrimSpace(row.SAciklama3)) if g == "CM1" || g == "CM2" { logger.Info("recipe sync skip: labor item", "s_kodu", row.SKodu, "group", g) continue } // In this version of URETIM DB, the table name is dbo.spUrtRecMBolumMik // but it uses _G suffixes for quantity and amount columns. // Also nHStokID_G stores the stock code string rather than just an ID. rawSKodu := strings.TrimSpace(row.SKodu) // Ensure a section entry (spUrtRecMBolum) exists for this hNo (Hammadde Turu) // in the current recipe, otherwise detail rows (Mik) cannot be linked properly. var sectionExists int _ = tx.QueryRowContext(ctx, ` SELECT COUNT(1) FROM dbo.spUrtRecMBolum WITH (NOLOCK) WHERE nUrtReceteID = @p1 AND nUrtMBolumID = @p2 `, receteID, hNo).Scan(§ionExists) if sectionExists <= 0 { logger.Info("creating missing recipe section", "n_urt_recete_id", receteID, "n_urt_m_bolum_id", hNo) _, _ = tx.ExecContext(ctx, ` INSERT INTO dbo.spUrtRecMBolum (nUrtReceteID, nUrtUBolumID, nUrtMBolumID, nUrtMTBolumID, sKullaniciAdi, dteIslemTarihi) VALUES (@p1, 13, @p2, @p3, @p4, GETDATE()) `, receteID, hNo, row.NUrtMTBolumID, user) } // Update quantity and prices if row already exists for (recete, hammadde, stok_code) // Using nHStokID_G (string code) for matching as per user screenshot. var exists int if err := tx.QueryRowContext(ctx, ` SELECT COUNT(1) FROM dbo.spUrtRecMBolumMik RMik WITH (NOLOCK) WHERE RMik.nUrtReceteID = @p1 AND RMik.nUrtMBolumID = @p2 AND LTRIM(RTRIM(RMik.nHStokID_G)) = @p3 `, receteID, hNo, rawSKodu).Scan(&exists); err == nil && exists > 0 { // Compute TRY unit price for recipe mirror columns. cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz)) in := row.FiyatGirilen unitTRYRec := in switch cur { case "USD": unitTRYRec = in * usdRate case "EUR": unitTRYRec = in * eurRate case "GBP": unitTRYRec = in * gbpRate case "TRY", "TL", "": unitTRYRec = in default: unitTRYRec = in } _, _ = tx.ExecContext(ctx, ` UPDATE dbo.spUrtRecMBolumMik SET lHMiktar_G = @p4, lHMaliyet_G = @p5, sKullaniciAdiDeg = @p6, dteIslemTarihiDeg = GETDATE(), bIslem = @p7 WHERE nUrtReceteID = @p1 AND nUrtMBolumID = @p2 AND LTRIM(RTRIM(nHStokID_G)) = @p3 `, receteID, hNo, rawSKodu, row.LMiktar, unitTRYRec, user, row.MaliyeteDahil) continue } // Insert missing: using _G columns and storing code in nHStokID_G. // Compute TRY unit price for recipe mirror columns. cur := strings.ToUpper(strings.TrimSpace(row.FiyatDoviz)) in := row.FiyatGirilen unitTRYRec := in switch cur { case "USD": unitTRYRec = in * usdRate case "EUR": unitTRYRec = in * eurRate case "GBP": unitTRYRec = in * gbpRate case "TRY", "TL", "": unitTRYRec = in default: unitTRYRec = in } _, insertErr := tx.ExecContext(ctx, ` INSERT INTO dbo.spUrtRecMBolumMik ( nUrtReceteID, nUrtUBolumID, nUrtRecMBolumID, nStokID, nHStokID_G, lHMiktar_G, lHFire_G, nMaliyetTipiID, lHMaliyet_G, lMiktar_G, sIslemKodu, nUrtMBolumID, nUrtMTBolumID, lHCarpan, bIslem, nSure, sAciklama, sKullaniciAdi, dteIslemTarihi, nMBolumSarfTipiNo ) VALUES ( @p1, 13, @p2, 0, @p3, -- nHStokID_G (Code) @p4, -- lHMiktar_G 0, 6, @p5, -- lHMaliyet_G 1, -- lMiktar_G '', @p6, @p7, 1, @p8, -- bIslem 0, NULL, @p9, GETDATE(), 1 ) `, receteID, nextRecDetID, rawSKodu, row.LMiktar, unitTRYRec, hNo, row.NUrtMTBolumID, row.MaliyeteDahil, user) if insertErr == nil { nextRecDetID += 1 } } } logger.Info("tx step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "commit") if err := tx.Commit(); err != nil { logger.Error("tx commit error", "err", err) http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError) return } committed = true logger.Info("tx commit ok", "trace_id", traceID, "n_onml_no", nOnMLNo) // V3: update base price table so pricing screens reflect latest costing. // Not transactional with URETIM DB; if this fails, URETIM save has already succeeded. if mssqlDB != nil { logger.Info("post-commit step", "trace_id", traceID, "n_onml_no", nOnMLNo, "step", "v3_base_price_upsert") if err := queries.UpsertV3ItemBasePriceUSD(ctx, mssqlDB, req.Header.UrunKodu, req.Header.MaliyetTarihi, totalUSD, user); err != nil { logger.Error("v3 base price upsert error", "err", err) http.Error(w, "URETIM kaydedildi ama V3 maliyet guncellenemedi", http.StatusInternalServerError) return } } _ = json.NewEncoder(w).Encode(models.ProductionProductCostingOnMLSaveResponse{NOnMLNo: nOnMLNo}) } // POST /api/pricing/production-product-costing/onml/delete // Deletes costing records created in URETIM (OnML header + details) and, if created by this app, V3 base price row. // IMPORTANT: Recipe tables are NOT touched. func PostProductionProductCostingOnMLDeleteHandler(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() 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.onml.delete") var req models.ProductionProductCostingOnMLDeleteRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Gecersiz JSON", http.StatusBadRequest) return } if req.NOnMLNo <= 0 { http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest) return } // Load header info for safe deletes and redirect behavior. var urunKodu string var maliyetTarihi time.Time err := uretimDB.QueryRowContext(ctx, ` SELECT TOP 1 LTRIM(RTRIM(ISNULL(UrunKodu,''))) AS UrunKodu, COALESCE(Tarihi, dteKayitTarihi, GETDATE()) AS Tarihi FROM dbo.spUrtOnMLMas WITH (NOLOCK) WHERE nOnMLNo = @p1 `, req.NOnMLNo).Scan(&urunKodu, &maliyetTarihi) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Kayit bulunamadi", http.StatusNotFound) return } logger.Error("header lookup error", "err", err) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } urunKodu = strings.TrimSpace(urunKodu) tx, err := uretimDB.BeginTx(ctx, nil) if err != nil { http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } defer func() { _ = tx.Rollback() }() // Delete details first, then header. if _, err := tx.ExecContext(ctx, ` DELETE FROM dbo.spUrtOnMLMasDet WHERE nOnMLNo = @p1 `, req.NOnMLNo); err != nil { logger.Error("delete detail error", "err", err) http.Error(w, "Detay silinemedi", http.StatusInternalServerError) return } if _, err := tx.ExecContext(ctx, ` DELETE FROM dbo.spUrtOnMLMas WHERE nOnMLNo = @p1 `, req.NOnMLNo); err != nil { logger.Error("delete header error", "err", err) http.Error(w, "Header silinemedi", http.StatusInternalServerError) return } if err := tx.Commit(); err != nil { logger.Error("tx commit error", "err", err) http.Error(w, "Silme tamamlanamadi", http.StatusInternalServerError) return } // V3: Delete base price ONLY if row was created by this app (CreatedUserName starts with BSSAPP). deletedBasePrice := false if mssqlDB != nil && urunKodu != "" { var createdBy sql.NullString var lastBy sql.NullString _ = mssqlDB.QueryRowContext(ctx, ` SELECT TOP 1 ISNULL(CreatedUserName,'') AS CreatedUserName, ISNULL(LastUpdatedUserName,'') AS LastUpdatedUserName FROM dbo.prItemBasePrice WITH (NOLOCK) WHERE ItemTypeCode = 1 AND LTRIM(RTRIM(ItemCode)) = @p1 AND ISNULL(CountryCode,'') = 'TR' AND ISNULL(SeasonCode,'') = '' AND ISNULL(BasePriceCode,0) = 1 `, urunKodu).Scan(&createdBy, &lastBy) created := strings.ToUpper(strings.TrimSpace(createdBy.String)) last := strings.ToUpper(strings.TrimSpace(lastBy.String)) if strings.HasPrefix(created, "BSSAPP") && strings.HasPrefix(last, "BSSAPP") { if _, err := mssqlDB.ExecContext(ctx, ` DELETE FROM dbo.prItemBasePrice WHERE ItemTypeCode = 1 AND LTRIM(RTRIM(ItemCode)) = @p1 AND ISNULL(CountryCode,'') = 'TR' AND ISNULL(SeasonCode,'') = '' AND ISNULL(BasePriceCode,0) = 1 `, urunKodu); err == nil { deletedBasePrice = true } } } logger.Info("delete done", "n_onml_no", req.NOnMLNo, "urun_kodu", urunKodu, "deleted_base_price", deletedBasePrice, "user", user) _ = json.NewEncoder(w).Encode(map[string]any{ "ok": true, "n_onml_no": req.NOnMLNo, "urun_kodu": urunKodu, "deleted_baseprice": deletedBasePrice, }) } // 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 } allowed, invalid, ferr := queries.FilterHammaddeTurleriForPart(ctx, uretimDB, req.NUrtMTBolumID, req.NHammaddeTurleri) if ferr != nil { logger.Error("hammadde validation error", "err", ferr) http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) return } // Soft-enforce: drop invalid types instead of hard failing. This prevents a "multi-part save" from failing // when the UI reuses the same hammadde list across multiple MT bolum selections. // We still return the ignored list so UI/logs can surface it. ignored := invalid id, err := queries.UpsertProductionProductCostingParcaMapping(ctx, uretimDB, req.UrunIlkGrubu, req.UrunAnaGrubu, req.UrunAltGrubu, req.NUrtMTBolumID, allowed, 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) if len(ignored) > 0 { logger.Warn("hammadde types ignored due to MT bolum mismatch", "nUrtMTBolumID", req.NUrtMTBolumID, "ignored_count", len(ignored)) } _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "id": id, "ignored": ignored}) } 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}) }