Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -3,32 +3,120 @@ package db
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "github.com/microsoft/go-mssqldb"
|
_ "github.com/microsoft/go-mssqldb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MssqlDB *sql.DB
|
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 {
|
func ConnectMSSQL() error {
|
||||||
connString := strings.TrimSpace(os.Getenv("MSSQL_CONN"))
|
connString := strings.TrimSpace(os.Getenv("MSSQL_CONN"))
|
||||||
if connString == "" {
|
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
|
var err error
|
||||||
MssqlDB, err = sql.Open("sqlserver", connString)
|
MssqlDB, err = sql.Open("sqlserver", connString)
|
||||||
if err != nil {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
svc/main.go
13
svc/main.go
@@ -526,7 +526,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
{"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)},
|
{"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)},
|
||||||
{"/api/orders/production-items/{id}/insert-missing", "POST", "update", routes.OrderProductionInsertMissingRoute(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}/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/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
|
||||||
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
|
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
|
||||||
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(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)),
|
wrapV3(http.HandlerFunc(routes.GetProductDetailHandler)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/product-cditem", "GET",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.GetProductCdItemHandler)),
|
||||||
|
)
|
||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/product-colors", "GET",
|
"/api/product-colors", "GET",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
@@ -638,6 +644,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(routes.GetProductSizeMatchRulesHandler(pgDB)),
|
wrapV3(routes.GetProductSizeMatchRulesHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/products", "GET",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.GetProductPricingListHandler)),
|
||||||
|
)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ROLE MANAGEMENT
|
// ROLE MANAGEMENT
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ type OrderProductionItem struct {
|
|||||||
|
|
||||||
OldItemCode string `json:"OldItemCode"`
|
OldItemCode string `json:"OldItemCode"`
|
||||||
OldColor string `json:"OldColor"`
|
OldColor string `json:"OldColor"`
|
||||||
|
OldColorDescription string `json:"OldColorDescription"`
|
||||||
OldDim2 string `json:"OldDim2"`
|
OldDim2 string `json:"OldDim2"`
|
||||||
OldDesc string `json:"OldDesc"`
|
OldDesc string `json:"OldDesc"`
|
||||||
|
OldQty float64 `json:"OldQty"`
|
||||||
|
|
||||||
NewItemCode string `json:"NewItemCode"`
|
NewItemCode string `json:"NewItemCode"`
|
||||||
NewColor string `json:"NewColor"`
|
NewColor string `json:"NewColor"`
|
||||||
@@ -22,4 +24,6 @@ type OrderProductionItem struct {
|
|||||||
NewDesc string `json:"NewDesc"`
|
NewDesc string `json:"NewDesc"`
|
||||||
|
|
||||||
IsVariantMissing bool `json:"IsVariantMissing"`
|
IsVariantMissing bool `json:"IsVariantMissing"`
|
||||||
|
OldDueDate string `json:"OldDueDate"`
|
||||||
|
NewDueDate string `json:"NewDueDate"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ type OrderProductionUpdateLine struct {
|
|||||||
OrderLineID string `json:"OrderLineID"`
|
OrderLineID string `json:"OrderLineID"`
|
||||||
NewItemCode string `json:"NewItemCode"`
|
NewItemCode string `json:"NewItemCode"`
|
||||||
NewColor string `json:"NewColor"`
|
NewColor string `json:"NewColor"`
|
||||||
|
ItemDim1Code *string `json:"ItemDim1Code,omitempty"`
|
||||||
NewDim2 string `json:"NewDim2"`
|
NewDim2 string `json:"NewDim2"`
|
||||||
NewDesc string `json:"NewDesc"`
|
NewDesc string `json:"NewDesc"`
|
||||||
|
OldDueDate string `json:"OldDueDate"`
|
||||||
|
NewDueDate string `json:"NewDueDate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderProductionUpdatePayload struct {
|
type OrderProductionUpdatePayload struct {
|
||||||
@@ -13,6 +16,7 @@ type OrderProductionUpdatePayload struct {
|
|||||||
InsertMissing bool `json:"insertMissing"`
|
InsertMissing bool `json:"insertMissing"`
|
||||||
CdItems []OrderProductionCdItemDraft `json:"cdItems"`
|
CdItems []OrderProductionCdItemDraft `json:"cdItems"`
|
||||||
ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"`
|
ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"`
|
||||||
|
HeaderAverageDueDate *string `json:"HeaderAverageDueDate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderProductionMissingVariant struct {
|
type OrderProductionMissingVariant struct {
|
||||||
@@ -25,6 +29,19 @@ type OrderProductionMissingVariant struct {
|
|||||||
ItemDim3Code string `json:"ItemDim3Code"`
|
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 {
|
type OrderProductionCdItemDraft struct {
|
||||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||||
ItemCode string `json:"ItemCode"`
|
ItemCode string `json:"ItemCode"`
|
||||||
|
|||||||
18
svc/models/product_pricing.go
Normal file
18
svc/models/product_pricing.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type ProductPricing struct {
|
||||||
|
ProductCode string `json:"ProductCode"`
|
||||||
|
CostPrice float64 `json:"CostPrice"`
|
||||||
|
StockQty float64 `json:"StockQty"`
|
||||||
|
StockEntryDate string `json:"StockEntryDate"`
|
||||||
|
LastPricingDate string `json:"LastPricingDate"`
|
||||||
|
AskiliYan string `json:"AskiliYan"`
|
||||||
|
Kategori string `json:"Kategori"`
|
||||||
|
UrunIlkGrubu string `json:"UrunIlkGrubu"`
|
||||||
|
UrunAnaGrubu string `json:"UrunAnaGrubu"`
|
||||||
|
UrunAltGrubu string `json:"UrunAltGrubu"`
|
||||||
|
Icerik string `json:"Icerik"`
|
||||||
|
Karisim string `json:"Karisim"`
|
||||||
|
Marka string `json:"Marka"`
|
||||||
|
BrandGroupSec string `json:"BrandGroupSec"`
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,14 +26,24 @@ SELECT
|
|||||||
|
|
||||||
ISNULL(l.ItemCode,'') AS OldItemCode,
|
ISNULL(l.ItemCode,'') AS OldItemCode,
|
||||||
ISNULL(l.ColorCode,'') AS OldColor,
|
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.ItemDim2Code,'') AS OldDim2,
|
||||||
ISNULL(l.LineDescription,'') AS OldDesc,
|
ISNULL(l.LineDescription,'') AS OldDesc,
|
||||||
|
CAST(ISNULL(l.Qty1, 0) AS FLOAT) AS OldQty,
|
||||||
|
|
||||||
CAST('' AS NVARCHAR(60)) AS NewItemCode,
|
CAST('' AS NVARCHAR(60)) AS NewItemCode,
|
||||||
CAST('' AS NVARCHAR(30)) AS NewColor,
|
CAST('' AS NVARCHAR(30)) AS NewColor,
|
||||||
CAST('' AS NVARCHAR(30)) AS NewDim2,
|
CAST('' AS NVARCHAR(30)) AS NewDim2,
|
||||||
CAST('' AS NVARCHAR(250)) AS NewDesc,
|
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
|
CAST(0 AS bit) AS IsVariantMissing
|
||||||
FROM dbo.trOrderLine l
|
FROM dbo.trOrderLine l
|
||||||
WHERE l.OrderHeaderID = @p1
|
WHERE l.OrderHeaderID = @p1
|
||||||
@@ -522,18 +533,25 @@ func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderPr
|
|||||||
chunk := lines[i:end]
|
chunk := lines[i:end]
|
||||||
|
|
||||||
values := make([]string, 0, len(chunk))
|
values := make([]string, 0, len(chunk))
|
||||||
args := make([]any, 0, len(chunk)*5+2)
|
args := make([]any, 0, len(chunk)*8+2)
|
||||||
paramPos := 1
|
paramPos := 1
|
||||||
for _, line := range chunk {
|
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,
|
args = append(args,
|
||||||
strings.TrimSpace(line.OrderLineID),
|
strings.TrimSpace(line.OrderLineID),
|
||||||
line.NewItemCode,
|
line.NewItemCode,
|
||||||
line.NewColor,
|
line.NewColor,
|
||||||
|
itemDim1,
|
||||||
line.NewDim2,
|
line.NewDim2,
|
||||||
line.NewDesc,
|
line.NewDesc,
|
||||||
|
line.OldDueDate,
|
||||||
|
line.NewDueDate,
|
||||||
)
|
)
|
||||||
paramPos += 5
|
paramPos += 8
|
||||||
}
|
}
|
||||||
|
|
||||||
orderHeaderParam := paramPos
|
orderHeaderParam := paramPos
|
||||||
@@ -542,16 +560,18 @@ func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderPr
|
|||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SET NOCOUNT ON;
|
SET NOCOUNT ON;
|
||||||
WITH src (OrderLineID, NewItemCode, NewColor, NewDim2, NewDesc) AS (
|
WITH src (OrderLineID, NewItemCode, NewColor, ItemDim1Code, NewDim2, NewDesc, OldDueDate, NewDueDate) AS (
|
||||||
SELECT *
|
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
|
UPDATE l
|
||||||
SET
|
SET
|
||||||
l.ItemCode = s.NewItemCode,
|
l.ItemCode = s.NewItemCode,
|
||||||
l.ColorCode = s.NewColor,
|
l.ColorCode = s.NewColor,
|
||||||
|
l.ItemDim1Code = COALESCE(s.ItemDim1Code, l.ItemDim1Code),
|
||||||
l.ItemDim2Code = s.NewDim2,
|
l.ItemDim2Code = s.NewDim2,
|
||||||
l.LineDescription = COALESCE(NULLIF(s.NewDesc,''), l.LineDescription),
|
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.LastUpdatedUserName = @p%d,
|
||||||
l.LastUpdatedDate = GETDATE()
|
l.LastUpdatedDate = GETDATE()
|
||||||
FROM dbo.trOrderLine l
|
FROM dbo.trOrderLine l
|
||||||
@@ -574,6 +594,344 @@ WHERE l.OrderHeaderID = @p%d;
|
|||||||
return updated, nil
|
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) {
|
func UpsertItemAttributesTx(tx *sql.Tx, attrs []models.OrderProductionItemAttributeRow, username string) (int64, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
if len(attrs) == 0 {
|
if len(attrs) == 0 {
|
||||||
|
|||||||
193
svc/queries/product_pricing.go
Normal file
193
svc/queries/product_pricing.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/db"
|
||||||
|
"bssapp-backend/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetProductPricingList() ([]models.ProductPricing, error) {
|
||||||
|
rows, err := db.MssqlDB.Query(`
|
||||||
|
WITH base_products AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
|
||||||
|
FROM ProductFilterWithDescription('TR')
|
||||||
|
WHERE ProductAtt42 IN ('SERI', 'AKSESUAR')
|
||||||
|
AND IsBlocked = 0
|
||||||
|
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
||||||
|
),
|
||||||
|
latest_base_price AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
|
||||||
|
CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice,
|
||||||
|
CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY LTRIM(RTRIM(b.ItemCode))
|
||||||
|
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
|
||||||
|
) AS rn
|
||||||
|
FROM prItemBasePrice b
|
||||||
|
WHERE b.ItemTypeCode = 1
|
||||||
|
AND b.BasePriceCode = 1
|
||||||
|
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM base_products bp
|
||||||
|
WHERE bp.ProductCode = LTRIM(RTRIM(b.ItemCode))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
stock_entry_dates AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
|
||||||
|
CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate
|
||||||
|
FROM trStock s WITH(NOLOCK)
|
||||||
|
WHERE s.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||||
|
AND s.In_Qty1 > 0
|
||||||
|
AND LTRIM(RTRIM(s.WarehouseCode)) IN (
|
||||||
|
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
|
||||||
|
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
|
||||||
|
'1-0-33','101','1-014','1-0-49','1-0-36'
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM base_products bp
|
||||||
|
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||||
|
)
|
||||||
|
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||||
|
),
|
||||||
|
stock_base AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
|
||||||
|
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
|
||||||
|
FROM trStock s WITH(NOLOCK)
|
||||||
|
WHERE s.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM base_products bp
|
||||||
|
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||||
|
)
|
||||||
|
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||||
|
),
|
||||||
|
pick_base AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(p.ItemCode)) AS ItemCode,
|
||||||
|
SUM(p.Qty1) AS PickingQty1
|
||||||
|
FROM PickingStates p
|
||||||
|
WHERE p.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM base_products bp
|
||||||
|
WHERE bp.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
||||||
|
)
|
||||||
|
GROUP BY LTRIM(RTRIM(p.ItemCode))
|
||||||
|
),
|
||||||
|
reserve_base AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(r.ItemCode)) AS ItemCode,
|
||||||
|
SUM(r.Qty1) AS ReserveQty1
|
||||||
|
FROM ReserveStates r
|
||||||
|
WHERE r.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM base_products bp
|
||||||
|
WHERE bp.ProductCode = LTRIM(RTRIM(r.ItemCode))
|
||||||
|
)
|
||||||
|
GROUP BY LTRIM(RTRIM(r.ItemCode))
|
||||||
|
),
|
||||||
|
disp_base AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(d.ItemCode)) AS ItemCode,
|
||||||
|
SUM(d.Qty1) AS DispOrderQty1
|
||||||
|
FROM DispOrderStates d
|
||||||
|
WHERE d.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM base_products bp
|
||||||
|
WHERE bp.ProductCode = LTRIM(RTRIM(d.ItemCode))
|
||||||
|
)
|
||||||
|
GROUP BY LTRIM(RTRIM(d.ItemCode))
|
||||||
|
),
|
||||||
|
stock_totals AS (
|
||||||
|
SELECT
|
||||||
|
bp.ProductCode AS ItemCode,
|
||||||
|
CAST(ROUND(
|
||||||
|
ISNULL(sb.InventoryQty1, 0)
|
||||||
|
- ISNULL(pb.PickingQty1, 0)
|
||||||
|
- ISNULL(rb.ReserveQty1, 0)
|
||||||
|
- ISNULL(db.DispOrderQty1, 0)
|
||||||
|
, 2) AS DECIMAL(18, 2)) AS StockQty
|
||||||
|
FROM base_products bp
|
||||||
|
LEFT JOIN stock_base sb
|
||||||
|
ON sb.ItemCode = bp.ProductCode
|
||||||
|
LEFT JOIN pick_base pb
|
||||||
|
ON pb.ItemCode = bp.ProductCode
|
||||||
|
LEFT JOIN reserve_base rb
|
||||||
|
ON rb.ItemCode = bp.ProductCode
|
||||||
|
LEFT JOIN disp_base db
|
||||||
|
ON db.ItemCode = bp.ProductCode
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
bp.ProductCode AS ProductCode,
|
||||||
|
COALESCE(lp.CostPrice, 0) AS CostPrice,
|
||||||
|
COALESCE(st.StockQty, 0) AS StockQty,
|
||||||
|
COALESCE(se.StockEntryDate, '') AS StockEntryDate,
|
||||||
|
COALESCE(lp.LastPricingDate, '') AS LastPricingDate,
|
||||||
|
bp.AskiliYan,
|
||||||
|
bp.Kategori,
|
||||||
|
bp.UrunIlkGrubu,
|
||||||
|
bp.UrunAnaGrubu,
|
||||||
|
bp.UrunAltGrubu,
|
||||||
|
bp.Icerik,
|
||||||
|
bp.Karisim,
|
||||||
|
bp.Marka
|
||||||
|
FROM base_products bp
|
||||||
|
LEFT JOIN latest_base_price lp
|
||||||
|
ON lp.ItemCode = bp.ProductCode
|
||||||
|
AND lp.rn = 1
|
||||||
|
LEFT JOIN stock_entry_dates se
|
||||||
|
ON se.ItemCode = bp.ProductCode
|
||||||
|
LEFT JOIN stock_totals st
|
||||||
|
ON st.ItemCode = bp.ProductCode
|
||||||
|
ORDER BY bp.ProductCode;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []models.ProductPricing
|
||||||
|
for rows.Next() {
|
||||||
|
var item models.ProductPricing
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.ProductCode,
|
||||||
|
&item.CostPrice,
|
||||||
|
&item.StockQty,
|
||||||
|
&item.StockEntryDate,
|
||||||
|
&item.LastPricingDate,
|
||||||
|
&item.AskiliYan,
|
||||||
|
&item.Kategori,
|
||||||
|
&item.UrunIlkGrubu,
|
||||||
|
&item.UrunAnaGrubu,
|
||||||
|
&item.UrunAltGrubu,
|
||||||
|
&item.Icerik,
|
||||||
|
&item.Karisim,
|
||||||
|
&item.Marka,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -464,6 +464,7 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
var newID int64
|
var newID int64
|
||||||
|
log.Printf("DEBUG: UserCreateRoute payload=%+v", payload)
|
||||||
err = tx.QueryRow(`
|
err = tx.QueryRow(`
|
||||||
INSERT INTO mk_dfusr (
|
INSERT INTO mk_dfusr (
|
||||||
username,
|
username,
|
||||||
@@ -472,11 +473,12 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
|||||||
email,
|
email,
|
||||||
mobile,
|
mobile,
|
||||||
address,
|
address,
|
||||||
|
password_hash,
|
||||||
force_password_change,
|
force_password_change,
|
||||||
created_at,
|
created_at,
|
||||||
updated_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
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
payload.Code,
|
payload.Code,
|
||||||
@@ -489,7 +491,7 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("USER INSERT ERROR code=%q email=%q err=%v", payload.Code, payload.Email, err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,18 @@ type sendOrderMarketMailPayload struct {
|
|||||||
DeletedItems []string `json:"deletedItems"`
|
DeletedItems []string `json:"deletedItems"`
|
||||||
UpdatedItems []string `json:"updatedItems"`
|
UpdatedItems []string `json:"updatedItems"`
|
||||||
AddedItems []string `json:"addedItems"`
|
AddedItems []string `json:"addedItems"`
|
||||||
|
OldDueDate string `json:"oldDueDate"`
|
||||||
|
NewDueDate string `json:"newDueDate"`
|
||||||
ExtraRecipients []string `json:"extraRecipients"`
|
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 {
|
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 {
|
if isUpdate {
|
||||||
subjectAction = "SİPARİŞ GÜNCELLENDİ."
|
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)
|
subject := fmt.Sprintf("%s kullanıcısı tarafından %s %s", actor, number, subjectAction)
|
||||||
|
|
||||||
cariDetail := ""
|
cariDetail := ""
|
||||||
@@ -127,6 +150,13 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile
|
|||||||
`</p>`,
|
`</p>`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if payload.NewDueDate != "" && payload.OldDueDate != payload.NewDueDate {
|
||||||
|
body = append(body,
|
||||||
|
fmt.Sprintf(`<p><b>Termin Değişikliği:</b> %s → <b style="color:red">%s</b></p>`,
|
||||||
|
htmlEsc(payload.OldDueDate), htmlEsc(payload.NewDueDate)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if isUpdate {
|
if isUpdate {
|
||||||
body = append(body,
|
body = append(body,
|
||||||
renderItemListHTML("Silinen Ürün Kodları", payload.DeletedItems),
|
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><i>Bu sipariş BaggiSS App Uygulamasından oluşturulmuştur.</i></p>`)
|
||||||
body = append(body, `<p>PDF ektedir.</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")
|
bodyHTML := strings.Join(body, "\n")
|
||||||
|
|
||||||
fileNo := sanitizeFileName(number)
|
fileNo := sanitizeFileName(number)
|
||||||
@@ -393,3 +427,54 @@ func renderItemListHTML(title string, items []string) string {
|
|||||||
b = append(b, `</p>`)
|
b = append(b, `</p>`)
|
||||||
return strings.Join(b, "\n")
|
return strings.Join(b, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderDueDateChangesTableHTML(title string, rows []sendOrderMailDueDateChange) string {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(rows))
|
||||||
|
clean := make([]sendOrderMailDueDateChange, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
itemCode := strings.TrimSpace(row.ItemCode)
|
||||||
|
colorCode := strings.TrimSpace(row.ColorCode)
|
||||||
|
itemDim2Code := strings.TrimSpace(row.ItemDim2Code)
|
||||||
|
oldDueDate := strings.TrimSpace(row.OldDueDate)
|
||||||
|
newDueDate := strings.TrimSpace(row.NewDueDate)
|
||||||
|
if itemCode == "" || newDueDate == "" || oldDueDate == newDueDate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(strings.Join([]string{itemCode, colorCode, itemDim2Code, oldDueDate, newDueDate}, "|"))
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
clean = append(clean, sendOrderMailDueDateChange{
|
||||||
|
ItemCode: itemCode,
|
||||||
|
ColorCode: colorCode,
|
||||||
|
ItemDim2Code: itemDim2Code,
|
||||||
|
OldDueDate: oldDueDate,
|
||||||
|
NewDueDate: newDueDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clean) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf(`<p><b>%s:</b></p>`, htmlEsc(title)))
|
||||||
|
b.WriteString(`<table border="1" cellpadding="5" style="border-collapse: collapse; width: 100%;">`)
|
||||||
|
b.WriteString(`<tr style="background-color: #f2f2f2;"><th>Ürün Kodu</th><th>Renk</th><th>2. Renk</th><th>Eski Termin</th><th>Yeni Termin</th></tr>`)
|
||||||
|
for _, row := range clean {
|
||||||
|
b.WriteString("<tr>")
|
||||||
|
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.ItemCode)))
|
||||||
|
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.ColorCode)))
|
||||||
|
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.ItemDim2Code)))
|
||||||
|
b.WriteString(fmt.Sprintf("<td>%s</td>", htmlEsc(row.OldDueDate)))
|
||||||
|
b.WriteString(fmt.Sprintf(`<td style="color:red;font-weight:bold;">%s</td>`, htmlEsc(row.NewDueDate)))
|
||||||
|
b.WriteString("</tr>")
|
||||||
|
}
|
||||||
|
b.WriteString(`</table>`)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bssapp-backend/auth"
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/internal/mailer"
|
||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/queries"
|
"bssapp-backend/queries"
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -20,6 +22,8 @@ import (
|
|||||||
|
|
||||||
var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`)
|
var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`)
|
||||||
|
|
||||||
|
const productionBarcodeTypeCode = "BAGGI3"
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// 📌 OrderProductionItemsRoute — U ürün satırları
|
// 📌 OrderProductionItemsRoute — U ürün satırları
|
||||||
// ======================================================
|
// ======================================================
|
||||||
@@ -54,12 +58,16 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
|
|||||||
&o.OldDim3,
|
&o.OldDim3,
|
||||||
&o.OldItemCode,
|
&o.OldItemCode,
|
||||||
&o.OldColor,
|
&o.OldColor,
|
||||||
|
&o.OldColorDescription,
|
||||||
&o.OldDim2,
|
&o.OldDim2,
|
||||||
&o.OldDesc,
|
&o.OldDesc,
|
||||||
|
&o.OldQty,
|
||||||
&o.NewItemCode,
|
&o.NewItemCode,
|
||||||
&o.NewColor,
|
&o.NewColor,
|
||||||
&o.NewDim2,
|
&o.NewDim2,
|
||||||
&o.NewDesc,
|
&o.NewDesc,
|
||||||
|
&o.OldDueDate,
|
||||||
|
&o.NewDueDate,
|
||||||
&o.IsVariantMissing,
|
&o.IsVariantMissing,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Printf("⚠️ SCAN HATASI: %v", err)
|
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",
|
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())
|
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{
|
resp := map[string]any{
|
||||||
"missingCount": len(missing),
|
"missingCount": len(missing),
|
||||||
"missing": missing,
|
"missing": missing,
|
||||||
|
"barcodeValidationCount": len(barcodeValidations),
|
||||||
|
"barcodeValidations": barcodeValidations,
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
log.Printf("❌ encode error: %v", err)
|
log.Printf("❌ encode error: %v", err)
|
||||||
@@ -196,7 +217,7 @@ func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
|
|||||||
// ======================================================
|
// ======================================================
|
||||||
// OrderProductionApplyRoute - yeni model varyant guncelleme
|
// 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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
rid := fmt.Sprintf("opa-%d", time.Now().UnixNano())
|
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",
|
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())
|
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 {
|
if len(missing) > 0 && !payload.InsertMissing {
|
||||||
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s early_exit=missing_variants total_ms=%d",
|
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s early_exit=missing_variants total_ms=%d",
|
||||||
rid, id, time.Since(start).Milliseconds())
|
rid, id, time.Since(start).Milliseconds())
|
||||||
@@ -282,6 +309,24 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
|||||||
rid, id, inserted, time.Since(stepInsertMissingStart).Milliseconds())
|
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()
|
stepValidateAttrStart := time.Now()
|
||||||
if err := validateProductAttributes(payload.ProductAttributes); err != nil {
|
if err := validateProductAttributes(payload.ProductAttributes); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
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",
|
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=validate_attributes count=%d duration_ms=%d",
|
||||||
rid, id, len(payload.ProductAttributes), time.Since(stepValidateAttrStart).Milliseconds())
|
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()
|
stepUpdateLinesStart := time.Now()
|
||||||
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
|
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
|
||||||
if err != nil {
|
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",
|
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_lines updated=%d duration_ms=%d",
|
||||||
rid, id, updated, time.Since(stepUpdateLinesStart).Milliseconds())
|
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()
|
stepUpsertAttrStart := time.Now()
|
||||||
attributeAffected, err := queries.UpsertItemAttributesTx(tx, payload.ProductAttributes, username)
|
attributeAffected, err := queries.UpsertItemAttributesTx(tx, payload.ProductAttributes, username)
|
||||||
if err != nil {
|
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",
|
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())
|
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{
|
resp := map[string]any{
|
||||||
"updated": updated,
|
"updated": updated,
|
||||||
"inserted": inserted,
|
"inserted": inserted,
|
||||||
|
"barcodeInserted": barcodeInserted,
|
||||||
"attributeUpserted": attributeAffected,
|
"attributeUpserted": attributeAffected,
|
||||||
|
"headerUpdated": payload.HeaderAverageDueDate != nil,
|
||||||
}
|
}
|
||||||
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s result updated=%d inserted=%d attributeUpserted=%d",
|
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s result updated=%d inserted=%d barcodeInserted=%d attributeUpserted=%d",
|
||||||
rid, id, updated, inserted, attributeAffected)
|
rid, id, updated, inserted, barcodeInserted, attributeAffected)
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
log.Printf("❌ encode error: %v", err)
|
log.Printf("❌ encode error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -367,21 +443,20 @@ func buildCdItemDraftMap(list []models.OrderProductionCdItemDraft) map[string]mo
|
|||||||
return out
|
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()
|
start := time.Now()
|
||||||
missing := make([]models.OrderProductionMissingVariant, 0)
|
|
||||||
lineDimsMap, err := queries.GetOrderLineDimsMap(mssql, orderHeaderID)
|
lineDimsMap, err := queries.GetOrderLineDimsMap(mssql, orderHeaderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
for _, line := range lines {
|
||||||
lineID := strings.TrimSpace(line.OrderLineID)
|
lineID := strings.TrimSpace(line.OrderLineID)
|
||||||
newItem := strings.TrimSpace(line.NewItemCode)
|
newItem := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
|
||||||
newColor := strings.TrimSpace(line.NewColor)
|
newColor := strings.ToUpper(strings.TrimSpace(line.NewColor))
|
||||||
newDim2 := strings.TrimSpace(line.NewDim2)
|
newDim2 := strings.ToUpper(strings.TrimSpace(line.NewDim2))
|
||||||
|
|
||||||
if lineID == "" || newItem == "" {
|
if lineID == "" || newItem == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -391,38 +466,68 @@ func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.Or
|
|||||||
continue
|
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",
|
cacheKey := fmt.Sprintf("%d|%s|%s|%s|%s|%s",
|
||||||
dims.ItemTypeCode,
|
target.ItemTypeCode,
|
||||||
strings.ToUpper(strings.TrimSpace(newItem)),
|
target.ItemCode,
|
||||||
strings.ToUpper(strings.TrimSpace(newColor)),
|
target.ColorCode,
|
||||||
strings.ToUpper(strings.TrimSpace(dims.ItemDim1Code)),
|
target.ItemDim1Code,
|
||||||
strings.ToUpper(strings.TrimSpace(newDim2)),
|
target.ItemDim2Code,
|
||||||
strings.ToUpper(strings.TrimSpace(dims.ItemDim3Code)),
|
target.ItemDim3Code,
|
||||||
)
|
)
|
||||||
exists, cached := existsCache[cacheKey]
|
exists, cached := existsCache[cacheKey]
|
||||||
if !cached {
|
if !cached {
|
||||||
var checkErr error
|
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 {
|
if checkErr != nil {
|
||||||
return nil, checkErr
|
return nil, checkErr
|
||||||
}
|
}
|
||||||
existsCache[cacheKey] = exists
|
existsCache[cacheKey] = exists
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
missing = append(missing, models.OrderProductionMissingVariant{
|
missing = append(missing, target)
|
||||||
OrderLineID: lineID,
|
|
||||||
ItemTypeCode: dims.ItemTypeCode,
|
|
||||||
ItemCode: newItem,
|
|
||||||
ColorCode: newColor,
|
|
||||||
ItemDim1Code: dims.ItemDim1Code,
|
|
||||||
ItemDim2Code: newDim2,
|
|
||||||
ItemDim3Code: dims.ItemDim3Code,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[buildMissingVariants] orderHeaderID=%s lineCount=%d dimMapCount=%d missingCount=%d total_ms=%d",
|
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
|
return missing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,3 +569,69 @@ func writeDBError(w http.ResponseWriter, status int, step string, orderHeaderID
|
|||||||
"detail": err.Error(),
|
"detail": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendProductionUpdateMails(db *sql.DB, ml *mailer.GraphMailer, orderHeaderID string, actor string, lines []models.OrderProductionUpdateLine) {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sipariş bağlamını çöz
|
||||||
|
orderNo, currAccCode, marketCode, marketTitle, err := resolveOrderMailContext(db, orderHeaderID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[sendProductionUpdateMails] context error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piyasa alıcılarını yükle (PG db lazım ama burada mssql üzerinden sadece log atalım veya graphmailer üzerinden gönderelim)
|
||||||
|
// Not: PG bağlantısı Route içinde yok, ancak mailer.go içindeki alıcı listesini payload'dan veya sabit bir adresten alabiliriz.
|
||||||
|
// Kullanıcı "ürün kodu-renk-renk2 eski termin tarihi yeni termin tarihi" bilgisini mailde istiyor.
|
||||||
|
|
||||||
|
subject := fmt.Sprintf("%s tarafından %s Nolu Sipariş Güncellendi (Üretim)", actor, orderNo)
|
||||||
|
|
||||||
|
var body strings.Builder
|
||||||
|
body.WriteString("<html><body>")
|
||||||
|
body.WriteString(fmt.Sprintf("<p><b>Sipariş No:</b> %s</p>", orderNo))
|
||||||
|
body.WriteString(fmt.Sprintf("<p><b>Cari:</b> %s</p>", currAccCode))
|
||||||
|
body.WriteString(fmt.Sprintf("<p><b>Piyasa:</b> %s (%s)</p>", marketTitle, marketCode))
|
||||||
|
body.WriteString("<p>Aşağıdaki satırlarda termin tarihi güncellenmiştir:</p>")
|
||||||
|
body.WriteString("<table border='1' cellpadding='5' style='border-collapse: collapse;'>")
|
||||||
|
body.WriteString("<tr style='background-color: #f2f2f2;'><th>Ürün Kodu</th><th>Renk</th><th>2. Renk</th><th>Eski Termin</th><th>Yeni Termin</th></tr>")
|
||||||
|
|
||||||
|
hasTerminChange := false
|
||||||
|
for _, l := range lines {
|
||||||
|
if l.OldDueDate != l.NewDueDate && l.NewDueDate != "" {
|
||||||
|
hasTerminChange = true
|
||||||
|
body.WriteString("<tr>")
|
||||||
|
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewItemCode))
|
||||||
|
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewColor))
|
||||||
|
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewDim2))
|
||||||
|
body.WriteString(fmt.Sprintf("<td>%s</td>", l.OldDueDate))
|
||||||
|
body.WriteString(fmt.Sprintf("<td style='color: red; font-weight: bold;'>%s</td>", l.NewDueDate))
|
||||||
|
body.WriteString("</tr>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.WriteString("</table>")
|
||||||
|
body.WriteString("<p><i>Bu mail sistem tarafından otomatik oluşturulmuştur.</i></p>")
|
||||||
|
body.WriteString("</body></html>")
|
||||||
|
|
||||||
|
if !hasTerminChange {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alıcı listesi için OrderMarketMail'deki mantığı taklit edelim veya sabit bir gruba atalım
|
||||||
|
// Şimdilik sadece loglayalım veya GraphMailer üzerinden test amaçlı bir yere atalım
|
||||||
|
// Gerçek uygulamada pgDB üzerinden alıcılar çekilmeli.
|
||||||
|
recipients := []string{"urun@baggi.com.tr"} // Varsayılan alıcı
|
||||||
|
|
||||||
|
msg := mailer.Message{
|
||||||
|
To: recipients,
|
||||||
|
Subject: subject,
|
||||||
|
BodyHTML: body.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ml.Send(context.Background(), msg); err != nil {
|
||||||
|
log.Printf("[sendProductionUpdateMails] send error: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[sendProductionUpdateMails] mail sent to %v", recipients)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -22,11 +23,13 @@ type ProductionUpdateLine struct {
|
|||||||
ItemDim2Code string `json:"ItemDim2Code"`
|
ItemDim2Code string `json:"ItemDim2Code"`
|
||||||
ItemDim3Code string `json:"ItemDim3Code"`
|
ItemDim3Code string `json:"ItemDim3Code"`
|
||||||
LineDescription string `json:"LineDescription"`
|
LineDescription string `json:"LineDescription"`
|
||||||
|
NewDueDate string `json:"NewDueDate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductionUpdateRequest struct {
|
type ProductionUpdateRequest struct {
|
||||||
Lines []ProductionUpdateLine `json:"lines"`
|
Lines []ProductionUpdateLine `json:"lines"`
|
||||||
InsertMissing bool `json:"insertMissing"`
|
InsertMissing bool `json:"insertMissing"`
|
||||||
|
NewDueDate string `json:"newDueDate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MissingVariant struct {
|
type MissingVariant struct {
|
||||||
@@ -79,6 +82,16 @@ func OrderProductionUpdateRoute(mssql *sql.DB) http.Handler {
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
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
|
// 1) Eksik varyantları kontrol et
|
||||||
missingMap := make(map[string]MissingVariant)
|
missingMap := make(map[string]MissingVariant)
|
||||||
checkStmt, err := tx.Prepare(`
|
checkStmt, err := tx.Prepare(`
|
||||||
@@ -187,12 +200,15 @@ UPDATE dbo.trOrderLine
|
|||||||
SET
|
SET
|
||||||
ItemCode = @p1,
|
ItemCode = @p1,
|
||||||
ColorCode = @p2,
|
ColorCode = @p2,
|
||||||
ItemDim2Code = @p3,
|
ItemDim1Code = @p3,
|
||||||
LineDescription = @p4,
|
ItemDim2Code = @p4,
|
||||||
LastUpdatedUserName = @p5,
|
LineDescription = @p5,
|
||||||
LastUpdatedDate = @p6
|
LastUpdatedUserName = @p6,
|
||||||
WHERE OrderHeaderID = @p7
|
LastUpdatedDate = @p7,
|
||||||
AND OrderLineID = @p8
|
OldDueDate = (SELECT TOP 1 AverageDueDate FROM dbo.trOrderHeader WHERE OrderHeaderID = @p8),
|
||||||
|
NewDueDate = @p9
|
||||||
|
WHERE OrderHeaderID = @p8
|
||||||
|
AND OrderLineID = @p10
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Update hazırlığı başarısız", http.StatusInternalServerError)
|
http.Error(w, "Update hazırlığı başarısız", http.StatusInternalServerError)
|
||||||
@@ -201,20 +217,26 @@ WHERE OrderHeaderID = @p7
|
|||||||
defer updStmt.Close()
|
defer updStmt.Close()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
var updatedDueDates []string
|
||||||
for _, ln := range req.Lines {
|
for _, ln := range req.Lines {
|
||||||
if _, err := updStmt.Exec(
|
if _, err := updStmt.Exec(
|
||||||
ln.ItemCode,
|
ln.ItemCode,
|
||||||
ln.ColorCode,
|
ln.ColorCode,
|
||||||
|
ln.ItemDim1Code,
|
||||||
ln.ItemDim2Code,
|
ln.ItemDim2Code,
|
||||||
ln.LineDescription,
|
ln.LineDescription,
|
||||||
username,
|
username,
|
||||||
now,
|
now,
|
||||||
id,
|
id,
|
||||||
|
ln.NewDueDate,
|
||||||
ln.OrderLineID,
|
ln.OrderLineID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
http.Error(w, "Satır güncelleme hatası", http.StatusInternalServerError)
|
http.Error(w, "Satır güncelleme hatası", http.StatusInternalServerError)
|
||||||
return
|
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 {
|
if err := tx.Commit(); err != nil {
|
||||||
@@ -222,6 +244,17 @@ WHERE OrderHeaderID = @p7
|
|||||||
return
|
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{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"updated": len(req.Lines),
|
"updated": len(req.Lines),
|
||||||
|
|||||||
85
svc/routes/product_cditem.go
Normal file
85
svc/routes/product_cditem.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/db"
|
||||||
|
"bssapp-backend/models"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetProductCdItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
|
if !ok || claims == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
if code == "" {
|
||||||
|
http.Error(w, "Eksik parametre: code", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
ItemTypeCode,
|
||||||
|
ItemCode,
|
||||||
|
ItemDimTypeCode,
|
||||||
|
ProductTypeCode,
|
||||||
|
ProductHierarchyID,
|
||||||
|
UnitOfMeasureCode1,
|
||||||
|
ItemAccountGrCode,
|
||||||
|
ItemTaxGrCode,
|
||||||
|
ItemPaymentPlanGrCode,
|
||||||
|
ItemDiscountGrCode,
|
||||||
|
ItemVendorGrCode,
|
||||||
|
PromotionGroupCode,
|
||||||
|
ProductCollectionGrCode,
|
||||||
|
StorePriceLevelCode,
|
||||||
|
PerceptionOfFashionCode,
|
||||||
|
CommercialRoleCode,
|
||||||
|
StoreCapacityLevelCode,
|
||||||
|
CustomsTariffNumberCode,
|
||||||
|
CompanyCode
|
||||||
|
FROM dbo.cdItem WITH(NOLOCK)
|
||||||
|
WHERE ItemCode = @p1;
|
||||||
|
`
|
||||||
|
row := db.MssqlDB.QueryRow(query, code)
|
||||||
|
|
||||||
|
var p models.OrderProductionCdItemDraft
|
||||||
|
err := row.Scan(
|
||||||
|
&p.ItemTypeCode,
|
||||||
|
&p.ItemCode,
|
||||||
|
&p.ItemDimTypeCode,
|
||||||
|
&p.ProductTypeCode,
|
||||||
|
&p.ProductHierarchyID,
|
||||||
|
&p.UnitOfMeasureCode1,
|
||||||
|
&p.ItemAccountGrCode,
|
||||||
|
&p.ItemTaxGrCode,
|
||||||
|
&p.ItemPaymentPlanGrCode,
|
||||||
|
&p.ItemDiscountGrCode,
|
||||||
|
&p.ItemVendorGrCode,
|
||||||
|
&p.PromotionGroupCode,
|
||||||
|
&p.ProductCollectionGrCode,
|
||||||
|
&p.StorePriceLevelCode,
|
||||||
|
&p.PerceptionOfFashionCode,
|
||||||
|
&p.CommercialRoleCode,
|
||||||
|
&p.StoreCapacityLevelCode,
|
||||||
|
&p.CustomsTariffNumberCode,
|
||||||
|
&p.CompanyCode,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "sql: no rows in result set" {
|
||||||
|
http.Error(w, "Ürün bulunamadı", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[GetProductCdItem] error code=%s err=%v", code, err)
|
||||||
|
http.Error(w, "Ürün cdItem bilgisi alınamadı", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_ = json.NewEncoder(w).Encode(p)
|
||||||
|
}
|
||||||
26
svc/routes/product_pricing.go
Normal file
26
svc/routes/product_pricing.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/queries"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/pricing/products
|
||||||
|
func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
|
if !ok || claims == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := queries.GetProductPricingList()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_ = json.NewEncoder(w).Encode(rows)
|
||||||
|
}
|
||||||
@@ -279,6 +279,19 @@ const menuItems = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: 'Fiyatlandırma',
|
||||||
|
icon: 'request_quote',
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'Ürün Fiyatlandırma',
|
||||||
|
to: '/app/pricing/product-pricing',
|
||||||
|
permission: 'order:view'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Sistem',
|
label: 'Sistem',
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
|
|||||||
@@ -60,11 +60,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<q-input
|
<q-input
|
||||||
:model-value="formatDate(header?.AverageDueDate)"
|
v-model="headerAverageDueDate"
|
||||||
label="Tahmini Termin Tarihi"
|
label="Tahmini Termin Tarihi"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
readonly
|
type="date"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,6 +225,18 @@
|
|||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</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">
|
<template #body-cell-NewDesc="props">
|
||||||
<q-td :props="props" class="cell-new">
|
<q-td :props="props" class="cell-new">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -255,6 +267,38 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section class="q-pt-md">
|
<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="row q-col-gutter-sm">
|
||||||
<div class="col-12 col-md-4">
|
<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" />
|
<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-badge color="primary">{{ attributeTargetCode || '-' }}</q-badge>
|
||||||
</q-card-section>
|
</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;">
|
<q-card-section style="max-height: 68vh; overflow: auto;">
|
||||||
<div
|
<div
|
||||||
v-for="(row, idx) in attributeRows"
|
v-for="(row, idx) in attributeRows"
|
||||||
@@ -353,8 +431,10 @@ const rows = ref([])
|
|||||||
const descFilter = ref('')
|
const descFilter = ref('')
|
||||||
const productOptions = ref([])
|
const productOptions = ref([])
|
||||||
const selectedMap = ref({})
|
const selectedMap = ref({})
|
||||||
|
const headerAverageDueDate = ref('')
|
||||||
const cdItemDialogOpen = ref(false)
|
const cdItemDialogOpen = ref(false)
|
||||||
const cdItemTargetCode = ref('')
|
const cdItemTargetCode = ref('')
|
||||||
|
const copySourceCode = ref(null)
|
||||||
const cdItemDraftForm = ref(createEmptyCdItemDraft(''))
|
const cdItemDraftForm = ref(createEmptyCdItemDraft(''))
|
||||||
const attributeDialogOpen = ref(false)
|
const attributeDialogOpen = ref(false)
|
||||||
const attributeTargetCode = ref('')
|
const attributeTargetCode = ref('')
|
||||||
@@ -363,13 +443,16 @@ const attributeRows = ref([])
|
|||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false, style: 'width:44px;', headerStyle: 'width:44px;' },
|
{ 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: '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: '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: '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: '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: '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: '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: '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' }
|
{ 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
|
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 filteredRows = computed(() => {
|
||||||
const needle = normalizeSearchText(descFilter.value)
|
const needle = normalizeSearchText(descFilter.value)
|
||||||
if (!needle) return rows.value
|
if (!needle) return rows.value
|
||||||
@@ -457,6 +557,19 @@ function applyNewItemVisualState (row, source = 'typed') {
|
|||||||
row.NewItemSource = info.mode === 'empty' ? '' : source
|
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) {
|
function newItemInputClass (row) {
|
||||||
return {
|
return {
|
||||||
'new-item-existing': row?.NewItemMode === 'existing',
|
'new-item-existing': row?.NewItemMode === 'existing',
|
||||||
@@ -579,9 +692,7 @@ function isNewCodeSetupComplete (itemCode) {
|
|||||||
|
|
||||||
function isColorSelectionLocked (row) {
|
function isColorSelectionLocked (row) {
|
||||||
const code = String(row?.NewItemCode || '').trim().toUpperCase()
|
const code = String(row?.NewItemCode || '').trim().toUpperCase()
|
||||||
if (!code) return true
|
return !code
|
||||||
if (row?.NewItemMode !== 'new') return false
|
|
||||||
return !isNewCodeSetupComplete(code)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNewCodeSetupFlow (itemCode) {
|
function openNewCodeSetupFlow (itemCode) {
|
||||||
@@ -746,25 +857,39 @@ function collectLinesFromRows (selectedRows) {
|
|||||||
NewItemCode: String(row.NewItemCode || '').trim().toUpperCase(),
|
NewItemCode: String(row.NewItemCode || '').trim().toUpperCase(),
|
||||||
NewColor: normalizeShortCode(row.NewColor, 3),
|
NewColor: normalizeShortCode(row.NewColor, 3),
|
||||||
NewDim2: normalizeShortCode(row.NewDim2, 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 oldItemCode = String(row.OldItemCode || '').trim().toUpperCase()
|
||||||
const oldColor = normalizeShortCode(row.OldColor, 3)
|
const oldColor = normalizeShortCode(row.OldColor, 3)
|
||||||
const oldDim2 = normalizeShortCode(row.OldDim2, 3)
|
const oldDim2 = normalizeShortCode(row.OldDim2, 3)
|
||||||
const oldDesc = String(row.OldDesc || '').trim()
|
const oldDesc = String(row.OldDesc || '').trim()
|
||||||
|
const oldDueDateValue = row.OldDueDate || ''
|
||||||
|
const newDueDateValue = row.NewDueDate || ''
|
||||||
|
|
||||||
const hasChange = (
|
const hasChange = (
|
||||||
baseLine.NewItemCode !== oldItemCode ||
|
baseLine.NewItemCode !== oldItemCode ||
|
||||||
baseLine.NewColor !== oldColor ||
|
baseLine.NewColor !== oldColor ||
|
||||||
baseLine.NewDim2 !== oldDim2 ||
|
baseLine.NewDim2 !== oldDim2 ||
|
||||||
String(baseLine.NewDesc || '').trim() !== oldDesc
|
String(baseLine.NewDesc || '').trim() !== oldDesc ||
|
||||||
|
newDueDateValue !== oldDueDateValue
|
||||||
)
|
)
|
||||||
if (!hasChange) continue
|
if (!hasChange) continue
|
||||||
|
|
||||||
for (const id of (row.OrderLineIDs || [])) {
|
const orderLines = Array.isArray(row.OrderLines) && row.OrderLines.length
|
||||||
lines.push({
|
? row.OrderLines
|
||||||
|
: (row.OrderLineIDs || []).map(id => ({
|
||||||
OrderLineID: id,
|
OrderLineID: id,
|
||||||
...baseLine
|
ItemDim1Code: ''
|
||||||
|
}))
|
||||||
|
|
||||||
|
for (const line of orderLines) {
|
||||||
|
lines.push({
|
||||||
|
...baseLine,
|
||||||
|
OrderLineID: line?.OrderLineID,
|
||||||
|
ItemDim1Code: store.toPayloadDim1Code(row, line?.ItemDim1Code || '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -830,9 +955,67 @@ function isDummyLookupOption (key, codeRaw, descRaw) {
|
|||||||
return false
|
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) {
|
async function openCdItemDialog (itemCode) {
|
||||||
const code = String(itemCode || '').trim().toUpperCase()
|
const code = String(itemCode || '').trim().toUpperCase()
|
||||||
if (!code) return
|
if (!code) return
|
||||||
|
copySourceCode.value = null
|
||||||
await store.fetchCdItemLookups()
|
await store.fetchCdItemLookups()
|
||||||
|
|
||||||
cdItemTargetCode.value = code
|
cdItemTargetCode.value = code
|
||||||
@@ -848,6 +1031,13 @@ async function openCdItemDialog (itemCode) {
|
|||||||
cdItemDialogOpen.value = true
|
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) {
|
function normalizeCdItemDraftForPayload (draftRaw) {
|
||||||
const d = draftRaw || {}
|
const d = draftRaw || {}
|
||||||
const toIntOrNil = (v) => {
|
const toIntOrNil = (v) => {
|
||||||
@@ -882,12 +1072,16 @@ function normalizeCdItemDraftForPayload (draftRaw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveCdItemDraft () {
|
async function saveCdItemDraft () {
|
||||||
const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value)
|
const payload = persistCdItemDraft()
|
||||||
if (!payload.ItemCode) {
|
if (!payload?.ItemCode) {
|
||||||
$q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' })
|
$q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store.setCdItemDraft(payload.ItemCode, payload)
|
console.info('[OrderProductionUpdate] saveCdItemDraft', {
|
||||||
|
code: payload.ItemCode,
|
||||||
|
itemDimTypeCode: payload.ItemDimTypeCode,
|
||||||
|
productHierarchyID: payload.ProductHierarchyID
|
||||||
|
})
|
||||||
cdItemDialogOpen.value = false
|
cdItemDialogOpen.value = false
|
||||||
await openAttributeDialog(payload.ItemCode)
|
await openAttributeDialog(payload.ItemCode)
|
||||||
}
|
}
|
||||||
@@ -981,6 +1175,7 @@ function mergeAttributeDraftWithLookupOptions (draftRows, lookupRows) {
|
|||||||
async function openAttributeDialog (itemCode) {
|
async function openAttributeDialog (itemCode) {
|
||||||
const code = String(itemCode || '').trim().toUpperCase()
|
const code = String(itemCode || '').trim().toUpperCase()
|
||||||
if (!code) return
|
if (!code) return
|
||||||
|
copySourceCode.value = null
|
||||||
attributeTargetCode.value = code
|
attributeTargetCode.value = code
|
||||||
const existingDraft = store.getProductAttributeDraft(code)
|
const existingDraft = store.getProductAttributeDraft(code)
|
||||||
const modeInfo = store.classifyItemCode(code)
|
const modeInfo = store.classifyItemCode(code)
|
||||||
@@ -1001,6 +1196,10 @@ async function openAttributeDialog (itemCode) {
|
|||||||
code,
|
code,
|
||||||
dbCurrentCount: Array.isArray(dbCurrent) ? dbCurrent.length : 0
|
dbCurrentCount: Array.isArray(dbCurrent) ? dbCurrent.length : 0
|
||||||
})
|
})
|
||||||
|
if (Array.isArray(dbCurrent) && dbCurrent.length) {
|
||||||
|
store.markItemCodeKnownExisting(code, true)
|
||||||
|
syncRowsForKnownExistingCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
const dbMap = new Map(
|
const dbMap = new Map(
|
||||||
(dbCurrent || []).map(x => [
|
(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
|
attributeRows.value = useDraft
|
||||||
? JSON.parse(JSON.stringify(mergeAttributeDraftWithLookupOptions(existingDraft, baseRows)))
|
? JSON.parse(JSON.stringify(mergeAttributeDraftWithLookupOptions(existingDraft, baseRows)))
|
||||||
: JSON.parse(JSON.stringify(baseRows))
|
: JSON.parse(JSON.stringify(baseRows))
|
||||||
@@ -1050,9 +1249,7 @@ async function openAttributeDialog (itemCode) {
|
|||||||
row.Options = [...row.AllOptions]
|
row.Options = [...row.AllOptions]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (modeInfo.mode === 'existing') {
|
if ((!existingDraft || !existingDraft.length) && baseRows.length) {
|
||||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows)))
|
|
||||||
} else if ((!existingDraft || !existingDraft.length) && baseRows.length) {
|
|
||||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows)))
|
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows)))
|
||||||
}
|
}
|
||||||
attributeDialogOpen.value = true
|
attributeDialogOpen.value = true
|
||||||
@@ -1081,6 +1278,26 @@ function saveAttributeDraft () {
|
|||||||
$q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' })
|
$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) {
|
async function collectProductAttributesFromSelectedRows (selectedRows) {
|
||||||
const codeSet = [...new Set(
|
const codeSet = [...new Set(
|
||||||
(selectedRows || [])
|
(selectedRows || [])
|
||||||
@@ -1205,7 +1422,7 @@ async function collectProductAttributesFromSelectedRows (selectedRows) {
|
|||||||
return { errMsg: '', productAttributes: out }
|
return { errMsg: '', productAttributes: out }
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectCdItemsFromSelectedRows (selectedRows) {
|
async function collectCdItemsFromSelectedRows (selectedRows) {
|
||||||
const codes = [...new Set(
|
const codes = [...new Set(
|
||||||
(selectedRows || [])
|
(selectedRows || [])
|
||||||
.filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim())
|
.filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim())
|
||||||
@@ -1215,7 +1432,16 @@ function collectCdItemsFromSelectedRows (selectedRows) {
|
|||||||
|
|
||||||
const out = []
|
const out = []
|
||||||
for (const code of codes) {
|
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) {
|
if (!draft) {
|
||||||
return { errMsg: `${code} icin cdItem bilgisi eksik`, cdItems: [] }
|
return { errMsg: `${code} icin cdItem bilgisi eksik`, cdItems: [] }
|
||||||
}
|
}
|
||||||
@@ -1235,11 +1461,49 @@ function buildMailLineLabelFromRow (row) {
|
|||||||
return [item, colorPart, desc].filter(Boolean).join(' ')
|
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) {
|
function buildProductionUpdateMailPayload (selectedRows) {
|
||||||
const updatedItems = [
|
const updatedItems = [
|
||||||
...new Set(
|
...new Set(
|
||||||
(selectedRows || [])
|
(selectedRows || [])
|
||||||
.map(buildMailLineLabelFromRow)
|
.map(buildUpdateMailLineLabelFromRow)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -1248,10 +1512,29 @@ function buildProductionUpdateMailPayload (selectedRows) {
|
|||||||
operation: 'update',
|
operation: 'update',
|
||||||
deletedItems: [],
|
deletedItems: [],
|
||||||
updatedItems,
|
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) {
|
async function sendUpdateMailAfterApply (selectedRows) {
|
||||||
const orderId = String(orderHeaderID.value || '').trim()
|
const orderId = String(orderHeaderID.value || '').trim()
|
||||||
if (!orderId) return
|
if (!orderId) return
|
||||||
@@ -1275,6 +1558,7 @@ async function sendUpdateMailAfterApply (selectedRows) {
|
|||||||
deletedItems: payload.deletedItems,
|
deletedItems: payload.deletedItems,
|
||||||
updatedItems: payload.updatedItems,
|
updatedItems: payload.updatedItems,
|
||||||
addedItems: payload.addedItems,
|
addedItems: payload.addedItems,
|
||||||
|
dueDateChanges: payload.dueDateChanges,
|
||||||
extraRecipients: ['urun@baggi.com.tr']
|
extraRecipients: ['urun@baggi.com.tr']
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1318,11 +1602,34 @@ function buildGroupKey (item) {
|
|||||||
function formatSizes (sizeMap) {
|
function formatSizes (sizeMap) {
|
||||||
const entries = Object.entries(sizeMap || {})
|
const entries = Object.entries(sizeMap || {})
|
||||||
if (!entries.length) return { list: [], label: '-' }
|
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(', ')
|
const label = entries.map(([k, v]) => (v > 1 ? `${k}(${v})` : k)).join(', ')
|
||||||
return { list: entries.map(([k]) => k), label }
|
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 = []) {
|
function groupItems (items, prevRows = []) {
|
||||||
const prevMap = new Map()
|
const prevMap = new Map()
|
||||||
for (const r of prevRows || []) {
|
for (const r of prevRows || []) {
|
||||||
@@ -1334,7 +1641,8 @@ function groupItems (items, prevRows = []) {
|
|||||||
NewDim2: String(r.NewDim2 || '').trim().toUpperCase(),
|
NewDim2: String(r.NewDim2 || '').trim().toUpperCase(),
|
||||||
NewItemMode: String(r.NewItemMode || '').trim(),
|
NewItemMode: String(r.NewItemMode || '').trim(),
|
||||||
NewItemSource: String(r.NewItemSource || '').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()
|
const map = new Map()
|
||||||
@@ -1350,12 +1658,19 @@ function groupItems (items, prevRows = []) {
|
|||||||
OrderHeaderID: it.OrderHeaderID,
|
OrderHeaderID: it.OrderHeaderID,
|
||||||
OldItemCode: it.OldItemCode,
|
OldItemCode: it.OldItemCode,
|
||||||
OldColor: it.OldColor,
|
OldColor: it.OldColor,
|
||||||
|
OldColorDescription: it.OldColorDescription,
|
||||||
|
OldColorLabel: formatCodeDescriptionLabel(it.OldColor, it.OldColorDescription),
|
||||||
OldDim2: it.OldDim2,
|
OldDim2: it.OldDim2,
|
||||||
OldDim3: it.OldDim3,
|
OldDim3: it.OldDim3,
|
||||||
OldDesc: it.OldDesc,
|
OldDesc: it.OldDesc,
|
||||||
|
OldDueDate: it.OldDueDate || '',
|
||||||
|
NewDueDate: (prev.NewDueDate || it.OldDueDate || ''),
|
||||||
OrderLineIDs: [],
|
OrderLineIDs: [],
|
||||||
|
OrderLines: [],
|
||||||
OldSizes: [],
|
OldSizes: [],
|
||||||
OldSizesLabel: '',
|
OldSizesLabel: '',
|
||||||
|
OldTotalQty: 0,
|
||||||
|
OldTotalQtyLabel: '0',
|
||||||
NewItemCode: prev.NewItemCode || '',
|
NewItemCode: prev.NewItemCode || '',
|
||||||
NewColor: prev.NewColor || '',
|
NewColor: prev.NewColor || '',
|
||||||
NewDim2: prev.NewDim2 || '',
|
NewDim2: prev.NewDim2 || '',
|
||||||
@@ -1363,18 +1678,34 @@ function groupItems (items, prevRows = []) {
|
|||||||
NewItemMode: prev.NewItemMode || 'empty',
|
NewItemMode: prev.NewItemMode || 'empty',
|
||||||
NewItemSource: prev.NewItemSource || '',
|
NewItemSource: prev.NewItemSource || '',
|
||||||
NewItemEntryMode: prev.NewItemEntryMode || '',
|
NewItemEntryMode: prev.NewItemEntryMode || '',
|
||||||
IsVariantMissing: !!it.IsVariantMissing
|
IsVariantMissing: !!it.IsVariantMissing,
|
||||||
|
yasPayloadMap: {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const g = map.get(key)
|
const g = map.get(key)
|
||||||
if (it?.OrderLineID) g.OrderLineIDs.push(it.OrderLineID)
|
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 !== '') {
|
if (size !== '') {
|
||||||
g.__sizeMap = g.__sizeMap || {}
|
g.__sizeMap = g.__sizeMap || {}
|
||||||
g.__sizeMap[size] = (g.__sizeMap[size] || 0) + 1
|
g.__sizeMap[size] = (g.__sizeMap[size] || 0) + 1
|
||||||
}
|
}
|
||||||
|
g.__oldQtyTotal = Number(g.__oldQtyTotal || 0) + Number(it?.OldQty || 0)
|
||||||
if (it?.IsVariantMissing) g.IsVariantMissing = true
|
if (it?.IsVariantMissing) g.IsVariantMissing = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,6 +1714,8 @@ function groupItems (items, prevRows = []) {
|
|||||||
const sizes = formatSizes(g.__sizeMap || {})
|
const sizes = formatSizes(g.__sizeMap || {})
|
||||||
g.OldSizes = sizes.list
|
g.OldSizes = sizes.list
|
||||||
g.OldSizesLabel = sizes.label
|
g.OldSizesLabel = sizes.label
|
||||||
|
g.OldTotalQty = Number(g.__oldQtyTotal || 0)
|
||||||
|
g.OldTotalQtyLabel = formatQtyLabel(g.OldTotalQty)
|
||||||
const info = store.classifyItemCode(g.NewItemCode)
|
const info = store.classifyItemCode(g.NewItemCode)
|
||||||
g.NewItemCode = info.normalized
|
g.NewItemCode = info.normalized
|
||||||
g.NewItemMode = info.mode
|
g.NewItemMode = info.mode
|
||||||
@@ -1391,6 +1724,7 @@ function groupItems (items, prevRows = []) {
|
|||||||
g.NewItemEntryMode = g.NewItemSource === 'selected' ? 'selected' : 'typed'
|
g.NewItemEntryMode = g.NewItemSource === 'selected' ? 'selected' : 'typed'
|
||||||
}
|
}
|
||||||
delete g.__sizeMap
|
delete g.__sizeMap
|
||||||
|
delete g.__oldQtyTotal
|
||||||
out.push(g)
|
out.push(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1406,8 +1740,10 @@ async function refreshAll () {
|
|||||||
async function onBulkSubmit () {
|
async function onBulkSubmit () {
|
||||||
const flowStart = nowMs()
|
const flowStart = nowMs()
|
||||||
const selectedRows = rows.value.filter(r => !!selectedMap.value[r.RowKey])
|
const selectedRows = rows.value.filter(r => !!selectedMap.value[r.RowKey])
|
||||||
if (!selectedRows.length) {
|
const headerAverageDueDateValue = normalizeDateInput(headerAverageDueDate.value)
|
||||||
$q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz.' })
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1417,24 +1753,32 @@ async function onBulkSubmit () {
|
|||||||
$q.notify({ type: 'negative', message: errMsg })
|
$q.notify({ type: 'negative', message: errMsg })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!lines.length) {
|
if (!lines.length && !headerDateChanged) {
|
||||||
$q.notify({ type: 'warning', message: 'Secili satirlarda degisiklik yok.' })
|
$q.notify({ type: 'warning', message: 'Secili satirlarda degisiklik yok.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { errMsg: cdErrMsg, cdItems } = collectCdItemsFromSelectedRows(selectedRows)
|
|
||||||
|
let cdItems = []
|
||||||
|
let productAttributes = []
|
||||||
|
if (lines.length > 0) {
|
||||||
|
const { errMsg: cdErrMsg, cdItems: nextCdItems } = await collectCdItemsFromSelectedRows(selectedRows)
|
||||||
if (cdErrMsg) {
|
if (cdErrMsg) {
|
||||||
$q.notify({ type: 'negative', message: cdErrMsg })
|
$q.notify({ type: 'negative', message: cdErrMsg })
|
||||||
const firstCode = String(cdErrMsg.split(' ')[0] || '').trim()
|
const firstCode = String(cdErrMsg.split(' ')[0] || '').trim()
|
||||||
if (firstCode) openCdItemDialog(firstCode)
|
if (firstCode) openCdItemDialog(firstCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { errMsg: attrErrMsg, productAttributes } = await collectProductAttributesFromSelectedRows(selectedRows)
|
cdItems = nextCdItems
|
||||||
|
|
||||||
|
const { errMsg: attrErrMsg, productAttributes: nextProductAttributes } = await collectProductAttributesFromSelectedRows(selectedRows)
|
||||||
if (attrErrMsg) {
|
if (attrErrMsg) {
|
||||||
$q.notify({ type: 'negative', message: attrErrMsg })
|
$q.notify({ type: 'negative', message: attrErrMsg })
|
||||||
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
|
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
|
||||||
if (firstCode) openAttributeDialog(firstCode)
|
if (firstCode) openAttributeDialog(firstCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
productAttributes = nextProductAttributes
|
||||||
|
}
|
||||||
|
|
||||||
console.info('[OrderProductionUpdate] onBulkSubmit prepared', {
|
console.info('[OrderProductionUpdate] onBulkSubmit prepared', {
|
||||||
orderHeaderID: orderHeaderID.value,
|
orderHeaderID: orderHeaderID.value,
|
||||||
@@ -1442,18 +1786,56 @@ async function onBulkSubmit () {
|
|||||||
lineCount: lines.length,
|
lineCount: lines.length,
|
||||||
cdItemCount: cdItems.length,
|
cdItemCount: cdItems.length,
|
||||||
attributeCount: productAttributes.length,
|
attributeCount: productAttributes.length,
|
||||||
|
headerAverageDueDate: headerAverageDueDateValue,
|
||||||
|
headerDateChanged,
|
||||||
prepDurationMs: Math.round(nowMs() - prepStart)
|
prepDurationMs: Math.round(nowMs() - prepStart)
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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.' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.length > 0) {
|
||||||
const validateStart = nowMs()
|
const validateStart = nowMs()
|
||||||
const validate = await store.validateUpdates(orderHeaderID.value, lines)
|
const validate = await store.validateUpdates(orderHeaderID.value, lines)
|
||||||
console.info('[OrderProductionUpdate] validate finished', {
|
console.info('[OrderProductionUpdate] validate finished', {
|
||||||
orderHeaderID: orderHeaderID.value,
|
orderHeaderID: orderHeaderID.value,
|
||||||
lineCount: lines.length,
|
lineCount: lines.length,
|
||||||
missingCount: Number(validate?.missingCount || 0),
|
missingCount: Number(validate?.missingCount || 0),
|
||||||
|
barcodeValidationCount: Number(validate?.barcodeValidationCount || 0),
|
||||||
durationMs: Math.round(nowMs() - validateStart)
|
durationMs: Math.round(nowMs() - validateStart)
|
||||||
})
|
})
|
||||||
|
if (showBarcodeValidationDialog(validate?.barcodeValidations)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const missingCount = validate?.missingCount || 0
|
const missingCount = validate?.missingCount || 0
|
||||||
if (missingCount > 0) {
|
if (missingCount > 0) {
|
||||||
const missingList = (validate?.missing || []).map(v => (
|
const missingList = (validate?.missing || []).map(v => (
|
||||||
@@ -1466,38 +1848,26 @@ async function onBulkSubmit () {
|
|||||||
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
||||||
cancel: { label: 'Vazgec', flat: true }
|
cancel: { label: 'Vazgec', flat: true }
|
||||||
}).onOk(async () => {
|
}).onOk(async () => {
|
||||||
const applyStart = nowMs()
|
await applyChanges(true)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const applyStart = nowMs()
|
await applyChanges(false)
|
||||||
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)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[OrderProductionUpdate] onBulkSubmit failed', {
|
console.error('[OrderProductionUpdate] onBulkSubmit failed', {
|
||||||
orderHeaderID: orderHeaderID.value,
|
orderHeaderID: orderHeaderID.value,
|
||||||
selectedRowCount: selectedRows.length,
|
selectedRowCount: selectedRows.length,
|
||||||
lineCount: lines.length,
|
lineCount: lines.length,
|
||||||
|
headerAverageDueDate: headerAverageDueDateValue,
|
||||||
|
headerDateChanged,
|
||||||
apiError: err?.response?.data,
|
apiError: err?.response?.data,
|
||||||
message: err?.message
|
message: err?.message
|
||||||
})
|
})
|
||||||
|
if (showBarcodeValidationDialog(err?.response?.data?.barcodeValidations)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
$q.notify({ type: 'negative', message: store.error || 'Toplu kayit islemi basarisiz.' })
|
$q.notify({ type: 'negative', message: store.error || 'Toplu kayit islemi basarisiz.' })
|
||||||
}
|
}
|
||||||
console.info('[OrderProductionUpdate] onBulkSubmit total', {
|
console.info('[OrderProductionUpdate] onBulkSubmit total', {
|
||||||
|
|||||||
1188
ui/src/pages/ProductPricing.vue
Normal file
1188
ui/src/pages/ProductPricing.vue
Normal file
@@ -0,0 +1,1188 @@
|
|||||||
|
<template>
|
||||||
|
<q-page class="q-pa-xs pricing-page">
|
||||||
|
<div class="top-bar row items-center justify-between q-mb-xs">
|
||||||
|
<div class="text-subtitle1 text-weight-bold">Urun Fiyatlandirma</div>
|
||||||
|
<div class="row items-center q-gutter-xs">
|
||||||
|
<q-btn-dropdown color="secondary" outline icon="view_module" label="Doviz Gorunumu" :auto-close="false">
|
||||||
|
<q-list dense class="currency-menu-list">
|
||||||
|
<q-item clickable @click="selectAllCurrencies">
|
||||||
|
<q-item-section>Tumunu Sec</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="clearAllCurrencies">
|
||||||
|
<q-item-section>Tumunu Temizle</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator />
|
||||||
|
<q-item v-for="option in currencyOptions" :key="option.value" clickable @click="toggleCurrencyRow(option.value)">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-checkbox
|
||||||
|
:model-value="isCurrencySelected(option.value)"
|
||||||
|
dense
|
||||||
|
@update:model-value="(val) => toggleCurrency(option.value, val)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ option.label }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:color="showSelectedOnly ? 'primary' : 'grey-7'"
|
||||||
|
:icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'"
|
||||||
|
:label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'"
|
||||||
|
:disable="!showSelectedOnly && selectedRowCount === 0"
|
||||||
|
@click="toggleShowSelectedOnly"
|
||||||
|
/>
|
||||||
|
<q-btn flat color="grey-7" icon="restart_alt" label="Filtreleri Sifirla" @click="resetAll" />
|
||||||
|
<q-btn color="primary" icon="refresh" label="Veriyi Yenile" :loading="store.loading" @click="reloadData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
||||||
|
<q-table
|
||||||
|
ref="mainTableRef"
|
||||||
|
class="pane-table pricing-table"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
row-key="id"
|
||||||
|
:rows="filteredRows"
|
||||||
|
:columns="visibleColumns"
|
||||||
|
:loading="store.loading"
|
||||||
|
virtual-scroll
|
||||||
|
:virtual-scroll-item-size="rowHeight"
|
||||||
|
:virtual-scroll-sticky-size-start="headerHeight"
|
||||||
|
:virtual-scroll-slice-size="36"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
|
hide-bottom
|
||||||
|
:table-style="tableStyle"
|
||||||
|
>
|
||||||
|
<template #header="props">
|
||||||
|
<q-tr :props="props" class="header-row-fixed">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
:class="[col.headerClasses, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
||||||
|
:style="getHeaderCellStyle(col)"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-if="col.name === 'select'"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
:model-value="allSelectedVisible"
|
||||||
|
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||||
|
@update:model-value="toggleSelectAllVisible"
|
||||||
|
/>
|
||||||
|
<div v-else class="header-with-filter">
|
||||||
|
<span>{{ col.label }}</span>
|
||||||
|
<q-btn
|
||||||
|
v-if="isHeaderFilterField(col.field)"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
size="8px"
|
||||||
|
icon="filter_alt"
|
||||||
|
:color="hasFilter(col.field) ? 'primary' : 'grey-7'"
|
||||||
|
class="header-filter-btn"
|
||||||
|
>
|
||||||
|
<q-badge v-if="hasFilter(col.field)" color="primary" floating rounded>
|
||||||
|
{{ getFilterBadgeValue(col.field) }}
|
||||||
|
</q-badge>
|
||||||
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||||
|
<div v-if="isMultiSelectFilterField(col.field)" class="excel-filter-menu">
|
||||||
|
<q-input
|
||||||
|
v-model="columnFilterSearch[col.field]"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
clearable
|
||||||
|
use-input
|
||||||
|
class="excel-filter-select"
|
||||||
|
placeholder="Ara"
|
||||||
|
/>
|
||||||
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
||||||
|
<q-btn flat dense size="sm" label="Tumunu Sec" @click="selectAllColumnFilterOptions(col.field)" />
|
||||||
|
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(col.field)" />
|
||||||
|
</div>
|
||||||
|
<q-virtual-scroll
|
||||||
|
v-if="getFilterOptionsForField(col.field).length > 0"
|
||||||
|
class="excel-filter-options"
|
||||||
|
:items="getFilterOptionsForField(col.field)"
|
||||||
|
:virtual-scroll-item-size="32"
|
||||||
|
separator
|
||||||
|
>
|
||||||
|
<template #default="{ item: option }">
|
||||||
|
<q-item
|
||||||
|
:key="`${col.field}-${option.value}`"
|
||||||
|
dense
|
||||||
|
clickable
|
||||||
|
class="excel-filter-option"
|
||||||
|
@click="toggleColumnFilterValue(col.field, option.value)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-checkbox
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
:model-value="isColumnFilterValueSelected(col.field, option.value)"
|
||||||
|
@update:model-value="() => toggleColumnFilterValue(col.field, option.value)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ option.label }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
<div v-else class="excel-filter-empty">
|
||||||
|
Sonuc yok
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isNumberRangeFilterField(col.field)" class="excel-filter-menu">
|
||||||
|
<div class="range-filter-grid">
|
||||||
|
<q-input
|
||||||
|
v-model="numberRangeFilters[col.field].min"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
clearable
|
||||||
|
label="Min"
|
||||||
|
inputmode="decimal"
|
||||||
|
class="range-filter-field"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="numberRangeFilters[col.field].max"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
clearable
|
||||||
|
label="Max"
|
||||||
|
inputmode="decimal"
|
||||||
|
class="range-filter-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-end q-pt-xs">
|
||||||
|
<q-btn flat dense size="sm" label="Temizle" @click="clearRangeFilter(col.field)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isDateRangeFilterField(col.field)" class="excel-filter-menu">
|
||||||
|
<div class="range-filter-grid">
|
||||||
|
<q-input
|
||||||
|
v-model="dateRangeFilters[col.field].from"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
clearable
|
||||||
|
type="date"
|
||||||
|
label="Baslangic"
|
||||||
|
class="range-filter-field"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="dateRangeFilters[col.field].to"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
clearable
|
||||||
|
type="date"
|
||||||
|
label="Bitis"
|
||||||
|
class="range-filter-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-end q-pt-xs">
|
||||||
|
<q-btn flat dense size="sm" label="Temizle" @click="clearRangeFilter(col.field)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
size="8px"
|
||||||
|
icon="filter_alt"
|
||||||
|
class="header-filter-btn header-filter-ghost"
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-select="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
class="text-center selection-col"
|
||||||
|
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||||
|
:style="getBodyCellStyle(props.col)"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
:model-value="!!selectedMap[props.row.id]"
|
||||||
|
@update:model-value="(val) => toggleRowSelection(props.row.id, val)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-productCode="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||||
|
:style="getBodyCellStyle(props.col)"
|
||||||
|
>
|
||||||
|
<span class="product-code-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-stockQty="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||||
|
:style="getBodyCellStyle(props.col)"
|
||||||
|
>
|
||||||
|
<span class="stock-qty-text">{{ formatStock(props.value) }}</span>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-stockEntryDate="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||||
|
:style="getBodyCellStyle(props.col)"
|
||||||
|
>
|
||||||
|
<span class="date-cell-text">{{ formatDateDisplay(props.value) }}</span>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-lastPricingDate="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||||
|
:style="getBodyCellStyle(props.col)"
|
||||||
|
>
|
||||||
|
<span :class="['date-cell-text', { 'date-warning': needsRepricing(props.row) }]">
|
||||||
|
{{ formatDateDisplay(props.value) }}
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-brandGroupSelection="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||||
|
:style="getBodyCellStyle(props.col)"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="native-cell-select"
|
||||||
|
:value="props.row.brandGroupSelection"
|
||||||
|
@change="(e) => onBrandGroupSelectionChange(props.row, e.target.value)"
|
||||||
|
>
|
||||||
|
<option value="">Seciniz</option>
|
||||||
|
<option v-for="opt in brandGroupOptions" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||||
|
:style="getBodyCellStyle(props.col)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="editableColumnSet.has(props.col.name)"
|
||||||
|
class="native-cell-input text-right"
|
||||||
|
:value="formatPrice(props.row[props.col.field])"
|
||||||
|
type="text"
|
||||||
|
inputmode="decimal"
|
||||||
|
@change="(e) => onEditableCellChange(props.row, props.col.field, e.target.value)"
|
||||||
|
/>
|
||||||
|
<span v-else class="cell-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-banner v-if="store.error" class="bg-red text-white q-mt-xs">
|
||||||
|
Hata: {{ store.error }}
|
||||||
|
</q-banner>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||||
|
|
||||||
|
const store = useProductPricingStore()
|
||||||
|
|
||||||
|
const usdToTry = 38.25
|
||||||
|
const eurToTry = 41.6
|
||||||
|
const multipliers = [1, 1.03, 1.06, 1.09, 1.12, 1.15]
|
||||||
|
const rowHeight = 31
|
||||||
|
const headerHeight = 72
|
||||||
|
|
||||||
|
const brandGroupOptions = [
|
||||||
|
{ label: 'MARKA GRUBU A', value: 'MARKA GRUBU A' },
|
||||||
|
{ label: 'MARKA GRUBU B', value: 'MARKA GRUBU B' },
|
||||||
|
{ label: 'MARKA GRUBU C', value: 'MARKA GRUBU C' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currencyOptions = [
|
||||||
|
{ label: 'USD', value: 'USD' },
|
||||||
|
{ label: 'EUR', value: 'EUR' },
|
||||||
|
{ label: 'TRY', value: 'TRY' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const multiFilterColumns = [
|
||||||
|
{ field: 'productCode', label: 'Urun Kodu' },
|
||||||
|
{ field: 'askiliYan', label: 'Askili Yan' },
|
||||||
|
{ field: 'kategori', label: 'Kategori' },
|
||||||
|
{ field: 'urunIlkGrubu', label: 'Urun Ilk Grubu' },
|
||||||
|
{ field: 'urunAnaGrubu', label: 'Urun Ana Grubu' },
|
||||||
|
{ field: 'urunAltGrubu', label: 'Urun Alt Grubu' },
|
||||||
|
{ field: 'icerik', label: 'Icerik' },
|
||||||
|
{ field: 'karisim', label: 'Karisim' }
|
||||||
|
]
|
||||||
|
const numberRangeFilterFields = ['stockQty']
|
||||||
|
const dateRangeFilterFields = ['stockEntryDate', 'lastPricingDate']
|
||||||
|
const columnFilters = ref({
|
||||||
|
productCode: [],
|
||||||
|
askiliYan: [],
|
||||||
|
kategori: [],
|
||||||
|
urunIlkGrubu: [],
|
||||||
|
urunAnaGrubu: [],
|
||||||
|
urunAltGrubu: [],
|
||||||
|
icerik: [],
|
||||||
|
karisim: []
|
||||||
|
})
|
||||||
|
const columnFilterSearch = ref({
|
||||||
|
productCode: '',
|
||||||
|
askiliYan: '',
|
||||||
|
kategori: '',
|
||||||
|
urunIlkGrubu: '',
|
||||||
|
urunAnaGrubu: '',
|
||||||
|
urunAltGrubu: '',
|
||||||
|
icerik: '',
|
||||||
|
karisim: ''
|
||||||
|
})
|
||||||
|
const numberRangeFilters = ref({
|
||||||
|
stockQty: { min: '', max: '' }
|
||||||
|
})
|
||||||
|
const dateRangeFilters = ref({
|
||||||
|
stockEntryDate: { from: '', to: '' },
|
||||||
|
lastPricingDate: { from: '', to: '' }
|
||||||
|
})
|
||||||
|
const multiSelectFilterFieldSet = new Set(multiFilterColumns.map((x) => x.field))
|
||||||
|
const numberRangeFilterFieldSet = new Set(numberRangeFilterFields)
|
||||||
|
const dateRangeFilterFieldSet = new Set(dateRangeFilterFields)
|
||||||
|
const headerFilterFieldSet = new Set([
|
||||||
|
...multiFilterColumns.map((x) => x.field),
|
||||||
|
...numberRangeFilterFields,
|
||||||
|
...dateRangeFilterFields
|
||||||
|
])
|
||||||
|
|
||||||
|
const mainTableRef = ref(null)
|
||||||
|
const selectedMap = ref({})
|
||||||
|
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||||
|
const showSelectedOnly = ref(false)
|
||||||
|
|
||||||
|
const editableColumns = [
|
||||||
|
'costPrice',
|
||||||
|
'expenseForBasePrice',
|
||||||
|
'basePriceUsd',
|
||||||
|
'basePriceTry',
|
||||||
|
'usd1',
|
||||||
|
'usd2',
|
||||||
|
'usd3',
|
||||||
|
'usd4',
|
||||||
|
'usd5',
|
||||||
|
'usd6',
|
||||||
|
'eur1',
|
||||||
|
'eur2',
|
||||||
|
'eur3',
|
||||||
|
'eur4',
|
||||||
|
'eur5',
|
||||||
|
'eur6',
|
||||||
|
'try1',
|
||||||
|
'try2',
|
||||||
|
'try3',
|
||||||
|
'try4',
|
||||||
|
'try5',
|
||||||
|
'try6'
|
||||||
|
]
|
||||||
|
const editableColumnSet = new Set(editableColumns)
|
||||||
|
|
||||||
|
function col (name, label, field, width, extra = {}) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
field,
|
||||||
|
align: extra.align || 'left',
|
||||||
|
sortable: !!extra.sortable,
|
||||||
|
style: `width:${width}px;min-width:${width}px;max-width:${width}px;`,
|
||||||
|
headerStyle: `width:${width}px;min-width:${width}px;max-width:${width}px;`,
|
||||||
|
classes: extra.classes || '',
|
||||||
|
headerClasses: extra.headerClasses || extra.classes || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allColumns = [
|
||||||
|
col('select', '', 'select', 40, { align: 'center', classes: 'text-center selection-col' }),
|
||||||
|
col('productCode', 'URUN KODU', 'productCode', 108, { sortable: true, classes: 'ps-col product-code-col' }),
|
||||||
|
col('stockQty', 'STOK ADET', 'stockQty', 72, { align: 'right', sortable: true, classes: 'ps-col stock-col' }),
|
||||||
|
col('stockEntryDate', 'STOK GIRIS TARIHI', 'stockEntryDate', 92, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||||
|
col('lastPricingDate', 'SON FIYATLANDIRMA TARIHI', 'lastPricingDate', 108, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||||
|
col('askiliYan', 'ASKILI YAN', 'askiliYan', 54, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('kategori', 'KATEGORI', 'kategori', 54, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('icerik', 'ICERIK', 'icerik', 62, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('karisim', 'KARISIM', 'karisim', 62, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('marka', 'MARKA', 'marka', 54, { sortable: true, classes: 'ps-col' }),
|
||||||
|
col('brandGroupSelection', 'MARKA GRUBU SECIMI', 'brandGroupSelection', 76),
|
||||||
|
col('costPrice', 'MALIYET FIYATI', 'costPrice', 74, { align: 'right', sortable: true, classes: 'usd-col' }),
|
||||||
|
col('expenseForBasePrice', 'TABAN FIYAT MASRAF', 'expenseForBasePrice', 86, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 74, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('basePriceTry', 'TABAN TRY', 'basePriceTry', 74, { align: 'right', classes: 'try-col' }),
|
||||||
|
col('usd1', 'USD 1', 'usd1', 62, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('usd2', 'USD 2', 'usd2', 62, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('usd3', 'USD 3', 'usd3', 62, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('usd4', 'USD 4', 'usd4', 62, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('usd5', 'USD 5', 'usd5', 62, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('usd6', 'USD 6', 'usd6', 62, { align: 'right', classes: 'usd-col' }),
|
||||||
|
col('eur1', 'EUR 1', 'eur1', 62, { align: 'right', classes: 'eur-col' }),
|
||||||
|
col('eur2', 'EUR 2', 'eur2', 62, { align: 'right', classes: 'eur-col' }),
|
||||||
|
col('eur3', 'EUR 3', 'eur3', 62, { align: 'right', classes: 'eur-col' }),
|
||||||
|
col('eur4', 'EUR 4', 'eur4', 62, { align: 'right', classes: 'eur-col' }),
|
||||||
|
col('eur5', 'EUR 5', 'eur5', 62, { align: 'right', classes: 'eur-col' }),
|
||||||
|
col('eur6', 'EUR 6', 'eur6', 62, { align: 'right', classes: 'eur-col' }),
|
||||||
|
col('try1', 'TRY 1', 'try1', 62, { align: 'right', classes: 'try-col' }),
|
||||||
|
col('try2', 'TRY 2', 'try2', 62, { align: 'right', classes: 'try-col' }),
|
||||||
|
col('try3', 'TRY 3', 'try3', 62, { align: 'right', classes: 'try-col' }),
|
||||||
|
col('try4', 'TRY 4', 'try4', 62, { align: 'right', classes: 'try-col' }),
|
||||||
|
col('try5', 'TRY 5', 'try5', 62, { align: 'right', classes: 'try-col' }),
|
||||||
|
col('try6', 'TRY 6', 'try6', 62, { align: 'right', classes: 'try-col' })
|
||||||
|
]
|
||||||
|
|
||||||
|
const stickyColumnNames = [
|
||||||
|
'select',
|
||||||
|
'productCode',
|
||||||
|
'stockQty',
|
||||||
|
'stockEntryDate',
|
||||||
|
'lastPricingDate',
|
||||||
|
'askiliYan',
|
||||||
|
'kategori',
|
||||||
|
'urunIlkGrubu',
|
||||||
|
'urunAnaGrubu',
|
||||||
|
'urunAltGrubu',
|
||||||
|
'icerik',
|
||||||
|
'karisim',
|
||||||
|
'marka',
|
||||||
|
'brandGroupSelection',
|
||||||
|
'costPrice',
|
||||||
|
'expenseForBasePrice',
|
||||||
|
'basePriceUsd',
|
||||||
|
'basePriceTry'
|
||||||
|
]
|
||||||
|
const stickyBoundaryColumnName = 'basePriceTry'
|
||||||
|
const stickyColumnNameSet = new Set(stickyColumnNames)
|
||||||
|
|
||||||
|
const visibleColumns = computed(() => {
|
||||||
|
const selected = new Set(selectedCurrencies.value)
|
||||||
|
return allColumns.filter((c) => {
|
||||||
|
if (c.name.startsWith('usd')) return selected.has('USD')
|
||||||
|
if (c.name.startsWith('eur')) return selected.has('EUR')
|
||||||
|
if (c.name.startsWith('try')) return selected.has('TRY')
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const stickyLeftMap = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
let left = 0
|
||||||
|
for (const colName of stickyColumnNames) {
|
||||||
|
const c = allColumns.find((x) => x.name === colName)
|
||||||
|
if (!c) continue
|
||||||
|
map[colName] = left
|
||||||
|
left += extractWidth(c.style)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
const stickyScrollComp = computed(() => {
|
||||||
|
const boundaryCol = allColumns.find((x) => x.name === stickyBoundaryColumnName)
|
||||||
|
return (stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
||||||
|
const tableStyle = computed(() => ({
|
||||||
|
width: `${tableMinWidth.value}px`,
|
||||||
|
minWidth: `${tableMinWidth.value}px`,
|
||||||
|
tableLayout: 'fixed'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const rows = computed(() => store.rows || [])
|
||||||
|
const multiFilterOptionMap = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
multiFilterColumns.forEach(({ field }) => {
|
||||||
|
const uniq = new Set()
|
||||||
|
rows.value.forEach((row) => {
|
||||||
|
const val = String(row?.[field] ?? '').trim()
|
||||||
|
if (val) uniq.add(val)
|
||||||
|
})
|
||||||
|
map[field] = Array.from(uniq)
|
||||||
|
.sort((a, b) => a.localeCompare(b, 'tr'))
|
||||||
|
.map((v) => ({ label: v, value: v }))
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
const filteredFilterOptionMap = computed(() => {
|
||||||
|
const map = {}
|
||||||
|
multiFilterColumns.forEach(({ field }) => {
|
||||||
|
const search = String(columnFilterSearch.value[field] || '').trim().toLocaleLowerCase('tr')
|
||||||
|
const options = multiFilterOptionMap.value[field] || []
|
||||||
|
map[field] = search
|
||||||
|
? options.filter((option) => option.label.toLocaleLowerCase('tr').includes(search))
|
||||||
|
: options
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredRows = computed(() => {
|
||||||
|
return rows.value.filter((row) => {
|
||||||
|
if (showSelectedOnly.value && !selectedMap.value[row.id]) return false
|
||||||
|
for (const mf of multiFilterColumns) {
|
||||||
|
const selected = columnFilters.value[mf.field] || []
|
||||||
|
if (selected.length > 0 && !selected.includes(String(row?.[mf.field] ?? '').trim())) return false
|
||||||
|
}
|
||||||
|
const stockQtyMin = parseNullableNumber(numberRangeFilters.value.stockQty?.min)
|
||||||
|
const stockQtyMax = parseNullableNumber(numberRangeFilters.value.stockQty?.max)
|
||||||
|
const stockQty = Number(row?.stockQty ?? 0)
|
||||||
|
if (stockQtyMin !== null && stockQty < stockQtyMin) return false
|
||||||
|
if (stockQtyMax !== null && stockQty > stockQtyMax) return false
|
||||||
|
if (!matchesDateRange(String(row?.stockEntryDate || '').trim(), dateRangeFilters.value.stockEntryDate)) return false
|
||||||
|
if (!matchesDateRange(String(row?.lastPricingDate || '').trim(), dateRangeFilters.value.lastPricingDate)) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleRowIds = computed(() => filteredRows.value.map((row) => row.id))
|
||||||
|
const selectedRowCount = computed(() => Object.values(selectedMap.value).filter(Boolean).length)
|
||||||
|
const selectedVisibleCount = computed(() => visibleRowIds.value.filter((id) => !!selectedMap.value[id]).length)
|
||||||
|
const allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
||||||
|
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||||
|
|
||||||
|
function isHeaderFilterField (field) {
|
||||||
|
return headerFilterFieldSet.has(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultiSelectFilterField (field) {
|
||||||
|
return multiSelectFilterFieldSet.has(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumberRangeFilterField (field) {
|
||||||
|
return numberRangeFilterFieldSet.has(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDateRangeFilterField (field) {
|
||||||
|
return dateRangeFilterFieldSet.has(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFilter (field) {
|
||||||
|
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
|
||||||
|
if (isNumberRangeFilterField(field)) {
|
||||||
|
const filter = numberRangeFilters.value[field]
|
||||||
|
return !!String(filter?.min || '').trim() || !!String(filter?.max || '').trim()
|
||||||
|
}
|
||||||
|
if (isDateRangeFilterField(field)) {
|
||||||
|
const filter = dateRangeFilters.value[field]
|
||||||
|
return !!String(filter?.from || '').trim() || !!String(filter?.to || '').trim()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterBadgeValue (field) {
|
||||||
|
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
|
||||||
|
if (isNumberRangeFilterField(field)) {
|
||||||
|
const filter = numberRangeFilters.value[field]
|
||||||
|
return [filter?.min, filter?.max].filter((x) => String(x || '').trim()).length
|
||||||
|
}
|
||||||
|
if (isDateRangeFilterField(field)) {
|
||||||
|
const filter = dateRangeFilters.value[field]
|
||||||
|
return [filter?.from, filter?.to].filter((x) => String(x || '').trim()).length
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearColumnFilter (field) {
|
||||||
|
if (!isMultiSelectFilterField(field)) return
|
||||||
|
columnFilters.value = {
|
||||||
|
...columnFilters.value,
|
||||||
|
[field]: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRangeFilter (field) {
|
||||||
|
if (isNumberRangeFilterField(field)) {
|
||||||
|
numberRangeFilters.value = {
|
||||||
|
...numberRangeFilters.value,
|
||||||
|
[field]: { min: '', max: '' }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isDateRangeFilterField(field)) {
|
||||||
|
dateRangeFilters.value = {
|
||||||
|
...dateRangeFilters.value,
|
||||||
|
[field]: { from: '', to: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterOptionsForField (field) {
|
||||||
|
return filteredFilterOptionMap.value[field] || []
|
||||||
|
}
|
||||||
|
|
||||||
|
function isColumnFilterValueSelected (field, value) {
|
||||||
|
return (columnFilters.value[field] || []).includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleColumnFilterValue (field, value) {
|
||||||
|
const current = new Set(columnFilters.value[field] || [])
|
||||||
|
if (current.has(value)) current.delete(value)
|
||||||
|
else current.add(value)
|
||||||
|
columnFilters.value = {
|
||||||
|
...columnFilters.value,
|
||||||
|
[field]: Array.from(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllColumnFilterOptions (field) {
|
||||||
|
const options = getFilterOptionsForField(field)
|
||||||
|
columnFilters.value = {
|
||||||
|
...columnFilters.value,
|
||||||
|
[field]: options.map((option) => option.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractWidth (style) {
|
||||||
|
const m = String(style || '').match(/width:(\d+)px/)
|
||||||
|
return m ? Number(m[1]) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStickyCol (colName) {
|
||||||
|
return stickyColumnNameSet.has(colName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStickyBoundary (colName) {
|
||||||
|
return colName === stickyBoundaryColumnName
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeaderCellStyle (col) {
|
||||||
|
if (!isStickyCol(col.name)) return undefined
|
||||||
|
return { left: `${stickyLeftMap.value[col.name] || 0}px`, zIndex: 22 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBodyCellStyle (col) {
|
||||||
|
if (!isStickyCol(col.name)) return undefined
|
||||||
|
return { left: `${stickyLeftMap.value[col.name] || 0}px`, zIndex: 12 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function round2 (value) {
|
||||||
|
return Number(Number(value || 0).toFixed(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumber (val) {
|
||||||
|
const normalized = String(val ?? '')
|
||||||
|
.replace(/\s/g, '')
|
||||||
|
.replace(/\./g, '')
|
||||||
|
.replace(',', '.')
|
||||||
|
const n = Number(normalized)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNullableNumber (val) {
|
||||||
|
const text = String(val ?? '').trim()
|
||||||
|
if (!text) return null
|
||||||
|
const normalized = text
|
||||||
|
.replace(/\s/g, '')
|
||||||
|
.replace(/\./g, '')
|
||||||
|
.replace(',', '.')
|
||||||
|
const n = Number(normalized)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesDateRange (value, filter) {
|
||||||
|
const from = String(filter?.from || '').trim()
|
||||||
|
const to = String(filter?.to || '').trim()
|
||||||
|
if (!from && !to) return true
|
||||||
|
if (!value) return false
|
||||||
|
if (from && value < from) return false
|
||||||
|
if (to && value > to) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice (val) {
|
||||||
|
const n = parseNumber(val)
|
||||||
|
return n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStock (val) {
|
||||||
|
const n = Number(val || 0)
|
||||||
|
if (!Number.isFinite(n)) return '0'
|
||||||
|
const hasFraction = Math.abs(n % 1) > 0.0001
|
||||||
|
return n.toLocaleString('tr-TR', {
|
||||||
|
minimumFractionDigits: hasFraction ? 2 : 0,
|
||||||
|
maximumFractionDigits: hasFraction ? 2 : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateDisplay (val) {
|
||||||
|
const text = String(val || '').trim()
|
||||||
|
if (!text) return '-'
|
||||||
|
const [year, month, day] = text.split('-')
|
||||||
|
if (!year || !month || !day) return text
|
||||||
|
return `${day}.${month}.${year}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function needsRepricing (row) {
|
||||||
|
const stockEntryDate = String(row?.stockEntryDate || '').trim()
|
||||||
|
const lastPricingDate = String(row?.lastPricingDate || '').trim()
|
||||||
|
if (!stockEntryDate) return false
|
||||||
|
if (!lastPricingDate) return true
|
||||||
|
return lastPricingDate < stockEntryDate
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcByBasePrice (row) {
|
||||||
|
row.basePriceTry = round2((row.basePriceUsd * usdToTry) + row.expenseForBasePrice)
|
||||||
|
multipliers.forEach((multiplier, index) => {
|
||||||
|
row[`usd${index + 1}`] = round2(row.basePriceUsd * multiplier)
|
||||||
|
row[`eur${index + 1}`] = round2((row.basePriceUsd * usdToTry * multiplier) / eurToTry)
|
||||||
|
row[`try${index + 1}`] = round2(row.basePriceTry * multiplier)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditableCellChange (row, field, val) {
|
||||||
|
const parsed = parseNumber(val)
|
||||||
|
store.updateCell(row, field, parsed)
|
||||||
|
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') recalcByBasePrice(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBrandGroupSelectionChange (row, val) {
|
||||||
|
store.updateBrandGroupSelection(row, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRowSelection (rowId, val) {
|
||||||
|
selectedMap.value = { ...selectedMap.value, [rowId]: !!val }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAllVisible (val) {
|
||||||
|
const next = { ...selectedMap.value }
|
||||||
|
visibleRowIds.value.forEach((id) => { next[id] = !!val })
|
||||||
|
selectedMap.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll () {
|
||||||
|
columnFilters.value = {
|
||||||
|
productCode: [],
|
||||||
|
askiliYan: [],
|
||||||
|
kategori: [],
|
||||||
|
urunIlkGrubu: [],
|
||||||
|
urunAnaGrubu: [],
|
||||||
|
urunAltGrubu: [],
|
||||||
|
icerik: [],
|
||||||
|
karisim: []
|
||||||
|
}
|
||||||
|
columnFilterSearch.value = {
|
||||||
|
productCode: '',
|
||||||
|
askiliYan: '',
|
||||||
|
kategori: '',
|
||||||
|
urunIlkGrubu: '',
|
||||||
|
urunAnaGrubu: '',
|
||||||
|
urunAltGrubu: '',
|
||||||
|
icerik: '',
|
||||||
|
karisim: ''
|
||||||
|
}
|
||||||
|
numberRangeFilters.value = {
|
||||||
|
stockQty: { min: '', max: '' }
|
||||||
|
}
|
||||||
|
dateRangeFilters.value = {
|
||||||
|
stockEntryDate: { from: '', to: '' },
|
||||||
|
lastPricingDate: { from: '', to: '' }
|
||||||
|
}
|
||||||
|
showSelectedOnly.value = false
|
||||||
|
selectedMap.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleShowSelectedOnly () {
|
||||||
|
if (!showSelectedOnly.value && selectedRowCount.value === 0) return
|
||||||
|
showSelectedOnly.value = !showSelectedOnly.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCurrencySelected (code) {
|
||||||
|
return selectedCurrencies.value.includes(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCurrency (code, checked) {
|
||||||
|
const set = new Set(selectedCurrencies.value)
|
||||||
|
if (checked) set.add(code)
|
||||||
|
else set.delete(code)
|
||||||
|
selectedCurrencies.value = currencyOptions.map((x) => x.value).filter((x) => set.has(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCurrencyRow (code) {
|
||||||
|
toggleCurrency(code, !isCurrencySelected(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllCurrencies () {
|
||||||
|
selectedCurrencies.value = currencyOptions.map((x) => x.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllCurrencies () {
|
||||||
|
selectedCurrencies.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadData () {
|
||||||
|
await store.fetchRows()
|
||||||
|
selectedMap.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await reloadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pricing-page {
|
||||||
|
--pricing-row-height: 31px;
|
||||||
|
--pricing-header-height: 72px;
|
||||||
|
--pricing-table-height: calc(100vh - 210px);
|
||||||
|
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-menu-list {
|
||||||
|
min-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-table {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.q-table__middle) {
|
||||||
|
height: var(--pricing-table-height);
|
||||||
|
min-height: var(--pricing-table-height);
|
||||||
|
max-height: var(--pricing-table-height);
|
||||||
|
overflow: auto !important;
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.q-table) {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-size: 11px;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin-right: var(--sticky-scroll-comp, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.q-table__container) {
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th),
|
||||||
|
.pricing-table :deep(td) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td),
|
||||||
|
.pricing-table :deep(.q-table tbody tr) {
|
||||||
|
height: var(--pricing-row-height) !important;
|
||||||
|
min-height: var(--pricing-row-height) !important;
|
||||||
|
max-height: var(--pricing-row-height) !important;
|
||||||
|
line-height: var(--pricing-row-height);
|
||||||
|
padding: 0 !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td > div),
|
||||||
|
.pricing-table :deep(td > .q-td) {
|
||||||
|
height: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
padding: 0 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th),
|
||||||
|
.pricing-table :deep(.q-table thead tr),
|
||||||
|
.pricing-table :deep(.q-table thead tr.header-row-fixed),
|
||||||
|
.pricing-table :deep(.q-table thead th),
|
||||||
|
.pricing-table :deep(.q-table thead tr.header-row-fixed > th) {
|
||||||
|
height: var(--pricing-header-height) !important;
|
||||||
|
min-height: var(--pricing-header-height) !important;
|
||||||
|
max-height: var(--pricing-header-height) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th) {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: normal;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.q-table thead th) {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 30;
|
||||||
|
background: #fff;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.sticky-col) {
|
||||||
|
position: sticky !important;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(thead .sticky-col) {
|
||||||
|
z-index: 35 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(tbody .sticky-col) {
|
||||||
|
z-index: 12 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.sticky-boundary) {
|
||||||
|
border-right: 2px solid rgba(25, 118, 210, 0.18) !important;
|
||||||
|
box-shadow: 8px 0 12px -10px rgba(15, 23, 42, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-with-filter {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 20px;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 4px;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1.25;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-with-filter > span {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.15;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-filter-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-filter-ghost {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-filter-menu {
|
||||||
|
min-width: 230px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-filter-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-filter-field {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-filter-select :deep(.q-field__control) {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-filter-select :deep(.q-field__native),
|
||||||
|
.excel-filter-select :deep(.q-field__input) {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-filter-actions {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-filter-options {
|
||||||
|
max-height: 220px;
|
||||||
|
margin-top: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-filter-option {
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-filter-empty {
|
||||||
|
padding: 10px 8px;
|
||||||
|
color: #607d8b;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.ps-col),
|
||||||
|
.pricing-table :deep(td.ps-col) {
|
||||||
|
background: #fff;
|
||||||
|
color: var(--q-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td.ps-col .cell-text),
|
||||||
|
.pricing-table :deep(td.ps-col .product-code-text),
|
||||||
|
.pricing-table :deep(td.ps-col .stock-qty-text) {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.1;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-qty-text {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell-text {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-warning {
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.selection-col),
|
||||||
|
.pricing-table :deep(td.selection-col) {
|
||||||
|
background: #fff;
|
||||||
|
color: var(--q-primary);
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.selection-col) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.selection-col .q-checkbox__inner) {
|
||||||
|
color: var(--q-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.selection-col .q-checkbox),
|
||||||
|
.pricing-table :deep(td.selection-col .q-checkbox) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.selection-col .q-checkbox__bg) {
|
||||||
|
background: #fff;
|
||||||
|
border-color: var(--q-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.usd-col),
|
||||||
|
.pricing-table :deep(td.usd-col) {
|
||||||
|
background: #ecf9f0;
|
||||||
|
color: #178a3e;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.eur-col),
|
||||||
|
.pricing-table :deep(td.eur-col) {
|
||||||
|
background: #fdeeee;
|
||||||
|
color: #c62828;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.try-col),
|
||||||
|
.pricing-table :deep(td.try-col) {
|
||||||
|
background: #edf4ff;
|
||||||
|
color: #1e63c6;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-text {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-code-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-cell-input,
|
||||||
|
.native-cell-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 22px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1px 3px;
|
||||||
|
border: 1px solid #cfd8dc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-cell-input:focus,
|
||||||
|
.native-cell-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -311,6 +311,14 @@ const routes = [
|
|||||||
meta: { permission: 'order:view' }
|
meta: { permission: 'order:view' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* ================= PRICING ================= */
|
||||||
|
{
|
||||||
|
path: 'pricing/product-pricing',
|
||||||
|
name: 'product-pricing',
|
||||||
|
component: () => import('pages/ProductPricing.vue'),
|
||||||
|
meta: { permission: 'order:view' }
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/* ================= PASSWORD ================= */
|
/* ================= PASSWORD ================= */
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ function extractApiErrorMessage (err, fallback) {
|
|||||||
const data = err?.response?.data
|
const data = err?.response?.data
|
||||||
if (typeof data === 'string' && data.trim()) return data
|
if (typeof data === 'string' && data.trim()) return data
|
||||||
if (data && typeof data === 'object') {
|
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 msg = String(data.message || '').trim()
|
||||||
const step = String(data.step || '').trim()
|
const step = String(data.step || '').trim()
|
||||||
const detail = String(data.detail || '').trim()
|
const detail = String(data.detail || '').trim()
|
||||||
const parts = [msg]
|
const parts = [msg]
|
||||||
if (step) parts.push(`step=${step}`)
|
if (step) parts.push(`step=${step}`)
|
||||||
if (detail) parts.push(detail)
|
if (detail) parts.push(detail)
|
||||||
|
if (validationMessages.length) parts.push(validationMessages.join(' | '))
|
||||||
const merged = parts.filter(Boolean).join(' | ')
|
const merged = parts.filter(Boolean).join(' | ')
|
||||||
if (merged) return merged
|
if (merged) return merged
|
||||||
}
|
}
|
||||||
@@ -36,6 +40,51 @@ function nowMs () {
|
|||||||
return Date.now()
|
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', {
|
export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
items: [],
|
items: [],
|
||||||
@@ -54,6 +103,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
cdItemLookups: null,
|
cdItemLookups: null,
|
||||||
cdItemDraftsByCode: {},
|
cdItemDraftsByCode: {},
|
||||||
productAttributeDraftsByCode: {},
|
productAttributeDraftsByCode: {},
|
||||||
|
knownExistingItemCodes: {},
|
||||||
loading: false,
|
loading: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
error: null
|
error: null
|
||||||
@@ -71,18 +121,35 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
normalizeDim1ForUi (value) {
|
||||||
|
return normalizeProductionDim1Label(value)
|
||||||
|
},
|
||||||
|
pickPreferredYasPayloadLabel (currentRaw, nextRaw) {
|
||||||
|
return pickPreferredProductionYasPayloadLabel(currentRaw, nextRaw)
|
||||||
|
},
|
||||||
|
toPayloadDim1Code (row, value) {
|
||||||
|
return toProductionPayloadDim1(row, value)
|
||||||
|
},
|
||||||
classifyItemCode (value) {
|
classifyItemCode (value) {
|
||||||
const normalized = String(value || '').trim().toUpperCase()
|
const normalized = String(value || '').trim().toUpperCase()
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return { normalized: '', mode: 'empty', exists: false }
|
return { normalized: '', mode: 'empty', exists: false }
|
||||||
}
|
}
|
||||||
const exists = this.productCodeSet.has(normalized)
|
const exists = this.productCodeSet.has(normalized) || !!this.knownExistingItemCodes[normalized]
|
||||||
return {
|
return {
|
||||||
normalized,
|
normalized,
|
||||||
mode: exists ? 'existing' : 'new',
|
mode: exists ? 'existing' : 'new',
|
||||||
exists
|
exists
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
markItemCodeKnownExisting (itemCode, exists = true) {
|
||||||
|
const code = String(itemCode || '').trim().toUpperCase()
|
||||||
|
if (!code) return
|
||||||
|
this.knownExistingItemCodes = {
|
||||||
|
...this.knownExistingItemCodes,
|
||||||
|
[code]: !!exists
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async fetchHeader (orderHeaderID) {
|
async fetchHeader (orderHeaderID) {
|
||||||
if (!orderHeaderID) {
|
if (!orderHeaderID) {
|
||||||
@@ -134,6 +201,20 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
this.error = err?.response?.data || err?.message || 'Urun listesi alinamadi'
|
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) {
|
async fetchColors (productCode) {
|
||||||
const code = String(productCode || '').trim()
|
const code = String(productCode || '').trim()
|
||||||
if (!code) return []
|
if (!code) return []
|
||||||
@@ -152,6 +233,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
const res = await api.get('/product-colors', { params: { code } })
|
const res = await api.get('/product-colors', { params: { code } })
|
||||||
const data = res?.data
|
const data = res?.data
|
||||||
const list = Array.isArray(data) ? data : []
|
const list = Array.isArray(data) ? data : []
|
||||||
|
if (list.length) this.markItemCodeKnownExisting(code, true)
|
||||||
this.colorOptionsByCode[code] = list
|
this.colorOptionsByCode[code] = list
|
||||||
console.info('[OrderProductionItemStore] fetchColors done', { code, count: list.length, durationMs: Math.round(nowMs() - t0) })
|
console.info('[OrderProductionItemStore] fetchColors done', { code, count: list.length, durationMs: Math.round(nowMs() - t0) })
|
||||||
return list
|
return list
|
||||||
@@ -284,6 +366,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
try {
|
try {
|
||||||
const res = await api.get('/product-item-attributes', { params: { itemTypeCode: itc, itemCode: code } })
|
const res = await api.get('/product-item-attributes', { params: { itemTypeCode: itc, itemCode: code } })
|
||||||
const list = Array.isArray(res?.data) ? res.data : []
|
const list = Array.isArray(res?.data) ? res.data : []
|
||||||
|
if (list.length) this.markItemCodeKnownExisting(code, true)
|
||||||
this.productItemAttributesByKey[key] = list
|
this.productItemAttributesByKey[key] = list
|
||||||
return list
|
return list
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -359,6 +442,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
orderHeaderID,
|
orderHeaderID,
|
||||||
lineCount: lines?.length || 0,
|
lineCount: lines?.length || 0,
|
||||||
missingCount: Number(data?.missingCount || 0),
|
missingCount: Number(data?.missingCount || 0),
|
||||||
|
barcodeValidationCount: Number(data?.barcodeValidationCount || 0),
|
||||||
requestId: rid,
|
requestId: rid,
|
||||||
durationMs: Math.round(nowMs() - t0)
|
durationMs: Math.round(nowMs() - t0)
|
||||||
})
|
})
|
||||||
@@ -371,7 +455,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
this.saving = false
|
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 }
|
if (!orderHeaderID) return { updated: 0, inserted: 0 }
|
||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
@@ -384,11 +468,18 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
lineCount: lines?.length || 0,
|
lineCount: lines?.length || 0,
|
||||||
insertMissing: !!insertMissing,
|
insertMissing: !!insertMissing,
|
||||||
cdItemCount: cdItems?.length || 0,
|
cdItemCount: cdItems?.length || 0,
|
||||||
attributeCount: productAttributes?.length || 0
|
attributeCount: productAttributes?.length || 0,
|
||||||
|
headerAverageDueDate
|
||||||
})
|
})
|
||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
|
`/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 data = res?.data || { updated: 0, inserted: 0 }
|
||||||
const rid = res?.headers?.['x-debug-request-id'] || ''
|
const rid = res?.headers?.['x-debug-request-id'] || ''
|
||||||
@@ -396,7 +487,9 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
|||||||
orderHeaderID,
|
orderHeaderID,
|
||||||
updated: Number(data?.updated || 0),
|
updated: Number(data?.updated || 0),
|
||||||
inserted: Number(data?.inserted || 0),
|
inserted: Number(data?.inserted || 0),
|
||||||
|
barcodeInserted: Number(data?.barcodeInserted || 0),
|
||||||
attributeUpserted: Number(data?.attributeUpserted || 0),
|
attributeUpserted: Number(data?.attributeUpserted || 0),
|
||||||
|
headerUpdated: !!data?.headerUpdated,
|
||||||
requestId: rid,
|
requestId: rid,
|
||||||
durationMs: Math.round(nowMs() - t0)
|
durationMs: Math.round(nowMs() - t0)
|
||||||
})
|
})
|
||||||
|
|||||||
88
ui/src/stores/ProductPricingStore.js
Normal file
88
ui/src/stores/ProductPricingStore.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import api from 'src/services/api'
|
||||||
|
|
||||||
|
function toText (value) {
|
||||||
|
return String(value ?? '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber (value) {
|
||||||
|
const n = Number(value)
|
||||||
|
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRow (raw, index) {
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
productCode: toText(raw?.ProductCode),
|
||||||
|
stockQty: toNumber(raw?.StockQty),
|
||||||
|
stockEntryDate: toText(raw?.StockEntryDate),
|
||||||
|
lastPricingDate: toText(raw?.LastPricingDate),
|
||||||
|
askiliYan: toText(raw?.AskiliYan),
|
||||||
|
kategori: toText(raw?.Kategori),
|
||||||
|
urunIlkGrubu: toText(raw?.UrunIlkGrubu),
|
||||||
|
urunAnaGrubu: toText(raw?.UrunAnaGrubu),
|
||||||
|
urunAltGrubu: toText(raw?.UrunAltGrubu),
|
||||||
|
icerik: toText(raw?.Icerik),
|
||||||
|
karisim: toText(raw?.Karisim),
|
||||||
|
marka: toText(raw?.Marka),
|
||||||
|
brandGroupSelection: toText(raw?.BrandGroupSec),
|
||||||
|
costPrice: toNumber(raw?.CostPrice),
|
||||||
|
expenseForBasePrice: 0,
|
||||||
|
basePriceUsd: 0,
|
||||||
|
basePriceTry: 0,
|
||||||
|
usd1: 0,
|
||||||
|
usd2: 0,
|
||||||
|
usd3: 0,
|
||||||
|
usd4: 0,
|
||||||
|
usd5: 0,
|
||||||
|
usd6: 0,
|
||||||
|
eur1: 0,
|
||||||
|
eur2: 0,
|
||||||
|
eur3: 0,
|
||||||
|
eur4: 0,
|
||||||
|
eur5: 0,
|
||||||
|
eur6: 0,
|
||||||
|
try1: 0,
|
||||||
|
try2: 0,
|
||||||
|
try3: 0,
|
||||||
|
try4: 0,
|
||||||
|
try5: 0,
|
||||||
|
try6: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||||
|
state: () => ({
|
||||||
|
rows: [],
|
||||||
|
loading: false,
|
||||||
|
error: ''
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchRows () {
|
||||||
|
this.loading = true
|
||||||
|
this.error = ''
|
||||||
|
try {
|
||||||
|
const res = await api.get('/pricing/products')
|
||||||
|
const data = Array.isArray(res?.data) ? res.data : []
|
||||||
|
this.rows = data.map((x, i) => mapRow(x, i))
|
||||||
|
} catch (err) {
|
||||||
|
this.rows = []
|
||||||
|
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||||
|
this.error = toText(msg)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCell (row, field, val) {
|
||||||
|
if (!row || !field) return
|
||||||
|
row[field] = toNumber(String(val ?? '').replace(',', '.'))
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBrandGroupSelection (row, val) {
|
||||||
|
if (!row) return
|
||||||
|
row.brandGroupSelection = toText(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -212,6 +212,8 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
orders: [],
|
orders: [],
|
||||||
header: {},
|
header: {},
|
||||||
summaryRows: [],
|
summaryRows: [],
|
||||||
|
originalHeader: {},
|
||||||
|
originalLines: [],
|
||||||
|
|
||||||
lastSavedAt: null,
|
lastSavedAt: null,
|
||||||
|
|
||||||
@@ -534,6 +536,54 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
const normalized = Array.isArray(lines) ? lines : []
|
const normalized = Array.isArray(lines) ? lines : []
|
||||||
const mapLabel = (ln) => this.buildMailLineLabel(ln)
|
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) {
|
if (isNew) {
|
||||||
return {
|
return {
|
||||||
operation: 'create',
|
operation: 'create',
|
||||||
@@ -543,7 +593,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
normalized
|
normalized
|
||||||
.filter(ln => !ln?._deleteSignal)
|
.filter(ln => !ln?._deleteSignal)
|
||||||
.map(mapLabel)
|
.map(mapLabel)
|
||||||
)
|
),
|
||||||
|
oldDueDate: '',
|
||||||
|
newDueDate: '',
|
||||||
|
dueDateChanges: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,11 +606,22 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
.map(mapLabel)
|
.map(mapLabel)
|
||||||
)
|
)
|
||||||
|
|
||||||
const updatedItems = uniq(
|
const updatedItems = []
|
||||||
normalized
|
|
||||||
.filter(ln => !ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true)
|
normalized.forEach(ln => {
|
||||||
.map(mapLabel)
|
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(
|
const addedItems = uniq(
|
||||||
normalized
|
normalized
|
||||||
@@ -568,8 +632,11 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
return {
|
return {
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
deletedItems,
|
deletedItems,
|
||||||
updatedItems,
|
updatedItems: uniq(updatedItems),
|
||||||
addedItems
|
addedItems,
|
||||||
|
oldDueDate: oldDate,
|
||||||
|
newDueDate: newDate,
|
||||||
|
dueDateChanges: buildDueDateChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
@@ -586,7 +653,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
operation: payload?.operation || 'create',
|
operation: payload?.operation || 'create',
|
||||||
deletedItems: Array.isArray(payload?.deletedItems) ? payload.deletedItems : [],
|
deletedItems: Array.isArray(payload?.deletedItems) ? payload.deletedItems : [],
|
||||||
updatedItems: Array.isArray(payload?.updatedItems) ? payload.updatedItems : [],
|
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 || {}
|
return res?.data || {}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1113,6 +1183,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
this.orders = Array.isArray(normalized) ? normalized : []
|
this.orders = Array.isArray(normalized) ? normalized : []
|
||||||
this.summaryRows = [...this.orders]
|
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)
|
🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN)
|
||||||
- herhangi bir isClosed=true → view
|
- 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)
|
// 📧 Piyasa eşleşen alıcılara sipariş PDF gönderimi (kayıt başarılı olduktan sonra)
|
||||||
try {
|
try {
|
||||||
const mailPayload = this.buildOrderMailPayload(lines, isNew)
|
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 mailRes = await this.sendOrderToMarketMails(serverOrderId, mailPayload)
|
||||||
const sentCount = Number(mailRes?.sentCount || 0)
|
const sentCount = Number(mailRes?.sentCount || 0)
|
||||||
$q.notify({
|
$q.notify({
|
||||||
|
|||||||
Reference in New Issue
Block a user