Files
bssapp/svc/routes/production_product_costing.go
2026-05-15 14:33:50 +03:00

2889 lines
91 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/models"
"bssapp-backend/queries"
"bssapp-backend/utils"
"context"
"database/sql"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"time"
)
func logProductionHasCostDetailEditorOptionItemDiagnostics(ctx context.Context, uretimDB *sql.DB, search string, limit int) {
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.detail-editor-options.diagnostics",
"search", search,
"limit", limit,
)
expectedColumns := []string{
"nStokID",
"sKodu",
"sAciklama",
"sModel",
"sBirimCinsi1",
"IsBlocked",
}
probes := []struct {
name string
sqlText string
}{
{
name: "projection:nStokID",
sqlText: `SELECT TOP 1 RTRIM(CONVERT(VARCHAR(32), ISNULL(S.nStokID, 0))) FROM dbo.tbStok S`,
},
{
name: "projection:sKodu",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:sAciklama",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:sModel",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:sBirimCinsi1",
sqlText: `SELECT TOP 1 LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(S.sBirimCinsi1, '')))) FROM dbo.tbStok S`,
},
{
name: "projection:IsBlocked",
sqlText: `SELECT TOP 1 RTRIM(CONVERT(VARCHAR(32), ISNULL(S.IsBlocked, 0))) FROM dbo.tbStok S`,
},
{
name: "filter:model_like_count",
sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE '_.%'`,
},
{
name: "filter:isblocked_count",
sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE ISNULL(S.IsBlocked, 0) = 0`,
},
{
name: "filter:final_count",
sqlText: `SELECT RTRIM(CONVERT(VARCHAR(32), COUNT(1))) FROM dbo.tbStok S WHERE ISNULL(S.IsBlocked, 0) = 0 AND LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE '_.%'`,
},
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics start search=%q limit=%d", search, limit)
for _, columnName := range expectedColumns {
var exists int
err := uretimDB.QueryRowContext(
ctx,
`SELECT COUNT(1)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'dbo'
AND TABLE_NAME = 'tbStok'
AND COLUMN_NAME = @p1`,
columnName,
).Scan(&exists)
if err != nil {
logger.Error("query error", "err", err)
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item diagnostics column check error column=%s err=%v", columnName, err)
continue
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics column=%s exists=%t", columnName, exists > 0)
}
for _, probe := range probes {
var sample sql.NullString
err := uretimDB.QueryRowContext(ctx, probe.sqlText).Scan(&sample)
if err != nil {
log.Printf("⚠️ [ProductionHasCostDetailEditorOptions] item diagnostics probe=%s err=%v", probe.name, err)
continue
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics probe=%s sample=%q", probe.name, strings.TrimSpace(sample.String))
}
log.Printf("[ProductionHasCostDetailEditorOptions] item diagnostics end search=%q limit=%d", search, limit)
}
// GET /api/pricing/production-product-costing/no-cost-products
func GetProductionNoCostProductsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
search := strings.TrimSpace(r.URL.Query().Get("search"))
fromDate := strings.TrimSpace(r.URL.Query().Get("from_date"))
rows, err := queries.GetProductionNoCostProducts(r.Context(), uretimDB, fromDate, search)
if err != nil {
log.Printf("❌ [ProductionNoCost] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.ProductionNoCostProductRow, 0, 200)
for rows.Next() {
var item models.ProductionNoCostProductRow
if err := rows.Scan(
&item.UretimSekli,
&item.UrtSiparisNo,
&item.IslemTarihi,
&item.FirmaKodu,
&item.FirmaAdi,
&item.SonIsEmriVeren,
&item.MMiktarG,
&item.MModelKodu,
&item.Kodu,
&item.ModelAdi,
&item.SKullaniciAdi,
&item.SKullaniciGunc,
); err != nil {
log.Printf("⚠️ [ProductionNoCost] scan error: %v", err)
continue
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionNoCost] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
}
// GET /api/pricing/production-product-costing/has-cost-products
func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
search := strings.TrimSpace(r.URL.Query().Get("search"))
offset := parsePositiveIntOrDefault(r.URL.Query().Get("offset"), 0)
limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 300)
if limit > 1000 {
limit = 1000
}
rows, err := queries.GetProductionHasCostProducts(r.Context(), uretimDB, search, offset, limit)
if err != nil {
log.Printf("❌ [ProductionHasCost] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.ProductionHasCostProductRow, 0, 200)
for rows.Next() {
var item models.ProductionHasCostProductRow
if err := rows.Scan(
&item.UretimSekli,
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
&item.Tarihi,
&item.DteKayitTarihi,
&item.SKullaniciAdi,
&item.LTutarTL,
&item.LTutarUSD,
&item.LTutarEURO,
&item.DteGuncellemeTarihi,
&item.SGuncellemeKullaniciAdi,
&item.NUrtReceteID,
&item.SAciklama,
&item.SonSiparisTarihi,
&item.MaliyetDurumu,
); err != nil {
log.Printf("⚠️ [ProductionHasCost] scan error: %v", err)
continue
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionHasCost] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
}
// GET /api/pricing/production-product-costing/has-cost-history
func GetProductionHasCostHistoryHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu"))
if productCode == "" {
http.Error(w, "urun_kodu zorunlu", http.StatusBadRequest)
return
}
rows, err := queries.GetProductionHasCostHistoryByProductCode(r.Context(), uretimDB, productCode)
if err != nil {
log.Printf("❌ [ProductionHasCostHistory] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.ProductionHasCostHistoryRow, 0, 100)
for rows.Next() {
var item models.ProductionHasCostHistoryRow
if err := rows.Scan(
&item.NOnMLNo,
&item.UrunKodu,
&item.UrunAdi,
&item.Tarihi,
&item.SKullaniciAdi,
&item.LTutarUSD,
&item.LTutarTL,
&item.LTutarEURO,
&item.SDovizCinsi,
&item.LTutarDoviz,
&item.DteGuncellemeTarihi,
&item.SGuncellemeKullaniciAdi,
&item.NUrtReceteID,
&item.SAciklama,
); err != nil {
log.Printf("⚠️ [ProductionHasCostHistory] scan error: %v", err)
continue
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionHasCostHistory] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(list)
}
// GET /api/pricing/production-product-costing/has-cost-detail-groups
func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
detailSource := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("detail_source")))
recipeCode := strings.TrimSpace(r.URL.Query().Get("recete_kodu"))
productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu"))
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.detail-groups",
"detail_source", detailSource,
"urun_kodu", productCode,
"recete_kodu", recipeCode,
)
if detailSource == "no-cost" || recipeCode != "" {
if recipeCode == "" {
logger.Warn("request invalid", "reason", "missing recete_kodu")
http.Error(w, "recete_kodu zorunlu", http.StatusBadRequest)
return
}
logger.Info("request start")
rows, err := queries.GetProductionNoCostDetailRowsByRecipeCode(ctx, uretimDB, recipeCode, productCode)
if err != nil {
logger.Error("query error", "err", err)
log.Printf("❌ [ProductionNoCostDetailGroups] query error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer rows.Close()
groups := make([]models.ProductionHasCostDetailGroup, 0, 16)
groupIndexByName := map[string]int{}
scannedRows := 0
scanErrors := 0
for rows.Next() {
var (
groupName string
groupTotal float64
groupTotalUSD float64
fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool
cmPriceTypeID sql.NullInt64
item models.ProductionHasCostDetailGroupItem
)
if err := rows.Scan(
&groupName,
&groupTotal,
&groupTotalUSD,
&item.NOnMLNo,
&item.NOnMLDetNo,
&item.NHammaddeTuruNo,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.SBeden,
&item.SAciklama2,
&item.LMiktar,
&item.LFiyat,
&item.LTutar,
&item.SFiyatTipi,
&item.SDovizCinsi,
&item.LDovizKuru,
&item.LDovizFiyati,
&fiyatGirilen,
&fiyatDoviz,
&maliyeteDahil,
&cmPriceTypeID,
&item.USDTutar,
&item.EURTutar,
&item.GBPTutar,
&item.SBirim,
&item.SHammaddeTuruAdi,
&item.SParcaAdi,
); err != nil {
scanErrors += 1
logger.Warn("scan error", "scan_index", scannedRows+scanErrors+1, "err", err)
log.Printf("⚠️ [ProductionNoCostDetailGroups] scan error: %v", err)
continue
}
scannedRows += 1
if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64)
*item.FiyatGirilen = fiyatGirilen.Float64
}
if fiyatDoviz.Valid {
item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String)
}
item.MaliyeteDahil = !maliyeteDahil.Valid || maliyeteDahil.Bool
if cmPriceTypeID.Valid {
value := int(cmPriceTypeID.Int64)
item.CMPriceTypeID = &value
}
idx, ok := groupIndexByName[groupName]
if !ok {
groups = append(groups, models.ProductionHasCostDetailGroup{
SAciklama3: groupName,
TotalTutar: groupTotal,
TotalUSDTutar: groupTotalUSD,
Items: make([]models.ProductionHasCostDetailGroupItem, 0, 8),
})
idx = len(groups) - 1
groupIndexByName[groupName] = idx
}
groups[idx].Items = append(groups[idx].Items, item)
}
if err := rows.Err(); err != nil {
logger.Error("rows error", "err", err)
log.Printf("⚠️ [ProductionNoCostDetailGroups] rows error: %v", err)
http.Error(w, "Veritabani satir hatasi", http.StatusInternalServerError)
return
}
logger.Info("request done", "group_count", len(groups), "row_count", scannedRows, "scan_errors", scanErrors)
log.Printf("[ProductionNoCostDetailGroups] done recete_kodu=%s groups=%d", recipeCode, len(groups))
_ = json.NewEncoder(w).Encode(groups)
return
}
rawOnMLNo := strings.TrimSpace(r.URL.Query().Get("n_onml_no"))
nOnMLNo, err := strconv.Atoi(rawOnMLNo)
if err != nil || nOnMLNo <= 0 {
logger.Warn("request invalid", "reason", "invalid n_onml_no", "raw_n_onml_no", rawOnMLNo)
http.Error(w, "n_onml_no zorunlu ve pozitif sayi olmali", http.StatusBadRequest)
return
}
logger = logger.With("n_onml_no", nOnMLNo)
logger.Info("request start")
log.Printf("[ProductionHasCostDetailGroups] start n_onml_no=%d", nOnMLNo)
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
if err != nil {
logger.Error("query error", "err", err)
log.Printf("❌ [ProductionHasCostDetailGroups] query error: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
groups := make([]models.ProductionHasCostDetailGroup, 0, 16)
groupIndexByName := map[string]int{}
scannedRows := 0
scanErrors := 0
for rows.Next() {
var (
groupName string
groupTotal float64
groupTotalUSD float64
fiyatGirilen sql.NullFloat64
fiyatDoviz sql.NullString
maliyeteDahil sql.NullBool
cmPriceTypeID sql.NullInt64
item models.ProductionHasCostDetailGroupItem
)
if err := rows.Scan(
&groupName,
&groupTotal,
&groupTotalUSD,
&item.NOnMLNo,
&item.NOnMLDetNo,
&item.NHammaddeTuruNo,
&item.SKodu,
&item.SAciklama,
&item.SRenk,
&item.SBeden,
&item.SAciklama2,
&item.LMiktar,
&item.LFiyat,
&item.LTutar,
&item.SFiyatTipi,
&item.SDovizCinsi,
&item.LDovizKuru,
&item.LDovizFiyati,
&fiyatGirilen,
&fiyatDoviz,
&maliyeteDahil,
&cmPriceTypeID,
&item.USDTutar,
&item.EURTutar,
&item.GBPTutar,
&item.SBirim,
&item.SHammaddeTuruAdi,
&item.SParcaAdi,
); err != nil {
log.Printf("⚠️ [ProductionHasCostDetailGroups] scan error: %v", err)
continue
}
scannedRows += 1
if fiyatGirilen.Valid {
item.FiyatGirilen = new(float64)
*item.FiyatGirilen = fiyatGirilen.Float64
}
if fiyatDoviz.Valid {
item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String)
}
item.MaliyeteDahil = maliyeteDahil.Valid && maliyeteDahil.Bool
if cmPriceTypeID.Valid {
value := int(cmPriceTypeID.Int64)
item.CMPriceTypeID = &value
}
idx, ok := groupIndexByName[groupName]
if !ok {
groups = append(groups, models.ProductionHasCostDetailGroup{
SAciklama3: groupName,
TotalTutar: groupTotal,
TotalUSDTutar: groupTotalUSD,
Items: make([]models.ProductionHasCostDetailGroupItem, 0, 24),
})
idx = len(groups) - 1
groupIndexByName[groupName] = idx
}
groups[idx].Items = append(groups[idx].Items, item)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ [ProductionHasCostDetailGroups] rows error: %v", err)
http.Error(w, "Veritabanı satır hatası", http.StatusInternalServerError)
return
}
logger.Info("request done", "group_count", len(groups), "row_count", scannedRows, "scan_errors", scanErrors)
log.Printf("[ProductionHasCostDetailGroups] done n_onml_no=%d groups=%d rows=%d scan_errors=%d", nOnMLNo, len(groups), scannedRows, scanErrors)
_ = json.NewEncoder(w).Encode(groups)
}
// GET /api/pricing/production-product-costing/has-cost-detail-header
func GetProductionHasCostDetailHeaderHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
uretimDB := db.GetUretimDB()
if uretimDB == nil {
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
return
}
mssqlDB := db.GetDB()
detailSource := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("detail_source")))
recipeCode := strings.TrimSpace(r.URL.Query().Get("recete_kodu"))
productCode := strings.TrimSpace(r.URL.Query().Get("urun_kodu"))
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With(
"handler", "production-product-costing.detail-header",
"detail_source", detailSource,
"urun_kodu", productCode,
"recete_kodu", recipeCode,
)
if detailSource == "no-cost" || recipeCode != "" {
if recipeCode == "" {
logger.Warn("request invalid", "reason", "missing recete_kodu")
http.Error(w, "recete_kodu zorunlu", http.StatusBadRequest)
return
}
logger.Info("request start")
row, err := queries.GetProductionNoCostDetailHeaderByRecipeCode(ctx, uretimDB, recipeCode, productCode)
if err != nil {
logger.Error("query prepare error", "err", err)
log.Printf("❌ [ProductionNoCostDetailHeader] query prepare error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
var item models.ProductionHasCostDetailHeader
if err := row.Scan(
&item.UretimiYapanFirma,
&item.SonIsEmriVeren,
&item.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
}
m, err := queries.LookupProductionProductCostingDefaultQtyByNos(ctx, uretimDB, req.NHammaddeTuruNos)
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
out := make([]models.ProductionProductCostingDefaultQtyLookupItem, 0, len(m))
for no, qty := range m {
out = append(out, models.ProductionProductCostingDefaultQtyLookupItem{
NHammaddeTuruNo: no,
LDefaultMiktar: qty,
})
}
_ = json.NewEncoder(w).Encode(out)
}
// 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 == "" {
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 {
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 {
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
}
defer func() { _ = tx.Rollback() }()
// 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 {
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}
}
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
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
for _, row := range req.Detail.Upserts {
if row.NOnMLDetNo <= 0 {
continue
}
if row.NHammaddeTuruNo <= 0 || strings.TrimSpace(row.SKodu) == "" {
http.Error(w, "Detay satirinda n_hammadde_turu_no ve 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
// 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)
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 {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
logger.Error("stok tipi lookup error", "err", err)
http.Error(w, "Stok tipi bulunamadi (tbStok sorgu hatasi)", http.StatusInternalServerError)
return
}
if nStokTipiID <= 0 {
http.Error(w, "Stok tipi bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
// 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()
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,''),NULL,@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,
); err != nil {
logger.Error("detail merge error", "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
// 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
}
// CM1/CM2 rows are cost-only and must NOT be written back into recipe tables.
// Source of truth: spUrtOnMLHammaddeTuru.sAciklama3 (e.g. 'CM2').
var grp sql.NullString
if err := tx.QueryRowContext(ctx, `
SELECT TOP 1 LTRIM(RTRIM(ISNULL(H.sAciklama3, '')))
FROM dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK)
WHERE H.nHammaddeTuruNo = @p1
`, hNo).Scan(&grp); err == nil {
g := strings.ToUpper(strings.TrimSpace(grp.String))
if g == "CM1" || g == "CM2" {
continue
}
}
// Resolve nHStokID from tbStok using sKodu (exact), then sModel fallback.
// IMPORTANT: nHStokID must be resolved; otherwise we'd update/insert too broadly.
rawSKodu := strings.TrimSpace(row.SKodu)
var nHStokID int
err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(S.nStokID, 0)
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(&nHStokID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
logger.Error("recipe stok lookup error", "err", err)
http.Error(w, "Recete sync stok bulunamadi", http.StatusInternalServerError)
return
}
if nHStokID == 0 {
http.Error(w, "Recete sync icin stok bulunamadi (s_kodu="+rawSKodu+")", http.StatusBadRequest)
return
}
// If we cannot resolve stock, we still try to insert by hammadde no only.
// Some recipes may omit nHStokID; but we prefer filling it when possible.
// Update quantity if row already exists for (recete, hammadde, stok)
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 RMik.nHStokID = @p3
`, receteID, hNo, nHStokID).Scan(&exists); err == nil && exists > 0 {
_, _ = tx.ExecContext(ctx, `
UPDATE dbo.spUrtRecMBolumMik
SET lHMiktar = @p4,
sKullaniciAdiDeg = @p5,
dteIslemTarihiDeg = GETDATE()
WHERE nUrtReceteID = @p1
AND nUrtMBolumID = @p2
AND nHStokID = @p3
`, receteID, hNo, nHStokID, row.LMiktar, user)
continue
}
// Insert missing: best-effort with minimal columns.
// NOTE: This assumes nUrtRecMBolumID can be a sequential int and that other columns are nullable/have defaults.
_, insertErr := tx.ExecContext(ctx, `
INSERT INTO dbo.spUrtRecMBolumMik (
nUrtReceteID,
nUrtUBolumID,
nUrtRecMBolumID,
nStokID,
nHStokID,
lHMiktar,
lHFire,
nMaliyetTipiID,
lHMaliyet,
lMiktar,
sIslemKodu,
nUrtMBolumID,
nUrtMTBolumID,
lHCarpan,
bIslem,
nSure,
sAciklama,
dteDovizTarihi,
sDovizCinsi,
lDovizOran,
lDovizFiyat,
sKullaniciAdi,
dteIslemTarihi,
nMBolumSarfTipiNo
)
VALUES (
@p1,
0,
@p2,
0,
@p3,
@p4,
0,
6,
0,
1,
'',
@p5,
@p6,
1,
0,
0,
NULL,
NULL,
NULL,
NULL,
NULL,
@p7,
GETDATE(),
1
)
`, receteID, nextRecDetID, nHStokID, row.LMiktar, hNo, row.NUrtMTBolumID, user)
if insertErr == nil {
nextRecDetID += 1
}
}
}
if err := tx.Commit(); err != nil {
logger.Error("tx commit error", "err", err)
http.Error(w, "Kaydetme tamamlanamadi", http.StatusInternalServerError)
return
}
// 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 {
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})
}
// 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})
}