Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-31 12:45:22 +03:00
parent 44439f7908
commit d7d871fb8a
19 changed files with 1608 additions and 158 deletions

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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