Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-04-14 16:17:43 +03:00
parent b1a3bbd3c5
commit 214677da1e
21 changed files with 3265 additions and 339 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"`
}

View File

@@ -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"`

View 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"`
}

View File

@@ -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 {

View 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
}

View File

@@ -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
}

View File

@@ -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 &rarr; <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()
}

View File

@@ -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)
}
}

View File

@@ -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),

View 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)
}

View 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)
}

View File

@@ -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',

View File

@@ -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', {

View 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>

View File

@@ -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 ================= */

View File

@@ -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)
})

View 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)
}
}
})

View File

@@ -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({