Files
bssapp/svc/routes/orderproductionitems.go
2026-04-14 16:51:27 +03:00

603 lines
22 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/internal/mailer"
"bssapp-backend/models"
"bssapp-backend/queries"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"time"
"github.com/gorilla/mux"
mssql "github.com/microsoft/go-mssqldb"
)
var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`)
const productionBarcodeTypeCode = "BAGGI3"
// ======================================================
// 📌 OrderProductionItemsRoute — U ürün satırları
// ======================================================
func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
rows, err := queries.GetOrderProductionItems(mssql, id)
if err != nil {
log.Printf("❌ SQL sorgu hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.OrderProductionItem, 0, 100)
for rows.Next() {
var o models.OrderProductionItem
if err := rows.Scan(
&o.OrderHeaderID,
&o.OrderLineID,
&o.ItemTypeCode,
&o.OldDim1,
&o.OldDim3,
&o.OldItemCode,
&o.OldColor,
&o.OldColorDescription,
&o.OldDim2,
&o.OldDesc,
&o.OldQty,
&o.NewItemCode,
&o.NewColor,
&o.NewDim2,
&o.NewDesc,
&o.OldDueDate,
&o.NewDueDate,
&o.IsVariantMissing,
); err != nil {
log.Printf("⚠️ SCAN HATASI: %v", err)
continue
}
list = append(list, o)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ rows.Err(): %v", err)
}
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
func OrderProductionCdItemLookupsRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rid := fmt.Sprintf("opl-%d", time.Now().UnixNano())
w.Header().Set("X-Debug-Request-Id", rid)
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s started", rid)
lookups, err := queries.GetOrderProductionLookupOptions(mssql)
if err != nil {
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s lookup error: %v", rid, err)
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Veritabani hatasi",
"step": "cditem-lookups",
"detail": err.Error(),
"requestId": rid,
})
return
}
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s success", rid)
if err := json.NewEncoder(w).Encode(lookups); err != nil {
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s encode error: %v", rid, err)
}
})
}
// ======================================================
// 📌 OrderProductionInsertMissingRoute — eksik varyantları ekler
// ======================================================
func OrderProductionInsertMissingRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if username == "" {
username = "system"
}
affected, err := queries.InsertMissingProductionVariants(mssql, id, username)
if err != nil {
log.Printf("❌ INSERT varyant hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
resp := map[string]any{
"inserted": affected,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionValidateRoute - yeni model varyant kontrolu
// ======================================================
func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rid := fmt.Sprintf("opv-%d", time.Now().UnixNano())
w.Header().Set("X-Debug-Request-Id", rid)
start := time.Now()
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s payload lineCount=%d insertMissing=%t cdItemCount=%d attributeCount=%d",
rid, id, len(payload.Lines), payload.InsertMissing, len(payload.CdItems), len(payload.ProductAttributes))
stepStart := time.Now()
missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil {
log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s step=build_missing failed duration_ms=%d err=%v",
rid, id, time.Since(stepStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "validate_missing_variants", id, "", len(payload.Lines), err)
return
}
log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s lineCount=%d missingCount=%d build_missing_ms=%d total_ms=%d",
rid, id, len(payload.Lines), len(missing), time.Since(stepStart).Milliseconds(), time.Since(start).Milliseconds())
resp := map[string]any{
"missingCount": len(missing),
"missing": missing,
"barcodeValidationCount": 0,
"barcodeValidations": []models.OrderProductionBarcodeValidation{},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionApplyRoute - yeni model varyant guncelleme
// ======================================================
func OrderProductionApplyRoute(mssql *sql.DB, ml *mailer.GraphMailer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rid := fmt.Sprintf("opa-%d", time.Now().UnixNano())
w.Header().Set("X-Debug-Request-Id", rid)
start := time.Now()
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s payload lineCount=%d insertMissing=%t cdItemCount=%d attributeCount=%d",
rid, id, len(payload.Lines), payload.InsertMissing, len(payload.CdItems), len(payload.ProductAttributes))
stepMissingStart := time.Now()
missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=build_missing failed duration_ms=%d err=%v",
rid, id, time.Since(stepMissingStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "apply_validate_missing_variants", id, "", len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s lineCount=%d missingCount=%d build_missing_ms=%d",
rid, id, len(payload.Lines), len(missing), time.Since(stepMissingStart).Milliseconds())
if len(missing) > 0 && !payload.InsertMissing {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s early_exit=missing_variants total_ms=%d",
rid, id, time.Since(start).Milliseconds())
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(map[string]any{
"missingCount": len(missing),
"missing": missing,
"message": "Eksik varyantlar var",
})
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if strings.TrimSpace(username) == "" {
username = "system"
}
stepBeginStart := time.Now()
tx, err := mssql.Begin()
if err != nil {
writeDBError(w, http.StatusInternalServerError, "begin_tx", id, username, len(payload.Lines), err)
return
}
defer tx.Rollback()
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=begin_tx duration_ms=%d", rid, id, time.Since(stepBeginStart).Milliseconds())
stepTxSettingsStart := time.Now()
if _, err := tx.Exec(`SET XACT_ABORT ON; SET LOCK_TIMEOUT 15000;`); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_settings", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=tx_settings duration_ms=%d", rid, id, time.Since(stepTxSettingsStart).Milliseconds())
var inserted int64
if payload.InsertMissing {
cdItemByCode := buildCdItemDraftMap(payload.CdItems)
stepInsertMissingStart := time.Now()
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username, cdItemByCode)
if err != nil {
writeDBError(w, http.StatusInternalServerError, "insert_missing_variants", id, username, len(missing), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=insert_missing inserted=%d duration_ms=%d",
rid, id, inserted, time.Since(stepInsertMissingStart).Milliseconds())
}
stepValidateAttrStart := time.Now()
if err := validateProductAttributes(payload.ProductAttributes); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=validate_attributes count=%d duration_ms=%d",
rid, id, len(payload.ProductAttributes), time.Since(stepValidateAttrStart).Milliseconds())
stepUpdateHeaderStart := time.Now()
if err := queries.UpdateOrderHeaderAverageDueDateTx(tx, id, payload.HeaderAverageDueDate, username); err != nil {
writeDBError(w, http.StatusInternalServerError, "update_order_header_average_due_date", id, username, 0, err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_header_average_due_date changed=%t duration_ms=%d",
rid, id, payload.HeaderAverageDueDate != nil, time.Since(stepUpdateHeaderStart).Milliseconds())
stepUpdateLinesStart := time.Now()
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
if err != nil {
writeDBError(w, http.StatusInternalServerError, "update_order_lines", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_lines updated=%d duration_ms=%d",
rid, id, updated, time.Since(stepUpdateLinesStart).Milliseconds())
stepUpsertBarcodeStart := time.Now()
barcodeInserted, err := queries.UpsertItemBarcodesTx(tx, id, payload.Lines, username)
if err != nil {
writeDBError(w, http.StatusInternalServerError, "upsert_item_barcodes", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_barcodes inserted=%d duration_ms=%d",
rid, id, barcodeInserted, time.Since(stepUpsertBarcodeStart).Milliseconds())
stepUpsertAttrStart := time.Now()
attributeAffected, err := queries.UpsertItemAttributesTx(tx, payload.ProductAttributes, username)
if err != nil {
writeDBError(w, http.StatusInternalServerError, "upsert_item_attributes", id, username, len(payload.ProductAttributes), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_attributes affected=%d duration_ms=%d",
rid, id, attributeAffected, time.Since(stepUpsertAttrStart).Milliseconds())
stepCommitStart := time.Now()
if err := tx.Commit(); err != nil {
writeDBError(w, http.StatusInternalServerError, "commit_tx", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=commit duration_ms=%d total_ms=%d",
rid, id, time.Since(stepCommitStart).Milliseconds(), time.Since(start).Milliseconds())
// Mail gönderim mantığı
if false && ml != nil {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[OrderProductionApplyRoute] mail panic recover: %v", r)
}
}()
sendProductionUpdateMails(mssql, ml, id, username, payload.Lines)
}()
}
resp := map[string]any{
"updated": updated,
"inserted": inserted,
"barcodeInserted": barcodeInserted,
"attributeUpserted": attributeAffected,
"headerUpdated": payload.HeaderAverageDueDate != nil,
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s result updated=%d inserted=%d barcodeInserted=%d attributeUpserted=%d",
rid, id, updated, inserted, barcodeInserted, attributeAffected)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
func validateProductAttributes(attrs []models.OrderProductionItemAttributeRow) error {
for _, a := range attrs {
if strings.TrimSpace(a.ItemCode) == "" {
return errors.New("Urun ozellikleri icin ItemCode zorunlu")
}
if !baggiModelCodeRegex.MatchString(strings.ToUpper(strings.TrimSpace(a.ItemCode))) {
return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999")
}
if a.ItemTypeCode <= 0 {
return errors.New("Urun ozellikleri icin ItemTypeCode zorunlu")
}
if a.AttributeTypeCode <= 0 {
return errors.New("Urun ozellikleri icin AttributeTypeCode zorunlu")
}
if strings.TrimSpace(a.AttributeCode) == "" {
return errors.New("Urun ozellikleri icin AttributeCode zorunlu")
}
}
return nil
}
func buildCdItemDraftMap(list []models.OrderProductionCdItemDraft) map[string]models.OrderProductionCdItemDraft {
out := make(map[string]models.OrderProductionCdItemDraft, len(list))
for _, item := range list {
code := strings.ToUpper(strings.TrimSpace(item.ItemCode))
if code == "" {
continue
}
item.ItemCode = code
if item.ItemTypeCode == 0 {
item.ItemTypeCode = 1
}
key := queries.NormalizeCdItemMapKey(item.ItemTypeCode, item.ItemCode)
out[key] = item
}
return out
}
func buildTargetVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
start := time.Now()
lineDimsMap, err := queries.GetOrderLineDimsMap(mssql, orderHeaderID)
if err != nil {
return nil, err
}
out := make([]models.OrderProductionMissingVariant, 0, len(lines))
seen := make(map[string]struct{}, len(lines))
for _, line := range lines {
lineID := strings.TrimSpace(line.OrderLineID)
newItem := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
newColor := strings.ToUpper(strings.TrimSpace(line.NewColor))
newDim2 := strings.ToUpper(strings.TrimSpace(line.NewDim2))
if lineID == "" || newItem == "" {
continue
}
dims, ok := lineDimsMap[lineID]
if !ok {
continue
}
dim1 := strings.ToUpper(strings.TrimSpace(dims.ItemDim1Code))
if line.ItemDim1Code != nil {
dim1 = strings.ToUpper(strings.TrimSpace(*line.ItemDim1Code))
}
dim3 := strings.ToUpper(strings.TrimSpace(dims.ItemDim3Code))
key := fmt.Sprintf("%d|%s|%s|%s|%s|%s", dims.ItemTypeCode, newItem, newColor, dim1, newDim2, dim3)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, models.OrderProductionMissingVariant{
OrderLineID: lineID,
ItemTypeCode: dims.ItemTypeCode,
ItemCode: newItem,
ColorCode: newColor,
ItemDim1Code: dim1,
ItemDim2Code: newDim2,
ItemDim3Code: dim3,
})
}
log.Printf("[buildTargetVariants] orderHeaderID=%s lineCount=%d dimMapCount=%d targetCount=%d total_ms=%d",
orderHeaderID, len(lines), len(lineDimsMap), len(out), time.Since(start).Milliseconds())
return out, nil
}
func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
start := time.Now()
targets, err := buildTargetVariants(mssql, orderHeaderID, lines)
if err != nil {
return nil, err
}
missing := make([]models.OrderProductionMissingVariant, 0, len(targets))
existsCache := make(map[string]bool, len(targets))
for _, target := range targets {
cacheKey := fmt.Sprintf("%d|%s|%s|%s|%s|%s",
target.ItemTypeCode,
target.ItemCode,
target.ColorCode,
target.ItemDim1Code,
target.ItemDim2Code,
target.ItemDim3Code,
)
exists, cached := existsCache[cacheKey]
if !cached {
var checkErr error
exists, checkErr = queries.VariantExists(mssql, target.ItemTypeCode, target.ItemCode, target.ColorCode, target.ItemDim1Code, target.ItemDim2Code, target.ItemDim3Code)
if checkErr != nil {
return nil, checkErr
}
existsCache[cacheKey] = exists
}
if !exists {
missing = append(missing, target)
}
}
log.Printf("[buildMissingVariants] orderHeaderID=%s lineCount=%d dimMapCount=%d missingCount=%d total_ms=%d",
orderHeaderID, len(lines), len(targets), len(missing), time.Since(start).Milliseconds())
return missing, nil
}
func validateUpdateLines(lines []models.OrderProductionUpdateLine) error {
for _, line := range lines {
if strings.TrimSpace(line.OrderLineID) == "" {
return errors.New("OrderLineID zorunlu")
}
code := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
if code == "" {
return errors.New("Yeni urun kodu zorunlu")
}
if !baggiModelCodeRegex.MatchString(code) {
return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999")
}
}
return nil
}
func writeDBError(w http.ResponseWriter, status int, step string, orderHeaderID string, username string, lineCount int, err error) {
var sqlErr mssql.Error
if errors.As(err, &sqlErr) {
log.Printf(
"❌ SQL error step=%s orderHeaderID=%s user=%s lineCount=%d number=%d state=%d class=%d server=%s proc=%s line=%d message=%s",
step, orderHeaderID, username, lineCount,
sqlErr.Number, sqlErr.State, sqlErr.Class, sqlErr.ServerName, sqlErr.ProcName, sqlErr.LineNo, sqlErr.Message,
)
} else {
log.Printf(
"❌ DB error step=%s orderHeaderID=%s user=%s lineCount=%d err=%v",
step, orderHeaderID, username, lineCount, err,
)
}
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Veritabani hatasi",
"step": step,
"detail": err.Error(),
})
}
func sendProductionUpdateMails(db *sql.DB, ml *mailer.GraphMailer, orderHeaderID string, actor string, lines []models.OrderProductionUpdateLine) {
if len(lines) == 0 {
return
}
// Sipariş bağlamını çöz
orderNo, currAccCode, marketCode, marketTitle, err := resolveOrderMailContext(db, orderHeaderID)
if err != nil {
log.Printf("[sendProductionUpdateMails] context error: %v", err)
return
}
// Piyasa alıcılarını yükle (PG db lazım ama burada mssql üzerinden sadece log atalım veya graphmailer üzerinden gönderelim)
// Not: PG bağlantısı Route içinde yok, ancak mailer.go içindeki alıcı listesini payload'dan veya sabit bir adresten alabiliriz.
// Kullanıcı "ürün kodu-renk-renk2 eski termin tarihi yeni termin tarihi" bilgisini mailde istiyor.
subject := fmt.Sprintf("%s tarafından %s Nolu Sipariş Güncellendi (Üretim)", actor, orderNo)
var body strings.Builder
body.WriteString("<html><body>")
body.WriteString(fmt.Sprintf("<p><b>Sipariş No:</b> %s</p>", orderNo))
body.WriteString(fmt.Sprintf("<p><b>Cari:</b> %s</p>", currAccCode))
body.WriteString(fmt.Sprintf("<p><b>Piyasa:</b> %s (%s)</p>", marketTitle, marketCode))
body.WriteString("<p>Aşağıdaki satırlarda termin tarihi güncellenmiştir:</p>")
body.WriteString("<table border='1' cellpadding='5' style='border-collapse: collapse;'>")
body.WriteString("<tr style='background-color: #f2f2f2;'><th>Ürün Kodu</th><th>Renk</th><th>2. Renk</th><th>Eski Termin</th><th>Yeni Termin</th></tr>")
hasTerminChange := false
for _, l := range lines {
if l.OldDueDate != l.NewDueDate && l.NewDueDate != "" {
hasTerminChange = true
body.WriteString("<tr>")
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewItemCode))
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewColor))
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewDim2))
body.WriteString(fmt.Sprintf("<td>%s</td>", l.OldDueDate))
body.WriteString(fmt.Sprintf("<td style='color: red; font-weight: bold;'>%s</td>", l.NewDueDate))
body.WriteString("</tr>")
}
}
body.WriteString("</table>")
body.WriteString("<p><i>Bu mail sistem tarafından otomatik oluşturulmuştur.</i></p>")
body.WriteString("</body></html>")
if !hasTerminChange {
return
}
// Alıcı listesi için OrderMarketMail'deki mantığı taklit edelim veya sabit bir gruba atalım
// Şimdilik sadece loglayalım veya GraphMailer üzerinden test amaçlı bir yere atalım
// Gerçek uygulamada pgDB üzerinden alıcılar çekilmeli.
recipients := []string{"urun@baggi.com.tr"} // Varsayılan alıcı
msg := mailer.Message{
To: recipients,
Subject: subject,
BodyHTML: body.String(),
}
if err := ml.Send(context.Background(), msg); err != nil {
log.Printf("[sendProductionUpdateMails] send error: %v", err)
} else {
log.Printf("[sendProductionUpdateMails] mail sent to %v", recipients)
}
}