Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -3,32 +3,120 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/microsoft/go-mssqldb"
|
||||
)
|
||||
|
||||
var MssqlDB *sql.DB
|
||||
|
||||
// ConnectMSSQL MSSQL baglantisini ortam degiskeninden baslatir.
|
||||
func envInt(name string, fallback int) int {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func ensureTimeoutValue(current string, desired int) string {
|
||||
cur, err := strconv.Atoi(strings.TrimSpace(current))
|
||||
if err == nil && cur >= desired {
|
||||
return strings.TrimSpace(current)
|
||||
}
|
||||
return strconv.Itoa(desired)
|
||||
}
|
||||
|
||||
func ensureMSSQLTimeouts(connString string, connectionTimeoutSec int, dialTimeoutSec int) string {
|
||||
raw := strings.TrimSpace(connString)
|
||||
if raw == "" {
|
||||
return raw
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(raw), "sqlserver://") {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return raw
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("connection timeout", ensureTimeoutValue(q.Get("connection timeout"), connectionTimeoutSec))
|
||||
q.Set("dial timeout", ensureTimeoutValue(q.Get("dial timeout"), dialTimeoutSec))
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
parts := strings.Split(raw, ";")
|
||||
foundConnectionTimeout := false
|
||||
foundDialTimeout := false
|
||||
|
||||
for i, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
eq := strings.Index(part, "=")
|
||||
if eq <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.ToLower(strings.TrimSpace(part[:eq]))
|
||||
value := strings.TrimSpace(part[eq+1:])
|
||||
|
||||
switch key {
|
||||
case "connection timeout":
|
||||
foundConnectionTimeout = true
|
||||
parts[i] = "connection timeout=" + ensureTimeoutValue(value, connectionTimeoutSec)
|
||||
case "dial timeout":
|
||||
foundDialTimeout = true
|
||||
parts[i] = "dial timeout=" + ensureTimeoutValue(value, dialTimeoutSec)
|
||||
}
|
||||
}
|
||||
|
||||
if !foundConnectionTimeout {
|
||||
parts = append(parts, "connection timeout="+strconv.Itoa(connectionTimeoutSec))
|
||||
}
|
||||
if !foundDialTimeout {
|
||||
parts = append(parts, "dial timeout="+strconv.Itoa(dialTimeoutSec))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ";")
|
||||
}
|
||||
|
||||
// ConnectMSSQL initializes the MSSQL connection from environment.
|
||||
func ConnectMSSQL() error {
|
||||
connString := strings.TrimSpace(os.Getenv("MSSQL_CONN"))
|
||||
if connString == "" {
|
||||
return fmt.Errorf("MSSQL_CONN tanımlı değil")
|
||||
return fmt.Errorf("MSSQL_CONN tanimli degil")
|
||||
}
|
||||
|
||||
connectionTimeoutSec := envInt("MSSQL_CONNECTION_TIMEOUT_SEC", 120)
|
||||
dialTimeoutSec := envInt("MSSQL_DIAL_TIMEOUT_SEC", connectionTimeoutSec)
|
||||
connString = ensureMSSQLTimeouts(connString, connectionTimeoutSec, dialTimeoutSec)
|
||||
|
||||
var err error
|
||||
MssqlDB, err = sql.Open("sqlserver", connString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MSSQL bağlantı hatası: %w", err)
|
||||
return fmt.Errorf("MSSQL baglanti hatasi: %w", err)
|
||||
}
|
||||
|
||||
MssqlDB.SetMaxOpenConns(envInt("MSSQL_MAX_OPEN_CONNS", 40))
|
||||
MssqlDB.SetMaxIdleConns(envInt("MSSQL_MAX_IDLE_CONNS", 40))
|
||||
MssqlDB.SetConnMaxLifetime(time.Duration(envInt("MSSQL_CONN_MAX_LIFETIME_MIN", 30)) * time.Minute)
|
||||
MssqlDB.SetConnMaxIdleTime(time.Duration(envInt("MSSQL_CONN_MAX_IDLE_MIN", 10)) * time.Minute)
|
||||
|
||||
if err = MssqlDB.Ping(); err != nil {
|
||||
return fmt.Errorf("MSSQL erişilemiyor: %w", err)
|
||||
return fmt.Errorf("MSSQL erisilemiyor: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("MSSQL bağlantısı başarılı")
|
||||
fmt.Printf("MSSQL baglantisi basarili (connection timeout=%ds, dial timeout=%ds)\n", connectionTimeoutSec, dialTimeoutSec)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
13
svc/main.go
13
svc/main.go
@@ -526,7 +526,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
{"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)},
|
||||
{"/api/orders/production-items/{id}/insert-missing", "POST", "update", routes.OrderProductionInsertMissingRoute(mssql)},
|
||||
{"/api/orders/production-items/{id}/validate", "POST", "update", routes.OrderProductionValidateRoute(mssql)},
|
||||
{"/api/orders/production-items/{id}/apply", "POST", "update", routes.OrderProductionApplyRoute(mssql)},
|
||||
{"/api/orders/production-items/{id}/apply", "POST", "update", routes.OrderProductionApplyRoute(mssql, ml)},
|
||||
{"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
|
||||
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
|
||||
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
|
||||
@@ -571,6 +571,12 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
wrapV3(http.HandlerFunc(routes.GetProductDetailHandler)),
|
||||
)
|
||||
|
||||
bindV3(r, pgDB,
|
||||
"/api/product-cditem", "GET",
|
||||
"order", "view",
|
||||
wrapV3(http.HandlerFunc(routes.GetProductCdItemHandler)),
|
||||
)
|
||||
|
||||
bindV3(r, pgDB,
|
||||
"/api/product-colors", "GET",
|
||||
"order", "view",
|
||||
@@ -638,6 +644,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
"order", "view",
|
||||
wrapV3(routes.GetProductSizeMatchRulesHandler(pgDB)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/pricing/products", "GET",
|
||||
"order", "view",
|
||||
wrapV3(http.HandlerFunc(routes.GetProductPricingListHandler)),
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// ROLE MANAGEMENT
|
||||
|
||||
@@ -11,15 +11,19 @@ type OrderProductionItem struct {
|
||||
OldDim1 string `json:"OldDim1"`
|
||||
OldDim3 string `json:"OldDim3"`
|
||||
|
||||
OldItemCode string `json:"OldItemCode"`
|
||||
OldColor string `json:"OldColor"`
|
||||
OldDim2 string `json:"OldDim2"`
|
||||
OldDesc string `json:"OldDesc"`
|
||||
OldItemCode string `json:"OldItemCode"`
|
||||
OldColor string `json:"OldColor"`
|
||||
OldColorDescription string `json:"OldColorDescription"`
|
||||
OldDim2 string `json:"OldDim2"`
|
||||
OldDesc string `json:"OldDesc"`
|
||||
OldQty float64 `json:"OldQty"`
|
||||
|
||||
NewItemCode string `json:"NewItemCode"`
|
||||
NewColor string `json:"NewColor"`
|
||||
NewDim2 string `json:"NewDim2"`
|
||||
NewDesc string `json:"NewDesc"`
|
||||
|
||||
IsVariantMissing bool `json:"IsVariantMissing"`
|
||||
IsVariantMissing bool `json:"IsVariantMissing"`
|
||||
OldDueDate string `json:"OldDueDate"`
|
||||
NewDueDate string `json:"NewDueDate"`
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package models
|
||||
|
||||
type OrderProductionUpdateLine struct {
|
||||
OrderLineID string `json:"OrderLineID"`
|
||||
NewItemCode string `json:"NewItemCode"`
|
||||
NewColor string `json:"NewColor"`
|
||||
NewDim2 string `json:"NewDim2"`
|
||||
NewDesc string `json:"NewDesc"`
|
||||
OrderLineID string `json:"OrderLineID"`
|
||||
NewItemCode string `json:"NewItemCode"`
|
||||
NewColor string `json:"NewColor"`
|
||||
ItemDim1Code *string `json:"ItemDim1Code,omitempty"`
|
||||
NewDim2 string `json:"NewDim2"`
|
||||
NewDesc string `json:"NewDesc"`
|
||||
OldDueDate string `json:"OldDueDate"`
|
||||
NewDueDate string `json:"NewDueDate"`
|
||||
}
|
||||
|
||||
type OrderProductionUpdatePayload struct {
|
||||
Lines []OrderProductionUpdateLine `json:"lines"`
|
||||
InsertMissing bool `json:"insertMissing"`
|
||||
CdItems []OrderProductionCdItemDraft `json:"cdItems"`
|
||||
ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"`
|
||||
Lines []OrderProductionUpdateLine `json:"lines"`
|
||||
InsertMissing bool `json:"insertMissing"`
|
||||
CdItems []OrderProductionCdItemDraft `json:"cdItems"`
|
||||
ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"`
|
||||
HeaderAverageDueDate *string `json:"HeaderAverageDueDate,omitempty"`
|
||||
}
|
||||
|
||||
type OrderProductionMissingVariant struct {
|
||||
@@ -25,6 +29,19 @@ type OrderProductionMissingVariant struct {
|
||||
ItemDim3Code string `json:"ItemDim3Code"`
|
||||
}
|
||||
|
||||
type OrderProductionBarcodeValidation struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Barcode string `json:"barcode,omitempty"`
|
||||
BarcodeTypeCode string `json:"barcodeTypeCode,omitempty"`
|
||||
ItemTypeCode int16 `json:"ItemTypeCode,omitempty"`
|
||||
ItemCode string `json:"ItemCode,omitempty"`
|
||||
ColorCode string `json:"ColorCode,omitempty"`
|
||||
ItemDim1Code string `json:"ItemDim1Code,omitempty"`
|
||||
ItemDim2Code string `json:"ItemDim2Code,omitempty"`
|
||||
ItemDim3Code string `json:"ItemDim3Code,omitempty"`
|
||||
}
|
||||
|
||||
type OrderProductionCdItemDraft struct {
|
||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||
ItemCode string `json:"ItemCode"`
|
||||
|
||||
18
svc/models/product_pricing.go
Normal file
18
svc/models/product_pricing.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
type ProductPricing struct {
|
||||
ProductCode string `json:"ProductCode"`
|
||||
CostPrice float64 `json:"CostPrice"`
|
||||
StockQty float64 `json:"StockQty"`
|
||||
StockEntryDate string `json:"StockEntryDate"`
|
||||
LastPricingDate string `json:"LastPricingDate"`
|
||||
AskiliYan string `json:"AskiliYan"`
|
||||
Kategori string `json:"Kategori"`
|
||||
UrunIlkGrubu string `json:"UrunIlkGrubu"`
|
||||
UrunAnaGrubu string `json:"UrunAnaGrubu"`
|
||||
UrunAltGrubu string `json:"UrunAltGrubu"`
|
||||
Icerik string `json:"Icerik"`
|
||||
Karisim string `json:"Karisim"`
|
||||
Marka string `json:"Marka"`
|
||||
BrandGroupSec string `json:"BrandGroupSec"`
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -25,14 +26,24 @@ SELECT
|
||||
|
||||
ISNULL(l.ItemCode,'') AS OldItemCode,
|
||||
ISNULL(l.ColorCode,'') AS OldColor,
|
||||
ISNULL((
|
||||
SELECT TOP 1 LTRIM(RTRIM(cd.ColorDescription))
|
||||
FROM dbo.cdColorDesc cd WITH (NOLOCK)
|
||||
WHERE cd.ColorCode = l.ColorCode
|
||||
AND cd.LangCode = N'TR'
|
||||
), '') AS OldColorDescription,
|
||||
ISNULL(l.ItemDim2Code,'') AS OldDim2,
|
||||
ISNULL(l.LineDescription,'') AS OldDesc,
|
||||
CAST(ISNULL(l.Qty1, 0) AS FLOAT) AS OldQty,
|
||||
|
||||
CAST('' AS NVARCHAR(60)) AS NewItemCode,
|
||||
CAST('' AS NVARCHAR(30)) AS NewColor,
|
||||
CAST('' AS NVARCHAR(30)) AS NewDim2,
|
||||
CAST('' AS NVARCHAR(250)) AS NewDesc,
|
||||
|
||||
CONVERT(NVARCHAR(10), l.DeliveryDate, 126) AS OldDueDate,
|
||||
CONVERT(NVARCHAR(10), l.DeliveryDate, 126) AS NewDueDate,
|
||||
|
||||
CAST(0 AS bit) AS IsVariantMissing
|
||||
FROM dbo.trOrderLine l
|
||||
WHERE l.OrderHeaderID = @p1
|
||||
@@ -522,18 +533,25 @@ func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderPr
|
||||
chunk := lines[i:end]
|
||||
|
||||
values := make([]string, 0, len(chunk))
|
||||
args := make([]any, 0, len(chunk)*5+2)
|
||||
args := make([]any, 0, len(chunk)*8+2)
|
||||
paramPos := 1
|
||||
for _, line := range chunk {
|
||||
values = append(values, fmt.Sprintf("(@p%d,@p%d,@p%d,@p%d,@p%d)", paramPos, paramPos+1, paramPos+2, paramPos+3, paramPos+4))
|
||||
var itemDim1 any
|
||||
if line.ItemDim1Code != nil {
|
||||
itemDim1 = strings.TrimSpace(*line.ItemDim1Code)
|
||||
}
|
||||
values = append(values, fmt.Sprintf("(@p%d,@p%d,@p%d,@p%d,@p%d,@p%d,@p%d,@p%d)", paramPos, paramPos+1, paramPos+2, paramPos+3, paramPos+4, paramPos+5, paramPos+6, paramPos+7))
|
||||
args = append(args,
|
||||
strings.TrimSpace(line.OrderLineID),
|
||||
line.NewItemCode,
|
||||
line.NewColor,
|
||||
itemDim1,
|
||||
line.NewDim2,
|
||||
line.NewDesc,
|
||||
line.OldDueDate,
|
||||
line.NewDueDate,
|
||||
)
|
||||
paramPos += 5
|
||||
paramPos += 8
|
||||
}
|
||||
|
||||
orderHeaderParam := paramPos
|
||||
@@ -542,16 +560,18 @@ func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderPr
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SET NOCOUNT ON;
|
||||
WITH src (OrderLineID, NewItemCode, NewColor, NewDim2, NewDesc) AS (
|
||||
WITH src (OrderLineID, NewItemCode, NewColor, ItemDim1Code, NewDim2, NewDesc, OldDueDate, NewDueDate) AS (
|
||||
SELECT *
|
||||
FROM (VALUES %s) AS v (OrderLineID, NewItemCode, NewColor, NewDim2, NewDesc)
|
||||
FROM (VALUES %s) AS v (OrderLineID, NewItemCode, NewColor, ItemDim1Code, NewDim2, NewDesc, OldDueDate, NewDueDate)
|
||||
)
|
||||
UPDATE l
|
||||
SET
|
||||
l.ItemCode = s.NewItemCode,
|
||||
l.ColorCode = s.NewColor,
|
||||
l.ItemDim1Code = COALESCE(s.ItemDim1Code, l.ItemDim1Code),
|
||||
l.ItemDim2Code = s.NewDim2,
|
||||
l.LineDescription = COALESCE(NULLIF(s.NewDesc,''), l.LineDescription),
|
||||
l.DeliveryDate = CASE WHEN ISDATE(s.NewDueDate) = 1 THEN CAST(s.NewDueDate AS DATETIME) ELSE l.DeliveryDate END,
|
||||
l.LastUpdatedUserName = @p%d,
|
||||
l.LastUpdatedDate = GETDATE()
|
||||
FROM dbo.trOrderLine l
|
||||
@@ -574,6 +594,344 @@ WHERE l.OrderHeaderID = @p%d;
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func UpdateOrderHeaderAverageDueDateTx(tx *sql.Tx, orderHeaderID string, averageDueDate *string, username string) error {
|
||||
if averageDueDate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dueDate := strings.TrimSpace(*averageDueDate)
|
||||
if dueDate != "" {
|
||||
if _, err := time.Parse("2006-01-02", dueDate); err != nil {
|
||||
return fmt.Errorf("invalid header average due date %q: %w", dueDate, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := tx.Exec(`
|
||||
UPDATE dbo.trOrderHeader
|
||||
SET
|
||||
AverageDueDate = CASE WHEN @p1 = '' THEN NULL ELSE CAST(@p1 AS DATETIME) END,
|
||||
LastUpdatedUserName = @p2,
|
||||
LastUpdatedDate = GETDATE()
|
||||
WHERE OrderHeaderID = @p3;
|
||||
`, dueDate, username, orderHeaderID)
|
||||
return err
|
||||
}
|
||||
|
||||
type sqlQueryRower interface {
|
||||
QueryRow(query string, args ...any) *sql.Row
|
||||
}
|
||||
|
||||
type plannedProductionBarcode struct {
|
||||
Barcode string
|
||||
BarcodeTypeCode string
|
||||
ItemTypeCode int16
|
||||
ItemCode string
|
||||
ColorCode string
|
||||
ItemDim1Code string
|
||||
ItemDim2Code string
|
||||
ItemDim3Code string
|
||||
}
|
||||
|
||||
func barcodeTypeExists(q sqlQueryRower, barcodeTypeCode string) (bool, error) {
|
||||
var exists int
|
||||
err := q.QueryRow(`
|
||||
SELECT TOP 1 1
|
||||
FROM dbo.cdBarcodeType
|
||||
WHERE BarcodeTypeCode = @p1
|
||||
`, strings.TrimSpace(barcodeTypeCode)).Scan(&exists)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func barcodeExists(q sqlQueryRower, barcode string) (bool, error) {
|
||||
var exists int
|
||||
err := q.QueryRow(`
|
||||
SELECT TOP 1 1
|
||||
FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK)
|
||||
WHERE Barcode = @p1
|
||||
`, strings.TrimSpace(barcode)).Scan(&exists)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func existingVariantBarcode(q sqlQueryRower, barcodeTypeCode string, itemTypeCode int16, itemCode string, colorCode string, dim1 string, dim2 string, dim3 string) (string, bool, error) {
|
||||
var barcode string
|
||||
err := q.QueryRow(`
|
||||
SELECT TOP 1 LTRIM(RTRIM(ISNULL(Barcode, '')))
|
||||
FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK)
|
||||
WHERE BarcodeTypeCode = @p1
|
||||
AND ItemTypeCode = @p2
|
||||
AND ISNULL(LTRIM(RTRIM(ItemCode)), '') = @p3
|
||||
AND ISNULL(LTRIM(RTRIM(ColorCode)), '') = @p4
|
||||
AND ISNULL(LTRIM(RTRIM(ItemDim1Code)), '') = @p5
|
||||
AND ISNULL(LTRIM(RTRIM(ItemDim2Code)), '') = @p6
|
||||
AND ISNULL(LTRIM(RTRIM(ItemDim3Code)), '') = @p7
|
||||
AND ISNULL(LTRIM(RTRIM(UnitOfMeasureCode)), '') = 'AD'
|
||||
ORDER BY TRY_CONVERT(BIGINT, NULLIF(LTRIM(RTRIM(Barcode)), '')) DESC, Barcode DESC
|
||||
`, strings.TrimSpace(barcodeTypeCode), itemTypeCode, strings.TrimSpace(itemCode), strings.TrimSpace(colorCode), strings.TrimSpace(dim1), strings.TrimSpace(dim2), strings.TrimSpace(dim3)).Scan(&barcode)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return strings.TrimSpace(barcode), true, nil
|
||||
}
|
||||
|
||||
func maxNumericBarcode(q sqlQueryRower) (int64, error) {
|
||||
var maxBarcode int64
|
||||
err := q.QueryRow(`
|
||||
SELECT ISNULL(MAX(TRY_CONVERT(BIGINT, NULLIF(LTRIM(RTRIM(Barcode)), ''))), 0)
|
||||
FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK)
|
||||
`).Scan(&maxBarcode)
|
||||
return maxBarcode, err
|
||||
}
|
||||
|
||||
func ValidateProductionBarcodePlan(q sqlQueryRower, variants []models.OrderProductionMissingVariant, barcodeTypeCode string) ([]models.OrderProductionBarcodeValidation, error) {
|
||||
typeCode := strings.ToUpper(strings.TrimSpace(barcodeTypeCode))
|
||||
if len(variants) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
validations := make([]models.OrderProductionBarcodeValidation, 0)
|
||||
typeExists, err := barcodeTypeExists(q, typeCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !typeExists {
|
||||
validations = append(validations, models.OrderProductionBarcodeValidation{
|
||||
Code: "invalid_barcode_type",
|
||||
Message: fmt.Sprintf("Barkod tipi bulunamadi: %s", typeCode),
|
||||
BarcodeTypeCode: typeCode,
|
||||
})
|
||||
return validations, nil
|
||||
}
|
||||
|
||||
sorted := append([]models.OrderProductionMissingVariant(nil), variants...)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
left := sorted[i]
|
||||
right := sorted[j]
|
||||
leftKey := fmt.Sprintf("%05d|%s|%s|%s|%s|%s", left.ItemTypeCode, left.ItemCode, left.ColorCode, left.ItemDim1Code, left.ItemDim2Code, left.ItemDim3Code)
|
||||
rightKey := fmt.Sprintf("%05d|%s|%s|%s|%s|%s", right.ItemTypeCode, right.ItemCode, right.ColorCode, right.ItemDim1Code, right.ItemDim2Code, right.ItemDim3Code)
|
||||
return leftKey < rightKey
|
||||
})
|
||||
|
||||
maxBarcode, err := maxNumericBarcode(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nextOffset := int64(0)
|
||||
planned := make(map[string]struct{}, len(sorted))
|
||||
for _, variant := range sorted {
|
||||
existingBarcode, exists, err := existingVariantBarcode(q, typeCode, variant.ItemTypeCode, variant.ItemCode, variant.ColorCode, variant.ItemDim1Code, variant.ItemDim2Code, variant.ItemDim3Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists && existingBarcode != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
nextOffset++
|
||||
barcode := strconv.FormatInt(maxBarcode+nextOffset, 10)
|
||||
if _, duplicated := planned[barcode]; duplicated {
|
||||
validations = append(validations, models.OrderProductionBarcodeValidation{
|
||||
Code: "barcode_duplicate_in_plan",
|
||||
Message: fmt.Sprintf("Planlanan barkod ayni istekte birden fazla kez olusuyor: %s", barcode),
|
||||
Barcode: barcode,
|
||||
BarcodeTypeCode: typeCode,
|
||||
ItemTypeCode: variant.ItemTypeCode,
|
||||
ItemCode: strings.TrimSpace(variant.ItemCode),
|
||||
ColorCode: strings.TrimSpace(variant.ColorCode),
|
||||
ItemDim1Code: strings.TrimSpace(variant.ItemDim1Code),
|
||||
ItemDim2Code: strings.TrimSpace(variant.ItemDim2Code),
|
||||
ItemDim3Code: strings.TrimSpace(variant.ItemDim3Code),
|
||||
})
|
||||
continue
|
||||
}
|
||||
planned[barcode] = struct{}{}
|
||||
|
||||
inUse, err := barcodeExists(q, barcode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inUse {
|
||||
validations = append(validations, models.OrderProductionBarcodeValidation{
|
||||
Code: "barcode_in_use",
|
||||
Message: fmt.Sprintf("Barkod daha once kullanilmis: %s (%s / %s / %s / %s)", barcode, strings.TrimSpace(variant.ItemCode), strings.TrimSpace(variant.ColorCode), strings.TrimSpace(variant.ItemDim1Code), strings.TrimSpace(variant.ItemDim2Code)),
|
||||
Barcode: barcode,
|
||||
BarcodeTypeCode: typeCode,
|
||||
ItemTypeCode: variant.ItemTypeCode,
|
||||
ItemCode: strings.TrimSpace(variant.ItemCode),
|
||||
ColorCode: strings.TrimSpace(variant.ColorCode),
|
||||
ItemDim1Code: strings.TrimSpace(variant.ItemDim1Code),
|
||||
ItemDim2Code: strings.TrimSpace(variant.ItemDim2Code),
|
||||
ItemDim3Code: strings.TrimSpace(variant.ItemDim3Code),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return validations, nil
|
||||
}
|
||||
|
||||
func UpsertItemBarcodesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderProductionUpdateLine, username string) (int64, error) {
|
||||
start := time.Now()
|
||||
if len(lines) == 0 {
|
||||
log.Printf("[UpsertItemBarcodesTx] lines=0 inserted=0 duration_ms=0")
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
lineIDs := make([]string, 0, len(lines))
|
||||
seen := make(map[string]struct{}, len(lines))
|
||||
for _, line := range lines {
|
||||
lineID := strings.TrimSpace(line.OrderLineID)
|
||||
if lineID == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[lineID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[lineID] = struct{}{}
|
||||
lineIDs = append(lineIDs, lineID)
|
||||
}
|
||||
if len(lineIDs) == 0 {
|
||||
log.Printf("[UpsertItemBarcodesTx] lines=%d uniqueLineIDs=0 inserted=0 duration_ms=%d", len(lines), time.Since(start).Milliseconds())
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
const chunkSize = 900
|
||||
var inserted int64
|
||||
|
||||
for i := 0; i < len(lineIDs); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > len(lineIDs) {
|
||||
end = len(lineIDs)
|
||||
}
|
||||
chunk := lineIDs[i:end]
|
||||
|
||||
values := make([]string, 0, len(chunk))
|
||||
args := make([]any, 0, len(chunk)+2)
|
||||
paramPos := 1
|
||||
for _, lineID := range chunk {
|
||||
values = append(values, fmt.Sprintf("(@p%d)", paramPos))
|
||||
args = append(args, lineID)
|
||||
paramPos++
|
||||
}
|
||||
|
||||
orderHeaderParam := paramPos
|
||||
usernameParam := paramPos + 1
|
||||
args = append(args, orderHeaderID, username)
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SET NOCOUNT ON;
|
||||
WITH srcLine (OrderLineID) AS (
|
||||
SELECT *
|
||||
FROM (VALUES %s) AS v (OrderLineID)
|
||||
),
|
||||
src AS (
|
||||
SELECT DISTINCT
|
||||
l.ItemTypeCode,
|
||||
UPPER(LTRIM(RTRIM(ISNULL(l.ItemCode, '')))) AS ItemCode,
|
||||
UPPER(LTRIM(RTRIM(ISNULL(l.ColorCode, '')))) AS ColorCode,
|
||||
UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim1Code, '')))) AS ItemDim1Code,
|
||||
UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim2Code, '')))) AS ItemDim2Code,
|
||||
CAST('' AS NVARCHAR(50)) AS ItemDim3Code
|
||||
FROM dbo.trOrderLine l WITH (UPDLOCK, HOLDLOCK)
|
||||
JOIN srcLine s
|
||||
ON CAST(l.OrderLineID AS NVARCHAR(50)) = s.OrderLineID
|
||||
WHERE l.OrderHeaderID = @p%d
|
||||
AND NULLIF(LTRIM(RTRIM(ISNULL(l.ItemCode, ''))), '') IS NOT NULL
|
||||
),
|
||||
missing AS (
|
||||
SELECT
|
||||
s.ItemTypeCode,
|
||||
s.ItemCode,
|
||||
s.ColorCode,
|
||||
s.ItemDim1Code,
|
||||
s.ItemDim2Code,
|
||||
s.ItemDim3Code,
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY s.ItemCode, s.ColorCode, s.ItemDim1Code, s.ItemDim2Code, s.ItemDim3Code
|
||||
) AS RowNo
|
||||
FROM src s
|
||||
LEFT JOIN dbo.prItemBarcode b WITH (UPDLOCK, HOLDLOCK)
|
||||
ON UPPER(LTRIM(RTRIM(ISNULL(b.BarcodeTypeCode, '')))) = 'BAGGI3'
|
||||
AND b.ItemTypeCode = s.ItemTypeCode
|
||||
AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemCode, '')))) = s.ItemCode
|
||||
AND UPPER(LTRIM(RTRIM(ISNULL(b.ColorCode, '')))) = s.ColorCode
|
||||
AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemDim1Code, '')))) = s.ItemDim1Code
|
||||
AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemDim2Code, '')))) = s.ItemDim2Code
|
||||
AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemDim3Code, '')))) = s.ItemDim3Code
|
||||
AND UPPER(LTRIM(RTRIM(ISNULL(b.UnitOfMeasureCode, '')))) = 'AD'
|
||||
WHERE b.Barcode IS NULL
|
||||
)
|
||||
INSERT INTO dbo.prItemBarcode (
|
||||
Barcode,
|
||||
BarcodeTypeCode,
|
||||
ItemTypeCode,
|
||||
ItemCode,
|
||||
ColorCode,
|
||||
ItemDim1Code,
|
||||
ItemDim2Code,
|
||||
ItemDim3Code,
|
||||
UnitOfMeasureCode,
|
||||
Qty,
|
||||
CreatedUserName,
|
||||
CreatedDate,
|
||||
LastUpdatedUserName,
|
||||
LastUpdatedDate,
|
||||
RowGuid
|
||||
)
|
||||
SELECT
|
||||
CAST(seed.MaxBarcode + m.RowNo AS NVARCHAR(50)) AS Barcode,
|
||||
'BAGGI3',
|
||||
m.ItemTypeCode,
|
||||
m.ItemCode,
|
||||
m.ColorCode,
|
||||
m.ItemDim1Code,
|
||||
m.ItemDim2Code,
|
||||
m.ItemDim3Code,
|
||||
'AD',
|
||||
1,
|
||||
@p%d,
|
||||
GETDATE(),
|
||||
@p%d,
|
||||
GETDATE(),
|
||||
NEWID()
|
||||
FROM missing m
|
||||
CROSS JOIN (
|
||||
SELECT ISNULL(MAX(TRY_CONVERT(BIGINT, NULLIF(LTRIM(RTRIM(Barcode)), ''))), 0) AS MaxBarcode
|
||||
FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK)
|
||||
) seed;
|
||||
|
||||
SELECT @@ROWCOUNT AS Inserted;
|
||||
`, strings.Join(values, ","), orderHeaderParam, usernameParam, usernameParam)
|
||||
|
||||
chunkStart := time.Now()
|
||||
var chunkInserted int64
|
||||
if err := tx.QueryRow(query, args...).Scan(&chunkInserted); err != nil {
|
||||
return inserted, fmt.Errorf("upsert item barcodes chunk failed chunkStart=%d chunkEnd=%d duration_ms=%d: %w", i, end, time.Since(chunkStart).Milliseconds(), err)
|
||||
}
|
||||
inserted += chunkInserted
|
||||
log.Printf("[UpsertItemBarcodesTx] orderHeaderID=%s chunk=%d-%d chunkInserted=%d cumulative=%d duration_ms=%d",
|
||||
orderHeaderID, i, end, chunkInserted, inserted, time.Since(chunkStart).Milliseconds())
|
||||
}
|
||||
|
||||
log.Printf("[UpsertItemBarcodesTx] orderHeaderID=%s lines=%d uniqueLineIDs=%d inserted=%d duration_ms=%d",
|
||||
orderHeaderID, len(lines), len(lineIDs), inserted, time.Since(start).Milliseconds())
|
||||
return inserted, nil
|
||||
}
|
||||
|
||||
func UpsertItemAttributesTx(tx *sql.Tx, attrs []models.OrderProductionItemAttributeRow, username string) (int64, error) {
|
||||
start := time.Now()
|
||||
if len(attrs) == 0 {
|
||||
|
||||
193
svc/queries/product_pricing.go
Normal file
193
svc/queries/product_pricing.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"bssapp-backend/db"
|
||||
"bssapp-backend/models"
|
||||
)
|
||||
|
||||
func GetProductPricingList() ([]models.ProductPricing, error) {
|
||||
rows, err := db.MssqlDB.Query(`
|
||||
WITH base_products AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
|
||||
FROM ProductFilterWithDescription('TR')
|
||||
WHERE ProductAtt42 IN ('SERI', 'AKSESUAR')
|
||||
AND IsBlocked = 0
|
||||
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
||||
),
|
||||
latest_base_price AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
|
||||
CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice,
|
||||
CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY LTRIM(RTRIM(b.ItemCode))
|
||||
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
|
||||
) AS rn
|
||||
FROM prItemBasePrice b
|
||||
WHERE b.ItemTypeCode = 1
|
||||
AND b.BasePriceCode = 1
|
||||
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(b.ItemCode))
|
||||
)
|
||||
),
|
||||
stock_entry_dates AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
|
||||
CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate
|
||||
FROM trStock s WITH(NOLOCK)
|
||||
WHERE s.ItemTypeCode = 1
|
||||
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||
AND s.In_Qty1 > 0
|
||||
AND LTRIM(RTRIM(s.WarehouseCode)) IN (
|
||||
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
|
||||
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
|
||||
'1-0-33','101','1-014','1-0-49','1-0-36'
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||
),
|
||||
stock_base AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
|
||||
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
|
||||
FROM trStock s WITH(NOLOCK)
|
||||
WHERE s.ItemTypeCode = 1
|
||||
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||
),
|
||||
pick_base AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(p.ItemCode)) AS ItemCode,
|
||||
SUM(p.Qty1) AS PickingQty1
|
||||
FROM PickingStates p
|
||||
WHERE p.ItemTypeCode = 1
|
||||
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(p.ItemCode))
|
||||
),
|
||||
reserve_base AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(r.ItemCode)) AS ItemCode,
|
||||
SUM(r.Qty1) AS ReserveQty1
|
||||
FROM ReserveStates r
|
||||
WHERE r.ItemTypeCode = 1
|
||||
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(r.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(r.ItemCode))
|
||||
),
|
||||
disp_base AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(d.ItemCode)) AS ItemCode,
|
||||
SUM(d.Qty1) AS DispOrderQty1
|
||||
FROM DispOrderStates d
|
||||
WHERE d.ItemTypeCode = 1
|
||||
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(d.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(d.ItemCode))
|
||||
),
|
||||
stock_totals AS (
|
||||
SELECT
|
||||
bp.ProductCode AS ItemCode,
|
||||
CAST(ROUND(
|
||||
ISNULL(sb.InventoryQty1, 0)
|
||||
- ISNULL(pb.PickingQty1, 0)
|
||||
- ISNULL(rb.ReserveQty1, 0)
|
||||
- ISNULL(db.DispOrderQty1, 0)
|
||||
, 2) AS DECIMAL(18, 2)) AS StockQty
|
||||
FROM base_products bp
|
||||
LEFT JOIN stock_base sb
|
||||
ON sb.ItemCode = bp.ProductCode
|
||||
LEFT JOIN pick_base pb
|
||||
ON pb.ItemCode = bp.ProductCode
|
||||
LEFT JOIN reserve_base rb
|
||||
ON rb.ItemCode = bp.ProductCode
|
||||
LEFT JOIN disp_base db
|
||||
ON db.ItemCode = bp.ProductCode
|
||||
)
|
||||
SELECT
|
||||
bp.ProductCode AS ProductCode,
|
||||
COALESCE(lp.CostPrice, 0) AS CostPrice,
|
||||
COALESCE(st.StockQty, 0) AS StockQty,
|
||||
COALESCE(se.StockEntryDate, '') AS StockEntryDate,
|
||||
COALESCE(lp.LastPricingDate, '') AS LastPricingDate,
|
||||
bp.AskiliYan,
|
||||
bp.Kategori,
|
||||
bp.UrunIlkGrubu,
|
||||
bp.UrunAnaGrubu,
|
||||
bp.UrunAltGrubu,
|
||||
bp.Icerik,
|
||||
bp.Karisim,
|
||||
bp.Marka
|
||||
FROM base_products bp
|
||||
LEFT JOIN latest_base_price lp
|
||||
ON lp.ItemCode = bp.ProductCode
|
||||
AND lp.rn = 1
|
||||
LEFT JOIN stock_entry_dates se
|
||||
ON se.ItemCode = bp.ProductCode
|
||||
LEFT JOIN stock_totals st
|
||||
ON st.ItemCode = bp.ProductCode
|
||||
ORDER BY bp.ProductCode;
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.ProductPricing
|
||||
for rows.Next() {
|
||||
var item models.ProductPricing
|
||||
if err := rows.Scan(
|
||||
&item.ProductCode,
|
||||
&item.CostPrice,
|
||||
&item.StockQty,
|
||||
&item.StockEntryDate,
|
||||
&item.LastPricingDate,
|
||||
&item.AskiliYan,
|
||||
&item.Kategori,
|
||||
&item.UrunIlkGrubu,
|
||||
&item.UrunAnaGrubu,
|
||||
&item.UrunAltGrubu,
|
||||
&item.Icerik,
|
||||
&item.Karisim,
|
||||
&item.Marka,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -464,6 +464,7 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
||||
defer tx.Rollback()
|
||||
|
||||
var newID int64
|
||||
log.Printf("DEBUG: UserCreateRoute payload=%+v", payload)
|
||||
err = tx.QueryRow(`
|
||||
INSERT INTO mk_dfusr (
|
||||
username,
|
||||
@@ -472,11 +473,12 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
||||
email,
|
||||
mobile,
|
||||
address,
|
||||
password_hash,
|
||||
force_password_change,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,true,NOW(),NOW())
|
||||
VALUES ($1,$2,$3,$4,$5,$6,'',true,NOW(),NOW())
|
||||
RETURNING id
|
||||
`,
|
||||
payload.Code,
|
||||
@@ -489,7 +491,7 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
||||
|
||||
if err != nil {
|
||||
log.Printf("USER INSERT ERROR code=%q email=%q err=%v", payload.Code, payload.Email, err)
|
||||
http.Error(w, "Kullanıcı oluşturulamadı", http.StatusInternalServerError)
|
||||
http.Error(w, fmt.Sprintf("Kullanıcı oluşturulamadı: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,23 @@ import (
|
||||
)
|
||||
|
||||
type sendOrderMarketMailPayload struct {
|
||||
OrderHeaderID string `json:"orderHeaderID"`
|
||||
Operation string `json:"operation"`
|
||||
DeletedItems []string `json:"deletedItems"`
|
||||
UpdatedItems []string `json:"updatedItems"`
|
||||
AddedItems []string `json:"addedItems"`
|
||||
ExtraRecipients []string `json:"extraRecipients"`
|
||||
OrderHeaderID string `json:"orderHeaderID"`
|
||||
Operation string `json:"operation"`
|
||||
DeletedItems []string `json:"deletedItems"`
|
||||
UpdatedItems []string `json:"updatedItems"`
|
||||
AddedItems []string `json:"addedItems"`
|
||||
OldDueDate string `json:"oldDueDate"`
|
||||
NewDueDate string `json:"newDueDate"`
|
||||
ExtraRecipients []string `json:"extraRecipients"`
|
||||
DueDateChanges []sendOrderMailDueDateChange `json:"dueDateChanges"`
|
||||
}
|
||||
|
||||
type sendOrderMailDueDateChange struct {
|
||||
ItemCode string `json:"itemCode"`
|
||||
ColorCode string `json:"colorCode"`
|
||||
ItemDim2Code string `json:"itemDim2Code"`
|
||||
OldDueDate string `json:"oldDueDate"`
|
||||
NewDueDate string `json:"newDueDate"`
|
||||
}
|
||||
|
||||
func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) http.HandlerFunc {
|
||||
@@ -108,6 +119,18 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile
|
||||
if isUpdate {
|
||||
subjectAction = "SİPARİŞ GÜNCELLENDİ."
|
||||
}
|
||||
if payload.NewDueDate != "" && payload.OldDueDate != payload.NewDueDate {
|
||||
subjectAction = "SİPARİŞ TERMİNİ GÜNCELLENDİ."
|
||||
}
|
||||
if isUpdate && subjectAction == "SİPARİŞ GÜNCELLENDİ." {
|
||||
// Satır bazlı termin kontrolü
|
||||
for _, item := range payload.UpdatedItems {
|
||||
if strings.Contains(item, "Termin:") {
|
||||
subjectAction = "SİPARİŞ TERMİNİ GÜNCELLENDİ."
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
subject := fmt.Sprintf("%s kullanıcısı tarafından %s %s", actor, number, subjectAction)
|
||||
|
||||
cariDetail := ""
|
||||
@@ -127,6 +150,13 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile
|
||||
`</p>`,
|
||||
)
|
||||
|
||||
if payload.NewDueDate != "" && payload.OldDueDate != payload.NewDueDate {
|
||||
body = append(body,
|
||||
fmt.Sprintf(`<p><b>Termin Değişikliği:</b> %s → <b style="color:red">%s</b></p>`,
|
||||
htmlEsc(payload.OldDueDate), htmlEsc(payload.NewDueDate)),
|
||||
)
|
||||
}
|
||||
|
||||
if isUpdate {
|
||||
body = append(body,
|
||||
renderItemListHTML("Silinen Ürün Kodları", payload.DeletedItems),
|
||||
@@ -137,6 +167,10 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile
|
||||
|
||||
body = append(body, `<p><i>Bu sipariş BaggiSS App Uygulamasından oluşturulmuştur.</i></p>`)
|
||||
body = append(body, `<p>PDF ektedir.</p>`)
|
||||
if dueDateTableHTML := renderDueDateChangesTableHTML("Termin DeÄŸiÅŸiklikleri", payload.DueDateChanges); dueDateTableHTML != "" {
|
||||
body = append(body, dueDateTableHTML)
|
||||
}
|
||||
|
||||
bodyHTML := strings.Join(body, "\n")
|
||||
|
||||
fileNo := sanitizeFileName(number)
|
||||
@@ -393,3 +427,54 @@ func renderItemListHTML(title string, items []string) string {
|
||||
b = append(b, `</p>`)
|
||||
return strings.Join(b, "\n")
|
||||
}
|
||||
|
||||
func renderDueDateChangesTableHTML(title string, rows []sendOrderMailDueDateChange) string {
|
||||
if len(rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(rows))
|
||||
clean := make([]sendOrderMailDueDateChange, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
itemCode := strings.TrimSpace(row.ItemCode)
|
||||
colorCode := strings.TrimSpace(row.ColorCode)
|
||||
itemDim2Code := strings.TrimSpace(row.ItemDim2Code)
|
||||
oldDueDate := strings.TrimSpace(row.OldDueDate)
|
||||
newDueDate := strings.TrimSpace(row.NewDueDate)
|
||||
if itemCode == "" || newDueDate == "" || oldDueDate == newDueDate {
|
||||
continue
|
||||
}
|
||||
key := strings.ToUpper(strings.Join([]string{itemCode, colorCode, itemDim2Code, oldDueDate, newDueDate}, "|"))
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
clean = append(clean, sendOrderMailDueDateChange{
|
||||
ItemCode: itemCode,
|
||||
ColorCode: colorCode,
|
||||
ItemDim2Code: itemDim2Code,
|
||||
OldDueDate: oldDueDate,
|
||||
NewDueDate: newDueDate,
|
||||
})
|
||||
}
|
||||
|
||||
if len(clean) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf(`<p><b>%s:</b></p>`, htmlEsc(title)))
|
||||
b.WriteString(`<table border="1" cellpadding="5" style="border-collapse: collapse; width: 100%;">`)
|
||||
b.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>`)
|
||||
for _, row := range clean {
|
||||
b.WriteString("<tr>")
|
||||
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.ItemCode)))
|
||||
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.ColorCode)))
|
||||
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.ItemDim2Code)))
|
||||
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.OldDueDate)))
|
||||
b.WriteString(fmt.Sprintf(`<td style="color:red;font-weight:bold;">%s</td>`, htmlEsc(row.NewDueDate)))
|
||||
b.WriteString("</tr>")
|
||||
}
|
||||
b.WriteString(`</table>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package routes
|
||||
|
||||
import (
|
||||
"bssapp-backend/auth"
|
||||
"bssapp-backend/internal/mailer"
|
||||
"bssapp-backend/models"
|
||||
"bssapp-backend/queries"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -20,6 +22,8 @@ import (
|
||||
|
||||
var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`)
|
||||
|
||||
const productionBarcodeTypeCode = "BAGGI3"
|
||||
|
||||
// ======================================================
|
||||
// 📌 OrderProductionItemsRoute — U ürün satırları
|
||||
// ======================================================
|
||||
@@ -54,12 +58,16 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
|
||||
&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)
|
||||
@@ -183,9 +191,22 @@ func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
|
||||
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())
|
||||
|
||||
targetVariants, err := buildTargetVariants(mssql, id, payload.Lines)
|
||||
if err != nil {
|
||||
writeDBError(w, http.StatusInternalServerError, "validate_barcode_targets", id, "", len(payload.Lines), err)
|
||||
return
|
||||
}
|
||||
barcodeValidations, err := queries.ValidateProductionBarcodePlan(mssql, targetVariants, productionBarcodeTypeCode)
|
||||
if err != nil {
|
||||
writeDBError(w, http.StatusInternalServerError, "validate_barcodes", id, "", len(payload.Lines), err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"missingCount": len(missing),
|
||||
"missing": missing,
|
||||
"missingCount": len(missing),
|
||||
"missing": missing,
|
||||
"barcodeValidationCount": len(barcodeValidations),
|
||||
"barcodeValidations": barcodeValidations,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("❌ encode error: %v", err)
|
||||
@@ -196,7 +217,7 @@ func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
|
||||
// ======================================================
|
||||
// OrderProductionApplyRoute - yeni model varyant guncelleme
|
||||
// ======================================================
|
||||
func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
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())
|
||||
@@ -232,6 +253,12 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
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())
|
||||
|
||||
targetVariants, err := buildTargetVariants(mssql, id, payload.Lines)
|
||||
if err != nil {
|
||||
writeDBError(w, http.StatusInternalServerError, "apply_barcode_targets", id, "", len(payload.Lines), err)
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -282,6 +309,24 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
rid, id, inserted, time.Since(stepInsertMissingStart).Milliseconds())
|
||||
}
|
||||
|
||||
stepValidateBarcodeStart := time.Now()
|
||||
barcodeValidations, err := queries.ValidateProductionBarcodePlan(tx, targetVariants, productionBarcodeTypeCode)
|
||||
if err != nil {
|
||||
writeDBError(w, http.StatusInternalServerError, "validate_barcodes_before_apply", id, username, len(payload.Lines), err)
|
||||
return
|
||||
}
|
||||
if len(barcodeValidations) > 0 {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"message": "Barkod validasyonu basarisiz",
|
||||
"barcodeValidationCount": len(barcodeValidations),
|
||||
"barcodeValidations": barcodeValidations,
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=validate_barcodes count=%d duration_ms=%d",
|
||||
rid, id, len(barcodeValidations), time.Since(stepValidateBarcodeStart).Milliseconds())
|
||||
|
||||
stepValidateAttrStart := time.Now()
|
||||
if err := validateProductAttributes(payload.ProductAttributes); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -290,6 +335,14 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
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 {
|
||||
@@ -299,6 +352,15 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
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 {
|
||||
@@ -316,13 +378,27 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
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 attributeUpserted=%d",
|
||||
rid, id, updated, inserted, attributeAffected)
|
||||
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)
|
||||
}
|
||||
@@ -367,21 +443,20 @@ func buildCdItemDraftMap(list []models.OrderProductionCdItemDraft) map[string]mo
|
||||
return out
|
||||
}
|
||||
|
||||
func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
|
||||
func buildTargetVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
|
||||
start := time.Now()
|
||||
missing := make([]models.OrderProductionMissingVariant, 0)
|
||||
lineDimsMap, err := queries.GetOrderLineDimsMap(mssql, orderHeaderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existsCache := make(map[string]bool, len(lines))
|
||||
|
||||
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.TrimSpace(line.NewItemCode)
|
||||
newColor := strings.TrimSpace(line.NewColor)
|
||||
newDim2 := strings.TrimSpace(line.NewDim2)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -391,38 +466,68 @@ func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.Or
|
||||
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",
|
||||
dims.ItemTypeCode,
|
||||
strings.ToUpper(strings.TrimSpace(newItem)),
|
||||
strings.ToUpper(strings.TrimSpace(newColor)),
|
||||
strings.ToUpper(strings.TrimSpace(dims.ItemDim1Code)),
|
||||
strings.ToUpper(strings.TrimSpace(newDim2)),
|
||||
strings.ToUpper(strings.TrimSpace(dims.ItemDim3Code)),
|
||||
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, dims.ItemTypeCode, newItem, newColor, dims.ItemDim1Code, newDim2, dims.ItemDim3Code)
|
||||
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, models.OrderProductionMissingVariant{
|
||||
OrderLineID: lineID,
|
||||
ItemTypeCode: dims.ItemTypeCode,
|
||||
ItemCode: newItem,
|
||||
ColorCode: newColor,
|
||||
ItemDim1Code: dims.ItemDim1Code,
|
||||
ItemDim2Code: newDim2,
|
||||
ItemDim3Code: dims.ItemDim3Code,
|
||||
})
|
||||
missing = append(missing, target)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[buildMissingVariants] orderHeaderID=%s lineCount=%d dimMapCount=%d missingCount=%d total_ms=%d",
|
||||
orderHeaderID, len(lines), len(lineDimsMap), len(missing), time.Since(start).Milliseconds())
|
||||
orderHeaderID, len(lines), len(targets), len(missing), time.Since(start).Milliseconds())
|
||||
return missing, nil
|
||||
}
|
||||
|
||||
@@ -464,3 +569,69 @@ func writeDBError(w http.ResponseWriter, status int, step string, orderHeaderID
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,19 +15,21 @@ import (
|
||||
)
|
||||
|
||||
type ProductionUpdateLine struct {
|
||||
OrderLineID string `json:"OrderLineID"`
|
||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||
ItemCode string `json:"ItemCode"`
|
||||
ColorCode string `json:"ColorCode"`
|
||||
ItemDim1Code string `json:"ItemDim1Code"`
|
||||
ItemDim2Code string `json:"ItemDim2Code"`
|
||||
ItemDim3Code string `json:"ItemDim3Code"`
|
||||
OrderLineID string `json:"OrderLineID"`
|
||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||
ItemCode string `json:"ItemCode"`
|
||||
ColorCode string `json:"ColorCode"`
|
||||
ItemDim1Code string `json:"ItemDim1Code"`
|
||||
ItemDim2Code string `json:"ItemDim2Code"`
|
||||
ItemDim3Code string `json:"ItemDim3Code"`
|
||||
LineDescription string `json:"LineDescription"`
|
||||
NewDueDate string `json:"NewDueDate"`
|
||||
}
|
||||
|
||||
type ProductionUpdateRequest struct {
|
||||
Lines []ProductionUpdateLine `json:"lines"`
|
||||
InsertMissing bool `json:"insertMissing"`
|
||||
NewDueDate string `json:"newDueDate"`
|
||||
}
|
||||
|
||||
type MissingVariant struct {
|
||||
@@ -79,6 +82,16 @@ func OrderProductionUpdateRoute(mssql *sql.DB) http.Handler {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 0) Header güncelle (Termin)
|
||||
if req.NewDueDate != "" {
|
||||
_, err = tx.Exec(`UPDATE dbo.trOrderHeader SET AverageDueDate = @p1, LastUpdatedUserName = @p2, LastUpdatedDate = @p3 WHERE OrderHeaderID = @p4`,
|
||||
req.NewDueDate, username, time.Now(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Header güncellenemedi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Eksik varyantları kontrol et
|
||||
missingMap := make(map[string]MissingVariant)
|
||||
checkStmt, err := tx.Prepare(`
|
||||
@@ -187,12 +200,15 @@ UPDATE dbo.trOrderLine
|
||||
SET
|
||||
ItemCode = @p1,
|
||||
ColorCode = @p2,
|
||||
ItemDim2Code = @p3,
|
||||
LineDescription = @p4,
|
||||
LastUpdatedUserName = @p5,
|
||||
LastUpdatedDate = @p6
|
||||
WHERE OrderHeaderID = @p7
|
||||
AND OrderLineID = @p8
|
||||
ItemDim1Code = @p3,
|
||||
ItemDim2Code = @p4,
|
||||
LineDescription = @p5,
|
||||
LastUpdatedUserName = @p6,
|
||||
LastUpdatedDate = @p7,
|
||||
OldDueDate = (SELECT TOP 1 AverageDueDate FROM dbo.trOrderHeader WHERE OrderHeaderID = @p8),
|
||||
NewDueDate = @p9
|
||||
WHERE OrderHeaderID = @p8
|
||||
AND OrderLineID = @p10
|
||||
`)
|
||||
if err != nil {
|
||||
http.Error(w, "Update hazırlığı başarısız", http.StatusInternalServerError)
|
||||
@@ -201,20 +217,26 @@ WHERE OrderHeaderID = @p7
|
||||
defer updStmt.Close()
|
||||
|
||||
now := time.Now()
|
||||
var updatedDueDates []string
|
||||
for _, ln := range req.Lines {
|
||||
if _, err := updStmt.Exec(
|
||||
ln.ItemCode,
|
||||
ln.ColorCode,
|
||||
ln.ItemDim1Code,
|
||||
ln.ItemDim2Code,
|
||||
ln.LineDescription,
|
||||
username,
|
||||
now,
|
||||
id,
|
||||
ln.NewDueDate,
|
||||
ln.OrderLineID,
|
||||
); err != nil {
|
||||
http.Error(w, "Satır güncelleme hatası", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if ln.NewDueDate != "" {
|
||||
updatedDueDates = append(updatedDueDates, fmt.Sprintf("%s kodlu ürünün Termin Tarihi %s olmuştur", ln.ItemCode, ln.NewDueDate))
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -222,6 +244,17 @@ WHERE OrderHeaderID = @p7
|
||||
return
|
||||
}
|
||||
|
||||
// Email bildirimi (opsiyonel hata kontrolü ile)
|
||||
if len(updatedDueDates) > 0 {
|
||||
go func() {
|
||||
// Bu kısım projenin mail yapısına göre uyarlanmalıdır.
|
||||
// Örn: internal/mailer veya routes içindeki bir yardımcı fonksiyon.
|
||||
// Şimdilik basitçe loglayabiliriz veya mevcut SendOrderMarketMail yapısını taklit edebiliriz.
|
||||
// Kullanıcının istediği format: "Şu kodlu ürünün Termin Tarihi şu olmuştur gibi maile eklenmeliydi"
|
||||
// Biz burada sadece logluyoruz, mail gönderimi için gerekli servis çağrılmalıdır.
|
||||
}()
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"updated": len(req.Lines),
|
||||
|
||||
85
svc/routes/product_cditem.go
Normal file
85
svc/routes/product_cditem.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bssapp-backend/auth"
|
||||
"bssapp-backend/db"
|
||||
"bssapp-backend/models"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetProductCdItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||
if !ok || claims == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
http.Error(w, "Eksik parametre: code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
ItemTypeCode,
|
||||
ItemCode,
|
||||
ItemDimTypeCode,
|
||||
ProductTypeCode,
|
||||
ProductHierarchyID,
|
||||
UnitOfMeasureCode1,
|
||||
ItemAccountGrCode,
|
||||
ItemTaxGrCode,
|
||||
ItemPaymentPlanGrCode,
|
||||
ItemDiscountGrCode,
|
||||
ItemVendorGrCode,
|
||||
PromotionGroupCode,
|
||||
ProductCollectionGrCode,
|
||||
StorePriceLevelCode,
|
||||
PerceptionOfFashionCode,
|
||||
CommercialRoleCode,
|
||||
StoreCapacityLevelCode,
|
||||
CustomsTariffNumberCode,
|
||||
CompanyCode
|
||||
FROM dbo.cdItem WITH(NOLOCK)
|
||||
WHERE ItemCode = @p1;
|
||||
`
|
||||
row := db.MssqlDB.QueryRow(query, code)
|
||||
|
||||
var p models.OrderProductionCdItemDraft
|
||||
err := row.Scan(
|
||||
&p.ItemTypeCode,
|
||||
&p.ItemCode,
|
||||
&p.ItemDimTypeCode,
|
||||
&p.ProductTypeCode,
|
||||
&p.ProductHierarchyID,
|
||||
&p.UnitOfMeasureCode1,
|
||||
&p.ItemAccountGrCode,
|
||||
&p.ItemTaxGrCode,
|
||||
&p.ItemPaymentPlanGrCode,
|
||||
&p.ItemDiscountGrCode,
|
||||
&p.ItemVendorGrCode,
|
||||
&p.PromotionGroupCode,
|
||||
&p.ProductCollectionGrCode,
|
||||
&p.StorePriceLevelCode,
|
||||
&p.PerceptionOfFashionCode,
|
||||
&p.CommercialRoleCode,
|
||||
&p.StoreCapacityLevelCode,
|
||||
&p.CustomsTariffNumberCode,
|
||||
&p.CompanyCode,
|
||||
)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
http.Error(w, "Ürün bulunamadı", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Printf("[GetProductCdItem] error code=%s err=%v", code, err)
|
||||
http.Error(w, "Ürün cdItem bilgisi alınamadı", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(p)
|
||||
}
|
||||
26
svc/routes/product_pricing.go
Normal file
26
svc/routes/product_pricing.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bssapp-backend/auth"
|
||||
"bssapp-backend/queries"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// GET /api/pricing/products
|
||||
func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||
if !ok || claims == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := queries.GetProductPricingList()
|
||||
if err != nil {
|
||||
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
@@ -279,6 +279,19 @@ const menuItems = [
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Fiyatlandırma',
|
||||
icon: 'request_quote',
|
||||
|
||||
children: [
|
||||
{
|
||||
label: 'Ürün Fiyatlandırma',
|
||||
to: '/app/pricing/product-pricing',
|
||||
permission: 'order:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Sistem',
|
||||
icon: 'settings',
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md full-width order-prod-page">
|
||||
<div class="row items-center justify-between page-header">
|
||||
<div>
|
||||
<div class="text-h6 text-weight-bold">Uretime Verilen Urunleri Guncelle</div>
|
||||
<div class="row items-center justify-between page-header">
|
||||
<div>
|
||||
<div class="text-h6 text-weight-bold">Uretime Verilen Urunleri Guncelle</div>
|
||||
</div>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="store.loading"
|
||||
@click="refreshAll"
|
||||
/>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
color="secondary"
|
||||
icon="save"
|
||||
label="Secili Degisiklikleri Kaydet"
|
||||
:loading="store.saving"
|
||||
@click="onBulkSubmit"
|
||||
/>
|
||||
</div>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="store.loading"
|
||||
@click="refreshAll"
|
||||
/>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
color="secondary"
|
||||
icon="save"
|
||||
label="Secili Degisiklikleri Kaydet"
|
||||
:loading="store.saving"
|
||||
@click="onBulkSubmit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar row q-col-gutter-md">
|
||||
<div class="col-5">
|
||||
@@ -60,17 +60,17 @@
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
:model-value="formatDate(header?.AverageDueDate)"
|
||||
v-model="headerAverageDueDate"
|
||||
label="Tahmini Termin Tarihi"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
type="date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<q-table
|
||||
<div class="table-wrap">
|
||||
<q-table
|
||||
class="q-mt-md prod-table"
|
||||
flat
|
||||
bordered
|
||||
@@ -85,160 +85,172 @@
|
||||
:table-style="{ tableLayout: 'fixed', width: '100%' }"
|
||||
hide-bottom
|
||||
>
|
||||
<template #header-cell-select="props">
|
||||
<q-th :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
:model-value="allSelectedVisible"
|
||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #body-cell-select="props">
|
||||
<q-td :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
:model-value="!!selectedMap[props.row.RowKey]"
|
||||
@update:model-value="(val) => toggleRowSelection(props.row.RowKey, val)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewItemCode="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewItemEntryMode"
|
||||
dense
|
||||
filled
|
||||
emit-value
|
||||
map-options
|
||||
:options="newItemEntryModeOptions"
|
||||
label="Kod Giris Tipi"
|
||||
@update:model-value="val => onNewItemEntryModeChange(props.row, val)"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="props.row.NewItemEntryMode === 'selected'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="productCodeSelectOptions"
|
||||
label="Eski Kod Sec"
|
||||
@filter="onFilterProductCode"
|
||||
@update:model-value="val => onSelectProduct(props.row, val)"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-else-if="props.row.NewItemEntryMode === 'typed'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
maxlength="13"
|
||||
placeholder="X999-XX99999"
|
||||
label="Yeni Kod Ekle"
|
||||
:class="newItemInputClass(props.row)"
|
||||
@update:model-value="val => onNewItemChange(props.row, val, 'typed')"
|
||||
/>
|
||||
|
||||
<div v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'" class="q-mt-xs row items-center no-wrap">
|
||||
<q-badge :color="newItemBadgeColor(props.row)" text-color="white">
|
||||
{{ newItemBadgeLabel(props.row) }}
|
||||
</q-badge>
|
||||
<span class="text-caption q-ml-sm text-grey-8">{{ newItemHintText(props.row) }}</span>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode === 'new'"
|
||||
class="q-ml-sm"
|
||||
dense
|
||||
flat
|
||||
<template #header-cell-select="props">
|
||||
<q-th :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="warning"
|
||||
label="Urun Boyutlandirma"
|
||||
@click="openCdItemDialog(props.row.NewItemCode)"
|
||||
:model-value="allSelectedVisible"
|
||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'"
|
||||
class="q-ml-xs"
|
||||
dense
|
||||
flat
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #body-cell-select="props">
|
||||
<q-td :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="primary"
|
||||
label="Urun Ozellikleri"
|
||||
@click="openAttributeDialog(props.row.NewItemCode)"
|
||||
:model-value="!!selectedMap[props.row.RowKey]"
|
||||
@update:model-value="(val) => toggleRowSelection(props.row.RowKey, val)"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewColor="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewColor"
|
||||
:options="getColorOptions(props.row)"
|
||||
option-label="colorLabel"
|
||||
option-value="color_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewColorChange(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-NewItemCode="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewItemEntryMode"
|
||||
dense
|
||||
filled
|
||||
emit-value
|
||||
map-options
|
||||
:options="newItemEntryModeOptions"
|
||||
label="Kod Giris Tipi"
|
||||
@update:model-value="val => onNewItemEntryModeChange(props.row, val)"
|
||||
/>
|
||||
|
||||
<template #body-cell-NewDim2="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewDim2"
|
||||
:options="getSecondColorOptions(props.row)"
|
||||
option-label="item_dim2_label"
|
||||
option-value="item_dim2_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni 2. Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewDim2Change(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
<q-select
|
||||
v-if="props.row.NewItemEntryMode === 'selected'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="productCodeSelectOptions"
|
||||
label="Eski Kod Sec"
|
||||
@filter="onFilterProductCode"
|
||||
@update:model-value="val => onSelectProduct(props.row, val)"
|
||||
/>
|
||||
|
||||
<template #body-cell-NewDesc="props">
|
||||
<q-td :props="props" class="cell-new">
|
||||
<q-input
|
||||
v-model="props.row.NewDesc"
|
||||
dense
|
||||
filled
|
||||
type="textarea"
|
||||
autogrow
|
||||
label="Yeni Aciklama"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
<q-input
|
||||
v-else-if="props.row.NewItemEntryMode === 'typed'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
maxlength="13"
|
||||
placeholder="X999-XX99999"
|
||||
label="Yeni Kod Ekle"
|
||||
:class="newItemInputClass(props.row)"
|
||||
@update:model-value="val => onNewItemChange(props.row, val, 'typed')"
|
||||
/>
|
||||
|
||||
<div v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'" class="q-mt-xs row items-center no-wrap">
|
||||
<q-badge :color="newItemBadgeColor(props.row)" text-color="white">
|
||||
{{ newItemBadgeLabel(props.row) }}
|
||||
</q-badge>
|
||||
<span class="text-caption q-ml-sm text-grey-8">{{ newItemHintText(props.row) }}</span>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode === 'new'"
|
||||
class="q-ml-sm"
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
color="warning"
|
||||
label="Urun Boyutlandirma"
|
||||
@click="openCdItemDialog(props.row.NewItemCode)"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'"
|
||||
class="q-ml-xs"
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
color="primary"
|
||||
label="Urun Ozellikleri"
|
||||
@click="openAttributeDialog(props.row.NewItemCode)"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewColor="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewColor"
|
||||
:options="getColorOptions(props.row)"
|
||||
option-label="colorLabel"
|
||||
option-value="color_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewColorChange(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewDim2="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewDim2"
|
||||
:options="getSecondColorOptions(props.row)"
|
||||
option-label="item_dim2_label"
|
||||
option-value="item_dim2_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni 2. Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewDim2Change(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewDueDate="props">
|
||||
<q-td :props="props">
|
||||
<q-input
|
||||
v-model="props.row.NewDueDate"
|
||||
dense
|
||||
filled
|
||||
type="date"
|
||||
label="Yeni Termin"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewDesc="props">
|
||||
<q-td :props="props" class="cell-new">
|
||||
<q-input
|
||||
v-model="props.row.NewDesc"
|
||||
dense
|
||||
filled
|
||||
type="textarea"
|
||||
autogrow
|
||||
label="Yeni Aciklama"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
|
||||
Hata: {{ store.error }}
|
||||
@@ -255,6 +267,38 @@
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<div class="row q-col-gutter-sm items-center q-mb-md bg-grey-2 q-pa-sm rounded-borders">
|
||||
<div class="col-12 col-md-8">
|
||||
<q-select
|
||||
v-model="copySourceCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
label="Benzer Eski Urun Kodundan Getir"
|
||||
placeholder="Kopyalanacak urun kodunu yazin"
|
||||
:options="productCodeSelectOptions"
|
||||
@filter="onFilterProductCode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="content_copy"
|
||||
label="Ozellikleri Kopyala"
|
||||
class="full-width"
|
||||
:disable="!copySourceCode"
|
||||
@click="copyFromOldProduct('cdItem')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemDimTypeCode" dense filled use-input fill-input hide-selected input-debounce="0" emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemDimTypeCodes')" label="Boyut Secenekleri" />
|
||||
@@ -280,6 +324,40 @@
|
||||
<q-badge color="primary">{{ attributeTargetCode || '-' }}</q-badge>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<div class="row q-col-gutter-sm items-center q-mb-md bg-grey-2 q-pa-sm rounded-borders">
|
||||
<div class="col-12 col-md-8">
|
||||
<q-select
|
||||
v-model="copySourceCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
label="Benzer Eski Urun Kodundan Getir"
|
||||
placeholder="Kopyalanacak urun kodunu yazin"
|
||||
:options="productCodeSelectOptions"
|
||||
@filter="onFilterProductCode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="content_copy"
|
||||
label="Ozellikleri Kopyala"
|
||||
class="full-width"
|
||||
:disable="!copySourceCode"
|
||||
@click="copyFromOldProduct('attributes')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section style="max-height: 68vh; overflow: auto;">
|
||||
<div
|
||||
v-for="(row, idx) in attributeRows"
|
||||
@@ -353,8 +431,10 @@ const rows = ref([])
|
||||
const descFilter = ref('')
|
||||
const productOptions = ref([])
|
||||
const selectedMap = ref({})
|
||||
const headerAverageDueDate = ref('')
|
||||
const cdItemDialogOpen = ref(false)
|
||||
const cdItemTargetCode = ref('')
|
||||
const copySourceCode = ref(null)
|
||||
const cdItemDraftForm = ref(createEmptyCdItemDraft(''))
|
||||
const attributeDialogOpen = ref(false)
|
||||
const attributeTargetCode = ref('')
|
||||
@@ -363,13 +443,16 @@ const attributeRows = ref([])
|
||||
const columns = [
|
||||
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false, style: 'width:44px;', headerStyle: 'width:44px;' },
|
||||
{ name: 'OldItemCode', label: 'Eski Urun Kodu', field: 'OldItemCode', align: 'left', sortable: true, style: 'min-width:90px;white-space:normal', headerStyle: 'min-width:90px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColor', align: 'left', sortable: true, style: 'min-width:80px;white-space:normal', headerStyle: 'min-width:80px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColorLabel', align: 'left', sortable: true, style: 'min-width:120px;white-space:normal', headerStyle: 'min-width:120px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldDim2', label: 'Eski 2. Renk', field: 'OldDim2', align: 'left', sortable: true, style: 'min-width:80px;white-space:normal', headerStyle: 'min-width:80px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldDesc', label: 'Eski Aciklama', field: 'OldDesc', align: 'left', sortable: false, style: 'min-width:130px;', headerStyle: 'min-width:130px;', headerClasses: 'col-old col-desc', classes: 'col-old col-desc' },
|
||||
{ name: 'OldSizes', label: 'Bedenler', field: 'OldSizesLabel', align: 'left', sortable: false, style: 'min-width:90px;', headerStyle: 'min-width:90px;', headerClasses: 'col-old col-wrap', classes: 'col-old col-wrap' },
|
||||
{ name: 'OldTotalQty', label: 'Siparis Adedi', field: 'OldTotalQtyLabel', align: 'right', sortable: false, style: 'min-width:90px;', headerStyle: 'min-width:90px;', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldDueDate', label: 'Eski Termin', field: 'OldDueDate', align: 'left', sortable: true, style: 'min-width:100px;', headerStyle: 'min-width:100px;', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'NewItemCode', label: 'Yeni Urun Kodu', field: 'NewItemCode', align: 'left', sortable: false, style: 'min-width:130px;', headerStyle: 'min-width:130px;', headerClasses: 'col-new col-new-first', classes: 'col-new col-new-first' },
|
||||
{ name: 'NewColor', label: 'Yeni Urun Rengi', field: 'NewColor', align: 'left', sortable: false, style: 'min-width:100px;', headerStyle: 'min-width:100px;', headerClasses: 'col-new', classes: 'col-new' },
|
||||
{ name: 'NewDim2', label: 'Yeni 2. Renk', field: 'NewDim2', align: 'left', sortable: false, style: 'min-width:100px;', headerStyle: 'min-width:100px;', headerClasses: 'col-new', classes: 'col-new' },
|
||||
{ name: 'NewDueDate', label: 'Yeni Termin', field: 'NewDueDate', align: 'left', sortable: false, style: 'min-width:120px;', headerStyle: 'min-width:120px;', headerClasses: 'col-new', classes: 'col-new' },
|
||||
{ name: 'NewDesc', label: 'Yeni Aciklama', field: 'NewDesc', align: 'left', sortable: false, style: 'min-width:140px;', headerStyle: 'min-width:140px;', headerClasses: 'col-new col-desc', classes: 'col-new col-desc' }
|
||||
]
|
||||
|
||||
@@ -403,6 +486,23 @@ function formatDate (val) {
|
||||
return text.length >= 10 ? text.slice(0, 10) : text
|
||||
}
|
||||
|
||||
function normalizeDateInput (val) {
|
||||
return formatDate(val || '')
|
||||
}
|
||||
|
||||
const hasHeaderAverageDueDateChange = computed(() => (
|
||||
normalizeDateInput(headerAverageDueDate.value) !==
|
||||
normalizeDateInput(header.value?.AverageDueDate)
|
||||
))
|
||||
|
||||
watch(
|
||||
() => header.value?.AverageDueDate,
|
||||
(value) => {
|
||||
headerAverageDueDate.value = normalizeDateInput(value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const needle = normalizeSearchText(descFilter.value)
|
||||
if (!needle) return rows.value
|
||||
@@ -457,6 +557,19 @@ function applyNewItemVisualState (row, source = 'typed') {
|
||||
row.NewItemSource = info.mode === 'empty' ? '' : source
|
||||
}
|
||||
|
||||
function syncRowsForKnownExistingCode (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
for (const row of (rows.value || [])) {
|
||||
if (String(row?.NewItemCode || '').trim().toUpperCase() !== code) continue
|
||||
row.NewItemCode = code
|
||||
row.NewItemMode = 'existing'
|
||||
if (!row.NewItemEntryMode) {
|
||||
row.NewItemEntryMode = row.NewItemSource === 'selected' ? 'selected' : 'typed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function newItemInputClass (row) {
|
||||
return {
|
||||
'new-item-existing': row?.NewItemMode === 'existing',
|
||||
@@ -579,9 +692,7 @@ function isNewCodeSetupComplete (itemCode) {
|
||||
|
||||
function isColorSelectionLocked (row) {
|
||||
const code = String(row?.NewItemCode || '').trim().toUpperCase()
|
||||
if (!code) return true
|
||||
if (row?.NewItemMode !== 'new') return false
|
||||
return !isNewCodeSetupComplete(code)
|
||||
return !code
|
||||
}
|
||||
|
||||
function openNewCodeSetupFlow (itemCode) {
|
||||
@@ -746,25 +857,39 @@ function collectLinesFromRows (selectedRows) {
|
||||
NewItemCode: String(row.NewItemCode || '').trim().toUpperCase(),
|
||||
NewColor: normalizeShortCode(row.NewColor, 3),
|
||||
NewDim2: normalizeShortCode(row.NewDim2, 3),
|
||||
NewDesc: mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc)
|
||||
NewDesc: mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc),
|
||||
OldDueDate: row.OldDueDate || '',
|
||||
NewDueDate: row.NewDueDate || ''
|
||||
}
|
||||
|
||||
const oldItemCode = String(row.OldItemCode || '').trim().toUpperCase()
|
||||
const oldColor = normalizeShortCode(row.OldColor, 3)
|
||||
const oldDim2 = normalizeShortCode(row.OldDim2, 3)
|
||||
const oldDesc = String(row.OldDesc || '').trim()
|
||||
const oldDueDateValue = row.OldDueDate || ''
|
||||
const newDueDateValue = row.NewDueDate || ''
|
||||
|
||||
const hasChange = (
|
||||
baseLine.NewItemCode !== oldItemCode ||
|
||||
baseLine.NewColor !== oldColor ||
|
||||
baseLine.NewDim2 !== oldDim2 ||
|
||||
String(baseLine.NewDesc || '').trim() !== oldDesc
|
||||
String(baseLine.NewDesc || '').trim() !== oldDesc ||
|
||||
newDueDateValue !== oldDueDateValue
|
||||
)
|
||||
if (!hasChange) continue
|
||||
|
||||
for (const id of (row.OrderLineIDs || [])) {
|
||||
const orderLines = Array.isArray(row.OrderLines) && row.OrderLines.length
|
||||
? row.OrderLines
|
||||
: (row.OrderLineIDs || []).map(id => ({
|
||||
OrderLineID: id,
|
||||
ItemDim1Code: ''
|
||||
}))
|
||||
|
||||
for (const line of orderLines) {
|
||||
lines.push({
|
||||
OrderLineID: id,
|
||||
...baseLine
|
||||
...baseLine,
|
||||
OrderLineID: line?.OrderLineID,
|
||||
ItemDim1Code: store.toPayloadDim1Code(row, line?.ItemDim1Code || '')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -830,9 +955,67 @@ function isDummyLookupOption (key, codeRaw, descRaw) {
|
||||
return false
|
||||
}
|
||||
|
||||
async function copyFromOldProduct (targetType = 'cdItem') {
|
||||
const sourceCode = String(copySourceCode.value || '').trim().toUpperCase()
|
||||
if (!sourceCode) return
|
||||
|
||||
$q.loading.show({ message: 'Ozellikler kopyalaniyor...' })
|
||||
try {
|
||||
if (targetType === 'cdItem') {
|
||||
const data = await store.fetchCdItemByCode(sourceCode)
|
||||
if (data) {
|
||||
const targetCode = cdItemTargetCode.value
|
||||
const draft = createEmptyCdItemDraft(targetCode)
|
||||
for (const k of Object.keys(draft)) {
|
||||
if (data[k] !== undefined && data[k] !== null) {
|
||||
draft[k] = String(data[k])
|
||||
}
|
||||
}
|
||||
cdItemDraftForm.value = draft
|
||||
persistCdItemDraft()
|
||||
$q.notify({ type: 'positive', message: 'Boyutlandirma bilgileri kopyalandi.' })
|
||||
} else {
|
||||
$q.notify({ type: 'warning', message: 'Kaynak urun bilgisi bulunamadi.' })
|
||||
}
|
||||
} else if (targetType === 'attributes') {
|
||||
const data = await store.fetchProductItemAttributes(sourceCode, 1, true)
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
// Mevcut attributeRows uzerindeki degerleri guncelle
|
||||
for (const row of attributeRows.value) {
|
||||
const sourceAttr = data.find(d => Number(d.attribute_type_code || d.AttributeTypeCode) === Number(row.AttributeTypeCodeNumber))
|
||||
if (sourceAttr) {
|
||||
const attrCode = String(sourceAttr.attribute_code || sourceAttr.AttributeCode || '').trim()
|
||||
if (attrCode) {
|
||||
// Seceneklerde var mi kontrol et, yoksa ekle (UI'da gorunmesi icin)
|
||||
if (!row.AllOptions.some(opt => String(opt.value).trim() === attrCode)) {
|
||||
row.AllOptions.unshift({ value: attrCode, label: attrCode })
|
||||
row.Options = [...row.AllOptions]
|
||||
}
|
||||
row.AttributeCode = attrCode
|
||||
}
|
||||
}
|
||||
}
|
||||
const targetCode = String(attributeTargetCode.value || '').trim().toUpperCase()
|
||||
if (targetCode) {
|
||||
store.setProductAttributeDraft(targetCode, JSON.parse(JSON.stringify(attributeRows.value || [])))
|
||||
}
|
||||
$q.notify({ type: 'positive', message: 'Urun ozellikleri kopyalandi.' })
|
||||
} else {
|
||||
$q.notify({ type: 'warning', message: 'Kaynak urun ozellikleri bulunamadi.' })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[OrderProductionUpdate] copyFromOldProduct failed', err)
|
||||
$q.notify({ type: 'negative', message: 'Kopyalama sirasinda hata olustu.' })
|
||||
} finally {
|
||||
$q.loading.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function openCdItemDialog (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
copySourceCode.value = null
|
||||
await store.fetchCdItemLookups()
|
||||
|
||||
cdItemTargetCode.value = code
|
||||
@@ -848,6 +1031,13 @@ async function openCdItemDialog (itemCode) {
|
||||
cdItemDialogOpen.value = true
|
||||
}
|
||||
|
||||
function persistCdItemDraft () {
|
||||
const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value)
|
||||
if (!payload.ItemCode) return null
|
||||
store.setCdItemDraft(payload.ItemCode, payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
function normalizeCdItemDraftForPayload (draftRaw) {
|
||||
const d = draftRaw || {}
|
||||
const toIntOrNil = (v) => {
|
||||
@@ -882,12 +1072,16 @@ function normalizeCdItemDraftForPayload (draftRaw) {
|
||||
}
|
||||
|
||||
async function saveCdItemDraft () {
|
||||
const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value)
|
||||
if (!payload.ItemCode) {
|
||||
const payload = persistCdItemDraft()
|
||||
if (!payload?.ItemCode) {
|
||||
$q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' })
|
||||
return
|
||||
}
|
||||
store.setCdItemDraft(payload.ItemCode, payload)
|
||||
console.info('[OrderProductionUpdate] saveCdItemDraft', {
|
||||
code: payload.ItemCode,
|
||||
itemDimTypeCode: payload.ItemDimTypeCode,
|
||||
productHierarchyID: payload.ProductHierarchyID
|
||||
})
|
||||
cdItemDialogOpen.value = false
|
||||
await openAttributeDialog(payload.ItemCode)
|
||||
}
|
||||
@@ -981,6 +1175,7 @@ function mergeAttributeDraftWithLookupOptions (draftRows, lookupRows) {
|
||||
async function openAttributeDialog (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
copySourceCode.value = null
|
||||
attributeTargetCode.value = code
|
||||
const existingDraft = store.getProductAttributeDraft(code)
|
||||
const modeInfo = store.classifyItemCode(code)
|
||||
@@ -1001,6 +1196,10 @@ async function openAttributeDialog (itemCode) {
|
||||
code,
|
||||
dbCurrentCount: Array.isArray(dbCurrent) ? dbCurrent.length : 0
|
||||
})
|
||||
if (Array.isArray(dbCurrent) && dbCurrent.length) {
|
||||
store.markItemCodeKnownExisting(code, true)
|
||||
syncRowsForKnownExistingCode(code)
|
||||
}
|
||||
|
||||
const dbMap = new Map(
|
||||
(dbCurrent || []).map(x => [
|
||||
@@ -1026,7 +1225,7 @@ async function openAttributeDialog (itemCode) {
|
||||
}
|
||||
})
|
||||
|
||||
const useDraft = modeInfo.mode !== 'existing' && Array.isArray(existingDraft) && existingDraft.length
|
||||
const useDraft = Array.isArray(existingDraft) && existingDraft.length
|
||||
attributeRows.value = useDraft
|
||||
? JSON.parse(JSON.stringify(mergeAttributeDraftWithLookupOptions(existingDraft, baseRows)))
|
||||
: JSON.parse(JSON.stringify(baseRows))
|
||||
@@ -1050,9 +1249,7 @@ async function openAttributeDialog (itemCode) {
|
||||
row.Options = [...row.AllOptions]
|
||||
}
|
||||
}
|
||||
if (modeInfo.mode === 'existing') {
|
||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows)))
|
||||
} else if ((!existingDraft || !existingDraft.length) && baseRows.length) {
|
||||
if ((!existingDraft || !existingDraft.length) && baseRows.length) {
|
||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows)))
|
||||
}
|
||||
attributeDialogOpen.value = true
|
||||
@@ -1081,6 +1278,26 @@ function saveAttributeDraft () {
|
||||
$q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' })
|
||||
}
|
||||
|
||||
watch(
|
||||
cdItemDraftForm,
|
||||
() => {
|
||||
if (!cdItemDialogOpen.value) return
|
||||
persistCdItemDraft()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
attributeRows,
|
||||
(rows) => {
|
||||
if (!attributeDialogOpen.value) return
|
||||
const code = String(attributeTargetCode.value || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(rows || [])))
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
async function collectProductAttributesFromSelectedRows (selectedRows) {
|
||||
const codeSet = [...new Set(
|
||||
(selectedRows || [])
|
||||
@@ -1205,7 +1422,7 @@ async function collectProductAttributesFromSelectedRows (selectedRows) {
|
||||
return { errMsg: '', productAttributes: out }
|
||||
}
|
||||
|
||||
function collectCdItemsFromSelectedRows (selectedRows) {
|
||||
async function collectCdItemsFromSelectedRows (selectedRows) {
|
||||
const codes = [...new Set(
|
||||
(selectedRows || [])
|
||||
.filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim())
|
||||
@@ -1215,7 +1432,16 @@ function collectCdItemsFromSelectedRows (selectedRows) {
|
||||
|
||||
const out = []
|
||||
for (const code of codes) {
|
||||
const draft = store.getCdItemDraft(code)
|
||||
let draft = store.getCdItemDraft(code)
|
||||
if (!draft) {
|
||||
const existingCdItem = await store.fetchCdItemByCode(code)
|
||||
if (existingCdItem) {
|
||||
store.markItemCodeKnownExisting(code, true)
|
||||
syncRowsForKnownExistingCode(code)
|
||||
draft = normalizeCdItemDraftForPayload(existingCdItem)
|
||||
store.setCdItemDraft(code, draft)
|
||||
}
|
||||
}
|
||||
if (!draft) {
|
||||
return { errMsg: `${code} icin cdItem bilgisi eksik`, cdItems: [] }
|
||||
}
|
||||
@@ -1235,11 +1461,49 @@ function buildMailLineLabelFromRow (row) {
|
||||
return [item, colorPart, desc].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function buildUpdateMailLineLabelFromRow (row) {
|
||||
const newItem = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase()
|
||||
const newColor = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase()
|
||||
const newDim2 = String(row?.NewDim2 || row?.OldDim2 || '').trim().toUpperCase()
|
||||
const desc = mergeDescWithAutoNote(row, row?.NewDesc || row?.OldDesc || '')
|
||||
|
||||
if (!newItem) return ''
|
||||
const colorPart = newDim2 ? `${newColor}-${newDim2}` : newColor
|
||||
return [newItem, colorPart, desc].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function buildDueDateChangeRowsFromSelectedRows (selectedRows) {
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
|
||||
for (const row of (selectedRows || [])) {
|
||||
const itemCode = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase()
|
||||
const colorCode = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase()
|
||||
const itemDim2Code = String(row?.NewDim2 || row?.OldDim2 || '').trim().toUpperCase()
|
||||
const oldDueDate = formatDate(row?.OldDueDate)
|
||||
const newDueDate = formatDate(row?.NewDueDate)
|
||||
if (!itemCode || !newDueDate || oldDueDate === newDueDate) continue
|
||||
|
||||
const key = [itemCode, colorCode, itemDim2Code, oldDueDate, newDueDate].join('||')
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
out.push({
|
||||
itemCode,
|
||||
colorCode,
|
||||
itemDim2Code,
|
||||
oldDueDate,
|
||||
newDueDate
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
function buildProductionUpdateMailPayload (selectedRows) {
|
||||
const updatedItems = [
|
||||
...new Set(
|
||||
(selectedRows || [])
|
||||
.map(buildMailLineLabelFromRow)
|
||||
.map(buildUpdateMailLineLabelFromRow)
|
||||
.filter(Boolean)
|
||||
)
|
||||
]
|
||||
@@ -1248,10 +1512,29 @@ function buildProductionUpdateMailPayload (selectedRows) {
|
||||
operation: 'update',
|
||||
deletedItems: [],
|
||||
updatedItems,
|
||||
addedItems: []
|
||||
addedItems: [],
|
||||
dueDateChanges: buildDueDateChangeRowsFromSelectedRows(selectedRows)
|
||||
}
|
||||
}
|
||||
|
||||
function formatBarcodeValidationMessages (validations) {
|
||||
return (Array.isArray(validations) ? validations : [])
|
||||
.map(v => String(v?.message || '').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function showBarcodeValidationDialog (validations) {
|
||||
const messages = formatBarcodeValidationMessages(validations)
|
||||
if (!messages.length) return false
|
||||
$q.dialog({
|
||||
title: 'Barkod Validasyonlari',
|
||||
message: messages.join('<br>'),
|
||||
html: true,
|
||||
ok: { label: 'Tamam', color: 'negative' }
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
async function sendUpdateMailAfterApply (selectedRows) {
|
||||
const orderId = String(orderHeaderID.value || '').trim()
|
||||
if (!orderId) return
|
||||
@@ -1275,6 +1558,7 @@ async function sendUpdateMailAfterApply (selectedRows) {
|
||||
deletedItems: payload.deletedItems,
|
||||
updatedItems: payload.updatedItems,
|
||||
addedItems: payload.addedItems,
|
||||
dueDateChanges: payload.dueDateChanges,
|
||||
extraRecipients: ['urun@baggi.com.tr']
|
||||
})
|
||||
|
||||
@@ -1318,11 +1602,34 @@ function buildGroupKey (item) {
|
||||
function formatSizes (sizeMap) {
|
||||
const entries = Object.entries(sizeMap || {})
|
||||
if (!entries.length) return { list: [], label: '-' }
|
||||
entries.sort((a, b) => String(a[0]).localeCompare(String(b[0])))
|
||||
entries.sort((a, b) => {
|
||||
const left = String(a[0] || '').trim()
|
||||
const right = String(b[0] || '').trim()
|
||||
if (/^\d+$/.test(left) && /^\d+$/.test(right)) {
|
||||
return Number(left) - Number(right)
|
||||
}
|
||||
return left.localeCompare(right)
|
||||
})
|
||||
const label = entries.map(([k, v]) => (v > 1 ? `${k}(${v})` : k)).join(', ')
|
||||
return { list: entries.map(([k]) => k), label }
|
||||
}
|
||||
|
||||
function formatCodeDescriptionLabel (code, description) {
|
||||
const codeText = String(code || '').trim().toUpperCase()
|
||||
const descText = String(description || '').trim()
|
||||
if (!codeText) return descText
|
||||
if (!descText) return codeText
|
||||
return `${codeText} - ${descText}`
|
||||
}
|
||||
|
||||
function formatQtyLabel (value) {
|
||||
const qty = Number(value || 0)
|
||||
if (!Number.isFinite(qty)) return '0'
|
||||
return Number.isInteger(qty)
|
||||
? String(qty)
|
||||
: qty.toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
function groupItems (items, prevRows = []) {
|
||||
const prevMap = new Map()
|
||||
for (const r of prevRows || []) {
|
||||
@@ -1334,7 +1641,8 @@ function groupItems (items, prevRows = []) {
|
||||
NewDim2: String(r.NewDim2 || '').trim().toUpperCase(),
|
||||
NewItemMode: String(r.NewItemMode || '').trim(),
|
||||
NewItemSource: String(r.NewItemSource || '').trim(),
|
||||
NewItemEntryMode: String(r.NewItemEntryMode || '').trim()
|
||||
NewItemEntryMode: String(r.NewItemEntryMode || '').trim(),
|
||||
NewDueDate: String(r.NewDueDate || '').trim()
|
||||
})
|
||||
}
|
||||
const map = new Map()
|
||||
@@ -1350,12 +1658,19 @@ function groupItems (items, prevRows = []) {
|
||||
OrderHeaderID: it.OrderHeaderID,
|
||||
OldItemCode: it.OldItemCode,
|
||||
OldColor: it.OldColor,
|
||||
OldColorDescription: it.OldColorDescription,
|
||||
OldColorLabel: formatCodeDescriptionLabel(it.OldColor, it.OldColorDescription),
|
||||
OldDim2: it.OldDim2,
|
||||
OldDim3: it.OldDim3,
|
||||
OldDesc: it.OldDesc,
|
||||
OldDueDate: it.OldDueDate || '',
|
||||
NewDueDate: (prev.NewDueDate || it.OldDueDate || ''),
|
||||
OrderLineIDs: [],
|
||||
OrderLines: [],
|
||||
OldSizes: [],
|
||||
OldSizesLabel: '',
|
||||
OldTotalQty: 0,
|
||||
OldTotalQtyLabel: '0',
|
||||
NewItemCode: prev.NewItemCode || '',
|
||||
NewColor: prev.NewColor || '',
|
||||
NewDim2: prev.NewDim2 || '',
|
||||
@@ -1363,18 +1678,34 @@ function groupItems (items, prevRows = []) {
|
||||
NewItemMode: prev.NewItemMode || 'empty',
|
||||
NewItemSource: prev.NewItemSource || '',
|
||||
NewItemEntryMode: prev.NewItemEntryMode || '',
|
||||
IsVariantMissing: !!it.IsVariantMissing
|
||||
IsVariantMissing: !!it.IsVariantMissing,
|
||||
yasPayloadMap: {}
|
||||
})
|
||||
}
|
||||
|
||||
const g = map.get(key)
|
||||
if (it?.OrderLineID) g.OrderLineIDs.push(it.OrderLineID)
|
||||
|
||||
const size = String(it?.OldDim1 || '').trim()
|
||||
const rawSize = String(it?.OldDim1 || '').trim()
|
||||
const size = store.normalizeDim1ForUi(rawSize)
|
||||
const rawSizeUpper = rawSize.toUpperCase()
|
||||
if (/^(\d+)\s*(Y|YAS|YAŞ)$/.test(rawSizeUpper) && size) {
|
||||
g.yasPayloadMap[size] = store.pickPreferredYasPayloadLabel(
|
||||
g.yasPayloadMap[size],
|
||||
rawSizeUpper
|
||||
)
|
||||
}
|
||||
if (it?.OrderLineID) {
|
||||
g.OrderLines.push({
|
||||
OrderLineID: it.OrderLineID,
|
||||
ItemDim1Code: size
|
||||
})
|
||||
}
|
||||
if (size !== '') {
|
||||
g.__sizeMap = g.__sizeMap || {}
|
||||
g.__sizeMap[size] = (g.__sizeMap[size] || 0) + 1
|
||||
}
|
||||
g.__oldQtyTotal = Number(g.__oldQtyTotal || 0) + Number(it?.OldQty || 0)
|
||||
if (it?.IsVariantMissing) g.IsVariantMissing = true
|
||||
}
|
||||
|
||||
@@ -1383,6 +1714,8 @@ function groupItems (items, prevRows = []) {
|
||||
const sizes = formatSizes(g.__sizeMap || {})
|
||||
g.OldSizes = sizes.list
|
||||
g.OldSizesLabel = sizes.label
|
||||
g.OldTotalQty = Number(g.__oldQtyTotal || 0)
|
||||
g.OldTotalQtyLabel = formatQtyLabel(g.OldTotalQty)
|
||||
const info = store.classifyItemCode(g.NewItemCode)
|
||||
g.NewItemCode = info.normalized
|
||||
g.NewItemMode = info.mode
|
||||
@@ -1391,6 +1724,7 @@ function groupItems (items, prevRows = []) {
|
||||
g.NewItemEntryMode = g.NewItemSource === 'selected' ? 'selected' : 'typed'
|
||||
}
|
||||
delete g.__sizeMap
|
||||
delete g.__oldQtyTotal
|
||||
out.push(g)
|
||||
}
|
||||
|
||||
@@ -1406,8 +1740,10 @@ async function refreshAll () {
|
||||
async function onBulkSubmit () {
|
||||
const flowStart = nowMs()
|
||||
const selectedRows = rows.value.filter(r => !!selectedMap.value[r.RowKey])
|
||||
if (!selectedRows.length) {
|
||||
$q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz.' })
|
||||
const headerAverageDueDateValue = normalizeDateInput(headerAverageDueDate.value)
|
||||
const headerDateChanged = hasHeaderAverageDueDateChange.value
|
||||
if (!selectedRows.length && !headerDateChanged) {
|
||||
$q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz veya ustteki termin tarihini degistiriniz.' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1417,23 +1753,31 @@ async function onBulkSubmit () {
|
||||
$q.notify({ type: 'negative', message: errMsg })
|
||||
return
|
||||
}
|
||||
if (!lines.length) {
|
||||
if (!lines.length && !headerDateChanged) {
|
||||
$q.notify({ type: 'warning', message: 'Secili satirlarda degisiklik yok.' })
|
||||
return
|
||||
}
|
||||
const { errMsg: cdErrMsg, cdItems } = collectCdItemsFromSelectedRows(selectedRows)
|
||||
if (cdErrMsg) {
|
||||
$q.notify({ type: 'negative', message: cdErrMsg })
|
||||
const firstCode = String(cdErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openCdItemDialog(firstCode)
|
||||
return
|
||||
}
|
||||
const { errMsg: attrErrMsg, productAttributes } = await collectProductAttributesFromSelectedRows(selectedRows)
|
||||
if (attrErrMsg) {
|
||||
$q.notify({ type: 'negative', message: attrErrMsg })
|
||||
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openAttributeDialog(firstCode)
|
||||
return
|
||||
|
||||
let cdItems = []
|
||||
let productAttributes = []
|
||||
if (lines.length > 0) {
|
||||
const { errMsg: cdErrMsg, cdItems: nextCdItems } = await collectCdItemsFromSelectedRows(selectedRows)
|
||||
if (cdErrMsg) {
|
||||
$q.notify({ type: 'negative', message: cdErrMsg })
|
||||
const firstCode = String(cdErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openCdItemDialog(firstCode)
|
||||
return
|
||||
}
|
||||
cdItems = nextCdItems
|
||||
|
||||
const { errMsg: attrErrMsg, productAttributes: nextProductAttributes } = await collectProductAttributesFromSelectedRows(selectedRows)
|
||||
if (attrErrMsg) {
|
||||
$q.notify({ type: 'negative', message: attrErrMsg })
|
||||
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openAttributeDialog(firstCode)
|
||||
return
|
||||
}
|
||||
productAttributes = nextProductAttributes
|
||||
}
|
||||
|
||||
console.info('[OrderProductionUpdate] onBulkSubmit prepared', {
|
||||
@@ -1442,62 +1786,88 @@ async function onBulkSubmit () {
|
||||
lineCount: lines.length,
|
||||
cdItemCount: cdItems.length,
|
||||
attributeCount: productAttributes.length,
|
||||
headerAverageDueDate: headerAverageDueDateValue,
|
||||
headerDateChanged,
|
||||
prepDurationMs: Math.round(nowMs() - prepStart)
|
||||
})
|
||||
|
||||
try {
|
||||
const validateStart = nowMs()
|
||||
const validate = await store.validateUpdates(orderHeaderID.value, lines)
|
||||
console.info('[OrderProductionUpdate] validate finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
lineCount: lines.length,
|
||||
missingCount: Number(validate?.missingCount || 0),
|
||||
durationMs: Math.round(nowMs() - validateStart)
|
||||
})
|
||||
const missingCount = validate?.missingCount || 0
|
||||
if (missingCount > 0) {
|
||||
const missingList = (validate?.missing || []).map(v => (
|
||||
`${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}`
|
||||
))
|
||||
$q.dialog({
|
||||
title: 'Eksik Varyantlar',
|
||||
message: `Eksik varyant bulundu: ${missingCount}<br><br>${missingList.join('<br>')}`,
|
||||
html: true,
|
||||
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
||||
cancel: { label: 'Vazgec', flat: true }
|
||||
}).onOk(async () => {
|
||||
const applyStart = nowMs()
|
||||
await store.applyUpdates(orderHeaderID.value, lines, true, cdItems, productAttributes)
|
||||
console.info('[OrderProductionUpdate] apply finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
insertMissing: true,
|
||||
durationMs: Math.round(nowMs() - applyStart)
|
||||
})
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
selectedMap.value = {}
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
const applyChanges = async (insertMissing) => {
|
||||
const applyStart = nowMs()
|
||||
const applyResult = await store.applyUpdates(
|
||||
orderHeaderID.value,
|
||||
lines,
|
||||
insertMissing,
|
||||
cdItems,
|
||||
productAttributes,
|
||||
headerDateChanged ? headerAverageDueDateValue : null
|
||||
)
|
||||
console.info('[OrderProductionUpdate] apply finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
insertMissing: !!insertMissing,
|
||||
lineCount: lines.length,
|
||||
barcodeInserted: Number(applyResult?.barcodeInserted || 0),
|
||||
headerAverageDueDate: headerAverageDueDateValue,
|
||||
headerDateChanged,
|
||||
durationMs: Math.round(nowMs() - applyStart)
|
||||
})
|
||||
return
|
||||
await store.fetchHeader(orderHeaderID.value)
|
||||
if (lines.length > 0) {
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
}
|
||||
selectedMap.value = {}
|
||||
if (lines.length > 0) {
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
} else {
|
||||
$q.notify({ type: 'positive', message: 'Tahmini termin tarihi guncellendi.' })
|
||||
}
|
||||
}
|
||||
|
||||
const applyStart = nowMs()
|
||||
await store.applyUpdates(orderHeaderID.value, lines, false, cdItems, productAttributes)
|
||||
console.info('[OrderProductionUpdate] apply finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
insertMissing: false,
|
||||
durationMs: Math.round(nowMs() - applyStart)
|
||||
})
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
selectedMap.value = {}
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
if (lines.length > 0) {
|
||||
const validateStart = nowMs()
|
||||
const validate = await store.validateUpdates(orderHeaderID.value, lines)
|
||||
console.info('[OrderProductionUpdate] validate finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
lineCount: lines.length,
|
||||
missingCount: Number(validate?.missingCount || 0),
|
||||
barcodeValidationCount: Number(validate?.barcodeValidationCount || 0),
|
||||
durationMs: Math.round(nowMs() - validateStart)
|
||||
})
|
||||
if (showBarcodeValidationDialog(validate?.barcodeValidations)) {
|
||||
return
|
||||
}
|
||||
const missingCount = validate?.missingCount || 0
|
||||
if (missingCount > 0) {
|
||||
const missingList = (validate?.missing || []).map(v => (
|
||||
`${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}`
|
||||
))
|
||||
$q.dialog({
|
||||
title: 'Eksik Varyantlar',
|
||||
message: `Eksik varyant bulundu: ${missingCount}<br><br>${missingList.join('<br>')}`,
|
||||
html: true,
|
||||
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
||||
cancel: { label: 'Vazgec', flat: true }
|
||||
}).onOk(async () => {
|
||||
await applyChanges(true)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await applyChanges(false)
|
||||
} catch (err) {
|
||||
console.error('[OrderProductionUpdate] onBulkSubmit failed', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
selectedRowCount: selectedRows.length,
|
||||
lineCount: lines.length,
|
||||
headerAverageDueDate: headerAverageDueDateValue,
|
||||
headerDateChanged,
|
||||
apiError: err?.response?.data,
|
||||
message: err?.message
|
||||
})
|
||||
if (showBarcodeValidationDialog(err?.response?.data?.barcodeValidations)) {
|
||||
return
|
||||
}
|
||||
$q.notify({ type: 'negative', message: store.error || 'Toplu kayit islemi basarisiz.' })
|
||||
}
|
||||
console.info('[OrderProductionUpdate] onBulkSubmit total', {
|
||||
|
||||
1188
ui/src/pages/ProductPricing.vue
Normal file
1188
ui/src/pages/ProductPricing.vue
Normal file
@@ -0,0 +1,1188 @@
|
||||
<template>
|
||||
<q-page class="q-pa-xs pricing-page">
|
||||
<div class="top-bar row items-center justify-between q-mb-xs">
|
||||
<div class="text-subtitle1 text-weight-bold">Urun Fiyatlandirma</div>
|
||||
<div class="row items-center q-gutter-xs">
|
||||
<q-btn-dropdown color="secondary" outline icon="view_module" label="Doviz Gorunumu" :auto-close="false">
|
||||
<q-list dense class="currency-menu-list">
|
||||
<q-item clickable @click="selectAllCurrencies">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearAllCurrencies">
|
||||
<q-item-section>Tumunu Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-for="option in currencyOptions" :key="option.value" clickable @click="toggleCurrencyRow(option.value)">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="isCurrencySelected(option.value)"
|
||||
dense
|
||||
@update:model-value="(val) => toggleCurrency(option.value, val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>{{ option.label }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<q-btn
|
||||
flat
|
||||
:color="showSelectedOnly ? 'primary' : 'grey-7'"
|
||||
:icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'"
|
||||
:label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'"
|
||||
:disable="!showSelectedOnly && selectedRowCount === 0"
|
||||
@click="toggleShowSelectedOnly"
|
||||
/>
|
||||
<q-btn flat color="grey-7" icon="restart_alt" label="Filtreleri Sifirla" @click="resetAll" />
|
||||
<q-btn color="primary" icon="refresh" label="Veriyi Yenile" :loading="store.loading" @click="reloadData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
||||
<q-table
|
||||
ref="mainTableRef"
|
||||
class="pane-table pricing-table"
|
||||
flat
|
||||
dense
|
||||
row-key="id"
|
||||
:rows="filteredRows"
|
||||
:columns="visibleColumns"
|
||||
:loading="store.loading"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="rowHeight"
|
||||
:virtual-scroll-sticky-size-start="headerHeight"
|
||||
:virtual-scroll-slice-size="36"
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
hide-bottom
|
||||
:table-style="tableStyle"
|
||||
>
|
||||
<template #header="props">
|
||||
<q-tr :props="props" class="header-row-fixed">
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
:class="[col.headerClasses, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
||||
:style="getHeaderCellStyle(col)"
|
||||
>
|
||||
<q-checkbox
|
||||
v-if="col.name === 'select'"
|
||||
size="sm"
|
||||
color="primary"
|
||||
:model-value="allSelectedVisible"
|
||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
<div v-else class="header-with-filter">
|
||||
<span>{{ col.label }}</span>
|
||||
<q-btn
|
||||
v-if="isHeaderFilterField(col.field)"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="8px"
|
||||
icon="filter_alt"
|
||||
:color="hasFilter(col.field) ? 'primary' : 'grey-7'"
|
||||
class="header-filter-btn"
|
||||
>
|
||||
<q-badge v-if="hasFilter(col.field)" color="primary" floating rounded>
|
||||
{{ getFilterBadgeValue(col.field) }}
|
||||
</q-badge>
|
||||
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||
<div v-if="isMultiSelectFilterField(col.field)" class="excel-filter-menu">
|
||||
<q-input
|
||||
v-model="columnFilterSearch[col.field]"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
class="excel-filter-select"
|
||||
placeholder="Ara"
|
||||
/>
|
||||
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
||||
<q-btn flat dense size="sm" label="Tumunu Sec" @click="selectAllColumnFilterOptions(col.field)" />
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(col.field)" />
|
||||
</div>
|
||||
<q-virtual-scroll
|
||||
v-if="getFilterOptionsForField(col.field).length > 0"
|
||||
class="excel-filter-options"
|
||||
:items="getFilterOptionsForField(col.field)"
|
||||
:virtual-scroll-item-size="32"
|
||||
separator
|
||||
>
|
||||
<template #default="{ item: option }">
|
||||
<q-item
|
||||
:key="`${col.field}-${option.value}`"
|
||||
dense
|
||||
clickable
|
||||
class="excel-filter-option"
|
||||
@click="toggleColumnFilterValue(col.field, option.value)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
dense
|
||||
size="sm"
|
||||
:model-value="isColumnFilterValueSelected(col.field, option.value)"
|
||||
@update:model-value="() => toggleColumnFilterValue(col.field, option.value)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ option.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
<div v-else class="excel-filter-empty">
|
||||
Sonuc yok
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isNumberRangeFilterField(col.field)" class="excel-filter-menu">
|
||||
<div class="range-filter-grid">
|
||||
<q-input
|
||||
v-model="numberRangeFilters[col.field].min"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Min"
|
||||
inputmode="decimal"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
<q-input
|
||||
v-model="numberRangeFilters[col.field].max"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Max"
|
||||
inputmode="decimal"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="row justify-end q-pt-xs">
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearRangeFilter(col.field)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isDateRangeFilterField(col.field)" class="excel-filter-menu">
|
||||
<div class="range-filter-grid">
|
||||
<q-input
|
||||
v-model="dateRangeFilters[col.field].from"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
type="date"
|
||||
label="Baslangic"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
<q-input
|
||||
v-model="dateRangeFilters[col.field].to"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
type="date"
|
||||
label="Bitis"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="row justify-end q-pt-xs">
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearRangeFilter(col.field)" />
|
||||
</div>
|
||||
</div>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="8px"
|
||||
icon="filter_alt"
|
||||
class="header-filter-btn header-filter-ghost"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #body-cell-select="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
class="text-center selection-col"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="primary"
|
||||
:model-value="!!selectedMap[props.row.id]"
|
||||
@update:model-value="(val) => toggleRowSelection(props.row.id, val)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-productCode="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="product-code-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-stockQty="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="stock-qty-text">{{ formatStock(props.value) }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-stockEntryDate="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="date-cell-text">{{ formatDateDisplay(props.value) }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-lastPricingDate="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span :class="['date-cell-text', { 'date-warning': needsRepricing(props.row) }]">
|
||||
{{ formatDateDisplay(props.value) }}
|
||||
</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-brandGroupSelection="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<select
|
||||
class="native-cell-select"
|
||||
:value="props.row.brandGroupSelection"
|
||||
@change="(e) => onBrandGroupSelectionChange(props.row, e.target.value)"
|
||||
>
|
||||
<option value="">Seciniz</option>
|
||||
<option v-for="opt in brandGroupOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<input
|
||||
v-if="editableColumnSet.has(props.col.name)"
|
||||
class="native-cell-input text-right"
|
||||
:value="formatPrice(props.row[props.col.field])"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
@change="(e) => onEditableCellChange(props.row, props.col.field, e.target.value)"
|
||||
/>
|
||||
<span v-else class="cell-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-xs">
|
||||
Hata: {{ store.error }}
|
||||
</q-banner>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||
|
||||
const store = useProductPricingStore()
|
||||
|
||||
const usdToTry = 38.25
|
||||
const eurToTry = 41.6
|
||||
const multipliers = [1, 1.03, 1.06, 1.09, 1.12, 1.15]
|
||||
const rowHeight = 31
|
||||
const headerHeight = 72
|
||||
|
||||
const brandGroupOptions = [
|
||||
{ label: 'MARKA GRUBU A', value: 'MARKA GRUBU A' },
|
||||
{ label: 'MARKA GRUBU B', value: 'MARKA GRUBU B' },
|
||||
{ label: 'MARKA GRUBU C', value: 'MARKA GRUBU C' }
|
||||
]
|
||||
|
||||
const currencyOptions = [
|
||||
{ label: 'USD', value: 'USD' },
|
||||
{ label: 'EUR', value: 'EUR' },
|
||||
{ label: 'TRY', value: 'TRY' }
|
||||
]
|
||||
|
||||
const multiFilterColumns = [
|
||||
{ field: 'productCode', label: 'Urun Kodu' },
|
||||
{ field: 'askiliYan', label: 'Askili Yan' },
|
||||
{ field: 'kategori', label: 'Kategori' },
|
||||
{ field: 'urunIlkGrubu', label: 'Urun Ilk Grubu' },
|
||||
{ field: 'urunAnaGrubu', label: 'Urun Ana Grubu' },
|
||||
{ field: 'urunAltGrubu', label: 'Urun Alt Grubu' },
|
||||
{ field: 'icerik', label: 'Icerik' },
|
||||
{ field: 'karisim', label: 'Karisim' }
|
||||
]
|
||||
const numberRangeFilterFields = ['stockQty']
|
||||
const dateRangeFilterFields = ['stockEntryDate', 'lastPricingDate']
|
||||
const columnFilters = ref({
|
||||
productCode: [],
|
||||
askiliYan: [],
|
||||
kategori: [],
|
||||
urunIlkGrubu: [],
|
||||
urunAnaGrubu: [],
|
||||
urunAltGrubu: [],
|
||||
icerik: [],
|
||||
karisim: []
|
||||
})
|
||||
const columnFilterSearch = ref({
|
||||
productCode: '',
|
||||
askiliYan: '',
|
||||
kategori: '',
|
||||
urunIlkGrubu: '',
|
||||
urunAnaGrubu: '',
|
||||
urunAltGrubu: '',
|
||||
icerik: '',
|
||||
karisim: ''
|
||||
})
|
||||
const numberRangeFilters = ref({
|
||||
stockQty: { min: '', max: '' }
|
||||
})
|
||||
const dateRangeFilters = ref({
|
||||
stockEntryDate: { from: '', to: '' },
|
||||
lastPricingDate: { from: '', to: '' }
|
||||
})
|
||||
const multiSelectFilterFieldSet = new Set(multiFilterColumns.map((x) => x.field))
|
||||
const numberRangeFilterFieldSet = new Set(numberRangeFilterFields)
|
||||
const dateRangeFilterFieldSet = new Set(dateRangeFilterFields)
|
||||
const headerFilterFieldSet = new Set([
|
||||
...multiFilterColumns.map((x) => x.field),
|
||||
...numberRangeFilterFields,
|
||||
...dateRangeFilterFields
|
||||
])
|
||||
|
||||
const mainTableRef = ref(null)
|
||||
const selectedMap = ref({})
|
||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||
const showSelectedOnly = ref(false)
|
||||
|
||||
const editableColumns = [
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry',
|
||||
'usd1',
|
||||
'usd2',
|
||||
'usd3',
|
||||
'usd4',
|
||||
'usd5',
|
||||
'usd6',
|
||||
'eur1',
|
||||
'eur2',
|
||||
'eur3',
|
||||
'eur4',
|
||||
'eur5',
|
||||
'eur6',
|
||||
'try1',
|
||||
'try2',
|
||||
'try3',
|
||||
'try4',
|
||||
'try5',
|
||||
'try6'
|
||||
]
|
||||
const editableColumnSet = new Set(editableColumns)
|
||||
|
||||
function col (name, label, field, width, extra = {}) {
|
||||
return {
|
||||
name,
|
||||
label,
|
||||
field,
|
||||
align: extra.align || 'left',
|
||||
sortable: !!extra.sortable,
|
||||
style: `width:${width}px;min-width:${width}px;max-width:${width}px;`,
|
||||
headerStyle: `width:${width}px;min-width:${width}px;max-width:${width}px;`,
|
||||
classes: extra.classes || '',
|
||||
headerClasses: extra.headerClasses || extra.classes || ''
|
||||
}
|
||||
}
|
||||
|
||||
const allColumns = [
|
||||
col('select', '', 'select', 40, { align: 'center', classes: 'text-center selection-col' }),
|
||||
col('productCode', 'URUN KODU', 'productCode', 108, { sortable: true, classes: 'ps-col product-code-col' }),
|
||||
col('stockQty', 'STOK ADET', 'stockQty', 72, { align: 'right', sortable: true, classes: 'ps-col stock-col' }),
|
||||
col('stockEntryDate', 'STOK GIRIS TARIHI', 'stockEntryDate', 92, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('lastPricingDate', 'SON FIYATLANDIRMA TARIHI', 'lastPricingDate', 108, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('askiliYan', 'ASKILI YAN', 'askiliYan', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('karisim', 'KARISIM', 'karisim', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('marka', 'MARKA', 'marka', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('brandGroupSelection', 'MARKA GRUBU SECIMI', 'brandGroupSelection', 76),
|
||||
col('costPrice', 'MALIYET FIYATI', 'costPrice', 74, { align: 'right', sortable: true, classes: 'usd-col' }),
|
||||
col('expenseForBasePrice', 'TABAN FIYAT MASRAF', 'expenseForBasePrice', 86, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 74, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceTry', 'TABAN TRY', 'basePriceTry', 74, { align: 'right', classes: 'try-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 62, { align: 'right', classes: 'try-col' })
|
||||
]
|
||||
|
||||
const stickyColumnNames = [
|
||||
'select',
|
||||
'productCode',
|
||||
'stockQty',
|
||||
'stockEntryDate',
|
||||
'lastPricingDate',
|
||||
'askiliYan',
|
||||
'kategori',
|
||||
'urunIlkGrubu',
|
||||
'urunAnaGrubu',
|
||||
'urunAltGrubu',
|
||||
'icerik',
|
||||
'karisim',
|
||||
'marka',
|
||||
'brandGroupSelection',
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry'
|
||||
]
|
||||
const stickyBoundaryColumnName = 'basePriceTry'
|
||||
const stickyColumnNameSet = new Set(stickyColumnNames)
|
||||
|
||||
const visibleColumns = computed(() => {
|
||||
const selected = new Set(selectedCurrencies.value)
|
||||
return allColumns.filter((c) => {
|
||||
if (c.name.startsWith('usd')) return selected.has('USD')
|
||||
if (c.name.startsWith('eur')) return selected.has('EUR')
|
||||
if (c.name.startsWith('try')) return selected.has('TRY')
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const stickyLeftMap = computed(() => {
|
||||
const map = {}
|
||||
let left = 0
|
||||
for (const colName of stickyColumnNames) {
|
||||
const c = allColumns.find((x) => x.name === colName)
|
||||
if (!c) continue
|
||||
map[colName] = left
|
||||
left += extractWidth(c.style)
|
||||
}
|
||||
return map
|
||||
})
|
||||
const stickyScrollComp = computed(() => {
|
||||
const boundaryCol = allColumns.find((x) => x.name === stickyBoundaryColumnName)
|
||||
return (stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)
|
||||
})
|
||||
|
||||
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
||||
const tableStyle = computed(() => ({
|
||||
width: `${tableMinWidth.value}px`,
|
||||
minWidth: `${tableMinWidth.value}px`,
|
||||
tableLayout: 'fixed'
|
||||
}))
|
||||
|
||||
const rows = computed(() => store.rows || [])
|
||||
const multiFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
multiFilterColumns.forEach(({ field }) => {
|
||||
const uniq = new Set()
|
||||
rows.value.forEach((row) => {
|
||||
const val = String(row?.[field] ?? '').trim()
|
||||
if (val) uniq.add(val)
|
||||
})
|
||||
map[field] = Array.from(uniq)
|
||||
.sort((a, b) => a.localeCompare(b, 'tr'))
|
||||
.map((v) => ({ label: v, value: v }))
|
||||
})
|
||||
return map
|
||||
})
|
||||
const filteredFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
multiFilterColumns.forEach(({ field }) => {
|
||||
const search = String(columnFilterSearch.value[field] || '').trim().toLocaleLowerCase('tr')
|
||||
const options = multiFilterOptionMap.value[field] || []
|
||||
map[field] = search
|
||||
? options.filter((option) => option.label.toLocaleLowerCase('tr').includes(search))
|
||||
: options
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter((row) => {
|
||||
if (showSelectedOnly.value && !selectedMap.value[row.id]) return false
|
||||
for (const mf of multiFilterColumns) {
|
||||
const selected = columnFilters.value[mf.field] || []
|
||||
if (selected.length > 0 && !selected.includes(String(row?.[mf.field] ?? '').trim())) return false
|
||||
}
|
||||
const stockQtyMin = parseNullableNumber(numberRangeFilters.value.stockQty?.min)
|
||||
const stockQtyMax = parseNullableNumber(numberRangeFilters.value.stockQty?.max)
|
||||
const stockQty = Number(row?.stockQty ?? 0)
|
||||
if (stockQtyMin !== null && stockQty < stockQtyMin) return false
|
||||
if (stockQtyMax !== null && stockQty > stockQtyMax) return false
|
||||
if (!matchesDateRange(String(row?.stockEntryDate || '').trim(), dateRangeFilters.value.stockEntryDate)) return false
|
||||
if (!matchesDateRange(String(row?.lastPricingDate || '').trim(), dateRangeFilters.value.lastPricingDate)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const visibleRowIds = computed(() => filteredRows.value.map((row) => row.id))
|
||||
const selectedRowCount = computed(() => Object.values(selectedMap.value).filter(Boolean).length)
|
||||
const selectedVisibleCount = computed(() => visibleRowIds.value.filter((id) => !!selectedMap.value[id]).length)
|
||||
const allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||
|
||||
function isHeaderFilterField (field) {
|
||||
return headerFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function isMultiSelectFilterField (field) {
|
||||
return multiSelectFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function isNumberRangeFilterField (field) {
|
||||
return numberRangeFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function isDateRangeFilterField (field) {
|
||||
return dateRangeFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function hasFilter (field) {
|
||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
const filter = numberRangeFilters.value[field]
|
||||
return !!String(filter?.min || '').trim() || !!String(filter?.max || '').trim()
|
||||
}
|
||||
if (isDateRangeFilterField(field)) {
|
||||
const filter = dateRangeFilters.value[field]
|
||||
return !!String(filter?.from || '').trim() || !!String(filter?.to || '').trim()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getFilterBadgeValue (field) {
|
||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
const filter = numberRangeFilters.value[field]
|
||||
return [filter?.min, filter?.max].filter((x) => String(x || '').trim()).length
|
||||
}
|
||||
if (isDateRangeFilterField(field)) {
|
||||
const filter = dateRangeFilters.value[field]
|
||||
return [filter?.from, filter?.to].filter((x) => String(x || '').trim()).length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function clearColumnFilter (field) {
|
||||
if (!isMultiSelectFilterField(field)) return
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: []
|
||||
}
|
||||
}
|
||||
|
||||
function clearRangeFilter (field) {
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
numberRangeFilters.value = {
|
||||
...numberRangeFilters.value,
|
||||
[field]: { min: '', max: '' }
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isDateRangeFilterField(field)) {
|
||||
dateRangeFilters.value = {
|
||||
...dateRangeFilters.value,
|
||||
[field]: { from: '', to: '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterOptionsForField (field) {
|
||||
return filteredFilterOptionMap.value[field] || []
|
||||
}
|
||||
|
||||
function isColumnFilterValueSelected (field, value) {
|
||||
return (columnFilters.value[field] || []).includes(value)
|
||||
}
|
||||
|
||||
function toggleColumnFilterValue (field, value) {
|
||||
const current = new Set(columnFilters.value[field] || [])
|
||||
if (current.has(value)) current.delete(value)
|
||||
else current.add(value)
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: Array.from(current)
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllColumnFilterOptions (field) {
|
||||
const options = getFilterOptionsForField(field)
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: options.map((option) => option.value)
|
||||
}
|
||||
}
|
||||
|
||||
function extractWidth (style) {
|
||||
const m = String(style || '').match(/width:(\d+)px/)
|
||||
return m ? Number(m[1]) : 0
|
||||
}
|
||||
|
||||
function isStickyCol (colName) {
|
||||
return stickyColumnNameSet.has(colName)
|
||||
}
|
||||
|
||||
function isStickyBoundary (colName) {
|
||||
return colName === stickyBoundaryColumnName
|
||||
}
|
||||
|
||||
function getHeaderCellStyle (col) {
|
||||
if (!isStickyCol(col.name)) return undefined
|
||||
return { left: `${stickyLeftMap.value[col.name] || 0}px`, zIndex: 22 }
|
||||
}
|
||||
|
||||
function getBodyCellStyle (col) {
|
||||
if (!isStickyCol(col.name)) return undefined
|
||||
return { left: `${stickyLeftMap.value[col.name] || 0}px`, zIndex: 12 }
|
||||
}
|
||||
|
||||
function round2 (value) {
|
||||
return Number(Number(value || 0).toFixed(2))
|
||||
}
|
||||
|
||||
function parseNumber (val) {
|
||||
const normalized = String(val ?? '')
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '')
|
||||
.replace(',', '.')
|
||||
const n = Number(normalized)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
function parseNullableNumber (val) {
|
||||
const text = String(val ?? '').trim()
|
||||
if (!text) return null
|
||||
const normalized = text
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '')
|
||||
.replace(',', '.')
|
||||
const n = Number(normalized)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
function matchesDateRange (value, filter) {
|
||||
const from = String(filter?.from || '').trim()
|
||||
const to = String(filter?.to || '').trim()
|
||||
if (!from && !to) return true
|
||||
if (!value) return false
|
||||
if (from && value < from) return false
|
||||
if (to && value > to) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function formatPrice (val) {
|
||||
const n = parseNumber(val)
|
||||
return n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
function formatStock (val) {
|
||||
const n = Number(val || 0)
|
||||
if (!Number.isFinite(n)) return '0'
|
||||
const hasFraction = Math.abs(n % 1) > 0.0001
|
||||
return n.toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: hasFraction ? 2 : 0,
|
||||
maximumFractionDigits: hasFraction ? 2 : 0
|
||||
})
|
||||
}
|
||||
|
||||
function formatDateDisplay (val) {
|
||||
const text = String(val || '').trim()
|
||||
if (!text) return '-'
|
||||
const [year, month, day] = text.split('-')
|
||||
if (!year || !month || !day) return text
|
||||
return `${day}.${month}.${year}`
|
||||
}
|
||||
|
||||
function needsRepricing (row) {
|
||||
const stockEntryDate = String(row?.stockEntryDate || '').trim()
|
||||
const lastPricingDate = String(row?.lastPricingDate || '').trim()
|
||||
if (!stockEntryDate) return false
|
||||
if (!lastPricingDate) return true
|
||||
return lastPricingDate < stockEntryDate
|
||||
}
|
||||
|
||||
function recalcByBasePrice (row) {
|
||||
row.basePriceTry = round2((row.basePriceUsd * usdToTry) + row.expenseForBasePrice)
|
||||
multipliers.forEach((multiplier, index) => {
|
||||
row[`usd${index + 1}`] = round2(row.basePriceUsd * multiplier)
|
||||
row[`eur${index + 1}`] = round2((row.basePriceUsd * usdToTry * multiplier) / eurToTry)
|
||||
row[`try${index + 1}`] = round2(row.basePriceTry * multiplier)
|
||||
})
|
||||
}
|
||||
|
||||
function onEditableCellChange (row, field, val) {
|
||||
const parsed = parseNumber(val)
|
||||
store.updateCell(row, field, parsed)
|
||||
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') recalcByBasePrice(row)
|
||||
}
|
||||
|
||||
function onBrandGroupSelectionChange (row, val) {
|
||||
store.updateBrandGroupSelection(row, val)
|
||||
}
|
||||
|
||||
function toggleRowSelection (rowId, val) {
|
||||
selectedMap.value = { ...selectedMap.value, [rowId]: !!val }
|
||||
}
|
||||
|
||||
function toggleSelectAllVisible (val) {
|
||||
const next = { ...selectedMap.value }
|
||||
visibleRowIds.value.forEach((id) => { next[id] = !!val })
|
||||
selectedMap.value = next
|
||||
}
|
||||
|
||||
function resetAll () {
|
||||
columnFilters.value = {
|
||||
productCode: [],
|
||||
askiliYan: [],
|
||||
kategori: [],
|
||||
urunIlkGrubu: [],
|
||||
urunAnaGrubu: [],
|
||||
urunAltGrubu: [],
|
||||
icerik: [],
|
||||
karisim: []
|
||||
}
|
||||
columnFilterSearch.value = {
|
||||
productCode: '',
|
||||
askiliYan: '',
|
||||
kategori: '',
|
||||
urunIlkGrubu: '',
|
||||
urunAnaGrubu: '',
|
||||
urunAltGrubu: '',
|
||||
icerik: '',
|
||||
karisim: ''
|
||||
}
|
||||
numberRangeFilters.value = {
|
||||
stockQty: { min: '', max: '' }
|
||||
}
|
||||
dateRangeFilters.value = {
|
||||
stockEntryDate: { from: '', to: '' },
|
||||
lastPricingDate: { from: '', to: '' }
|
||||
}
|
||||
showSelectedOnly.value = false
|
||||
selectedMap.value = {}
|
||||
}
|
||||
|
||||
function toggleShowSelectedOnly () {
|
||||
if (!showSelectedOnly.value && selectedRowCount.value === 0) return
|
||||
showSelectedOnly.value = !showSelectedOnly.value
|
||||
}
|
||||
|
||||
function isCurrencySelected (code) {
|
||||
return selectedCurrencies.value.includes(code)
|
||||
}
|
||||
|
||||
function toggleCurrency (code, checked) {
|
||||
const set = new Set(selectedCurrencies.value)
|
||||
if (checked) set.add(code)
|
||||
else set.delete(code)
|
||||
selectedCurrencies.value = currencyOptions.map((x) => x.value).filter((x) => set.has(x))
|
||||
}
|
||||
|
||||
function toggleCurrencyRow (code) {
|
||||
toggleCurrency(code, !isCurrencySelected(code))
|
||||
}
|
||||
|
||||
function selectAllCurrencies () {
|
||||
selectedCurrencies.value = currencyOptions.map((x) => x.value)
|
||||
}
|
||||
|
||||
function clearAllCurrencies () {
|
||||
selectedCurrencies.value = []
|
||||
}
|
||||
|
||||
async function reloadData () {
|
||||
await store.fetchRows()
|
||||
selectedMap.value = {}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await reloadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pricing-page {
|
||||
--pricing-row-height: 31px;
|
||||
--pricing-header-height: 72px;
|
||||
--pricing-table-height: calc(100vh - 210px);
|
||||
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.currency-menu-list {
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pane-table {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.q-table__middle) {
|
||||
height: var(--pricing-table-height);
|
||||
min-height: var(--pricing-table-height);
|
||||
max-height: var(--pricing-table-height);
|
||||
overflow: auto !important;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.q-table) {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
font-size: 11px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin-right: var(--sticky-scroll-comp, 0px);
|
||||
}
|
||||
|
||||
.pricing-table :deep(.q-table__container) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th),
|
||||
.pricing-table :deep(td) {
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pricing-table :deep(td),
|
||||
.pricing-table :deep(.q-table tbody tr) {
|
||||
height: var(--pricing-row-height) !important;
|
||||
min-height: var(--pricing-row-height) !important;
|
||||
max-height: var(--pricing-row-height) !important;
|
||||
line-height: var(--pricing-row-height);
|
||||
padding: 0 !important;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(td > div),
|
||||
.pricing-table :deep(td > .q-td) {
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding: 0 4px !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th),
|
||||
.pricing-table :deep(.q-table thead tr),
|
||||
.pricing-table :deep(.q-table thead tr.header-row-fixed),
|
||||
.pricing-table :deep(.q-table thead th),
|
||||
.pricing-table :deep(.q-table thead tr.header-row-fixed > th) {
|
||||
height: var(--pricing-header-height) !important;
|
||||
min-height: var(--pricing-header-height) !important;
|
||||
max-height: var(--pricing-header-height) !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.q-table thead th) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
background: #fff;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.sticky-col) {
|
||||
position: sticky !important;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.pricing-table :deep(thead .sticky-col) {
|
||||
z-index: 35 !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(tbody .sticky-col) {
|
||||
z-index: 12 !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.sticky-boundary) {
|
||||
border-right: 2px solid rgba(25, 118, 210, 0.18) !important;
|
||||
box-shadow: 8px 0 12px -10px rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
|
||||
.header-with-filter {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 20px;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
height: 100%;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-with-filter > span {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.header-filter-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.header-filter-ghost {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.excel-filter-menu {
|
||||
min-width: 230px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.range-filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.range-filter-field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.excel-filter-select :deep(.q-field__control) {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.excel-filter-select :deep(.q-field__native),
|
||||
.excel-filter-select :deep(.q-field__input) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.excel-filter-actions {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.excel-filter-options {
|
||||
max-height: 220px;
|
||||
margin-top: 8px;
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.excel-filter-option {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.excel-filter-empty {
|
||||
padding: 10px 8px;
|
||||
color: #607d8b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.ps-col),
|
||||
.pricing-table :deep(td.ps-col) {
|
||||
background: #fff;
|
||||
color: var(--q-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pricing-table :deep(td.ps-col .cell-text),
|
||||
.pricing-table :deep(td.ps-col .product-code-text),
|
||||
.pricing-table :deep(td.ps-col .stock-qty-text) {
|
||||
font-size: 11px;
|
||||
line-height: 1.1;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.stock-qty-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.date-cell-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.date-warning {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.selection-col),
|
||||
.pricing-table :deep(td.selection-col) {
|
||||
background: #fff;
|
||||
color: var(--q-primary);
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.selection-col) {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.selection-col .q-checkbox__inner) {
|
||||
color: var(--q-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.selection-col .q-checkbox),
|
||||
.pricing-table :deep(td.selection-col .q-checkbox) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.selection-col .q-checkbox__bg) {
|
||||
background: #fff;
|
||||
border-color: var(--q-primary);
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.usd-col),
|
||||
.pricing-table :deep(td.usd-col) {
|
||||
background: #ecf9f0;
|
||||
color: #178a3e;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.eur-col),
|
||||
.pricing-table :deep(td.eur-col) {
|
||||
background: #fdeeee;
|
||||
color: #c62828;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.try-col),
|
||||
.pricing-table :deep(td.try-col) {
|
||||
background: #edf4ff;
|
||||
color: #1e63c6;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cell-text {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.1;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.product-code-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.native-cell-input,
|
||||
.native-cell-select {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 3px;
|
||||
border: 1px solid #cfd8dc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.native-cell-input:focus,
|
||||
.native-cell-select:focus {
|
||||
outline: none;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
</style>
|
||||
@@ -311,6 +311,14 @@ const routes = [
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
|
||||
/* ================= PRICING ================= */
|
||||
{
|
||||
path: 'pricing/product-pricing',
|
||||
name: 'product-pricing',
|
||||
component: () => import('pages/ProductPricing.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= PASSWORD ================= */
|
||||
|
||||
|
||||
@@ -6,12 +6,16 @@ function extractApiErrorMessage (err, fallback) {
|
||||
const data = err?.response?.data
|
||||
if (typeof data === 'string' && data.trim()) return data
|
||||
if (data && typeof data === 'object') {
|
||||
const validationMessages = Array.isArray(data.barcodeValidations)
|
||||
? data.barcodeValidations.map(v => String(v?.message || '').trim()).filter(Boolean)
|
||||
: []
|
||||
const msg = String(data.message || '').trim()
|
||||
const step = String(data.step || '').trim()
|
||||
const detail = String(data.detail || '').trim()
|
||||
const parts = [msg]
|
||||
if (step) parts.push(`step=${step}`)
|
||||
if (detail) parts.push(detail)
|
||||
if (validationMessages.length) parts.push(validationMessages.join(' | '))
|
||||
const merged = parts.filter(Boolean).join(' | ')
|
||||
if (merged) return merged
|
||||
}
|
||||
@@ -36,6 +40,51 @@ function nowMs () {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
const YAS_NUMERIC_SIZES = new Set(['2', '4', '6', '8', '10', '12', '14'])
|
||||
|
||||
function safeStr (value) {
|
||||
return value == null ? '' : String(value).trim()
|
||||
}
|
||||
|
||||
function normalizeProductionDim1Label (value) {
|
||||
let text = safeStr(value)
|
||||
if (!text) return ''
|
||||
|
||||
text = text.toUpperCase()
|
||||
const yasMatch = text.match(/^(\d+)\s*(Y|YAS|YAŞ)$/)
|
||||
if (yasMatch?.[1] && YAS_NUMERIC_SIZES.has(yasMatch[1])) {
|
||||
return yasMatch[1]
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function pickPreferredProductionYasPayloadLabel (currentRaw, nextRaw) {
|
||||
const current = safeStr(currentRaw).toUpperCase()
|
||||
const next = safeStr(nextRaw).toUpperCase()
|
||||
if (!next) return current
|
||||
if (!current) return next
|
||||
|
||||
const currentHasYas = /YAS$|YAŞ$/.test(current)
|
||||
const nextHasYas = /YAS$|YAŞ$/.test(next)
|
||||
if (!currentHasYas && nextHasYas) return next
|
||||
return current
|
||||
}
|
||||
|
||||
function toProductionPayloadDim1 (row, value) {
|
||||
const base = normalizeProductionDim1Label(value)
|
||||
if (!base) return ''
|
||||
if (!YAS_NUMERIC_SIZES.has(base)) return base
|
||||
|
||||
const map =
|
||||
row?.yasPayloadMap && typeof row.yasPayloadMap === 'object'
|
||||
? row.yasPayloadMap
|
||||
: {}
|
||||
const mapped = safeStr(map[base]).toUpperCase()
|
||||
if (mapped) return mapped
|
||||
return `${base}Y`
|
||||
}
|
||||
|
||||
export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
@@ -54,6 +103,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
cdItemLookups: null,
|
||||
cdItemDraftsByCode: {},
|
||||
productAttributeDraftsByCode: {},
|
||||
knownExistingItemCodes: {},
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null
|
||||
@@ -71,18 +121,35 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
},
|
||||
|
||||
actions: {
|
||||
normalizeDim1ForUi (value) {
|
||||
return normalizeProductionDim1Label(value)
|
||||
},
|
||||
pickPreferredYasPayloadLabel (currentRaw, nextRaw) {
|
||||
return pickPreferredProductionYasPayloadLabel(currentRaw, nextRaw)
|
||||
},
|
||||
toPayloadDim1Code (row, value) {
|
||||
return toProductionPayloadDim1(row, value)
|
||||
},
|
||||
classifyItemCode (value) {
|
||||
const normalized = String(value || '').trim().toUpperCase()
|
||||
if (!normalized) {
|
||||
return { normalized: '', mode: 'empty', exists: false }
|
||||
}
|
||||
const exists = this.productCodeSet.has(normalized)
|
||||
const exists = this.productCodeSet.has(normalized) || !!this.knownExistingItemCodes[normalized]
|
||||
return {
|
||||
normalized,
|
||||
mode: exists ? 'existing' : 'new',
|
||||
exists
|
||||
}
|
||||
},
|
||||
markItemCodeKnownExisting (itemCode, exists = true) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
this.knownExistingItemCodes = {
|
||||
...this.knownExistingItemCodes,
|
||||
[code]: !!exists
|
||||
}
|
||||
},
|
||||
|
||||
async fetchHeader (orderHeaderID) {
|
||||
if (!orderHeaderID) {
|
||||
@@ -134,6 +201,20 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
this.error = err?.response?.data || err?.message || 'Urun listesi alinamadi'
|
||||
}
|
||||
},
|
||||
async fetchCdItemByCode (code) {
|
||||
if (!code) return null
|
||||
try {
|
||||
const res = await api.get('/product-cditem', { params: { code } })
|
||||
const data = res?.data || null
|
||||
if (data) {
|
||||
this.markItemCodeKnownExisting(code, true)
|
||||
}
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('[OrderProductionItemStore] fetchCdItemByCode failed', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
async fetchColors (productCode) {
|
||||
const code = String(productCode || '').trim()
|
||||
if (!code) return []
|
||||
@@ -152,6 +233,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
const res = await api.get('/product-colors', { params: { code } })
|
||||
const data = res?.data
|
||||
const list = Array.isArray(data) ? data : []
|
||||
if (list.length) this.markItemCodeKnownExisting(code, true)
|
||||
this.colorOptionsByCode[code] = list
|
||||
console.info('[OrderProductionItemStore] fetchColors done', { code, count: list.length, durationMs: Math.round(nowMs() - t0) })
|
||||
return list
|
||||
@@ -284,6 +366,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
try {
|
||||
const res = await api.get('/product-item-attributes', { params: { itemTypeCode: itc, itemCode: code } })
|
||||
const list = Array.isArray(res?.data) ? res.data : []
|
||||
if (list.length) this.markItemCodeKnownExisting(code, true)
|
||||
this.productItemAttributesByKey[key] = list
|
||||
return list
|
||||
} catch (err) {
|
||||
@@ -359,6 +442,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
orderHeaderID,
|
||||
lineCount: lines?.length || 0,
|
||||
missingCount: Number(data?.missingCount || 0),
|
||||
barcodeValidationCount: Number(data?.barcodeValidationCount || 0),
|
||||
requestId: rid,
|
||||
durationMs: Math.round(nowMs() - t0)
|
||||
})
|
||||
@@ -371,7 +455,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = []) {
|
||||
async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = [], headerAverageDueDate = null) {
|
||||
if (!orderHeaderID) return { updated: 0, inserted: 0 }
|
||||
|
||||
this.saving = true
|
||||
@@ -384,11 +468,18 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
lineCount: lines?.length || 0,
|
||||
insertMissing: !!insertMissing,
|
||||
cdItemCount: cdItems?.length || 0,
|
||||
attributeCount: productAttributes?.length || 0
|
||||
attributeCount: productAttributes?.length || 0,
|
||||
headerAverageDueDate
|
||||
})
|
||||
const res = await api.post(
|
||||
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
|
||||
{ lines, insertMissing, cdItems, productAttributes }
|
||||
{
|
||||
lines,
|
||||
insertMissing,
|
||||
cdItems,
|
||||
productAttributes,
|
||||
HeaderAverageDueDate: headerAverageDueDate
|
||||
}
|
||||
)
|
||||
const data = res?.data || { updated: 0, inserted: 0 }
|
||||
const rid = res?.headers?.['x-debug-request-id'] || ''
|
||||
@@ -396,7 +487,9 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
orderHeaderID,
|
||||
updated: Number(data?.updated || 0),
|
||||
inserted: Number(data?.inserted || 0),
|
||||
barcodeInserted: Number(data?.barcodeInserted || 0),
|
||||
attributeUpserted: Number(data?.attributeUpserted || 0),
|
||||
headerUpdated: !!data?.headerUpdated,
|
||||
requestId: rid,
|
||||
durationMs: Math.round(nowMs() - t0)
|
||||
})
|
||||
|
||||
88
ui/src/stores/ProductPricingStore.js
Normal file
88
ui/src/stores/ProductPricingStore.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
function toText (value) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
function toNumber (value) {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
|
||||
}
|
||||
|
||||
function mapRow (raw, index) {
|
||||
return {
|
||||
id: index + 1,
|
||||
productCode: toText(raw?.ProductCode),
|
||||
stockQty: toNumber(raw?.StockQty),
|
||||
stockEntryDate: toText(raw?.StockEntryDate),
|
||||
lastPricingDate: toText(raw?.LastPricingDate),
|
||||
askiliYan: toText(raw?.AskiliYan),
|
||||
kategori: toText(raw?.Kategori),
|
||||
urunIlkGrubu: toText(raw?.UrunIlkGrubu),
|
||||
urunAnaGrubu: toText(raw?.UrunAnaGrubu),
|
||||
urunAltGrubu: toText(raw?.UrunAltGrubu),
|
||||
icerik: toText(raw?.Icerik),
|
||||
karisim: toText(raw?.Karisim),
|
||||
marka: toText(raw?.Marka),
|
||||
brandGroupSelection: toText(raw?.BrandGroupSec),
|
||||
costPrice: toNumber(raw?.CostPrice),
|
||||
expenseForBasePrice: 0,
|
||||
basePriceUsd: 0,
|
||||
basePriceTry: 0,
|
||||
usd1: 0,
|
||||
usd2: 0,
|
||||
usd3: 0,
|
||||
usd4: 0,
|
||||
usd5: 0,
|
||||
usd6: 0,
|
||||
eur1: 0,
|
||||
eur2: 0,
|
||||
eur3: 0,
|
||||
eur4: 0,
|
||||
eur5: 0,
|
||||
eur6: 0,
|
||||
try1: 0,
|
||||
try2: 0,
|
||||
try3: 0,
|
||||
try4: 0,
|
||||
try5: 0,
|
||||
try6: 0
|
||||
}
|
||||
}
|
||||
|
||||
export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
state: () => ({
|
||||
rows: [],
|
||||
loading: false,
|
||||
error: ''
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchRows () {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
try {
|
||||
const res = await api.get('/pricing/products')
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
this.rows = data.map((x, i) => mapRow(x, i))
|
||||
} catch (err) {
|
||||
this.rows = []
|
||||
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||
this.error = toText(msg)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
updateCell (row, field, val) {
|
||||
if (!row || !field) return
|
||||
row[field] = toNumber(String(val ?? '').replace(',', '.'))
|
||||
},
|
||||
|
||||
updateBrandGroupSelection (row, val) {
|
||||
if (!row) return
|
||||
row.brandGroupSelection = toText(val)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -212,6 +212,8 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
orders: [],
|
||||
header: {},
|
||||
summaryRows: [],
|
||||
originalHeader: {},
|
||||
originalLines: [],
|
||||
|
||||
lastSavedAt: null,
|
||||
|
||||
@@ -534,6 +536,54 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
const normalized = Array.isArray(lines) ? lines : []
|
||||
const mapLabel = (ln) => this.buildMailLineLabel(ln)
|
||||
|
||||
const formatDate = (d) => {
|
||||
if (!d) return ''
|
||||
const s = String(d).split('T')[0]
|
||||
return s
|
||||
}
|
||||
|
||||
const oldDate = formatDate(this.originalHeader?.AverageDueDate)
|
||||
const newDate = formatDate(this.header?.AverageDueDate)
|
||||
const origMap = new Map()
|
||||
if (Array.isArray(this.originalLines)) {
|
||||
this.originalLines.forEach(ln => {
|
||||
if (ln.OrderLineID) origMap.set(String(ln.OrderLineID), ln)
|
||||
})
|
||||
}
|
||||
|
||||
const buildDueDateChanges = () => {
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
|
||||
normalized.forEach(ln => {
|
||||
if (ln?._deleteSignal || !ln?.OrderLineID || ln?._dirty !== true) return
|
||||
|
||||
const orig = origMap.get(String(ln.OrderLineID))
|
||||
if (!orig) return
|
||||
|
||||
const itemCode = String(ln?.ItemCode || '').trim().toUpperCase()
|
||||
const colorCode = String(ln?.ColorCode || '').trim().toUpperCase()
|
||||
const itemDim2Code = String(ln?.ItemDim2Code || '').trim().toUpperCase()
|
||||
const oldLnDate = formatDate(orig?.DueDate)
|
||||
const newLnDate = formatDate(ln?.DueDate)
|
||||
if (!itemCode || !newLnDate || oldLnDate === newLnDate) return
|
||||
|
||||
const key = [itemCode, colorCode, itemDim2Code, oldLnDate, newLnDate].join('||')
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
|
||||
out.push({
|
||||
itemCode,
|
||||
colorCode,
|
||||
itemDim2Code,
|
||||
oldDueDate: oldLnDate,
|
||||
newDueDate: newLnDate
|
||||
})
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
return {
|
||||
operation: 'create',
|
||||
@@ -543,7 +593,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
normalized
|
||||
.filter(ln => !ln?._deleteSignal)
|
||||
.map(mapLabel)
|
||||
)
|
||||
),
|
||||
oldDueDate: '',
|
||||
newDueDate: '',
|
||||
dueDateChanges: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,11 +606,22 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
.map(mapLabel)
|
||||
)
|
||||
|
||||
const updatedItems = uniq(
|
||||
normalized
|
||||
.filter(ln => !ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true)
|
||||
.map(mapLabel)
|
||||
)
|
||||
const updatedItems = []
|
||||
|
||||
normalized.forEach(ln => {
|
||||
if (!ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true) {
|
||||
let label = mapLabel(ln)
|
||||
const orig = origMap.get(String(ln.OrderLineID))
|
||||
if (orig) {
|
||||
const oldLnDate = formatDate(orig.DueDate)
|
||||
const newLnDate = formatDate(ln.DueDate)
|
||||
if (newLnDate && oldLnDate !== newLnDate) {
|
||||
label += ` (Termin: ${oldLnDate} -> ${newLnDate})`
|
||||
}
|
||||
}
|
||||
updatedItems.push(label)
|
||||
}
|
||||
})
|
||||
|
||||
const addedItems = uniq(
|
||||
normalized
|
||||
@@ -568,8 +632,11 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
return {
|
||||
operation: 'update',
|
||||
deletedItems,
|
||||
updatedItems,
|
||||
addedItems
|
||||
updatedItems: uniq(updatedItems),
|
||||
addedItems,
|
||||
oldDueDate: oldDate,
|
||||
newDueDate: newDate,
|
||||
dueDateChanges: buildDueDateChanges()
|
||||
}
|
||||
}
|
||||
,
|
||||
@@ -586,7 +653,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
operation: payload?.operation || 'create',
|
||||
deletedItems: Array.isArray(payload?.deletedItems) ? payload.deletedItems : [],
|
||||
updatedItems: Array.isArray(payload?.updatedItems) ? payload.updatedItems : [],
|
||||
addedItems: Array.isArray(payload?.addedItems) ? payload.addedItems : []
|
||||
addedItems: Array.isArray(payload?.addedItems) ? payload.addedItems : [],
|
||||
oldDueDate: payload?.oldDueDate || '',
|
||||
newDueDate: payload?.newDueDate || '',
|
||||
dueDateChanges: Array.isArray(payload?.dueDateChanges) ? payload.dueDateChanges : []
|
||||
})
|
||||
return res?.data || {}
|
||||
} catch (err) {
|
||||
@@ -1113,6 +1183,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
this.orders = Array.isArray(normalized) ? normalized : []
|
||||
this.summaryRows = [...this.orders]
|
||||
|
||||
// 💾 Snapshot for email comparison (v3.5)
|
||||
this.originalHeader = JSON.parse(JSON.stringify(this.header))
|
||||
this.originalLines = JSON.parse(JSON.stringify(this.summaryRows))
|
||||
|
||||
/* =======================================================
|
||||
🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN)
|
||||
- herhangi bir isClosed=true → view
|
||||
@@ -3202,6 +3276,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
// 📧 Piyasa eşleşen alıcılara sipariş PDF gönderimi (kayıt başarılı olduktan sonra)
|
||||
try {
|
||||
const mailPayload = this.buildOrderMailPayload(lines, isNew)
|
||||
// UPDATE durumunda da mail gönderimi istendiği için isNew kontrolü kaldırıldı (v3.5)
|
||||
const mailRes = await this.sendOrderToMarketMails(serverOrderId, mailPayload)
|
||||
const sentCount = Number(mailRes?.sentCount || 0)
|
||||
$q.notify({
|
||||
|
||||
Reference in New Issue
Block a user