Compare commits
2 Commits
44439f7908
...
ed80e4f492
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed80e4f492 | ||
|
|
d7d871fb8a |
@@ -522,6 +522,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
{"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)},
|
||||
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
|
||||
{"/api/orders/production-list", "GET", "update", routes.OrderProductionListRoute(mssql)},
|
||||
{"/api/orders/production-items/cditem-lookups", "GET", "view", routes.OrderProductionCdItemLookupsRoute(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}/validate", "POST", "update", routes.OrderProductionValidateRoute(mssql)},
|
||||
@@ -587,6 +588,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
"order", "view",
|
||||
wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/product-attributes", "GET",
|
||||
"order", "view",
|
||||
wrapV3(http.HandlerFunc(routes.GetProductAttributesHandler)),
|
||||
)
|
||||
bindV3(r, pgDB,
|
||||
"/api/product-stock-query", "GET",
|
||||
"order", "view",
|
||||
|
||||
@@ -9,16 +9,72 @@ type OrderProductionUpdateLine struct {
|
||||
}
|
||||
|
||||
type OrderProductionUpdatePayload struct {
|
||||
Lines []OrderProductionUpdateLine `json:"lines"`
|
||||
InsertMissing bool `json:"insertMissing"`
|
||||
Lines []OrderProductionUpdateLine `json:"lines"`
|
||||
InsertMissing bool `json:"insertMissing"`
|
||||
CdItems []OrderProductionCdItemDraft `json:"cdItems"`
|
||||
ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"`
|
||||
}
|
||||
|
||||
type OrderProductionMissingVariant struct {
|
||||
OrderLineID string `json:"OrderLineID"`
|
||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||
ItemCode string `json:"ItemCode"`
|
||||
ColorCode string `json:"ColorCode"`
|
||||
ItemDim1Code string `json:"ItemDim1Code"`
|
||||
ItemDim2Code string `json:"ItemDim2Code"`
|
||||
ItemDim3Code string `json:"ItemDim3Code"`
|
||||
OrderLineID string `json:"OrderLineID"`
|
||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||
ItemCode string `json:"ItemCode"`
|
||||
ColorCode string `json:"ColorCode"`
|
||||
ItemDim1Code string `json:"ItemDim1Code"`
|
||||
ItemDim2Code string `json:"ItemDim2Code"`
|
||||
ItemDim3Code string `json:"ItemDim3Code"`
|
||||
}
|
||||
|
||||
type OrderProductionCdItemDraft struct {
|
||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||
ItemCode string `json:"ItemCode"`
|
||||
ItemDimTypeCode *int16 `json:"ItemDimTypeCode"`
|
||||
ProductTypeCode *int16 `json:"ProductTypeCode"`
|
||||
ProductHierarchyID *int `json:"ProductHierarchyID"`
|
||||
UnitOfMeasureCode1 *string `json:"UnitOfMeasureCode1"`
|
||||
ItemAccountGrCode *string `json:"ItemAccountGrCode"`
|
||||
ItemTaxGrCode *string `json:"ItemTaxGrCode"`
|
||||
ItemPaymentPlanGrCode *string `json:"ItemPaymentPlanGrCode"`
|
||||
ItemDiscountGrCode *string `json:"ItemDiscountGrCode"`
|
||||
ItemVendorGrCode *string `json:"ItemVendorGrCode"`
|
||||
PromotionGroupCode *string `json:"PromotionGroupCode"`
|
||||
ProductCollectionGrCode *string `json:"ProductCollectionGrCode"`
|
||||
StorePriceLevelCode *string `json:"StorePriceLevelCode"`
|
||||
PerceptionOfFashionCode *string `json:"PerceptionOfFashionCode"`
|
||||
CommercialRoleCode *string `json:"CommercialRoleCode"`
|
||||
StoreCapacityLevelCode *string `json:"StoreCapacityLevelCode"`
|
||||
CustomsTariffNumberCode *string `json:"CustomsTariffNumberCode"`
|
||||
CompanyCode *string `json:"CompanyCode"`
|
||||
}
|
||||
|
||||
type OrderProductionLookupOption struct {
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type OrderProductionItemAttributeRow struct {
|
||||
ItemTypeCode int16 `json:"ItemTypeCode"`
|
||||
ItemCode string `json:"ItemCode"`
|
||||
AttributeTypeCode int `json:"AttributeTypeCode"`
|
||||
AttributeCode string `json:"AttributeCode"`
|
||||
}
|
||||
|
||||
type OrderProductionCdItemLookups struct {
|
||||
ItemDimTypeCodes []OrderProductionLookupOption `json:"itemDimTypeCodes"`
|
||||
ProductTypeCodes []OrderProductionLookupOption `json:"productTypeCodes"`
|
||||
ProductHierarchyIDs []OrderProductionLookupOption `json:"productHierarchyIDs"`
|
||||
UnitOfMeasureCode1List []OrderProductionLookupOption `json:"unitOfMeasureCode1List"`
|
||||
ItemAccountGrCodes []OrderProductionLookupOption `json:"itemAccountGrCodes"`
|
||||
ItemTaxGrCodes []OrderProductionLookupOption `json:"itemTaxGrCodes"`
|
||||
ItemPaymentPlanGrCodes []OrderProductionLookupOption `json:"itemPaymentPlanGrCodes"`
|
||||
ItemDiscountGrCodes []OrderProductionLookupOption `json:"itemDiscountGrCodes"`
|
||||
ItemVendorGrCodes []OrderProductionLookupOption `json:"itemVendorGrCodes"`
|
||||
PromotionGroupCodes []OrderProductionLookupOption `json:"promotionGroupCodes"`
|
||||
ProductCollectionGrCodes []OrderProductionLookupOption `json:"productCollectionGrCodes"`
|
||||
StorePriceLevelCodes []OrderProductionLookupOption `json:"storePriceLevelCodes"`
|
||||
PerceptionOfFashionCodes []OrderProductionLookupOption `json:"perceptionOfFashionCodes"`
|
||||
CommercialRoleCodes []OrderProductionLookupOption `json:"commercialRoleCodes"`
|
||||
StoreCapacityLevelCodes []OrderProductionLookupOption `json:"storeCapacityLevelCodes"`
|
||||
CustomsTariffNumbers []OrderProductionLookupOption `json:"customsTariffNumbers"`
|
||||
CompanyCodes []OrderProductionLookupOption `json:"companyCodes"`
|
||||
}
|
||||
|
||||
9
svc/models/productattributes.go
Normal file
9
svc/models/productattributes.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
type ProductAttributeOption struct {
|
||||
ItemTypeCode int16 `json:"item_type_code"`
|
||||
AttributeTypeCode int `json:"attribute_type_code"`
|
||||
AttributeTypeDescription string `json:"attribute_type_description"`
|
||||
AttributeCode string `json:"attribute_code"`
|
||||
AttributeDescription string `json:"attribute_description"`
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package models
|
||||
|
||||
type ProductSecondColor struct {
|
||||
ProductCode string `json:"product_code"`
|
||||
ColorCode string `json:"color_code"`
|
||||
ItemDim2Code string `json:"item_dim2_code"`
|
||||
ProductCode string `json:"product_code"`
|
||||
ColorCode string `json:"color_code"`
|
||||
ItemDim2Code string `json:"item_dim2_code"`
|
||||
ColorDescription string `json:"color_description"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package queries
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bssapp-backend/models"
|
||||
)
|
||||
@@ -74,10 +75,17 @@ INSERT INTO dbo.prItemVariant (
|
||||
ItemDim2Code,
|
||||
ItemDim3Code,
|
||||
PLU,
|
||||
IsSalesOrderClosed,
|
||||
IsPurchaseOrderClosed,
|
||||
IsLocked,
|
||||
IsBlocked,
|
||||
CreatedUserName,
|
||||
CreatedDate,
|
||||
LastUpdatedUserName,
|
||||
LastUpdatedDate
|
||||
LastUpdatedDate,
|
||||
RowGuid,
|
||||
UseInternet,
|
||||
IsStoreOrderClosed
|
||||
)
|
||||
SELECT
|
||||
m.ItemTypeCode,
|
||||
@@ -87,10 +95,17 @@ SELECT
|
||||
m.ItemDim2Code,
|
||||
m.ItemDim3Code,
|
||||
mp.BasePlu + ROW_NUMBER() OVER (ORDER BY m.ItemCode, m.ColorCode, m.ItemDim1Code, m.ItemDim2Code, m.ItemDim3Code),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@p2,
|
||||
GETDATE(),
|
||||
@p2,
|
||||
GETDATE()
|
||||
GETDATE(),
|
||||
NEWID(),
|
||||
0,
|
||||
0
|
||||
FROM Missing m
|
||||
CROSS JOIN MaxPlu mp;
|
||||
`
|
||||
@@ -143,7 +158,12 @@ WHERE ItemTypeCode = @p1
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func InsertMissingVariantsTx(tx *sql.Tx, missing []models.OrderProductionMissingVariant, username string) (int64, error) {
|
||||
func InsertMissingVariantsTx(
|
||||
tx *sql.Tx,
|
||||
missing []models.OrderProductionMissingVariant,
|
||||
username string,
|
||||
cdItemByCode map[string]models.OrderProductionCdItemDraft,
|
||||
) (int64, error) {
|
||||
if len(missing) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -161,7 +181,16 @@ FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
|
||||
for i, v := range missing {
|
||||
itemKey := strconv.FormatInt(int64(v.ItemTypeCode), 10) + "|" + v.ItemCode
|
||||
if _, ok := ensuredItems[itemKey]; !ok {
|
||||
if err := ensureCdItemTx(tx, v.ItemTypeCode, v.ItemCode, username); err != nil {
|
||||
draft, hasDraft := cdItemByCode[itemKey]
|
||||
if !hasDraft {
|
||||
draft, hasDraft = cdItemByCode[NormalizeCdItemMapKey(v.ItemTypeCode, v.ItemCode)]
|
||||
}
|
||||
var draftPtr *models.OrderProductionCdItemDraft
|
||||
if hasDraft {
|
||||
tmp := draft
|
||||
draftPtr = &tmp
|
||||
}
|
||||
if err := ensureCdItemTx(tx, v.ItemTypeCode, v.ItemCode, username, draftPtr); err != nil {
|
||||
return inserted, err
|
||||
}
|
||||
ensuredItems[itemKey] = struct{}{}
|
||||
@@ -187,14 +216,26 @@ INSERT INTO dbo.prItemVariant (
|
||||
ItemDim2Code,
|
||||
ItemDim3Code,
|
||||
PLU,
|
||||
IsSalesOrderClosed,
|
||||
IsPurchaseOrderClosed,
|
||||
IsLocked,
|
||||
IsBlocked,
|
||||
CreatedUserName,
|
||||
CreatedDate,
|
||||
LastUpdatedUserName,
|
||||
LastUpdatedDate
|
||||
LastUpdatedDate,
|
||||
RowGuid,
|
||||
UseInternet,
|
||||
IsStoreOrderClosed
|
||||
)
|
||||
VALUES (
|
||||
@p1, @p2, @p3, @p4, @p5, @p6,
|
||||
@p7, @p8, GETDATE(), @p8, GETDATE()
|
||||
@p7,
|
||||
0, 0, 0, 0,
|
||||
@p8, GETDATE(), @p8, GETDATE(),
|
||||
NEWID(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
`, v.ItemTypeCode, v.ItemCode, v.ColorCode, v.ItemDim1Code, v.ItemDim2Code, v.ItemDim3Code, plu, username)
|
||||
if err != nil {
|
||||
@@ -207,7 +248,17 @@ VALUES (
|
||||
return inserted, nil
|
||||
}
|
||||
|
||||
func ensureCdItemTx(tx *sql.Tx, itemTypeCode int16, itemCode string, username string) error {
|
||||
func NormalizeCdItemMapKey(itemTypeCode int16, itemCode string) string {
|
||||
return strconv.FormatInt(int64(itemTypeCode), 10) + "|" + strings.ToUpper(strings.TrimSpace(itemCode))
|
||||
}
|
||||
|
||||
func ensureCdItemTx(
|
||||
tx *sql.Tx,
|
||||
itemTypeCode int16,
|
||||
itemCode string,
|
||||
username string,
|
||||
draft *models.OrderProductionCdItemDraft,
|
||||
) error {
|
||||
_, err := tx.Exec(`
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
@@ -269,28 +320,90 @@ BEGIN
|
||||
INSERT INTO dbo.cdItem (
|
||||
ItemTypeCode, ItemCode,
|
||||
ItemDimTypeCode, ProductTypeCode, ProductHierarchyID,
|
||||
UnitOfMeasureCode1, UnitConvertRate, UnitConvertRateNotFixed,
|
||||
UnitOfMeasureCode1, UnitOfMeasureCode2, UnitConvertRate, UnitConvertRateNotFixed,
|
||||
UseInternet, UsePOS, UseStore, EnablePartnerCompanies, UseManufacturing, UseSerialNumber,
|
||||
GenerateOpticalDataMatrixCode, ByWeight, SupplyPeriod, GuaranteePeriod, ShelfLife, OrderLeadTime,
|
||||
IsFixedExpense, IsBlocked, IsLocked, LockedDate, IsSalesOrderClosed, IsPurchaseOrderClosed,
|
||||
ItemAccountGrCode, ItemTaxGrCode, ItemPaymentPlanGrCode, ItemDiscountGrCode, ItemVendorGrCode,
|
||||
PromotionGroupCode, PromotionGroupCode2, ProductCollectionGrCode, StorePriceLevelCode, PerceptionOfFashionCode,
|
||||
CommercialRoleCode, StoreCapacityLevelCode, CustomsTariffNumberCode, IsFixedExpense, BOMEntityCode, CompanyCode,
|
||||
IsBlocked, IsLocked, LockedDate, IsSalesOrderClosed, IsPurchaseOrderClosed,
|
||||
CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate, RowGuid,
|
||||
UseRoll, UseBatch, MaxCreditCardInstallmentCount, GenerateSerialNumber,
|
||||
IsSubsequentDeliveryForR, IsSubsequentDeliveryForRI, IsUTSDeclaratedItem, IsStoreOrderClosed
|
||||
IsSubsequentDeliveryForR, IsSubsequentDeliveryForRI,
|
||||
IGACommissionGroup, UniFreeCommissionGroup, CustomsProductGroupCode, IsUTSDeclaratedItem, IsStoreOrderClosed
|
||||
)
|
||||
VALUES (
|
||||
@p1, @p2,
|
||||
2, 1, 2,
|
||||
'AD', 0, 0,
|
||||
0, 1, 1, 0, 1, 0,
|
||||
'AD', '', 0, 0,
|
||||
1, 1, 1, 0, 1, 0,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, '1900-01-01', 0, 0,
|
||||
'', '10%', '', '', '',
|
||||
'', '', '0', '0', '0',
|
||||
'0', '', '', 0, '', '1',
|
||||
0, 0, '1900-01-01', 0, 0,
|
||||
@p3, GETDATE(), @p3, GETDATE(), NEWID(),
|
||||
0, 0, 12, 0,
|
||||
0, 0, 0, 0
|
||||
0, 0,
|
||||
'', '', '0', 0, 0
|
||||
);
|
||||
END
|
||||
END
|
||||
`, itemTypeCode, itemCode, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if draft == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
UPDATE dbo.cdItem
|
||||
SET
|
||||
ItemDimTypeCode = COALESCE(@p3, ItemDimTypeCode),
|
||||
ProductTypeCode = COALESCE(@p4, ProductTypeCode),
|
||||
ProductHierarchyID = COALESCE(@p5, ProductHierarchyID),
|
||||
UnitOfMeasureCode1 = COALESCE(NULLIF(@p6,''), UnitOfMeasureCode1),
|
||||
ItemAccountGrCode = COALESCE(NULLIF(@p7,''), ItemAccountGrCode),
|
||||
ItemTaxGrCode = COALESCE(NULLIF(@p8,''), ItemTaxGrCode),
|
||||
ItemPaymentPlanGrCode = COALESCE(NULLIF(@p9,''), ItemPaymentPlanGrCode),
|
||||
ItemDiscountGrCode = COALESCE(NULLIF(@p10,''), ItemDiscountGrCode),
|
||||
ItemVendorGrCode = COALESCE(NULLIF(@p11,''), ItemVendorGrCode),
|
||||
PromotionGroupCode = COALESCE(NULLIF(@p12,''), PromotionGroupCode),
|
||||
ProductCollectionGrCode = COALESCE(NULLIF(@p13,''), ProductCollectionGrCode),
|
||||
StorePriceLevelCode = COALESCE(NULLIF(@p14,''), StorePriceLevelCode),
|
||||
PerceptionOfFashionCode = COALESCE(NULLIF(@p15,''), PerceptionOfFashionCode),
|
||||
CommercialRoleCode = COALESCE(NULLIF(@p16,''), CommercialRoleCode),
|
||||
StoreCapacityLevelCode = COALESCE(NULLIF(@p17,''), StoreCapacityLevelCode),
|
||||
CustomsTariffNumberCode = COALESCE(NULLIF(@p18,''), CustomsTariffNumberCode),
|
||||
CompanyCode = COALESCE(NULLIF(@p19,''), CompanyCode),
|
||||
LastUpdatedUserName = @p20,
|
||||
LastUpdatedDate = GETDATE()
|
||||
WHERE ItemTypeCode = @p1
|
||||
AND ItemCode = @p2;
|
||||
`,
|
||||
itemTypeCode,
|
||||
itemCode,
|
||||
draft.ItemDimTypeCode,
|
||||
draft.ProductTypeCode,
|
||||
draft.ProductHierarchyID,
|
||||
draft.UnitOfMeasureCode1,
|
||||
draft.ItemAccountGrCode,
|
||||
draft.ItemTaxGrCode,
|
||||
draft.ItemPaymentPlanGrCode,
|
||||
draft.ItemDiscountGrCode,
|
||||
draft.ItemVendorGrCode,
|
||||
draft.PromotionGroupCode,
|
||||
draft.ProductCollectionGrCode,
|
||||
draft.StorePriceLevelCode,
|
||||
draft.PerceptionOfFashionCode,
|
||||
draft.CommercialRoleCode,
|
||||
draft.StoreCapacityLevelCode,
|
||||
draft.CustomsTariffNumberCode,
|
||||
draft.CompanyCode,
|
||||
username,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -317,3 +430,141 @@ WHERE OrderHeaderID = @p6 AND OrderLineID = @p7
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func UpsertItemAttributesTx(tx *sql.Tx, attrs []models.OrderProductionItemAttributeRow, username string) (int64, error) {
|
||||
if len(attrs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var affected int64
|
||||
for _, a := range attrs {
|
||||
res, err := tx.Exec(`
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM dbo.prItemAttribute
|
||||
WHERE ItemTypeCode = @p1
|
||||
AND ItemCode = @p2
|
||||
AND AttributeTypeCode = @p3
|
||||
)
|
||||
BEGIN
|
||||
UPDATE dbo.prItemAttribute
|
||||
SET
|
||||
AttributeCode = @p4,
|
||||
LastUpdatedUserName = @p5,
|
||||
LastUpdatedDate = GETDATE()
|
||||
WHERE ItemTypeCode = @p1
|
||||
AND ItemCode = @p2
|
||||
AND AttributeTypeCode = @p3
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT INTO dbo.prItemAttribute (
|
||||
ItemTypeCode,
|
||||
ItemCode,
|
||||
AttributeTypeCode,
|
||||
AttributeCode,
|
||||
CreatedUserName,
|
||||
CreatedDate,
|
||||
LastUpdatedUserName,
|
||||
LastUpdatedDate,
|
||||
RowGuid
|
||||
)
|
||||
VALUES (
|
||||
@p1,
|
||||
@p2,
|
||||
@p3,
|
||||
@p4,
|
||||
@p5,
|
||||
GETDATE(),
|
||||
@p5,
|
||||
GETDATE(),
|
||||
NEWID()
|
||||
)
|
||||
END
|
||||
`, a.ItemTypeCode, a.ItemCode, a.AttributeTypeCode, a.AttributeCode, username)
|
||||
if err != nil {
|
||||
return affected, err
|
||||
}
|
||||
if rows, err := res.RowsAffected(); err == nil {
|
||||
affected += rows
|
||||
}
|
||||
}
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
func GetOrderProductionLookupOptions(mssql *sql.DB) (models.OrderProductionCdItemLookups, error) {
|
||||
out := models.OrderProductionCdItemLookups{}
|
||||
|
||||
queryPairs := []struct {
|
||||
Query string
|
||||
Target *[]models.OrderProductionLookupOption
|
||||
}{
|
||||
{`SELECT
|
||||
CAST(t.ItemDimTypeCode AS NVARCHAR(50)) AS Code,
|
||||
ISNULL(d.ItemDimTypeDescription, CAST(t.ItemDimTypeCode AS NVARCHAR(50))) AS [Description]
|
||||
FROM dbo.bsItemDimType t WITH(NOLOCK)
|
||||
LEFT JOIN dbo.bsItemDimTypeDesc d WITH(NOLOCK)
|
||||
ON d.ItemDimTypeCode = t.ItemDimTypeCode
|
||||
AND d.LangCode = 'TR'
|
||||
WHERE ISNULL(t.IsBlocked, 0) = 0
|
||||
ORDER BY t.ItemDimTypeCode`, &out.ItemDimTypeCodes},
|
||||
{`SELECT DISTINCT CAST(ProductTypeCode AS NVARCHAR(50)) AS Code, CAST(ProductTypeCode AS NVARCHAR(50)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ProductTypeCode IS NOT NULL ORDER BY Code`, &out.ProductTypeCodes},
|
||||
{`SELECT
|
||||
CAST(h.ProductHierarchyID AS NVARCHAR(50)) AS Code,
|
||||
LTRIM(RTRIM(
|
||||
CONCAT(
|
||||
CAST(ISNULL(h.ProductHierarchyLevelCode01, 0) AS NVARCHAR(50)),
|
||||
CASE
|
||||
WHEN ISNULL(d.ProductHierarchyLevelDescription, '') <> '' THEN CONCAT(' - ', d.ProductHierarchyLevelDescription)
|
||||
ELSE ''
|
||||
END
|
||||
)
|
||||
)) AS [Description]
|
||||
FROM dbo.dfProductHierarchy h WITH(NOLOCK)
|
||||
LEFT JOIN dbo.cdProductHierarchyLevelDesc d WITH(NOLOCK)
|
||||
ON d.ProductHierarchyLevelCode = h.ProductHierarchyLevelCode01
|
||||
AND d.LangCode = 'TR'
|
||||
ORDER BY h.ProductHierarchyID`, &out.ProductHierarchyIDs},
|
||||
{`SELECT DISTINCT CAST(UnitOfMeasureCode1 AS NVARCHAR(50)) AS Code, CAST(UnitOfMeasureCode1 AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(UnitOfMeasureCode1,'') <> '' ORDER BY Code`, &out.UnitOfMeasureCode1List},
|
||||
{`SELECT DISTINCT CAST(ItemAccountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemAccountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemAccountGrCode,'') <> '' ORDER BY Code`, &out.ItemAccountGrCodes},
|
||||
{`SELECT DISTINCT CAST(ItemTaxGrCode AS NVARCHAR(50)) AS Code, CAST(ItemTaxGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemTaxGrCode,'') <> '' ORDER BY Code`, &out.ItemTaxGrCodes},
|
||||
{`SELECT DISTINCT CAST(ItemPaymentPlanGrCode AS NVARCHAR(50)) AS Code, CAST(ItemPaymentPlanGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemPaymentPlanGrCode,'') <> '' ORDER BY Code`, &out.ItemPaymentPlanGrCodes},
|
||||
{`SELECT DISTINCT CAST(ItemDiscountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemDiscountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemDiscountGrCode,'') <> '' ORDER BY Code`, &out.ItemDiscountGrCodes},
|
||||
{`SELECT DISTINCT CAST(ItemVendorGrCode AS NVARCHAR(50)) AS Code, CAST(ItemVendorGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemVendorGrCode,'') <> '' ORDER BY Code`, &out.ItemVendorGrCodes},
|
||||
{`SELECT DISTINCT CAST(PromotionGroupCode AS NVARCHAR(50)) AS Code, CAST(PromotionGroupCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(PromotionGroupCode,'') <> '' ORDER BY Code`, &out.PromotionGroupCodes},
|
||||
{`SELECT DISTINCT CAST(ProductCollectionGrCode AS NVARCHAR(50)) AS Code, CAST(ProductCollectionGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ProductCollectionGrCode,'') <> '' ORDER BY Code`, &out.ProductCollectionGrCodes},
|
||||
{`SELECT DISTINCT CAST(StorePriceLevelCode AS NVARCHAR(50)) AS Code, CAST(StorePriceLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(StorePriceLevelCode,'') <> '' ORDER BY Code`, &out.StorePriceLevelCodes},
|
||||
{`SELECT DISTINCT CAST(PerceptionOfFashionCode AS NVARCHAR(50)) AS Code, CAST(PerceptionOfFashionCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(PerceptionOfFashionCode,'') <> '' ORDER BY Code`, &out.PerceptionOfFashionCodes},
|
||||
{`SELECT DISTINCT CAST(CommercialRoleCode AS NVARCHAR(50)) AS Code, CAST(CommercialRoleCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(CommercialRoleCode,'') <> '' ORDER BY Code`, &out.CommercialRoleCodes},
|
||||
{`SELECT DISTINCT CAST(StoreCapacityLevelCode AS NVARCHAR(50)) AS Code, CAST(StoreCapacityLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(StoreCapacityLevelCode,'') <> '' ORDER BY Code`, &out.StoreCapacityLevelCodes},
|
||||
{`SELECT DISTINCT CAST(CustomsTariffNumberCode AS NVARCHAR(50)) AS Code, CAST(CustomsTariffNumberCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(CustomsTariffNumberCode,'') <> '' ORDER BY Code`, &out.CustomsTariffNumbers},
|
||||
{`SELECT DISTINCT CAST(CompanyCode AS NVARCHAR(50)) AS Code, CAST(CompanyCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(CompanyCode,'') <> '' ORDER BY Code`, &out.CompanyCodes},
|
||||
}
|
||||
|
||||
for _, pair := range queryPairs {
|
||||
rows, err := mssql.Query(pair.Query)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
list := make([]models.OrderProductionLookupOption, 0, 64)
|
||||
for rows.Next() {
|
||||
var item models.OrderProductionLookupOption
|
||||
if err := rows.Scan(&item.Code, &item.Description); err != nil {
|
||||
rows.Close()
|
||||
return out, err
|
||||
}
|
||||
item.Code = strings.TrimSpace(item.Code)
|
||||
item.Description = strings.TrimSpace(item.Description)
|
||||
list = append(list, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return out, err
|
||||
}
|
||||
rows.Close()
|
||||
*pair.Target = list
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
42
svc/queries/productattributes.go
Normal file
42
svc/queries/productattributes.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package queries
|
||||
|
||||
const GetProductAttributes = `
|
||||
;WITH TypeDesc AS (
|
||||
SELECT
|
||||
t.ItemTypeCode,
|
||||
t.AttributeTypeCode,
|
||||
ISNULL(t.AttributeTypeDescription, CAST(t.AttributeTypeCode AS NVARCHAR(30))) AS AttributeTypeDescription
|
||||
FROM dbo.cdItemAttributeTypeDesc AS t WITH(NOLOCK)
|
||||
WHERE t.ItemTypeCode = @p1
|
||||
AND t.LangCode = 'TR'
|
||||
),
|
||||
Attr AS (
|
||||
SELECT
|
||||
a.ItemTypeCode,
|
||||
a.AttributeTypeCode,
|
||||
ISNULL(a.AttributeCode, '') AS AttributeCode,
|
||||
ISNULL(d.AttributeDescription, ISNULL(a.AttributeCode, '')) AS AttributeDescription
|
||||
FROM dbo.cdItemAttribute AS a WITH(NOLOCK)
|
||||
LEFT JOIN dbo.cdItemAttributeDesc AS d WITH(NOLOCK)
|
||||
ON d.ItemTypeCode = a.ItemTypeCode
|
||||
AND d.AttributeTypeCode = a.AttributeTypeCode
|
||||
AND d.AttributeCode = a.AttributeCode
|
||||
AND d.LangCode = 'TR'
|
||||
WHERE a.ItemTypeCode = @p1
|
||||
AND ISNULL(a.IsBlocked, 0) = 0
|
||||
),
|
||||
SELECT
|
||||
a.ItemTypeCode,
|
||||
a.AttributeTypeCode,
|
||||
ISNULL(NULLIF(td.AttributeTypeDescription, ''), CAST(a.AttributeTypeCode AS NVARCHAR(30))) AS AttributeTypeDescription,
|
||||
a.AttributeCode,
|
||||
a.AttributeDescription
|
||||
FROM Attr a
|
||||
LEFT JOIN TypeDesc td
|
||||
ON td.ItemTypeCode = a.ItemTypeCode
|
||||
AND td.AttributeTypeCode = a.AttributeTypeCode
|
||||
ORDER BY
|
||||
a.AttributeTypeCode,
|
||||
CASE WHEN a.AttributeCode = '-' THEN 0 ELSE 1 END,
|
||||
a.AttributeCode;
|
||||
`
|
||||
@@ -4,7 +4,8 @@ const GetProductSecondColors = `
|
||||
SELECT DISTINCT
|
||||
Product.ProductCode,
|
||||
ISNULL(prItemVariant.ColorCode, '') AS ColorCode,
|
||||
ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code
|
||||
ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code,
|
||||
ISNULL(ColorDesc.ColorDescription, '') AS ColorDescription
|
||||
FROM prItemVariant WITH(NOLOCK)
|
||||
INNER JOIN ProductFilterWithDescription('TR') AS Product
|
||||
ON prItemVariant.ItemCode = Product.ProductCode
|
||||
@@ -14,5 +15,10 @@ FROM prItemVariant WITH(NOLOCK)
|
||||
WHERE Product.ProductCode = @ProductCode
|
||||
AND prItemVariant.ColorCode = @ColorCode
|
||||
AND ISNULL(prItemVariant.ItemDim2Code, '') <> ''
|
||||
GROUP BY Product.ProductCode, prItemVariant.ItemDim2Code, prItemVariant.ColorCode
|
||||
GROUP BY
|
||||
Product.ProductCode,
|
||||
prItemVariant.ItemDim2Code,
|
||||
prItemVariant.ColorCode,
|
||||
ColorDesc.ColorDescription
|
||||
ORDER BY prItemVariant.ItemDim2Code
|
||||
`
|
||||
|
||||
@@ -1597,6 +1597,17 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa
|
||||
layout := newPdfLayout(pdf)
|
||||
catSizes := buildCategorySizeMap(rows)
|
||||
|
||||
normalizeYetiskinGarsonTokenGo := func(v string) string {
|
||||
s := strings.ToUpper(strings.TrimSpace(v))
|
||||
if strings.Contains(s, "GARSON") {
|
||||
return "GARSON"
|
||||
}
|
||||
if strings.Contains(s, "YETISKIN") || strings.Contains(s, "YETİSKİN") {
|
||||
return "YETISKIN"
|
||||
}
|
||||
return "GENEL"
|
||||
}
|
||||
|
||||
// Grup: ÜRÜN ANA GRUBU
|
||||
type group struct {
|
||||
Name string
|
||||
@@ -1609,15 +1620,19 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa
|
||||
var order []string
|
||||
|
||||
for _, r := range rows {
|
||||
name := strings.TrimSpace(r.GroupMain)
|
||||
if name == "" {
|
||||
name = "GENEL"
|
||||
ana := strings.TrimSpace(r.GroupMain)
|
||||
if ana == "" {
|
||||
ana = "GENEL"
|
||||
}
|
||||
g, ok := groups[name]
|
||||
yg := normalizeYetiskinGarsonTokenGo(r.YetiskinGarson)
|
||||
name := strings.TrimSpace(fmt.Sprintf("%s %s", yg, ana))
|
||||
groupKey := fmt.Sprintf("%s::%s", yg, ana)
|
||||
|
||||
g, ok := groups[groupKey]
|
||||
if !ok {
|
||||
g = &group{Name: name}
|
||||
groups[name] = g
|
||||
order = append(order, name)
|
||||
groups[groupKey] = g
|
||||
order = append(order, groupKey)
|
||||
}
|
||||
g.Rows = append(g.Rows, r)
|
||||
g.Adet += r.TotalQty
|
||||
@@ -1673,8 +1688,8 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa
|
||||
newPage(firstPage, true)
|
||||
firstPage = false
|
||||
|
||||
for _, name := range order {
|
||||
g := groups[name]
|
||||
for _, key := range order {
|
||||
g := groups[key]
|
||||
|
||||
for _, row := range g.Rows {
|
||||
rh := calcRowHeight(pdf, layout, row)
|
||||
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
mssql "github.com/microsoft/go-mssqldb"
|
||||
)
|
||||
|
||||
var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`)
|
||||
|
||||
// ======================================================
|
||||
// 📌 OrderProductionItemsRoute — U ürün satırları
|
||||
// ======================================================
|
||||
@@ -73,6 +76,23 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func OrderProductionCdItemLookupsRoute(mssql *sql.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
lookups, err := queries.GetOrderProductionLookupOptions(mssql)
|
||||
if err != nil {
|
||||
log.Printf("[OrderProductionCdItemLookupsRoute] lookup error: %v", err)
|
||||
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(lookups); err != nil {
|
||||
log.Printf("[OrderProductionCdItemLookupsRoute] encode error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
// 📌 OrderProductionInsertMissingRoute — eksik varyantları ekler
|
||||
// ======================================================
|
||||
@@ -208,13 +228,24 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
|
||||
var inserted int64
|
||||
if payload.InsertMissing {
|
||||
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username)
|
||||
cdItemByCode := buildCdItemDraftMap(payload.CdItems)
|
||||
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username, cdItemByCode)
|
||||
if err != nil {
|
||||
writeDBError(w, http.StatusInternalServerError, "insert_missing_variants", id, username, len(missing), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateProductAttributes(payload.ProductAttributes); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
attributeAffected, err := queries.UpsertItemAttributesTx(tx, payload.ProductAttributes, username)
|
||||
if err != nil {
|
||||
writeDBError(w, http.StatusInternalServerError, "upsert_item_attributes", id, username, len(payload.ProductAttributes), err)
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
|
||||
if err != nil {
|
||||
writeDBError(w, http.StatusInternalServerError, "update_order_lines", id, username, len(payload.Lines), err)
|
||||
@@ -227,8 +258,9 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"updated": updated,
|
||||
"inserted": inserted,
|
||||
"updated": updated,
|
||||
"inserted": inserted,
|
||||
"attributeUpserted": attributeAffected,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("❌ encode error: %v", err)
|
||||
@@ -236,6 +268,44 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func validateProductAttributes(attrs []models.OrderProductionItemAttributeRow) error {
|
||||
for _, a := range attrs {
|
||||
if strings.TrimSpace(a.ItemCode) == "" {
|
||||
return errors.New("Urun ozellikleri icin ItemCode zorunlu")
|
||||
}
|
||||
if !baggiModelCodeRegex.MatchString(strings.ToUpper(strings.TrimSpace(a.ItemCode))) {
|
||||
return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999")
|
||||
}
|
||||
if a.ItemTypeCode <= 0 {
|
||||
return errors.New("Urun ozellikleri icin ItemTypeCode zorunlu")
|
||||
}
|
||||
if a.AttributeTypeCode <= 0 {
|
||||
return errors.New("Urun ozellikleri icin AttributeTypeCode zorunlu")
|
||||
}
|
||||
if strings.TrimSpace(a.AttributeCode) == "" {
|
||||
return errors.New("Urun ozellikleri icin AttributeCode zorunlu")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCdItemDraftMap(list []models.OrderProductionCdItemDraft) map[string]models.OrderProductionCdItemDraft {
|
||||
out := make(map[string]models.OrderProductionCdItemDraft, len(list))
|
||||
for _, item := range list {
|
||||
code := strings.ToUpper(strings.TrimSpace(item.ItemCode))
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
item.ItemCode = code
|
||||
if item.ItemTypeCode == 0 {
|
||||
item.ItemTypeCode = 1
|
||||
}
|
||||
key := queries.NormalizeCdItemMapKey(item.ItemTypeCode, item.ItemCode)
|
||||
out[key] = item
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
|
||||
missing := make([]models.OrderProductionMissingVariant, 0)
|
||||
|
||||
@@ -279,9 +349,13 @@ func validateUpdateLines(lines []models.OrderProductionUpdateLine) error {
|
||||
if strings.TrimSpace(line.OrderLineID) == "" {
|
||||
return errors.New("OrderLineID zorunlu")
|
||||
}
|
||||
if strings.TrimSpace(line.NewItemCode) == "" {
|
||||
code := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
|
||||
if code == "" {
|
||||
return errors.New("Yeni urun kodu zorunlu")
|
||||
}
|
||||
if !baggiModelCodeRegex.MatchString(code) {
|
||||
return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,18 +22,69 @@ type ProductSizeMatchResponse struct {
|
||||
Schemas map[string][]string `json:"schemas"`
|
||||
}
|
||||
|
||||
func defaultSizeSchemas() map[string][]string {
|
||||
func fallbackTakSchema() map[string][]string {
|
||||
return map[string][]string{
|
||||
"tak": {"44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68", "70", "72", "74"},
|
||||
"ayk": {"39", "40", "41", "42", "43", "44", "45"},
|
||||
"ayk_garson": {"22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "STD"},
|
||||
"yas": {"2", "4", "6", "8", "10", "12", "14"},
|
||||
"pan": {"38", "40", "42", "44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68"},
|
||||
"gom": {"XS", "S", "M", "L", "XL", "2XL", "3XL", "4XL", "5XL", "6XL", "7XL"},
|
||||
"aksbir": {" ", "44", "STD", "110", "115", "120", "125", "130", "135"},
|
||||
"tak": {"44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68", "70", "72", "74"},
|
||||
}
|
||||
}
|
||||
|
||||
func parseSizeValuesCSV(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
seen := map[string]struct{}{}
|
||||
for _, p := range parts {
|
||||
v := strings.TrimSpace(p)
|
||||
if v == "" {
|
||||
v = " "
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func loadSizeSchemas(pgDB *sql.DB) (map[string][]string, error) {
|
||||
rows, err := pgDB.Query(`
|
||||
SELECT
|
||||
COALESCE(group_key, ''),
|
||||
COALESCE(size_values, '')
|
||||
FROM mk_size_group
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
schemas := map[string][]string{}
|
||||
for rows.Next() {
|
||||
var groupKey string
|
||||
var sizeValues string
|
||||
if err := rows.Scan(&groupKey, &sizeValues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := strings.TrimSpace(groupKey)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
schemas[key] = parseSizeValuesCSV(sizeValues)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(schemas) == 0 {
|
||||
schemas = fallbackTakSchema()
|
||||
}
|
||||
if _, ok := schemas["tak"]; !ok {
|
||||
schemas["tak"] = fallbackTakSchema()["tak"]
|
||||
}
|
||||
|
||||
return schemas, nil
|
||||
}
|
||||
|
||||
func loadProductSizeMatchData(pgDB *sql.DB) (*ProductSizeMatchResponse, error) {
|
||||
rows, err := pgDB.Query(`
|
||||
SELECT
|
||||
@@ -58,9 +109,13 @@ func loadProductSizeMatchData(pgDB *sql.DB) (*ProductSizeMatchResponse, error) {
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
schemas, err := loadSizeSchemas(pgDB)
|
||||
if err != nil {
|
||||
schemas = fallbackTakSchema()
|
||||
}
|
||||
resp := &ProductSizeMatchResponse{
|
||||
Rules: make([]ProductSizeMatchRule, 0),
|
||||
Schemas: defaultSizeSchemas(),
|
||||
Schemas: schemas,
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
|
||||
54
svc/routes/productattributes.go
Normal file
54
svc/routes/productattributes.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bssapp-backend/auth"
|
||||
"bssapp-backend/db"
|
||||
"bssapp-backend/models"
|
||||
"bssapp-backend/queries"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetProductAttributesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||
if !ok || claims == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
itemTypeCode := int16(1)
|
||||
if raw := r.URL.Query().Get("itemTypeCode"); raw != "" {
|
||||
v, err := strconv.Atoi(raw)
|
||||
if err != nil || v <= 0 {
|
||||
http.Error(w, "itemTypeCode gecersiz", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
itemTypeCode = int16(v)
|
||||
}
|
||||
|
||||
rows, err := db.MssqlDB.Query(queries.GetProductAttributes, itemTypeCode)
|
||||
if err != nil {
|
||||
http.Error(w, "Product attributes alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]models.ProductAttributeOption, 0, 256)
|
||||
for rows.Next() {
|
||||
var x models.ProductAttributeOption
|
||||
if err := rows.Scan(
|
||||
&x.ItemTypeCode,
|
||||
&x.AttributeTypeCode,
|
||||
&x.AttributeTypeDescription,
|
||||
&x.AttributeCode,
|
||||
&x.AttributeDescription,
|
||||
); err != nil {
|
||||
continue
|
||||
}
|
||||
list = append(list, x)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(list)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func GetProductSecondColorsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var list []models.ProductSecondColor
|
||||
for rows.Next() {
|
||||
var c models.ProductSecondColor
|
||||
if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ItemDim2Code); err != nil {
|
||||
if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ItemDim2Code, &c.ColorDescription); err != nil {
|
||||
log.Println("⚠️ Satır okunamadı:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
75
ui/.quasar/prod-spa/app.js
Normal file
75
ui/.quasar/prod-spa/app.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||
* DO NOT EDIT.
|
||||
*
|
||||
* You are probably looking on adding startup/initialization code.
|
||||
* Use "quasar new boot <name>" and add it there.
|
||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
||||
*
|
||||
* Boot files are your "main.js"
|
||||
**/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { Quasar } from 'quasar'
|
||||
import { markRaw } from 'vue'
|
||||
import RootComponent from 'app/src/App.vue'
|
||||
|
||||
import createStore from 'app/src/stores/index'
|
||||
import createRouter from 'app/src/router/index'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default async function (createAppFn, quasarUserOptions) {
|
||||
|
||||
|
||||
// Create the app instance.
|
||||
// Here we inject into it the Quasar UI, the router & possibly the store.
|
||||
const app = createAppFn(RootComponent)
|
||||
|
||||
|
||||
|
||||
app.use(Quasar, quasarUserOptions)
|
||||
|
||||
|
||||
|
||||
|
||||
const store = typeof createStore === 'function'
|
||||
? await createStore({})
|
||||
: createStore
|
||||
|
||||
|
||||
app.use(store)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const router = markRaw(
|
||||
typeof createRouter === 'function'
|
||||
? await createRouter({store})
|
||||
: createRouter
|
||||
)
|
||||
|
||||
|
||||
// make router instance available in store
|
||||
|
||||
store.use(({ store }) => { store.router = router })
|
||||
|
||||
|
||||
|
||||
// Expose the app, the router and the store.
|
||||
// Note that we are not mounting the app here, since bootstrapping will be
|
||||
// different depending on whether we are in a browser or on the server.
|
||||
return {
|
||||
app,
|
||||
store,
|
||||
router
|
||||
}
|
||||
}
|
||||
154
ui/.quasar/prod-spa/client-entry.js
Normal file
154
ui/.quasar/prod-spa/client-entry.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||
* DO NOT EDIT.
|
||||
*
|
||||
* You are probably looking on adding startup/initialization code.
|
||||
* Use "quasar new boot <name>" and add it there.
|
||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
||||
*
|
||||
* Boot files are your "main.js"
|
||||
**/
|
||||
|
||||
|
||||
import { createApp } from 'vue'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import '@quasar/extras/roboto-font/roboto-font.css'
|
||||
|
||||
import '@quasar/extras/material-icons/material-icons.css'
|
||||
|
||||
|
||||
|
||||
|
||||
// We load Quasar stylesheet file
|
||||
import 'quasar/dist/quasar.sass'
|
||||
|
||||
|
||||
|
||||
|
||||
import 'src/css/app.css'
|
||||
|
||||
|
||||
import createQuasarApp from './app.js'
|
||||
import quasarUserOptions from './quasar-user-options.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const publicPath = `/`
|
||||
|
||||
|
||||
async function start ({
|
||||
app,
|
||||
router
|
||||
, store
|
||||
}, bootFiles) {
|
||||
|
||||
let hasRedirected = false
|
||||
const getRedirectUrl = url => {
|
||||
try { return router.resolve(url).href }
|
||||
catch (err) {}
|
||||
|
||||
return Object(url) === url
|
||||
? null
|
||||
: url
|
||||
}
|
||||
const redirect = url => {
|
||||
hasRedirected = true
|
||||
|
||||
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
|
||||
window.location.href = url
|
||||
return
|
||||
}
|
||||
|
||||
const href = getRedirectUrl(url)
|
||||
|
||||
// continue if we didn't fail to resolve the url
|
||||
if (href !== null) {
|
||||
window.location.href = href
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
const urlPath = window.location.href.replace(window.location.origin, '')
|
||||
|
||||
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
|
||||
try {
|
||||
await bootFiles[i]({
|
||||
app,
|
||||
router,
|
||||
store,
|
||||
ssrContext: null,
|
||||
redirect,
|
||||
urlPath,
|
||||
publicPath
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
if (err && err.url) {
|
||||
redirect(err.url)
|
||||
return
|
||||
}
|
||||
|
||||
console.error('[Quasar] boot error:', err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRedirected === true) return
|
||||
|
||||
|
||||
app.use(router)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.mount('#q-app')
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
createQuasarApp(createApp, quasarUserOptions)
|
||||
|
||||
.then(app => {
|
||||
// eventually remove this when Cordova/Capacitor/Electron support becomes old
|
||||
const [ method, mapFn ] = Promise.allSettled !== void 0
|
||||
? [
|
||||
'allSettled',
|
||||
bootFiles => bootFiles.map(result => {
|
||||
if (result.status === 'rejected') {
|
||||
console.error('[Quasar] boot error:', result.reason)
|
||||
return
|
||||
}
|
||||
return result.value.default
|
||||
})
|
||||
]
|
||||
: [
|
||||
'all',
|
||||
bootFiles => bootFiles.map(entry => entry.default)
|
||||
]
|
||||
|
||||
return Promise[ method ]([
|
||||
|
||||
import(/* webpackMode: "eager" */ 'boot/dayjs')
|
||||
|
||||
]).then(bootFiles => {
|
||||
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
|
||||
start(app, boot)
|
||||
})
|
||||
})
|
||||
|
||||
116
ui/.quasar/prod-spa/client-prefetch.js
Normal file
116
ui/.quasar/prod-spa/client-prefetch.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||
* DO NOT EDIT.
|
||||
*
|
||||
* You are probably looking on adding startup/initialization code.
|
||||
* Use "quasar new boot <name>" and add it there.
|
||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
||||
*
|
||||
* Boot files are your "main.js"
|
||||
**/
|
||||
|
||||
|
||||
|
||||
import App from 'app/src/App.vue'
|
||||
let appPrefetch = typeof App.preFetch === 'function'
|
||||
? App.preFetch
|
||||
: (
|
||||
// Class components return the component options (and the preFetch hook) inside __c property
|
||||
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
|
||||
? App.__c.preFetch
|
||||
: false
|
||||
)
|
||||
|
||||
|
||||
function getMatchedComponents (to, router) {
|
||||
const route = to
|
||||
? (to.matched ? to : router.resolve(to).route)
|
||||
: router.currentRoute.value
|
||||
|
||||
if (!route) { return [] }
|
||||
|
||||
const matched = route.matched.filter(m => m.components !== void 0)
|
||||
|
||||
if (matched.length === 0) { return [] }
|
||||
|
||||
return Array.prototype.concat.apply([], matched.map(m => {
|
||||
return Object.keys(m.components).map(key => {
|
||||
const comp = m.components[key]
|
||||
return {
|
||||
path: m.path,
|
||||
c: comp
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
export function addPreFetchHooks ({ router, store, publicPath }) {
|
||||
// Add router hook for handling preFetch.
|
||||
// Doing it after initial route is resolved so that we don't double-fetch
|
||||
// the data that we already have. Using router.beforeResolve() so that all
|
||||
// async components are resolved.
|
||||
router.beforeResolve((to, from, next) => {
|
||||
const
|
||||
urlPath = window.location.href.replace(window.location.origin, ''),
|
||||
matched = getMatchedComponents(to, router),
|
||||
prevMatched = getMatchedComponents(from, router)
|
||||
|
||||
let diffed = false
|
||||
const preFetchList = matched
|
||||
.filter((m, i) => {
|
||||
return diffed || (diffed = (
|
||||
!prevMatched[i] ||
|
||||
prevMatched[i].c !== m.c ||
|
||||
m.path.indexOf('/:') > -1 // does it has params?
|
||||
))
|
||||
})
|
||||
.filter(m => m.c !== void 0 && (
|
||||
typeof m.c.preFetch === 'function'
|
||||
// Class components return the component options (and the preFetch hook) inside __c property
|
||||
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
|
||||
))
|
||||
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
|
||||
|
||||
|
||||
if (appPrefetch !== false) {
|
||||
preFetchList.unshift(appPrefetch)
|
||||
appPrefetch = false
|
||||
}
|
||||
|
||||
|
||||
if (preFetchList.length === 0) {
|
||||
return next()
|
||||
}
|
||||
|
||||
let hasRedirected = false
|
||||
const redirect = url => {
|
||||
hasRedirected = true
|
||||
next(url)
|
||||
}
|
||||
const proceed = () => {
|
||||
|
||||
if (hasRedirected === false) { next() }
|
||||
}
|
||||
|
||||
|
||||
|
||||
preFetchList.reduce(
|
||||
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
|
||||
store,
|
||||
currentRoute: to,
|
||||
previousRoute: from,
|
||||
redirect,
|
||||
urlPath,
|
||||
publicPath
|
||||
})),
|
||||
Promise.resolve()
|
||||
)
|
||||
.then(proceed)
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
proceed()
|
||||
})
|
||||
})
|
||||
}
|
||||
23
ui/.quasar/prod-spa/quasar-user-options.js
Normal file
23
ui/.quasar/prod-spa/quasar-user-options.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||
* DO NOT EDIT.
|
||||
*
|
||||
* You are probably looking on adding startup/initialization code.
|
||||
* Use "quasar new boot <name>" and add it there.
|
||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
||||
*
|
||||
* Boot files are your "main.js"
|
||||
**/
|
||||
|
||||
import lang from 'quasar/lang/tr.js'
|
||||
|
||||
|
||||
|
||||
import {Loading,Dialog,Notify} from 'quasar'
|
||||
|
||||
|
||||
|
||||
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }
|
||||
|
||||
@@ -323,12 +323,12 @@
|
||||
======================================================== -->
|
||||
<div class="order-scroll-y" :class="{ 'compact-grid-header': compactGridHeader }"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll -->
|
||||
<div class="order-grid-body">
|
||||
<template v-for="grp in groupedRows" :key="grp.name">
|
||||
<template v-for="grp in groupedRows" :key="grp.groupKey">
|
||||
<div :class="['summary-group', grp.open ? 'open' : 'closed']">
|
||||
|
||||
<!-- 🟡 Sub-header -->
|
||||
<div class="order-sub-header" @click="toggleGroup(grp.name)">
|
||||
<div class="sub-left">{{ grp.name }}</div>
|
||||
<div class="order-sub-header" @click="toggleGroup(grp.groupKey)">
|
||||
<div class="sub-left">{{ grp.displayName }}</div>
|
||||
|
||||
<div class="sub-center">
|
||||
<div
|
||||
@@ -348,10 +348,10 @@
|
||||
|
||||
|
||||
<div class="order-text-caption">
|
||||
Toplam {{ grp.name }} Adet: {{ grp.toplamAdet }}
|
||||
Toplam {{ grp.displayName }} Adet: {{ grp.toplamAdet }}
|
||||
</div>
|
||||
<div class="order-text-caption">
|
||||
Toplam {{ grp.name }} Tutar:
|
||||
Toplam {{ grp.displayName }} Tutar:
|
||||
{{ Number(grp.toplamTutar || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
|
||||
{{ form.pb || aktifPB }}
|
||||
</div>
|
||||
@@ -747,6 +747,14 @@
|
||||
@click="removeSelected"
|
||||
:disable="isClosedRow || isViewOnly || !canMutateRows"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="canMutateRows"
|
||||
flat
|
||||
color="warning"
|
||||
label="Bedenleri Sıfırla"
|
||||
@click="onZeroBedenClick"
|
||||
:disable="isClosedRow || isViewOnly || !canMutateRows"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="canMutateRows"
|
||||
flat
|
||||
@@ -1362,6 +1370,21 @@ const selectedRow = computed(() => {
|
||||
=========================================================== */
|
||||
const groupOpen = reactive({})
|
||||
|
||||
function normalizeYetiskinGarsonToken (row) {
|
||||
const raw = String(
|
||||
row?.yetiskinGarson ||
|
||||
row?.YETISKIN_GARSON ||
|
||||
row?.YetiskinGarson ||
|
||||
row?.kategori ||
|
||||
row?.Kategori ||
|
||||
''
|
||||
).toUpperCase()
|
||||
|
||||
if (raw.includes('GARSON')) return 'GARSON'
|
||||
if (raw.includes('YETISKIN') || raw.includes('YETİSKİN')) return 'YETISKIN'
|
||||
return 'GENEL'
|
||||
}
|
||||
|
||||
const groupedRows = computed(() => {
|
||||
const rows = Array.isArray(summaryRows.value) ? summaryRows.value : []
|
||||
const buckets = {}
|
||||
@@ -1376,33 +1399,44 @@ const groupedRows = computed(() => {
|
||||
const ana = (row?.urunAnaGrubu || 'GENEL')
|
||||
.toUpperCase()
|
||||
.trim()
|
||||
const yg = normalizeYetiskinGarsonToken(row)
|
||||
const grpKey = String(row?.grpKey || 'tak').trim() || 'tak'
|
||||
const bucketKey = `${yg}::${ana}::${grpKey}`
|
||||
|
||||
if (!buckets[ana]) {
|
||||
buckets[ana] = {
|
||||
if (!buckets[bucketKey]) {
|
||||
buckets[bucketKey] = {
|
||||
name: ana,
|
||||
yg,
|
||||
displayName: `${yg} ${ana}`,
|
||||
rows: [],
|
||||
toplamAdet: 0,
|
||||
toplamTutar: 0,
|
||||
open: groupOpen[ana] ?? true,
|
||||
open: groupOpen[bucketKey] ?? true,
|
||||
|
||||
// 🔑 TEK KAYNAK
|
||||
grpKey: row.grpKey
|
||||
grpKey
|
||||
}
|
||||
order.push(ana)
|
||||
order.push(bucketKey)
|
||||
}
|
||||
|
||||
const bucket = buckets[ana]
|
||||
const bucket = buckets[bucketKey]
|
||||
bucket.rows.push(row)
|
||||
bucket.toplamAdet += Number(row.adet || 0)
|
||||
bucket.toplamTutar += Number(row.tutar || 0)
|
||||
}
|
||||
|
||||
return order.map(name => {
|
||||
const grp = buckets[name]
|
||||
return order.map(bucketKey => {
|
||||
const grp = buckets[bucketKey]
|
||||
const schema = schemaMap?.[grp.grpKey]
|
||||
const schemaTitle = String(schema?.title || grp.grpKey || '').trim()
|
||||
const displayName = schemaTitle
|
||||
? `${grp.yg} ${grp.name} (${schemaTitle})`
|
||||
: `${grp.yg} ${grp.name}`
|
||||
|
||||
return {
|
||||
...grp,
|
||||
displayName,
|
||||
groupKey: bucketKey,
|
||||
bedenValues: schema?.values || []
|
||||
}
|
||||
})
|
||||
@@ -3016,6 +3050,25 @@ const onSaveAndNextColor = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
function onZeroBedenClick () {
|
||||
if (!Array.isArray(form.bedenLabels) || !form.bedenLabels.length) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Sıfırlanacak beden alanı bulunamadı.'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
form.bedenler = form.bedenLabels.map(() => 0)
|
||||
updateTotals(form)
|
||||
|
||||
$q.notify({
|
||||
type: 'info',
|
||||
message: 'Beden adetleri sıfırlandı.',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
filled
|
||||
maxlength="13"
|
||||
label="Yeni Urun"
|
||||
@update:model-value="val => onNewItemChange(props.row, val)"
|
||||
:class="newItemInputClass(props.row)"
|
||||
@update:model-value="val => onNewItemChange(props.row, val, 'typed')"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="arrow_drop_down" class="cursor-pointer" />
|
||||
@@ -145,6 +146,32 @@
|
||||
</div>
|
||||
</q-menu>
|
||||
</q-input>
|
||||
<div v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'" class="q-mt-xs row items-center no-wrap">
|
||||
<q-badge :color="newItemBadgeColor(props.row)" text-color="white">
|
||||
{{ newItemBadgeLabel(props.row) }}
|
||||
</q-badge>
|
||||
<span class="text-caption q-ml-sm text-grey-8">{{ newItemHintText(props.row) }}</span>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode === 'new'"
|
||||
class="q-ml-sm"
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
color="warning"
|
||||
label="cdItem Bilgisi"
|
||||
@click="openCdItemDialog(props.row.NewItemCode)"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode === 'new'"
|
||||
class="q-ml-xs"
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
color="primary"
|
||||
label="Urun Ozellikleri"
|
||||
@click="openAttributeDialog(props.row.NewItemCode)"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
@@ -157,13 +184,10 @@
|
||||
option-value="color_code"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
new-value-mode="add-unique"
|
||||
dense
|
||||
filled
|
||||
label="Yeni Renk"
|
||||
@update:model-value="() => onNewColorChange(props.row)"
|
||||
@new-value="(val, done) => onCreateColorValue(props.row, val, done)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
@@ -173,16 +197,13 @@
|
||||
<q-select
|
||||
v-model="props.row.NewDim2"
|
||||
:options="getSecondColorOptions(props.row)"
|
||||
option-label="item_dim2_code"
|
||||
option-label="item_dim2_label"
|
||||
option-value="item_dim2_code"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
new-value-mode="add-unique"
|
||||
dense
|
||||
filled
|
||||
label="Yeni 2. Renk"
|
||||
@new-value="(val, done) => onCreateSecondColorValue(props.row, val, done)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
@@ -205,6 +226,127 @@
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
|
||||
Hata: {{ store.error }}
|
||||
</q-banner>
|
||||
|
||||
<q-dialog v-model="cdItemDialogOpen" persistent>
|
||||
<q-card style="min-width: 980px; max-width: 98vw;">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6">Yeni Kod cdItem Bilgileri</div>
|
||||
<q-space />
|
||||
<q-badge color="warning" text-color="black">
|
||||
{{ cdItemTargetCode || '-' }}
|
||||
</q-badge>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemDimTypeCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemDimTypeCodes')" label="ItemDimTypeCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ProductTypeCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('productTypeCodes')" label="ProductTypeCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ProductHierarchyID" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('productHierarchyIDs')" label="ProductHierarchyID" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.UnitOfMeasureCode1" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('unitOfMeasureCode1List')" label="UnitOfMeasureCode1" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemAccountGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemAccountGrCodes')" label="ItemAccountGrCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemTaxGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemTaxGrCodes')" label="ItemTaxGrCode" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemPaymentPlanGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemPaymentPlanGrCodes')" label="ItemPaymentPlanGrCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemDiscountGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemDiscountGrCodes')" label="ItemDiscountGrCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemVendorGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemVendorGrCodes')" label="ItemVendorGrCode" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.PromotionGroupCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('promotionGroupCodes')" label="PromotionGroupCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ProductCollectionGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('productCollectionGrCodes')" label="ProductCollectionGrCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.StorePriceLevelCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('storePriceLevelCodes')" label="StorePriceLevelCode" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.PerceptionOfFashionCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('perceptionOfFashionCodes')" label="PerceptionOfFashionCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.CommercialRoleCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('commercialRoleCodes')" label="CommercialRoleCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.StoreCapacityLevelCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('storeCapacityLevelCodes')" label="StoreCapacityLevelCode" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select v-model="cdItemDraftForm.CustomsTariffNumberCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('customsTariffNumbers')" label="CustomsTariffNumberCode" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select v-model="cdItemDraftForm.CompanyCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('companyCodes')" label="CompanyCode" />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Vazgec" color="grey-8" v-close-popup />
|
||||
<q-btn color="primary" label="Taslagi Kaydet" @click="saveCdItemDraft" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="attributeDialogOpen" persistent>
|
||||
<q-card style="min-width: 1100px; max-width: 98vw;">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6">Urun Ozellikleri (2. Pop-up)</div>
|
||||
<q-space />
|
||||
<q-badge color="primary">{{ attributeTargetCode || '-' }}</q-badge>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section style="max-height: 68vh; overflow: auto;">
|
||||
<div class="text-caption text-grey-7 q-mb-sm">
|
||||
Ilk etap dummy: isBlocked=0 kabul edilmis satirlar gibi listelenir.
|
||||
</div>
|
||||
<div
|
||||
v-for="(row, idx) in attributeRows"
|
||||
:key="`${row.AttributeTypeCodeNumber}-${idx}`"
|
||||
class="row q-col-gutter-sm q-mb-xs items-center"
|
||||
>
|
||||
<div class="col-12 col-md-5">
|
||||
<q-input :model-value="row.TypeLabel" dense filled readonly />
|
||||
</div>
|
||||
<div class="col-12 col-md-7">
|
||||
<q-select
|
||||
v-model="row.AttributeCode"
|
||||
dense
|
||||
filled
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="row.Options"
|
||||
label="AttributeCode - AttributeDescription"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Vazgec" color="grey-8" v-close-popup />
|
||||
<q-btn color="primary" label="Ozellikleri Kaydet" @click="saveAttributeDraft" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
@@ -219,6 +361,8 @@ import { normalizeSearchText } from 'src/utils/searchText'
|
||||
const route = useRoute()
|
||||
const $q = useQuasar()
|
||||
const store = useOrderProductionItemStore()
|
||||
const BAGGI_CODE_PATTERN = /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/
|
||||
const BAGGI_CODE_ERROR = 'Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999'
|
||||
|
||||
const orderHeaderID = computed(() => String(route.params.orderHeaderID || '').trim())
|
||||
const header = computed(() => store.header || {})
|
||||
@@ -235,6 +379,12 @@ const descFilter = ref('')
|
||||
const productOptions = ref([])
|
||||
const productSearch = ref('')
|
||||
const selectedMap = ref({})
|
||||
const cdItemDialogOpen = ref(false)
|
||||
const cdItemTargetCode = ref('')
|
||||
const cdItemDraftForm = ref(createEmptyCdItemDraft(''))
|
||||
const attributeDialogOpen = ref(false)
|
||||
const attributeTargetCode = ref('')
|
||||
const attributeRows = ref([])
|
||||
|
||||
const columns = [
|
||||
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false, style: 'width:44px;', headerStyle: 'width:44px;' },
|
||||
@@ -300,24 +450,70 @@ const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(k => !!s
|
||||
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
|
||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||
|
||||
function onSelectProduct (row, code) {
|
||||
productSearch.value = ''
|
||||
onNewItemChange(row, code)
|
||||
function applyNewItemVisualState (row, source = 'typed') {
|
||||
const info = store.classifyItemCode(row?.NewItemCode || '')
|
||||
row.NewItemCode = info.normalized
|
||||
row.NewItemMode = info.mode
|
||||
row.NewItemSource = info.mode === 'empty' ? '' : source
|
||||
}
|
||||
|
||||
function onNewItemChange (row, val) {
|
||||
function newItemInputClass (row) {
|
||||
return {
|
||||
'new-item-existing': row?.NewItemMode === 'existing',
|
||||
'new-item-new': row?.NewItemMode === 'new'
|
||||
}
|
||||
}
|
||||
|
||||
function newItemBadgeColor (row) {
|
||||
return row?.NewItemMode === 'existing' ? 'positive' : 'warning'
|
||||
}
|
||||
|
||||
function newItemBadgeLabel (row) {
|
||||
return row?.NewItemMode === 'existing' ? 'MEVCUT KOD' : 'YENI KOD'
|
||||
}
|
||||
|
||||
function newItemHintText (row) {
|
||||
if (row?.NewItemMode === 'existing') {
|
||||
return row?.NewItemSource === 'selected'
|
||||
? 'Urun listesinden secildi'
|
||||
: 'Elle girildi (sistemde bulundu)'
|
||||
}
|
||||
if (row?.NewItemMode === 'new') {
|
||||
return store.getCdItemDraft(row?.NewItemCode) ? 'Yeni kod: cdItem taslagi hazir' : 'Yeni kod: cdItem taslagi gerekli'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function onSelectProduct (row, code) {
|
||||
productSearch.value = ''
|
||||
onNewItemChange(row, code, 'selected')
|
||||
}
|
||||
|
||||
function onNewItemChange (row, val, source = 'typed') {
|
||||
const prevCode = String(row?.NewItemCode || '').trim().toUpperCase()
|
||||
const next = String(val || '').trim().toUpperCase()
|
||||
if (next.length > 13) {
|
||||
$q.notify({ type: 'negative', message: 'Model kodu en fazla 13 karakter olabilir.' })
|
||||
row.NewItemCode = next.slice(0, 13)
|
||||
applyNewItemVisualState(row, source)
|
||||
return
|
||||
}
|
||||
if (next.length === 13 && !isValidBaggiModelCode(next)) {
|
||||
$q.notify({ type: 'negative', message: BAGGI_CODE_ERROR })
|
||||
row.NewItemCode = prevCode
|
||||
applyNewItemVisualState(row, source)
|
||||
return
|
||||
}
|
||||
row.NewItemCode = next ? next.toUpperCase() : ''
|
||||
applyNewItemVisualState(row, source)
|
||||
row.NewColor = ''
|
||||
row.NewDim2 = ''
|
||||
if (row.NewItemCode) {
|
||||
store.fetchColors(row.NewItemCode)
|
||||
}
|
||||
if (row.NewItemMode === 'new' && isValidBaggiModelCode(row.NewItemCode) && row.NewItemCode !== prevCode) {
|
||||
openCdItemDialog(row.NewItemCode)
|
||||
}
|
||||
}
|
||||
|
||||
function onNewColorChange (row) {
|
||||
@@ -341,7 +537,11 @@ function getSecondColorOptions (row) {
|
||||
const code = row?.NewItemCode || ''
|
||||
const color = row?.NewColor || ''
|
||||
const key = `${code}::${color}`
|
||||
return store.secondColorOptionsByKey[key] || []
|
||||
const list = store.secondColorOptionsByKey[key] || []
|
||||
return list.map(c => ({
|
||||
...c,
|
||||
item_dim2_label: `${c.item_dim2_code} - ${c.color_description || ''}`.trim()
|
||||
}))
|
||||
}
|
||||
|
||||
function toggleRowSelection (rowKey, checked) {
|
||||
@@ -360,33 +560,12 @@ function toggleSelectAllVisible (checked) {
|
||||
selectedMap.value = next
|
||||
}
|
||||
|
||||
function onCreateColorValue (row, val, done) {
|
||||
const code = normalizeShortCode(val, 3)
|
||||
if (!code) {
|
||||
done(null)
|
||||
return
|
||||
}
|
||||
row.NewColor = code
|
||||
onNewColorChange(row)
|
||||
done(code, 'add-unique')
|
||||
}
|
||||
|
||||
function onCreateSecondColorValue (row, val, done) {
|
||||
const code = normalizeShortCode(val, 3)
|
||||
if (!code) {
|
||||
done(null)
|
||||
return
|
||||
}
|
||||
row.NewDim2 = code
|
||||
done(code, 'add-unique')
|
||||
}
|
||||
|
||||
function normalizeShortCode (value, maxLen) {
|
||||
return String(value || '').trim().toUpperCase().slice(0, maxLen)
|
||||
}
|
||||
|
||||
function isValidBaggiModelCode (code) {
|
||||
return /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/.test(code)
|
||||
return BAGGI_CODE_PATTERN.test(code)
|
||||
}
|
||||
|
||||
function validateRowInput (row) {
|
||||
@@ -398,7 +577,7 @@ function validateRowInput (row) {
|
||||
|
||||
if (!newItemCode) return 'Yeni model kodu zorunludur.'
|
||||
if (!isValidBaggiModelCode(newItemCode)) {
|
||||
return 'Girdiginiz yapi BAGGI kod yapisina uygun degildir. Format: X999-XXX99999'
|
||||
return BAGGI_CODE_ERROR
|
||||
}
|
||||
if (oldColor && !newColor) return 'Eski kayitta 1. renk oldugu icin yeni 1. renk zorunludur.'
|
||||
if (newColor && newColor.length !== 3) return 'Yeni 1. renk kodu 3 karakter olmalidir.'
|
||||
@@ -437,6 +616,238 @@ function collectLinesFromRows (selectedRows) {
|
||||
return { errMsg: '', lines }
|
||||
}
|
||||
|
||||
function createEmptyCdItemDraft (itemCode) {
|
||||
return {
|
||||
ItemTypeCode: '1',
|
||||
ItemCode: String(itemCode || '').trim().toUpperCase(),
|
||||
ItemDimTypeCode: '',
|
||||
ProductTypeCode: '',
|
||||
ProductHierarchyID: '',
|
||||
UnitOfMeasureCode1: '',
|
||||
ItemAccountGrCode: '',
|
||||
ItemTaxGrCode: '',
|
||||
ItemPaymentPlanGrCode: '',
|
||||
ItemDiscountGrCode: '',
|
||||
ItemVendorGrCode: '',
|
||||
PromotionGroupCode: '',
|
||||
ProductCollectionGrCode: '',
|
||||
StorePriceLevelCode: '',
|
||||
PerceptionOfFashionCode: '',
|
||||
CommercialRoleCode: '',
|
||||
StoreCapacityLevelCode: '',
|
||||
CustomsTariffNumberCode: '',
|
||||
CompanyCode: ''
|
||||
}
|
||||
}
|
||||
|
||||
function lookupOptions (key) {
|
||||
const list = store.cdItemLookups?.[key] || []
|
||||
return list.map(x => {
|
||||
const code = String(x?.code || '').trim()
|
||||
const desc = String(x?.description || '').trim()
|
||||
return {
|
||||
value: code,
|
||||
label: desc ? `${code} - ${desc}` : code
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function openCdItemDialog (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
await store.fetchCdItemLookups()
|
||||
|
||||
cdItemTargetCode.value = code
|
||||
const existing = store.getCdItemDraft(code)
|
||||
const draft = createEmptyCdItemDraft(code)
|
||||
if (existing) {
|
||||
for (const [k, v] of Object.entries(existing)) {
|
||||
if (v == null) continue
|
||||
draft[k] = String(v)
|
||||
}
|
||||
}
|
||||
cdItemDraftForm.value = draft
|
||||
cdItemDialogOpen.value = true
|
||||
}
|
||||
|
||||
function normalizeCdItemDraftForPayload (draftRaw) {
|
||||
const d = draftRaw || {}
|
||||
const toIntOrNil = (v) => {
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) && n > 0 ? n : null
|
||||
}
|
||||
const toStrOrNil = (v) => {
|
||||
const s = String(v || '').trim()
|
||||
return s || null
|
||||
}
|
||||
return {
|
||||
ItemTypeCode: toIntOrNil(d.ItemTypeCode) || 1,
|
||||
ItemCode: String(d.ItemCode || '').trim().toUpperCase(),
|
||||
ItemDimTypeCode: toIntOrNil(d.ItemDimTypeCode),
|
||||
ProductTypeCode: toIntOrNil(d.ProductTypeCode),
|
||||
ProductHierarchyID: toIntOrNil(d.ProductHierarchyID),
|
||||
UnitOfMeasureCode1: toStrOrNil(d.UnitOfMeasureCode1),
|
||||
ItemAccountGrCode: toStrOrNil(d.ItemAccountGrCode),
|
||||
ItemTaxGrCode: toStrOrNil(d.ItemTaxGrCode),
|
||||
ItemPaymentPlanGrCode: toStrOrNil(d.ItemPaymentPlanGrCode),
|
||||
ItemDiscountGrCode: toStrOrNil(d.ItemDiscountGrCode),
|
||||
ItemVendorGrCode: toStrOrNil(d.ItemVendorGrCode),
|
||||
PromotionGroupCode: toStrOrNil(d.PromotionGroupCode),
|
||||
ProductCollectionGrCode: toStrOrNil(d.ProductCollectionGrCode),
|
||||
StorePriceLevelCode: toStrOrNil(d.StorePriceLevelCode),
|
||||
PerceptionOfFashionCode: toStrOrNil(d.PerceptionOfFashionCode),
|
||||
CommercialRoleCode: toStrOrNil(d.CommercialRoleCode),
|
||||
StoreCapacityLevelCode: toStrOrNil(d.StoreCapacityLevelCode),
|
||||
CustomsTariffNumberCode: toStrOrNil(d.CustomsTariffNumberCode),
|
||||
CompanyCode: toStrOrNil(d.CompanyCode)
|
||||
}
|
||||
}
|
||||
|
||||
function saveCdItemDraft () {
|
||||
const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value)
|
||||
if (!payload.ItemCode) {
|
||||
$q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' })
|
||||
return
|
||||
}
|
||||
store.setCdItemDraft(payload.ItemCode, payload)
|
||||
cdItemDialogOpen.value = false
|
||||
}
|
||||
|
||||
function createDummyAttributeRows () {
|
||||
const sharedOptions = [
|
||||
{ value: 'DAMATLIK', label: 'DAMATLIK - DAMATLIK' },
|
||||
{ value: 'TAKIM', label: 'TAKIM - TAKIM ELBISE' },
|
||||
{ value: 'CEKET', label: 'CEKET - CEKET' },
|
||||
{ value: 'PANTOLON', label: 'PANTOLON - PANTOLON' }
|
||||
]
|
||||
const rows = [{
|
||||
AttributeTypeCodeNumber: 1,
|
||||
TypeLabel: '1-001 Urun Ana Grubu',
|
||||
AttributeCode: '',
|
||||
Options: sharedOptions
|
||||
}]
|
||||
for (let i = 2; i <= 50; i++) {
|
||||
const code = String(i).padStart(3, '0')
|
||||
rows.push({
|
||||
AttributeTypeCodeNumber: i,
|
||||
TypeLabel: `1-${code} Dummy Ozellik ${i}`,
|
||||
AttributeCode: '',
|
||||
Options: sharedOptions
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
function buildAttributeRowsFromLookup (list) {
|
||||
const grouped = new Map()
|
||||
for (const it of (list || [])) {
|
||||
const typeCode = Number(it?.attribute_type_code || 0)
|
||||
if (!typeCode) continue
|
||||
if (!grouped.has(typeCode)) {
|
||||
grouped.set(typeCode, {
|
||||
typeCode,
|
||||
typeDesc: String(it?.attribute_type_description || '').trim() || String(typeCode),
|
||||
options: []
|
||||
})
|
||||
}
|
||||
const g = grouped.get(typeCode)
|
||||
const code = String(it?.attribute_code || '').trim()
|
||||
const desc = String(it?.attribute_description || '').trim()
|
||||
g.options.push({
|
||||
value: code,
|
||||
label: `${code} - ${desc || code}`
|
||||
})
|
||||
}
|
||||
|
||||
const rows = [...grouped.values()]
|
||||
.sort((a, b) => a.typeCode - b.typeCode)
|
||||
.map(g => ({
|
||||
AttributeTypeCodeNumber: g.typeCode,
|
||||
TypeLabel: `${g.typeCode}-${g.typeDesc}`,
|
||||
AttributeCode: '',
|
||||
Options: g.options
|
||||
}))
|
||||
return rows
|
||||
}
|
||||
|
||||
async function openAttributeDialog (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
attributeTargetCode.value = code
|
||||
const existing = store.getProductAttributeDraft(code)
|
||||
const fetched = await store.fetchProductAttributes(1)
|
||||
const fromLookup = buildAttributeRowsFromLookup(fetched)
|
||||
const baseRows = fromLookup.length ? fromLookup : createDummyAttributeRows()
|
||||
attributeRows.value = Array.isArray(existing) && existing.length
|
||||
? JSON.parse(JSON.stringify(existing))
|
||||
: baseRows
|
||||
attributeDialogOpen.value = true
|
||||
}
|
||||
|
||||
function saveAttributeDraft () {
|
||||
const code = String(attributeTargetCode.value || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
for (const row of (attributeRows.value || [])) {
|
||||
const selected = String(row?.AttributeCode || '').trim()
|
||||
if (!selected) {
|
||||
$q.notify({ type: 'negative', message: `Urun ozelliklerinde secim zorunlu: ${row?.TypeLabel || ''}` })
|
||||
return
|
||||
}
|
||||
}
|
||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(attributeRows.value || [])))
|
||||
attributeDialogOpen.value = false
|
||||
$q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' })
|
||||
}
|
||||
|
||||
function collectProductAttributesFromSelectedRows (selectedRows) {
|
||||
const codeSet = [...new Set(
|
||||
(selectedRows || [])
|
||||
.map(r => String(r?.NewItemCode || '').trim().toUpperCase())
|
||||
.filter(Boolean)
|
||||
)]
|
||||
const out = []
|
||||
|
||||
for (const code of codeSet) {
|
||||
const rows = store.getProductAttributeDraft(code)
|
||||
if (!Array.isArray(rows) || !rows.length) {
|
||||
return { errMsg: `${code} icin urun ozellikleri secilmedi`, productAttributes: [] }
|
||||
}
|
||||
for (const row of rows) {
|
||||
const attributeTypeCode = Number(row?.AttributeTypeCodeNumber || 0)
|
||||
const attributeCode = String(row?.AttributeCode || '').trim()
|
||||
if (!attributeTypeCode || !attributeCode) {
|
||||
return { errMsg: `${code} icin urun ozellikleri eksik`, productAttributes: [] }
|
||||
}
|
||||
out.push({
|
||||
ItemTypeCode: 1,
|
||||
ItemCode: code,
|
||||
AttributeTypeCode: attributeTypeCode,
|
||||
AttributeCode: attributeCode
|
||||
})
|
||||
}
|
||||
}
|
||||
return { errMsg: '', productAttributes: out }
|
||||
}
|
||||
|
||||
function collectCdItemsFromSelectedRows (selectedRows) {
|
||||
const codes = [...new Set(
|
||||
(selectedRows || [])
|
||||
.filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim())
|
||||
.map(r => String(r.NewItemCode).trim().toUpperCase())
|
||||
)]
|
||||
if (!codes.length) return { errMsg: '', cdItems: [] }
|
||||
|
||||
const out = []
|
||||
for (const code of codes) {
|
||||
const draft = store.getCdItemDraft(code)
|
||||
if (!draft) {
|
||||
return { errMsg: `${code} icin cdItem bilgisi eksik`, cdItems: [] }
|
||||
}
|
||||
out.push(normalizeCdItemDraftForPayload(draft))
|
||||
}
|
||||
return { errMsg: '', cdItems: out }
|
||||
}
|
||||
|
||||
function buildMailLineLabelFromRow (row) {
|
||||
const item = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase()
|
||||
const color1 = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase()
|
||||
@@ -516,14 +927,23 @@ function formatSizes (sizeMap) {
|
||||
function groupItems (items, prevRows = []) {
|
||||
const prevMap = new Map()
|
||||
for (const r of prevRows || []) {
|
||||
if (r?.RowKey) prevMap.set(r.RowKey, String(r.NewDesc || '').trim())
|
||||
if (!r?.RowKey) continue
|
||||
prevMap.set(r.RowKey, {
|
||||
NewDesc: String(r.NewDesc || '').trim(),
|
||||
NewItemCode: String(r.NewItemCode || '').trim().toUpperCase(),
|
||||
NewColor: String(r.NewColor || '').trim().toUpperCase(),
|
||||
NewDim2: String(r.NewDim2 || '').trim().toUpperCase(),
|
||||
NewItemMode: String(r.NewItemMode || '').trim(),
|
||||
NewItemSource: String(r.NewItemSource || '').trim()
|
||||
})
|
||||
}
|
||||
const map = new Map()
|
||||
|
||||
for (const it of items) {
|
||||
const key = buildGroupKey(it)
|
||||
if (!map.has(key)) {
|
||||
const prevDesc = prevMap.get(key) || ''
|
||||
const prev = prevMap.get(key) || {}
|
||||
const prevDesc = prev.NewDesc || ''
|
||||
const fallbackDesc = String((it?.NewDesc || it?.OldDesc) || '').trim()
|
||||
map.set(key, {
|
||||
RowKey: key,
|
||||
@@ -536,10 +956,12 @@ function groupItems (items, prevRows = []) {
|
||||
OrderLineIDs: [],
|
||||
OldSizes: [],
|
||||
OldSizesLabel: '',
|
||||
NewItemCode: '',
|
||||
NewColor: '',
|
||||
NewDim2: '',
|
||||
NewItemCode: prev.NewItemCode || '',
|
||||
NewColor: prev.NewColor || '',
|
||||
NewDim2: prev.NewDim2 || '',
|
||||
NewDesc: prevDesc || fallbackDesc,
|
||||
NewItemMode: prev.NewItemMode || 'empty',
|
||||
NewItemSource: prev.NewItemSource || '',
|
||||
IsVariantMissing: !!it.IsVariantMissing
|
||||
})
|
||||
}
|
||||
@@ -560,6 +982,10 @@ function groupItems (items, prevRows = []) {
|
||||
const sizes = formatSizes(g.__sizeMap || {})
|
||||
g.OldSizes = sizes.list
|
||||
g.OldSizesLabel = sizes.label
|
||||
const info = store.classifyItemCode(g.NewItemCode)
|
||||
g.NewItemCode = info.normalized
|
||||
g.NewItemMode = info.mode
|
||||
if (info.mode === 'empty') g.NewItemSource = ''
|
||||
delete g.__sizeMap
|
||||
out.push(g)
|
||||
}
|
||||
@@ -589,6 +1015,20 @@ async function onBulkSubmit () {
|
||||
$q.notify({ type: 'negative', message: 'Secili satirlarda guncellenecek kayit bulunamadi.' })
|
||||
return
|
||||
}
|
||||
const { errMsg: cdErrMsg, cdItems } = collectCdItemsFromSelectedRows(selectedRows)
|
||||
if (cdErrMsg) {
|
||||
$q.notify({ type: 'negative', message: cdErrMsg })
|
||||
const firstCode = String(cdErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openCdItemDialog(firstCode)
|
||||
return
|
||||
}
|
||||
const { errMsg: attrErrMsg, productAttributes } = collectProductAttributesFromSelectedRows(selectedRows)
|
||||
if (attrErrMsg) {
|
||||
$q.notify({ type: 'negative', message: attrErrMsg })
|
||||
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openAttributeDialog(firstCode)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const validate = await store.validateUpdates(orderHeaderID.value, lines)
|
||||
@@ -604,7 +1044,7 @@ async function onBulkSubmit () {
|
||||
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
||||
cancel: { label: 'Vazgec', flat: true }
|
||||
}).onOk(async () => {
|
||||
await store.applyUpdates(orderHeaderID.value, lines, true)
|
||||
await store.applyUpdates(orderHeaderID.value, lines, true, cdItems, productAttributes)
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
selectedMap.value = {}
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
@@ -612,7 +1052,7 @@ async function onBulkSubmit () {
|
||||
return
|
||||
}
|
||||
|
||||
await store.applyUpdates(orderHeaderID.value, lines, false)
|
||||
await store.applyUpdates(orderHeaderID.value, lines, false, cdItems, productAttributes)
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
selectedMap.value = {}
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
@@ -738,6 +1178,16 @@ async function onBulkSubmit () {
|
||||
background: #e3f3ff;
|
||||
}
|
||||
|
||||
.prod-table :deep(.new-item-existing .q-field__control) {
|
||||
background: #eaf9ef !important;
|
||||
border-left: 3px solid #21ba45;
|
||||
}
|
||||
|
||||
.prod-table :deep(.new-item-new .q-field__control) {
|
||||
background: #fff5e9 !important;
|
||||
border-left: 3px solid #f2a100;
|
||||
}
|
||||
|
||||
.prod-table :deep(td.col-desc),
|
||||
.prod-table :deep(th.col-desc),
|
||||
.prod-table :deep(td.col-wrap),
|
||||
|
||||
@@ -2,40 +2,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
function normalizeTextForMatch (v) {
|
||||
return String(v || '')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
}
|
||||
|
||||
// Production ekranlari icin beden grup tespiti helper'i.
|
||||
// Ozel kural:
|
||||
// YETISKIN/GARSON = GARSON ve URUN ANA GRUBU "GOMLEK ATA YAKA" veya "GOMLEK KLASIK" ise => yas
|
||||
export function detectProductionBedenGroup (bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '') {
|
||||
const list = Array.isArray(bedenList) ? bedenList : []
|
||||
const hasLetterSizes = list
|
||||
.map(v => String(v || '').trim().toUpperCase())
|
||||
.some(v => ['XS', 'S', 'M', 'L', 'XL', '2XL', '3XL', '4XL', '5XL', '6XL', '7XL'].includes(v))
|
||||
|
||||
const ana = normalizeTextForMatch(urunAnaGrubu)
|
||||
const kat = normalizeTextForMatch(urunKategori)
|
||||
const yg = normalizeTextForMatch(yetiskinGarson)
|
||||
|
||||
if ((kat.includes('GARSON') || yg.includes('GARSON')) &&
|
||||
(ana.includes('GOMLEK ATAYAKA') || ana.includes('GOMLEK ATA YAKA') || ana.includes('GOMLEK KLASIK'))) {
|
||||
return 'yas'
|
||||
}
|
||||
|
||||
if (hasLetterSizes) return 'gom'
|
||||
if ((ana.includes('AYAKKABI') || kat.includes('AYAKKABI')) && (kat.includes('GARSON') || yg.includes('GARSON'))) return 'ayk_garson'
|
||||
if (kat.includes('GARSON') || yg.includes('GARSON') || ana.includes('GARSON')) return 'yas'
|
||||
if (ana.includes('PANTOLON') && kat.includes('YETISKIN')) return 'pan'
|
||||
if (ana.includes('AKSESUAR')) return 'aksbir'
|
||||
return 'tak'
|
||||
}
|
||||
|
||||
function extractApiErrorMessage (err, fallback) {
|
||||
const data = err?.response?.data
|
||||
if (typeof data === 'string' && data.trim()) return data
|
||||
@@ -70,12 +36,40 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
products: [],
|
||||
colorOptionsByCode: {},
|
||||
secondColorOptionsByKey: {},
|
||||
productAttributesByItemType: {},
|
||||
cdItemLookups: null,
|
||||
cdItemDraftsByCode: {},
|
||||
productAttributeDraftsByCode: {},
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
productCodeSet (state) {
|
||||
const set = new Set()
|
||||
for (const p of (state.products || [])) {
|
||||
const code = String(p?.ProductCode || '').trim().toUpperCase()
|
||||
if (code) set.add(code)
|
||||
}
|
||||
return set
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
classifyItemCode (value) {
|
||||
const normalized = String(value || '').trim().toUpperCase()
|
||||
if (!normalized) {
|
||||
return { normalized: '', mode: 'empty', exists: false }
|
||||
}
|
||||
const exists = this.productCodeSet.has(normalized)
|
||||
return {
|
||||
normalized,
|
||||
mode: exists ? 'existing' : 'new',
|
||||
exists
|
||||
}
|
||||
},
|
||||
|
||||
async fetchHeader (orderHeaderID) {
|
||||
if (!orderHeaderID) {
|
||||
this.header = null
|
||||
@@ -166,6 +160,62 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
return []
|
||||
}
|
||||
},
|
||||
async fetchProductAttributes (itemTypeCode = 1) {
|
||||
const key = String(itemTypeCode || 1)
|
||||
if (this.productAttributesByItemType[key]) {
|
||||
return this.productAttributesByItemType[key]
|
||||
}
|
||||
try {
|
||||
const res = await api.get('/product-attributes', { params: { itemTypeCode } })
|
||||
const list = Array.isArray(res?.data) ? res.data : []
|
||||
this.productAttributesByItemType[key] = list
|
||||
return list
|
||||
} catch (err) {
|
||||
this.error = err?.response?.data || err?.message || 'Urun ozellikleri alinamadi'
|
||||
return []
|
||||
}
|
||||
},
|
||||
async fetchCdItemLookups (force = false) {
|
||||
if (this.cdItemLookups && !force) return this.cdItemLookups
|
||||
try {
|
||||
const res = await api.get('/orders/production-items/cditem-lookups')
|
||||
this.cdItemLookups = res?.data || null
|
||||
return this.cdItemLookups
|
||||
} catch (err) {
|
||||
this.error = err?.response?.data || err?.message || 'cdItem lookup listesi alinamadi'
|
||||
return null
|
||||
}
|
||||
},
|
||||
setCdItemDraft (itemCode, draft) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
this.cdItemDraftsByCode = {
|
||||
...this.cdItemDraftsByCode,
|
||||
[code]: {
|
||||
...(draft || {}),
|
||||
ItemCode: code,
|
||||
ItemTypeCode: Number(draft?.ItemTypeCode || 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
getCdItemDraft (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return null
|
||||
return this.cdItemDraftsByCode[code] || null
|
||||
},
|
||||
setProductAttributeDraft (itemCode, rows) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
this.productAttributeDraftsByCode = {
|
||||
...this.productAttributeDraftsByCode,
|
||||
[code]: Array.isArray(rows) ? rows : []
|
||||
}
|
||||
},
|
||||
getProductAttributeDraft (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return []
|
||||
return this.productAttributeDraftsByCode[code] || []
|
||||
},
|
||||
async validateUpdates (orderHeaderID, lines) {
|
||||
if (!orderHeaderID) return { missingCount: 0, missing: [] }
|
||||
|
||||
@@ -186,7 +236,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
async applyUpdates (orderHeaderID, lines, insertMissing) {
|
||||
async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = []) {
|
||||
if (!orderHeaderID) return { updated: 0, inserted: 0 }
|
||||
|
||||
this.saving = true
|
||||
@@ -195,7 +245,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
try {
|
||||
const res = await api.post(
|
||||
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
|
||||
{ lines, insertMissing }
|
||||
{ lines, insertMissing, cdItems, productAttributes }
|
||||
)
|
||||
return res?.data || { updated: 0, inserted: 0 }
|
||||
} catch (err) {
|
||||
|
||||
@@ -42,20 +42,21 @@ export function buildComboKey(row, beden) {
|
||||
|
||||
|
||||
|
||||
export const BEDEN_SCHEMA = [
|
||||
{ key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
|
||||
{ key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] },
|
||||
{ key: 'ayk_garson', title: 'AYAKKABI GARSON', values: ['22','23','24','25','26','27','28','29','30','31','32','33','34','35','STD'] },
|
||||
{ key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] },
|
||||
{ key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] },
|
||||
{ key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
|
||||
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] }
|
||||
]
|
||||
const SIZE_GROUP_TITLES = {
|
||||
tak: 'TAKIM ELBISE',
|
||||
ayk: 'AYAKKABI',
|
||||
ayk_garson: 'AYAKKABI GARSON',
|
||||
yas: 'YAS',
|
||||
pan: 'PANTOLON',
|
||||
gom: 'GOMLEK',
|
||||
aksbir: 'AKSESUAR'
|
||||
}
|
||||
|
||||
export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
|
||||
m[g.key] = g
|
||||
return m
|
||||
}, {})
|
||||
const FALLBACK_SCHEMA_MAP = {
|
||||
tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] }
|
||||
}
|
||||
|
||||
export const schemaByKey = { ...FALLBACK_SCHEMA_MAP }
|
||||
|
||||
const productSizeMatchCache = {
|
||||
loaded: false,
|
||||
@@ -111,6 +112,23 @@ function setProductSizeMatchCache(payload) {
|
||||
productSizeMatchCache.schemas = normalizedSchemas
|
||||
}
|
||||
|
||||
function buildSchemaMapFromCacheSchemas() {
|
||||
const out = {}
|
||||
const src = productSizeMatchCache.schemas || {}
|
||||
for (const [keyRaw, valuesRaw] of Object.entries(src)) {
|
||||
const key = String(keyRaw || '').trim()
|
||||
if (!key) continue
|
||||
const values = Array.isArray(valuesRaw) ? valuesRaw : []
|
||||
out[key] = {
|
||||
key,
|
||||
title: SIZE_GROUP_TITLES[key] || key.toUpperCase(),
|
||||
values: values.map(v => String(v == null ? '' : v))
|
||||
}
|
||||
}
|
||||
if (!out.tak) out.tak = { ...FALLBACK_SCHEMA_MAP.tak }
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
export const stockMap = ref({})
|
||||
export const bedenStock = ref([])
|
||||
@@ -257,25 +275,17 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
,
|
||||
/* ===========================================================
|
||||
🧩 initSchemaMap — BEDEN ŞEMA İNİT
|
||||
- TEK SOURCE OF TRUTH: BEDEN_SCHEMA
|
||||
- TEK SOURCE OF TRUTH: SQL mk_size_group (cache)
|
||||
=========================================================== */
|
||||
initSchemaMap() {
|
||||
if (this.schemaMap && Object.keys(this.schemaMap).length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = {}
|
||||
|
||||
for (const g of BEDEN_SCHEMA) {
|
||||
map[g.key] = {
|
||||
key: g.key,
|
||||
title: g.title,
|
||||
values: [...g.values]
|
||||
}
|
||||
this.schemaMap = buildSchemaMapFromCacheSchemas()
|
||||
if (!Object.keys(this.schemaMap).length) {
|
||||
this.schemaMap = { ...FALLBACK_SCHEMA_MAP }
|
||||
}
|
||||
|
||||
this.schemaMap = map
|
||||
|
||||
console.log(
|
||||
'🧩 schemaMap INIT edildi:',
|
||||
Object.keys(this.schemaMap)
|
||||
@@ -284,17 +294,20 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
|
||||
async ensureProductSizeMatchRules($q = null, force = false) {
|
||||
if (!force && productSizeMatchCache.loaded && productSizeMatchCache.rules.length > 0) {
|
||||
this.schemaMap = buildSchemaMapFromCacheSchemas()
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.get('/product-size-match/rules')
|
||||
setProductSizeMatchCache(res?.data || {})
|
||||
this.schemaMap = buildSchemaMapFromCacheSchemas()
|
||||
return true
|
||||
} catch (err) {
|
||||
if (force) {
|
||||
resetProductSizeMatchCache()
|
||||
}
|
||||
this.schemaMap = { ...FALLBACK_SCHEMA_MAP }
|
||||
console.warn('⚠ product-size-match rules alınamadı:', err)
|
||||
$q?.notify?.({
|
||||
type: 'warning',
|
||||
@@ -4044,6 +4057,8 @@ export function toSummaryRowFromForm(form) {
|
||||
|
||||
urunAnaGrubu: form.urunAnaGrubu || '',
|
||||
urunAltGrubu: form.urunAltGrubu || '',
|
||||
kategori: form.kategori || '',
|
||||
yetiskinGarson: form.yetiskinGarson || form.askiliyan || '',
|
||||
aciklama: form.aciklama || '',
|
||||
|
||||
fiyat: Number(form.fiyat || 0),
|
||||
|
||||
Reference in New Issue
Block a user