Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-04 13:20:58 +03:00
parent 4dc0415546
commit 96d782e474
17 changed files with 1946 additions and 565 deletions

View File

@@ -561,6 +561,18 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
wrapV3(http.HandlerFunc(routes.GetProductStockQueryHandler)), wrapV3(http.HandlerFunc(routes.GetProductStockQueryHandler)),
) )
bindV3(r, pgDB,
"/api/product-stock-attribute-options", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockAttributeOptionsHandler)),
)
bindV3(r, pgDB,
"/api/product-stock-query-by-attributes", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockQueryByAttributesHandler)),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/product-images", "GET", "/api/product-images", "GET",
"order", "view", "order", "view",

View File

@@ -2,6 +2,7 @@ package queries
import ( import (
"database/sql" "database/sql"
"strconv"
"bssapp-backend/models" "bssapp-backend/models"
) )
@@ -156,7 +157,16 @@ FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
} }
var inserted int64 var inserted int64
ensuredItems := make(map[string]struct{}, len(missing))
for i, v := range missing { 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 {
return inserted, err
}
ensuredItems[itemKey] = struct{}{}
}
plu := basePlu + int64(i) + 1 plu := basePlu + int64(i) + 1
res, err := tx.Exec(` res, err := tx.Exec(`
IF NOT EXISTS ( IF NOT EXISTS (
@@ -197,6 +207,93 @@ VALUES (
return inserted, nil return inserted, nil
} }
func ensureCdItemTx(tx *sql.Tx, itemTypeCode int16, itemCode string, username string) error {
_, err := tx.Exec(`
IF NOT EXISTS (
SELECT 1
FROM dbo.cdItem
WHERE ItemTypeCode = @p1
AND ItemCode = @p2
)
BEGIN
;WITH Template AS (
SELECT TOP 1
ItemDimTypeCode, ProductTypeCode, ProductHierarchyID,
UnitOfMeasureCode1, UnitOfMeasureCode2, UnitConvertRate, UnitConvertRateNotFixed,
UseInternet, UsePOS, UseStore, EnablePartnerCompanies, UseManufacturing, UseSerialNumber,
GenerateOpticalDataMatrixCode, ByWeight, SupplyPeriod, GuaranteePeriod, ShelfLife, OrderLeadTime,
ItemAccountGrCode, ItemTaxGrCode, ItemPaymentPlanGrCode, ItemDiscountGrCode, ItemVendorGrCode,
PromotionGroupCode, PromotionGroupCode2, ProductCollectionGrCode, StorePriceLevelCode, PerceptionOfFashionCode,
CommercialRoleCode, StoreCapacityLevelCode, CustomsTariffNumberCode, IsFixedExpense, BOMEntityCode, CompanyCode,
IsBlocked, IsLocked, LockedDate, IsSalesOrderClosed, IsPurchaseOrderClosed, UseRoll, UseBatch,
MaxCreditCardInstallmentCount, GenerateSerialNumber, IsSubsequentDeliveryForR, IsSubsequentDeliveryForRI,
IGACommissionGroup, UniFreeCommissionGroup, CustomsProductGroupCode, IsUTSDeclaratedItem, IsStoreOrderClosed
FROM dbo.cdItem WITH (UPDLOCK, HOLDLOCK)
WHERE ItemTypeCode = @p1
AND ItemCode LIKE 'U%'
ORDER BY CreatedDate DESC
)
INSERT INTO dbo.cdItem (
ItemTypeCode, ItemCode,
ItemDimTypeCode, ProductTypeCode, ProductHierarchyID,
UnitOfMeasureCode1, UnitOfMeasureCode2, UnitConvertRate, UnitConvertRateNotFixed,
UseInternet, UsePOS, UseStore, EnablePartnerCompanies, UseManufacturing, UseSerialNumber,
GenerateOpticalDataMatrixCode, ByWeight, SupplyPeriod, GuaranteePeriod, ShelfLife, OrderLeadTime,
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,
IGACommissionGroup, UniFreeCommissionGroup, CustomsProductGroupCode, IsUTSDeclaratedItem, IsStoreOrderClosed
)
SELECT
@p1, @p2,
t.ItemDimTypeCode, t.ProductTypeCode, t.ProductHierarchyID,
t.UnitOfMeasureCode1, t.UnitOfMeasureCode2, t.UnitConvertRate, t.UnitConvertRateNotFixed,
t.UseInternet, t.UsePOS, t.UseStore, t.EnablePartnerCompanies, t.UseManufacturing, t.UseSerialNumber,
t.GenerateOpticalDataMatrixCode, t.ByWeight, t.SupplyPeriod, t.GuaranteePeriod, t.ShelfLife, t.OrderLeadTime,
t.ItemAccountGrCode, t.ItemTaxGrCode, t.ItemPaymentPlanGrCode, t.ItemDiscountGrCode, t.ItemVendorGrCode,
t.PromotionGroupCode, t.PromotionGroupCode2, t.ProductCollectionGrCode, t.StorePriceLevelCode, t.PerceptionOfFashionCode,
t.CommercialRoleCode, t.StoreCapacityLevelCode, t.CustomsTariffNumberCode, t.IsFixedExpense, t.BOMEntityCode, t.CompanyCode,
t.IsBlocked, t.IsLocked, t.LockedDate, t.IsSalesOrderClosed, t.IsPurchaseOrderClosed,
@p3, GETDATE(), @p3, GETDATE(), NEWID(),
t.UseRoll, t.UseBatch, t.MaxCreditCardInstallmentCount, t.GenerateSerialNumber,
t.IsSubsequentDeliveryForR, t.IsSubsequentDeliveryForRI,
t.IGACommissionGroup, t.UniFreeCommissionGroup, t.CustomsProductGroupCode, t.IsUTSDeclaratedItem, t.IsStoreOrderClosed
FROM Template t;
IF @@ROWCOUNT = 0
BEGIN
INSERT INTO dbo.cdItem (
ItemTypeCode, ItemCode,
ItemDimTypeCode, ProductTypeCode, ProductHierarchyID,
UnitOfMeasureCode1, UnitConvertRate, UnitConvertRateNotFixed,
UseInternet, UsePOS, UseStore, EnablePartnerCompanies, UseManufacturing, UseSerialNumber,
GenerateOpticalDataMatrixCode, ByWeight, SupplyPeriod, GuaranteePeriod, ShelfLife, OrderLeadTime,
IsFixedExpense, IsBlocked, IsLocked, LockedDate, IsSalesOrderClosed, IsPurchaseOrderClosed,
CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate, RowGuid,
UseRoll, UseBatch, MaxCreditCardInstallmentCount, GenerateSerialNumber,
IsSubsequentDeliveryForR, IsSubsequentDeliveryForRI, IsUTSDeclaratedItem, IsStoreOrderClosed
)
VALUES (
@p1, @p2,
2, 1, 2,
'AD', 0, 0,
0, 1, 1, 0, 1, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, '1900-01-01', 0, 0,
@p3, GETDATE(), @p3, GETDATE(), NEWID(),
0, 0, 12, 0,
0, 0, 0, 0
);
END
END
`, itemTypeCode, itemCode, username)
return err
}
func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderProductionUpdateLine, username string) (int64, error) { func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderProductionUpdateLine, username string) (int64, error) {
var updated int64 var updated int64
for _, line := range lines { for _, line := range lines {

View File

@@ -1,11 +1,11 @@
package queries package queries
// GetProductStockQuery: // GetProductStockQuery:
// Ürün kodu bazlı, optimize stok + attribute sorgusu. // Urun kodu bazli, STOCK/PICK/RESERVE/DISP ayrik CTE ile optimize sorgu.
const GetProductStockQuery = ` const GetProductStockQuery = `
DECLARE @ProductCode NVARCHAR(50) = @p1; DECLARE @ProductCode NVARCHAR(50) = @p1;
;WITH INV AS ;WITH STOCK AS
( (
SELECT SELECT
CompanyCode, CompanyCode,
@@ -19,180 +19,216 @@ DECLARE @ProductCode NVARCHAR(50) = @p1;
ItemDim1Code, ItemDim1Code,
ItemDim2Code, ItemDim2Code,
ItemDim3Code, ItemDim3Code,
SUM(PickingQty1) AS PickingQty1, SUM(In_Qty1 - Out_Qty1) AS InventoryQty1
SUM(ReserveQty1) AS ReserveQty1, FROM trStock WITH(NOLOCK)
SUM(DispOrderQty1) AS DispOrderQty1,
SUM(InventoryQty1) AS InventoryQty1
FROM
(
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13
UNION ALL
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
0, Qty1, 0, 0
FROM ReserveStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13
UNION ALL
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
0, 0, Qty1, 0
FROM DispOrderStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13
UNION ALL
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
0, 0, 0, SUM(In_Qty1 - Out_Qty1)
FROM trStock WITH (NOLOCK)
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
) X
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
),
Attr AS
(
SELECT TOP 1
ProductCode,
ProductDescription,
ProductAtt01Desc,
ProductAtt02Desc,
ProductAtt10Desc,
ProductAtt11Desc,
ProductAtt21Desc,
ProductAtt22Desc,
ProductAtt23Desc,
ProductAtt24Desc,
ProductAtt25Desc,
ProductAtt26Desc,
ProductAtt27Desc,
ProductAtt28Desc,
ProductAtt29Desc,
ProductAtt30Desc,
ProductAtt31Desc,
ProductAtt32Desc,
ProductAtt33Desc,
ProductAtt34Desc,
ProductAtt35Desc,
ProductAtt36Desc,
ProductAtt37Desc,
ProductAtt38Desc,
ProductAtt39Desc,
ProductAtt40Desc,
ProductAtt41Desc,
ProductAtt42Desc,
ProductAtt43Desc,
ProductAtt44Desc,
ProductAtt45Desc,
ProductAtt46Desc
FROM ProductFilterWithDescription('TR')
WHERE ProductCode = @ProductCode
AND LEN(ProductCode) = 13
AND LEN(@ProductCode) = 13
),
Price AS
(
SELECT TOP 1
Price
FROM prItemBasePrice WITH (NOLOCK)
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
ORDER BY PriceDate DESC GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
),
PICK AS
(
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS PickingQty1
FROM PickingStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
),
RESERVE AS
(
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS ReserveQty1
FROM ReserveStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
),
DISP AS
(
SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS DispOrderQty1
FROM DispOrderStates
WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode
AND LEN(ItemCode) = 13
AND LEN(@ProductCode) = 13
GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code
) )
SELECT SELECT
I.WarehouseCode AS Depo_Kodu, S.WarehouseCode AS Depo_Kodu,
W.WarehouseDescription AS Depo_Adi, W.WarehouseDescription AS Depo_Adi,
IT.ItemTypeDescription AS InventoryType,
I.ItemCode AS Urun_Kodu, bsItemTypeDesc.ItemTypeDescription AS InventoryType,
A.ProductDescription AS Madde_Aciklamasi,
I.ColorCode AS Renk_Kodu, S.ItemCode AS Urun_Kodu,
P.ProductDescription AS Madde_Aciklamasi,
S.ColorCode AS Renk_Kodu,
C.ColorDescription AS Renk_Aciklamasi, C.ColorDescription AS Renk_Aciklamasi,
I.ItemDim1Code AS Beden,
I.ItemDim2Code AS Yaka, S.ItemDim1Code AS Beden,
ROUND(I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1, U.RoundDigit) AS Kullanilabilir_Envanter, S.ItemDim2Code AS Yaka,
A.ProductAtt01Desc AS URUN_ANA_GRUBU,
A.ProductAtt02Desc AS URUN_ALT_GRUBU, ROUND(
A.ProductAtt10Desc AS MARKA, S.InventoryQty1
A.ProductAtt11Desc AS DR, - ISNULL(PK.PickingQty1,0)
A.ProductAtt21Desc AS KALIP, - ISNULL(RS.ReserveQty1,0)
A.ProductAtt22Desc AS IKINCI_PARCA_KALIP, - ISNULL(DP.DispOrderQty1,0),
A.ProductAtt23Desc AS PACA_GENISLIGI, cdUnitOfMeasure.RoundDigit
A.ProductAtt24Desc AS UCUNCU_PARCA_KALIP, ) AS Kullanilabilir_Envanter,
A.ProductAtt25Desc AS UCUNCU_PARCA_MODEL,
A.ProductAtt26Desc AS BIRINCI_PARCA_KUMAS, P.ProductAtt01Desc AS URUN_ANA_GRUBU,
A.ProductAtt27Desc AS IKINCI_PARCA_KUMAS, P.ProductAtt02Desc AS URUN_ALT_GRUBU,
A.ProductAtt28Desc AS UCUNCU_PARCA_KUMAS, P.ProductAtt10Desc AS MARKA,
A.ProductAtt29Desc AS BIRINCI_PARCA_KARISIM, P.ProductAtt11Desc AS DR,
A.ProductAtt30Desc AS IKINCI_PARCA_KARISIM, P.ProductAtt21Desc AS KALIP,
A.ProductAtt31Desc AS UCUNCU_PARCA_KARISIM, P.ProductAtt22Desc AS IKINCI_PARCA_KALIP,
A.ProductAtt32Desc AS YAKA_TIPI, P.ProductAtt23Desc AS PACA_GENISLIGI,
A.ProductAtt33Desc AS DUGME, P.ProductAtt24Desc AS UCUNCU_PARCA_KALIP,
A.ProductAtt34Desc AS YIRTMAC, P.ProductAtt25Desc AS UCUNCU_PARCA_MODEL,
A.ProductAtt35Desc AS SEZON_YILI, P.ProductAtt26Desc AS BIRINCI_PARCA_KUMAS,
A.ProductAtt36Desc AS MEVSIM, P.ProductAtt27Desc AS IKINCI_PARCA_KUMAS,
A.ProductAtt37Desc AS TABAN, P.ProductAtt28Desc AS UCUNCU_PARCA_KUMAS,
A.ProductAtt38Desc AS BIRINCI_PARCA_FIT, P.ProductAtt29Desc AS BIRINCI_PARCA_KARISIM,
A.ProductAtt39Desc AS IKINCI_PARCA_FIT, P.ProductAtt30Desc AS IKINCI_PARCA_KARISIM,
A.ProductAtt40Desc AS BOS2, P.ProductAtt31Desc AS UCUNCU_PARCA_KARISIM,
A.ProductAtt41Desc AS KISA_KAR, P.ProductAtt32Desc AS YAKA_TIPI,
A.ProductAtt42Desc AS SERI_FASON, P.ProductAtt33Desc AS DUGME,
A.ProductAtt43Desc AS STOK_GIRIS_YONTEMI, P.ProductAtt34Desc AS YIRTMAC,
A.ProductAtt44Desc AS YETISKIN_GARSON, P.ProductAtt35Desc AS SEZON_YILI,
A.ProductAtt45Desc AS ASKILI_YAN, P.ProductAtt36Desc AS MEVSIM,
A.ProductAtt46Desc AS BOS3, P.ProductAtt37Desc AS TABAN,
P.Price AS Fiyat P.ProductAtt38Desc AS BIRINCI_PARCA_FIT,
FROM INV I P.ProductAtt39Desc AS IKINCI_PARCA_FIT,
JOIN cdItem CI WITH (NOLOCK) P.ProductAtt40Desc AS BOS2,
ON CI.ItemTypeCode = I.ItemTypeCode P.ProductAtt41Desc AS KISA_KAR,
AND CI.ItemCode = I.ItemCode P.ProductAtt42Desc AS SERI_FASON,
LEFT JOIN cdUnitOfMeasure U WITH (NOLOCK) P.ProductAtt43Desc AS STOK_GIRIS_YONTEMI,
ON U.UnitOfMeasureCode = CI.UnitOfMeasureCode1 P.ProductAtt44Desc AS YETISKIN_GARSON,
LEFT JOIN cdWarehouseDesc W WITH (NOLOCK) P.ProductAtt45Desc AS ASKILI_YAN,
ON W.WarehouseCode = I.WarehouseCode P.ProductAtt46Desc AS BOS3,
AND W.LangCode = 'TR'
LEFT JOIN bsItemTypeDesc IT WITH (NOLOCK) prFilteredBasePrice.Price AS Fiyat
ON IT.ItemTypeCode = I.ItemTypeCode
AND IT.LangCode = 'TR' FROM STOCK S
LEFT JOIN cdColorDesc C WITH (NOLOCK)
ON C.ColorCode = I.ColorCode LEFT JOIN PICK PK
AND C.LangCode = 'TR' ON PK.CompanyCode=S.CompanyCode
CROSS JOIN Attr A AND PK.OfficeCode=S.OfficeCode
OUTER APPLY (SELECT TOP 1 Price FROM Price) P AND PK.StoreTypeCode=S.StoreTypeCode
AND PK.StoreCode=S.StoreCode
AND PK.WarehouseCode=S.WarehouseCode
AND PK.ItemTypeCode=S.ItemTypeCode
AND PK.ItemCode=S.ItemCode
AND PK.ColorCode=S.ColorCode
AND PK.ItemDim1Code=S.ItemDim1Code
AND PK.ItemDim2Code=S.ItemDim2Code
AND PK.ItemDim3Code=S.ItemDim3Code
LEFT JOIN RESERVE RS
ON RS.CompanyCode=S.CompanyCode
AND RS.OfficeCode=S.OfficeCode
AND RS.StoreTypeCode=S.StoreTypeCode
AND RS.StoreCode=S.StoreCode
AND RS.WarehouseCode=S.WarehouseCode
AND RS.ItemTypeCode=S.ItemTypeCode
AND RS.ItemCode=S.ItemCode
AND RS.ColorCode=S.ColorCode
AND RS.ItemDim1Code=S.ItemDim1Code
AND RS.ItemDim2Code=S.ItemDim2Code
AND RS.ItemDim3Code=S.ItemDim3Code
LEFT JOIN DISP DP
ON DP.CompanyCode=S.CompanyCode
AND DP.OfficeCode=S.OfficeCode
AND DP.StoreTypeCode=S.StoreTypeCode
AND DP.StoreCode=S.StoreCode
AND DP.WarehouseCode=S.WarehouseCode
AND DP.ItemTypeCode=S.ItemTypeCode
AND DP.ItemCode=S.ItemCode
AND DP.ColorCode=S.ColorCode
AND DP.ItemDim1Code=S.ItemDim1Code
AND DP.ItemDim2Code=S.ItemDim2Code
AND DP.ItemDim3Code=S.ItemDim3Code
JOIN cdItem WITH(NOLOCK)
ON S.ItemCode = cdItem.ItemCode
AND S.ItemTypeCode = cdItem.ItemTypeCode
LEFT JOIN cdUnitOfMeasure WITH(NOLOCK)
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
LEFT JOIN ProductFilterWithDescription('TR') P
ON P.ProductCode = S.ItemCode
LEFT JOIN bsItemTypeDesc WITH(NOLOCK)
ON bsItemTypeDesc.ItemTypeCode = S.ItemTypeCode
AND bsItemTypeDesc.LangCode='TR'
LEFT JOIN cdWarehouseDesc W WITH(NOLOCK)
ON W.WarehouseCode = S.WarehouseCode
AND W.LangCode='TR'
LEFT JOIN cdColorDesc C WITH(NOLOCK)
ON C.ColorCode = S.ColorCode
AND C.LangCode='TR'
LEFT JOIN (
SELECT
ItemCode,
ItemTypeCode,
Price,
ROW_NUMBER() OVER (PARTITION BY ItemCode, ItemTypeCode ORDER BY PriceDate DESC) AS RowNum
FROM prItemBasePrice WITH(NOLOCK)
) prFilteredBasePrice
ON prFilteredBasePrice.ItemCode = S.ItemCode
AND prFilteredBasePrice.ItemTypeCode = S.ItemTypeCode
AND prFilteredBasePrice.RowNum = 1
WHERE WHERE
I.ItemTypeCode = 1 S.ItemTypeCode IN (1)
AND I.ItemCode = @ProductCode AND S.ItemCode = @ProductCode
AND LEN(I.ItemCode) = 13 AND LEN(S.ItemCode) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
AND (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0 AND (
AND CI.IsBlocked = 0 S.InventoryQty1
AND I.WarehouseCode IN - ISNULL(PK.PickingQty1,0)
- ISNULL(RS.ReserveQty1,0)
- ISNULL(DP.DispOrderQty1,0)
) > 0
AND cdItem.IsBlocked = 0
AND S.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',

View File

@@ -0,0 +1,239 @@
package queries
// GetProductStockAttributeOptionsQuery:
// Urun ozellik filtre secenekleri (distinct aciklamalar).
const GetProductStockAttributeOptionsQuery = `
WITH PF AS
(
SELECT
LTRIM(RTRIM(ProductAtt01Desc)) AS ProductAtt01Desc,
LTRIM(RTRIM(ProductAtt02Desc)) AS ProductAtt02Desc,
LTRIM(RTRIM(ProductAtt10Desc)) AS ProductAtt10Desc,
LTRIM(RTRIM(ProductAtt11Desc)) AS ProductAtt11Desc,
LTRIM(RTRIM(ProductAtt21Desc)) AS ProductAtt21Desc,
LTRIM(RTRIM(ProductAtt35Desc)) AS ProductAtt35Desc,
LTRIM(RTRIM(ProductAtt36Desc)) AS ProductAtt36Desc,
LTRIM(RTRIM(ProductAtt44Desc)) AS ProductAtt44Desc
FROM ProductFilterWithDescription('TR')
WHERE LEN(ProductCode) = 13
)
SELECT 'att01' AS FieldName, ProductAtt01Desc AS FieldValue FROM PF WHERE ProductAtt01Desc <> '' GROUP BY ProductAtt01Desc
UNION ALL
SELECT 'att02', ProductAtt02Desc FROM PF WHERE ProductAtt02Desc <> '' GROUP BY ProductAtt02Desc
UNION ALL
SELECT 'att10', ProductAtt10Desc FROM PF WHERE ProductAtt10Desc <> '' GROUP BY ProductAtt10Desc
UNION ALL
SELECT 'att11', ProductAtt11Desc FROM PF WHERE ProductAtt11Desc <> '' GROUP BY ProductAtt11Desc
UNION ALL
SELECT 'att21', ProductAtt21Desc FROM PF WHERE ProductAtt21Desc <> '' GROUP BY ProductAtt21Desc
UNION ALL
SELECT 'att35', ProductAtt35Desc FROM PF WHERE ProductAtt35Desc <> '' GROUP BY ProductAtt35Desc
UNION ALL
SELECT 'att36', ProductAtt36Desc FROM PF WHERE ProductAtt36Desc <> '' GROUP BY ProductAtt36Desc
UNION ALL
SELECT 'att44', ProductAtt44Desc FROM PF WHERE ProductAtt44Desc <> '' GROUP BY ProductAtt44Desc;
`
// GetProductStockQueryByAttributes:
// Urun ozelliklerine gore stok detay sorgusu.
const GetProductStockQueryByAttributes = `
DECLARE @Att01 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p1)), '');
DECLARE @Att02 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p2)), '');
DECLARE @Att10 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p3)), '');
DECLARE @Att11 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p4)), '');
DECLARE @Att21 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p5)), '');
DECLARE @Att35 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p6)), '');
DECLARE @Att36 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p7)), '');
DECLARE @Att44 NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p8)), '');
;WITH AttrFiltered AS
(
SELECT
ProductCode,
ProductDescription,
ProductAtt01Desc,
ProductAtt02Desc,
ProductAtt10Desc,
ProductAtt11Desc,
ProductAtt21Desc,
ProductAtt22Desc,
ProductAtt23Desc,
ProductAtt24Desc,
ProductAtt25Desc,
ProductAtt26Desc,
ProductAtt27Desc,
ProductAtt28Desc,
ProductAtt29Desc,
ProductAtt30Desc,
ProductAtt31Desc,
ProductAtt32Desc,
ProductAtt33Desc,
ProductAtt34Desc,
ProductAtt35Desc,
ProductAtt36Desc,
ProductAtt37Desc,
ProductAtt38Desc,
ProductAtt39Desc,
ProductAtt40Desc,
ProductAtt41Desc,
ProductAtt42Desc,
ProductAtt43Desc,
ProductAtt44Desc,
ProductAtt45Desc,
ProductAtt46Desc
FROM ProductFilterWithDescription('TR')
WHERE LEN(ProductCode) = 13
AND (@Att01 IS NULL OR ProductAtt01Desc = @Att01)
AND (@Att02 IS NULL OR ProductAtt02Desc = @Att02)
AND (@Att10 IS NULL OR ProductAtt10Desc = @Att10)
AND (@Att11 IS NULL OR ProductAtt11Desc = @Att11)
AND (@Att21 IS NULL OR ProductAtt21Desc = @Att21)
AND (@Att35 IS NULL OR ProductAtt35Desc = @Att35)
AND (@Att36 IS NULL OR ProductAtt36Desc = @Att36)
AND (@Att44 IS NULL OR ProductAtt44Desc = @Att44)
),
INV AS
(
SELECT
X.CompanyCode,
X.OfficeCode,
X.StoreTypeCode,
X.StoreCode,
X.WarehouseCode,
X.ItemTypeCode,
X.ItemCode,
X.ColorCode,
X.ItemDim1Code,
X.ItemDim2Code,
X.ItemDim3Code,
SUM(X.PickingQty1) AS PickingQty1,
SUM(X.ReserveQty1) AS ReserveQty1,
SUM(X.DispOrderQty1) AS DispOrderQty1,
SUM(X.InventoryQty1) AS InventoryQty1
FROM
(
SELECT
P.CompanyCode, P.OfficeCode, P.StoreTypeCode, P.StoreCode, P.WarehouseCode,
P.ItemTypeCode, P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code,
P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates P
INNER JOIN AttrFiltered AF ON AF.ProductCode = P.ItemCode
WHERE P.ItemTypeCode = 1
AND LEN(P.ItemCode) = 13
UNION ALL
SELECT
R.CompanyCode, R.OfficeCode, R.StoreTypeCode, R.StoreCode, R.WarehouseCode,
R.ItemTypeCode, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code,
0, R.Qty1, 0, 0
FROM ReserveStates R
INNER JOIN AttrFiltered AF ON AF.ProductCode = R.ItemCode
WHERE R.ItemTypeCode = 1
AND LEN(R.ItemCode) = 13
UNION ALL
SELECT
D.CompanyCode, D.OfficeCode, D.StoreTypeCode, D.StoreCode, D.WarehouseCode,
D.ItemTypeCode, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code,
0, 0, D.Qty1, 0
FROM DispOrderStates D
INNER JOIN AttrFiltered AF ON AF.ProductCode = D.ItemCode
WHERE D.ItemTypeCode = 1
AND LEN(D.ItemCode) = 13
UNION ALL
SELECT
T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode,
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code,
0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1)
FROM trStock T WITH (NOLOCK)
INNER JOIN AttrFiltered AF ON AF.ProductCode = T.ItemCode
WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13
GROUP BY
T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode,
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code
) X
GROUP BY
X.CompanyCode, X.OfficeCode, X.StoreTypeCode, X.StoreCode, X.WarehouseCode,
X.ItemTypeCode, X.ItemCode, X.ColorCode, X.ItemDim1Code, X.ItemDim2Code, X.ItemDim3Code
)
SELECT
I.WarehouseCode AS Depo_Kodu,
W.WarehouseDescription AS Depo_Adi,
IT.ItemTypeDescription AS InventoryType,
I.ItemCode AS Urun_Kodu,
AF.ProductDescription AS Madde_Aciklamasi,
I.ColorCode AS Renk_Kodu,
C.ColorDescription AS Renk_Aciklamasi,
I.ItemDim1Code AS Beden,
I.ItemDim2Code AS Yaka,
ROUND(I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1, U.RoundDigit) AS Kullanilabilir_Envanter,
AF.ProductAtt01Desc AS URUN_ANA_GRUBU,
AF.ProductAtt02Desc AS URUN_ALT_GRUBU,
AF.ProductAtt10Desc AS MARKA,
AF.ProductAtt11Desc AS DR,
AF.ProductAtt21Desc AS KALIP,
AF.ProductAtt22Desc AS IKINCI_PARCA_KALIP,
AF.ProductAtt23Desc AS PACA_GENISLIGI,
AF.ProductAtt24Desc AS UCUNCU_PARCA_KALIP,
AF.ProductAtt25Desc AS UCUNCU_PARCA_MODEL,
AF.ProductAtt26Desc AS BIRINCI_PARCA_KUMAS,
AF.ProductAtt27Desc AS IKINCI_PARCA_KUMAS,
AF.ProductAtt28Desc AS UCUNCU_PARCA_KUMAS,
AF.ProductAtt29Desc AS BIRINCI_PARCA_KARISIM,
AF.ProductAtt30Desc AS IKINCI_PARCA_KARISIM,
AF.ProductAtt31Desc AS UCUNCU_PARCA_KARISIM,
AF.ProductAtt32Desc AS YAKA_TIPI,
AF.ProductAtt33Desc AS DUGME,
AF.ProductAtt34Desc AS YIRTMAC,
AF.ProductAtt35Desc AS SEZON_YILI,
AF.ProductAtt36Desc AS MEVSIM,
AF.ProductAtt37Desc AS TABAN,
AF.ProductAtt38Desc AS BIRINCI_PARCA_FIT,
AF.ProductAtt39Desc AS IKINCI_PARCA_FIT,
AF.ProductAtt40Desc AS BOS2,
AF.ProductAtt41Desc AS KISA_KAR,
AF.ProductAtt42Desc AS SERI_FASON,
AF.ProductAtt43Desc AS STOK_GIRIS_YONTEMI,
AF.ProductAtt44Desc AS YETISKIN_GARSON,
AF.ProductAtt45Desc AS ASKILI_YAN,
AF.ProductAtt46Desc AS BOS3,
P.Price AS Fiyat
FROM INV I
INNER JOIN AttrFiltered AF
ON AF.ProductCode = I.ItemCode
JOIN cdItem CI WITH (NOLOCK)
ON CI.ItemTypeCode = I.ItemTypeCode
AND CI.ItemCode = I.ItemCode
LEFT JOIN cdUnitOfMeasure U WITH (NOLOCK)
ON U.UnitOfMeasureCode = CI.UnitOfMeasureCode1
LEFT JOIN cdWarehouseDesc W WITH (NOLOCK)
ON W.WarehouseCode = I.WarehouseCode
AND W.LangCode = 'TR'
LEFT JOIN bsItemTypeDesc IT WITH (NOLOCK)
ON IT.ItemTypeCode = I.ItemTypeCode
AND IT.LangCode = 'TR'
LEFT JOIN cdColorDesc C WITH (NOLOCK)
ON C.ColorCode = I.ColorCode
AND C.LangCode = 'TR'
OUTER APPLY (
SELECT TOP 1 Price
FROM prItemBasePrice PB WITH (NOLOCK)
WHERE PB.ItemTypeCode = 1
AND PB.ItemCode = I.ItemCode
AND LEN(PB.ItemCode) = 13
ORDER BY PB.PriceDate DESC
) P
WHERE
I.ItemTypeCode = 1
AND LEN(I.ItemCode) = 13
AND (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0
AND CI.IsBlocked = 0
AND I.WarehouseCode IN
(
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36'
);
`

View File

@@ -12,6 +12,7 @@ import (
"strings" "strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
mssql "github.com/microsoft/go-mssqldb"
) )
// ====================================================== // ======================================================
@@ -136,8 +137,7 @@ func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
missing, err := buildMissingVariants(mssql, id, payload.Lines) missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil { if err != nil {
log.Printf("validate error: %v", err) writeDBError(w, http.StatusInternalServerError, "validate_missing_variants", id, "", len(payload.Lines), err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return return
} }
@@ -176,8 +176,7 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
missing, err := buildMissingVariants(mssql, id, payload.Lines) missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil { if err != nil {
log.Printf("apply validate error: %v", err) writeDBError(w, http.StatusInternalServerError, "apply_validate_missing_variants", id, "", len(payload.Lines), err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return return
} }
@@ -202,7 +201,7 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
tx, err := mssql.Begin() tx, err := mssql.Begin()
if err != nil { if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) writeDBError(w, http.StatusInternalServerError, "begin_tx", id, username, len(payload.Lines), err)
return return
} }
defer tx.Rollback() defer tx.Rollback()
@@ -211,22 +210,19 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
if payload.InsertMissing { if payload.InsertMissing {
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username) inserted, err = queries.InsertMissingVariantsTx(tx, missing, username)
if err != nil { if err != nil {
log.Printf("insert missing error: %v", err) writeDBError(w, http.StatusInternalServerError, "insert_missing_variants", id, username, len(missing), err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return return
} }
} }
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username) updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
if err != nil { if err != nil {
log.Printf("update order lines error: %v", err) writeDBError(w, http.StatusInternalServerError, "update_order_lines", id, username, len(payload.Lines), err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return return
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
log.Printf("commit error: %v", err) writeDBError(w, http.StatusInternalServerError, "commit_tx", id, username, len(payload.Lines), err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return return
} }
@@ -289,3 +285,26 @@ func validateUpdateLines(lines []models.OrderProductionUpdateLine) error {
} }
return nil return nil
} }
func writeDBError(w http.ResponseWriter, status int, step string, orderHeaderID string, username string, lineCount int, err error) {
var sqlErr mssql.Error
if errors.As(err, &sqlErr) {
log.Printf(
"❌ SQL error step=%s orderHeaderID=%s user=%s lineCount=%d number=%d state=%d class=%d server=%s proc=%s line=%d message=%s",
step, orderHeaderID, username, lineCount,
sqlErr.Number, sqlErr.State, sqlErr.Class, sqlErr.ServerName, sqlErr.ProcName, sqlErr.LineNo, sqlErr.Message,
)
} else {
log.Printf(
"❌ DB error step=%s orderHeaderID=%s user=%s lineCount=%d err=%v",
step, orderHeaderID, username, lineCount, err,
)
}
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Veritabani hatasi",
"step": step,
"detail": err.Error(),
})
}

View File

@@ -0,0 +1,128 @@
package routes
import (
"bssapp-backend/db"
"bssapp-backend/queries"
"context"
"encoding/json"
"log"
"net/http"
"strings"
"time"
)
// GetProductStockAttributeOptionsHandler
// GET /api/product-stock-attribute-options
func GetProductStockAttributeOptionsHandler(w http.ResponseWriter, _ *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
rows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductStockAttributeOptionsQuery)
if err != nil {
log.Printf("[PRODUCT-STOCK-ATTR-OPTIONS] SQL hatasi: %v", err)
http.Error(w, "SQL hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
result := map[string][]string{
"att01": {},
"att02": {},
"att10": {},
"att11": {},
"att21": {},
"att35": {},
"att36": {},
"att44": {},
}
for rows.Next() {
var fieldName, fieldValue string
if err := rows.Scan(&fieldName, &fieldValue); err != nil {
continue
}
fieldName = strings.TrimSpace(fieldName)
fieldValue = strings.TrimSpace(fieldValue)
if fieldName == "" || fieldValue == "" {
continue
}
result[fieldName] = append(result[fieldName], fieldValue)
}
if err := rows.Err(); err != nil {
http.Error(w, "Satir okuma hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(result)
}
// GetProductStockQueryByAttributesHandler
// GET /api/product-stock-query-by-attributes?att01=...&att02=...
func GetProductStockQueryByAttributesHandler(w http.ResponseWriter, r *http.Request) {
att01 := strings.TrimSpace(r.URL.Query().Get("att01"))
att02 := strings.TrimSpace(r.URL.Query().Get("att02"))
att10 := strings.TrimSpace(r.URL.Query().Get("att10"))
att11 := strings.TrimSpace(r.URL.Query().Get("att11"))
att21 := strings.TrimSpace(r.URL.Query().Get("att21"))
att35 := strings.TrimSpace(r.URL.Query().Get("att35"))
att36 := strings.TrimSpace(r.URL.Query().Get("att36"))
att44 := strings.TrimSpace(r.URL.Query().Get("att44"))
hasAny := att01 != "" || att02 != "" || att10 != "" || att11 != "" || att21 != "" || att35 != "" || att36 != "" || att44 != ""
if !hasAny {
http.Error(w, "En az bir urun ozelligi secilmelidir", http.StatusBadRequest)
return
}
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
log.Printf(
"[PRODUCT-STOCK-BY-ATTRS] request att01=%q att02=%q att10=%q att11=%q att21=%q att35=%q att36=%q att44=%q",
att01, att02, att10, att11, att21, att35, att36, att44,
)
rows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductStockQueryByAttributes, att01, att02, att10, att11, att21, att35, att36, att44)
if err != nil {
log.Printf("[PRODUCT-STOCK-BY-ATTRS] SQL hatasi: %v", err)
http.Error(w, "SQL hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
http.Error(w, "Kolon bilgisi alinamadi", http.StatusInternalServerError)
return
}
result := make([]map[string]interface{}, 0, 256)
for rows.Next() {
raw := make([]interface{}, len(columns))
dest := make([]interface{}, len(columns))
for i := range raw {
dest[i] = &raw[i]
}
if err := rows.Scan(dest...); err != nil {
continue
}
rowMap := make(map[string]interface{}, len(columns))
for i, c := range columns {
rowMap[c] = normalizeSQLValue(raw[i])
}
result = append(result, rowMap)
}
if err := rows.Err(); err != nil {
log.Printf("[PRODUCT-STOCK-BY-ATTRS] rows err elapsed=%s err=%v", time.Since(start), err)
http.Error(w, "Satir okuma hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
log.Printf("[PRODUCT-STOCK-BY-ATTRS] success rows=%d elapsed=%s", len(result), time.Since(start))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(result)
}

View File

@@ -1,75 +0,0 @@
/* 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

@@ -1,154 +0,0 @@
/* 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

@@ -1,116 +0,0 @@
/* 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

@@ -1,23 +0,0 @@
/* 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

@@ -270,6 +270,11 @@ const menuItems = [
label: 'Ürün Kodundan Stok Sorgula', label: 'Ürün Kodundan Stok Sorgula',
to: '/app/product-stock-query', to: '/app/product-stock-query',
permission: 'order:view' permission: 'order:view'
},
{
label: 'Ürün Özelliklerinden Stok Bul',
to: '/app/product-stock-by-attributes',
permission: 'order:view'
} }
] ]
}, },

View File

@@ -556,7 +556,14 @@ async function onBulkSubmit () {
await store.fetchItems(orderHeaderID.value) await store.fetchItems(orderHeaderID.value)
selectedMap.value = {} selectedMap.value = {}
} catch (err) { } catch (err) {
$q.notify({ type: 'negative', message: 'Toplu kayit islemi basarisiz.' }) console.error('[OrderProductionUpdate] onBulkSubmit failed', {
orderHeaderID: orderHeaderID.value,
selectedRowCount: selectedRows.length,
lineCount: lines.length,
apiError: err?.response?.data,
message: err?.message
})
$q.notify({ type: 'negative', message: store.error || 'Toplu kayit islemi basarisiz.' })
} }
} }
</script> </script>

View File

@@ -0,0 +1,1127 @@
<template>
<q-page
v-if="canReadOrder"
class="order-page q-pa-md"
:style="{ '--grid-header-h': gridHeaderHeight }"
>
<div class="sticky-stack">
<div class="filter-bar row q-col-gutter-md q-mb-sm">
<div
v-for="def in attrDefs"
:key="def.key"
class="col-12 col-md-3"
>
<q-select
v-model="filters[def.key]"
:options="filteredAttrOptions[def.key] || []"
:label="def.label"
filled
dense
clearable
use-input
input-debounce="250"
:loading="loadingAttributeOptions"
@filter="(val, update) => filterAttributeOptions(def.key, val, update)"
@keyup.enter="fetchStockByAttributes"
/>
</div>
<div class="col-auto">
<q-btn
color="primary"
icon="search"
label="Sorgula"
:loading="loadingStock"
:disable="!hasAnyFilter"
@click="fetchStockByAttributes"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="grey-8"
icon="restart_alt"
label="Sıfırla"
@click="resetForm"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="primary"
icon="unfold_more"
:label="allDetailsExpanded ? 'Tüm Depoları Kapat' : 'Tüm Depoları Göster'"
:disable="!level1Groups.length"
@click="toggleAllDetails"
/>
</div>
</div>
<div class="save-toolbar">
<div class="text-subtitle2 text-weight-bold">Ürün Özelliklerinden Stok Bul</div>
</div>
<div
v-if="showGridHeader"
class="order-grid-header"
>
<div class="col-fixed model">MODEL</div>
<div class="col-fixed renk">RENK</div>
<div class="col-fixed ana">ÜRÜN ANA GRUBU</div>
<div class="col-fixed alt">ÜRÜN ALT GRUBU</div>
<div class="col-fixed aciklama-col">AÇIKLAMA</div>
<div class="beden-block">
<div class="grp-row">
<div class="grp-title">{{ activeSchema?.title || 'BEDEN' }}</div>
<div class="grp-body">
<div
v-for="v in sizeLabels"
:key="'hdr-' + activeGrpKey + '-' + v"
class="grp-cell hdr"
>
{{ v }}
</div>
</div>
</div>
</div>
<div class="total-row">
<div class="total-cell">ADET</div>
</div>
</div>
</div>
<q-banner
v-if="errorMessage"
class="bg-red-1 text-negative q-my-sm rounded-borders"
dense
>
{{ errorMessage }}
</q-banner>
<q-banner
v-else-if="!loadingStock && !level1Groups.length"
class="bg-blue-1 text-primary q-my-sm rounded-borders"
dense
>
Ürün özelliği seçip sorgulayın.
</q-banner>
<div class="order-scroll-y">
<div v-if="level1Groups.length" class="order-grid-body">
<template v-for="grp1 in level1Groups" :key="grp1.key">
<div class="summary-group open">
<div class="order-sub-header level-1" @click="toggleOpen(grp1.key)">
<div class="sub-col level1-merged">
<div class="text-weight-bold">{{ grp1.productCode }}</div>
<div class="text-caption">{{ grp1.productDesc || '-' }}</div>
</div>
<div class="sub-center level1-center">
<div class="beden-row values-top">
<div v-for="sz in sizeLabels" :key="`v1-${grp1.key}-${sz}`" class="beden-cell">
{{ Number(grp1.sizeTotals[sz] || 0) > 0 ? formatNumber(grp1.sizeTotals[sz]) : '' }}
</div>
</div>
<div class="beden-row headers">
<div v-for="sz in sizeLabels" :key="`h1-${grp1.key}-${sz}`" class="beden-cell">
{{ sz }}
</div>
</div>
</div>
<div class="sub-right level1-right">
<div class="top-total">{{ formatNumber(grp1.totalQty) }}</div>
<div class="bottom-row">
<div class="bottom-label">ADET</div>
</div>
<div class="icon-row">
<q-icon :name="isOpen(grp1.key) ? 'expand_less' : 'expand_more'" size="18px" />
</div>
</div>
</div>
<template v-if="isOpen(grp1.key)">
<template v-for="grp2 in grp1.children" :key="grp2.key">
<div class="order-sub-header level-2" @click="onLevel2Click(grp1.productCode, grp2)">
<div class="sub-col model">{{ grp1.productCode || '-' }}</div>
<div class="sub-col renk">
<div class="renk-kodu">{{ grp2.colorCode || '-' }}{{ grp2.secondColor ? '-' + grp2.secondColor : '' }}</div>
<div class="renk-aciklama">{{ grp2.colorDesc || '-' }}</div>
</div>
<div class="sub-col ana">{{ grp2.urunAnaGrubu || '-' }}</div>
<div class="sub-col alt">{{ grp2.urunAltGrubu || '-' }}</div>
<div class="sub-col aciklama">{{ grp2.aciklama || '-' }}</div>
<div class="sub-center level2-center">
<div class="beden-row values-top">
<div v-for="sz in sizeLabels" :key="`top2-${grp2.key}-${sz}`" class="beden-cell">
{{ Number(grp2.sizeTotals[sz] || 0) > 0 ? formatNumber(grp2.sizeTotals[sz]) : '' }}
</div>
</div>
<div class="beden-row headers">
<div v-for="sz in sizeLabels" :key="`h2-${grp2.key}-${sz}`" class="beden-cell">
{{ sz }}
</div>
</div>
</div>
<div class="sub-right level2-right">
<div class="top-total">{{ formatNumber(grp2.totalQty) }}</div>
<div class="bottom-row">
<div class="bottom-label">ADET</div>
<q-icon :name="isOpen(grp2.key) ? 'expand_less' : 'expand_more'" size="18px" />
</div>
</div>
</div>
<template v-if="isOpen(grp2.key)">
<div class="detail-table-wrap">
<div
v-for="row in buildLevel2Rows(grp2)"
:key="row.rowKey"
class="summary-row"
>
<div class="cell depo-merged">{{ row.depoAdi || '-' }}</div>
<div class="grp-area">
<div class="grp-row">
<div
v-for="v in sizeLabels"
:key="row.rowKey + '-sz-' + v"
class="cell beden"
>
{{ resolveBedenValue(row.bedenMap, row.grpKey, v) }}
</div>
</div>
</div>
<div class="cell adet">{{ formatNumber(row.adet) }}</div>
</div>
</div>
</template>
</template>
</template>
</div>
</template>
</div>
</div>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">Bu modüle erişim yetkiniz yok.</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useQuasar } from 'quasar'
import api from 'src/services/api'
import { usePermission } from 'src/composables/usePermission'
import {
detectBedenGroup,
normalizeBedenLabel,
schemaByKey as storeSchemaByKey,
useOrderEntryStore
} from 'src/stores/orderentryStore'
const $q = useQuasar()
const { canRead } = usePermission()
const canReadOrder = canRead('order')
const orderStore = useOrderEntryStore()
const attrDefs = [
{ key: 'att01', label: 'Urun Ana Grubu' },
{ key: 'att02', label: 'Urun Alt Grubu' },
{ key: 'att10', label: 'Marka' },
{ key: 'att11', label: 'DR' },
{ key: 'att21', label: 'Kalip' },
{ key: 'att35', label: 'Sezon Yili' },
{ key: 'att36', label: 'Mevsim' },
{ key: 'att44', label: 'Yetiskin/Garson' }
]
const loadingAttributeOptions = ref(false)
const loadingStock = ref(false)
const errorMessage = ref('')
const filters = ref({
att01: '',
att02: '',
att10: '',
att11: '',
att21: '',
att35: '',
att36: '',
att44: ''
})
const attributeOptions = ref({})
const filteredAttrOptions = ref({})
const rawRows = ref([])
const activeSchema = ref(storeSchemaByKey.tak)
const activeGrpKey = ref('tak')
const openState = ref({})
const hasAnyFilter = computed(() =>
attrDefs.some((def) => String(filters.value?.[def.key] || '').trim() !== '')
)
const sizeLabels = computed(() => activeSchema.value?.values || [])
const showGridHeader = computed(() =>
!loadingStock.value && level1Groups.value.length > 0
)
const allDetailsExpanded = computed(() => {
const groups = level1Groups.value || []
if (!groups.length) return false
const detailKeys = []
for (const g1 of groups) {
for (const g2 of g1.children || []) {
detailKeys.push(g2.key)
for (const g3 of g2.children || []) detailKeys.push(g3.key)
}
}
if (!detailKeys.length) return false
return detailKeys.every((k) => openState.value[k] === true)
})
const gridHeaderHeight = computed(() =>
showGridHeader.value ? '56px' : '0px'
)
function emptySizeTotals() {
const map = {}
for (const s of sizeLabels.value) map[s] = 0
return map
}
function parseNumber(value) {
if (typeof value === 'number') return Number.isFinite(value) ? value : 0
const text = String(value ?? '').trim()
if (!text) return 0
const normalized = text.replace(/\./g, '').replace(',', '.')
const n = Number.parseFloat(normalized)
return Number.isFinite(n) ? n : 0
}
function formatNumber(v) {
return parseNumber(v).toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
}
function normalizeSize(v) {
return normalizeBedenLabel(v)
}
function resolveBedenValue(bedenMap, grpKey, bedenLabel) {
const map = bedenMap?.[grpKey]
if (!map || typeof map !== 'object') return 0
return Number(map[bedenLabel] || 0)
}
function isOpen(key) {
return openState.value[key] !== false
}
function toggleOpen(key) {
openState.value[key] = !isOpen(key)
}
function initOpenState(groups, expandDetails = false) {
const next = {}
for (const g1 of groups) {
next[g1.key] = true
for (const g2 of g1.children) {
next[g2.key] = expandDetails
for (const g3 of g2.children) {
next[g3.key] = expandDetails
}
}
}
openState.value = next
}
function expandAllDetails() {
initOpenState(level1Groups.value || [], true)
}
function collapseAllDetails() {
initOpenState(level1Groups.value || [], false)
}
function toggleAllDetails() {
if (allDetailsExpanded.value) {
collapseAllDetails()
return
}
expandAllDetails()
}
function buildLevel3Rows(grp3) {
const byKey = new Map()
const gk = activeGrpKey.value || 'tak'
for (const item of grp3.items || []) {
const model = String(item.Urun_Kodu || '').trim()
const renk = String(item.Renk_Kodu || '').trim()
const renk2 = String(item.Yaka || '').trim()
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim()
const aciklama = String(item.Madde_Aciklamasi || '').trim()
const beden = normalizeSize(item.Beden || '')
const qty = parseNumber(item.Kullanilabilir_Envanter)
const rowKey = [model, renk, renk2, grp3.depoKodu || '', grp3.depoAdi || ''].join('|')
if (!byKey.has(rowKey)) {
byKey.set(rowKey, {
rowKey,
model,
renk,
renk2,
urunAnaGrubu,
urunAltGrubu,
aciklama,
grpKey: gk,
bedenMap: { [gk]: {} },
adet: 0,
depoAdi: grp3.depoAdi || '-'
})
}
const row = byKey.get(rowKey)
row.bedenMap[gk][beden] = Number(row.bedenMap[gk][beden] || 0) + qty
row.adet += qty
}
return Array.from(byKey.values())
}
function buildLevel2Rows(grp2) {
const merged = []
for (const grp3 of grp2.children || []) {
merged.push(...buildLevel3Rows(grp3))
}
return merged
}
const level1Groups = computed(() => {
const l1Map = new Map()
for (const item of rawRows.value) {
const productCode = String(item.Urun_Kodu || '').trim()
const productDesc = String(item.Madde_Aciklamasi || '').trim()
const colorCode = String(item.Renk_Kodu || '').trim()
const colorDesc = String(item.Renk_Aciklamasi || '').trim()
const secondColor = String(item.Yaka || '').trim()
const depoKodu = String(item.Depo_Kodu || '').trim()
const depoAdi = String(item.Depo_Adi || '').trim()
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim()
const aciklama = String(item.Madde_Aciklamasi || '').trim()
const beden = normalizeSize(item.Beden || '')
const qty = parseNumber(item.Kullanilabilir_Envanter)
if (!l1Map.has(productCode)) {
l1Map.set(productCode, {
key: `L1|${productCode}`,
productCode,
productDesc,
sizeTotals: emptySizeTotals(),
totalQty: 0,
childrenMap: new Map()
})
}
const l1 = l1Map.get(productCode)
if (Object.prototype.hasOwnProperty.call(l1.sizeTotals, beden)) {
l1.sizeTotals[beden] += qty
}
l1.totalQty += qty
const l2Key = `${colorCode}|${secondColor}`
if (!l1.childrenMap.has(l2Key)) {
l1.childrenMap.set(l2Key, {
key: `L2|${productCode}|${l2Key}`,
colorCode,
colorDesc,
secondColor,
urunAnaGrubu,
urunAltGrubu,
aciklama,
sizeTotals: emptySizeTotals(),
totalQty: 0,
childrenMap: new Map()
})
}
const l2 = l1.childrenMap.get(l2Key)
if (Object.prototype.hasOwnProperty.call(l2.sizeTotals, beden)) {
l2.sizeTotals[beden] += qty
}
l2.totalQty += qty
const l3Key = `${depoKodu}|${depoAdi}`
if (!l2.childrenMap.has(l3Key)) {
l2.childrenMap.set(l3Key, {
key: `L3|${productCode}|${l2Key}|${l3Key}`,
depoKodu,
depoAdi,
sizeTotals: emptySizeTotals(),
totalQty: 0,
items: []
})
}
const l3 = l2.childrenMap.get(l3Key)
if (Object.prototype.hasOwnProperty.call(l3.sizeTotals, beden)) {
l3.sizeTotals[beden] += qty
}
l3.totalQty += qty
l3.items.push({
...item,
_rowKey: `${productCode}|${colorCode}|${secondColor}|${depoKodu}|${beden}`
})
}
return Array.from(l1Map.values()).map((l1) => ({
...l1,
children: Array.from(l1.childrenMap.values()).map((l2) => ({
...l2,
children: Array.from(l2.childrenMap.values())
}))
}))
})
function filterAttributeOptions(field, val, update) {
const source = Array.isArray(attributeOptions.value?.[field])
? attributeOptions.value[field]
: []
if (!val) {
update(() => {
filteredAttrOptions.value[field] = [...source]
})
return
}
const needle = String(val || '').toLocaleLowerCase('tr-TR')
update(() => {
filteredAttrOptions.value[field] = source.filter((opt) =>
String(opt || '').toLocaleLowerCase('tr-TR').includes(needle)
)
})
}
async function loadAttributeOptions() {
loadingAttributeOptions.value = true
try {
const res = await api.get('/product-stock-attribute-options')
const payload = res?.data && typeof res.data === 'object' ? res.data : {}
const next = {}
const nextFiltered = {}
for (const def of attrDefs) {
const arr = Array.isArray(payload?.[def.key]) ? payload[def.key] : []
const list = arr
.map((x) => String(x || '').trim())
.filter((x) => x.length > 0)
.sort((a, b) => a.localeCompare(b, 'tr'))
next[def.key] = list
nextFiltered[def.key] = [...list]
}
attributeOptions.value = next
filteredAttrOptions.value = nextFiltered
} catch (err) {
errorMessage.value = 'Urun ozellik secenekleri alinamadi.'
console.error('loadAttributeOptions error:', err)
} finally {
loadingAttributeOptions.value = false
}
}
async function fetchStockByAttributes() {
if (!hasAnyFilter.value) return
const params = {}
for (const def of attrDefs) {
const val = String(filters.value?.[def.key] || '').trim()
if (val) params[def.key] = val
}
loadingStock.value = true
errorMessage.value = ''
try {
if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) {
orderStore.initSchemaMap()
}
const res = await api.get('/product-stock-query-by-attributes', { params })
const list = Array.isArray(res?.data) ? res.data : []
if (!list.length) {
rawRows.value = []
openState.value = {}
return
}
const first = list[0] || {}
const grpKey = detectBedenGroup(
list.map((x) => x?.Beden || ''),
first?.URUN_ANA_GRUBU || '',
first?.YETISKIN_GARSON || ''
)
const schemaMap = Object.keys(orderStore.schemaMap || {}).length
? orderStore.schemaMap
: storeSchemaByKey
activeGrpKey.value = grpKey || 'tak'
activeSchema.value = schemaMap?.[grpKey] || storeSchemaByKey.tak
rawRows.value = list
initOpenState(level1Groups.value)
} catch (err) {
console.error('fetchStockByAttributes error:', err)
rawRows.value = []
openState.value = {}
errorMessage.value = 'Stok sorgulama sirasinda hata olustu.'
$q.notify({
type: 'negative',
position: 'top-right',
message: 'Stok sorgusu basarisiz.'
})
} finally {
loadingStock.value = false
}
}
function onLevel2Click(_productCode, grp2) {
toggleOpen(grp2.key)
}
function resetForm() {
filters.value = {
att01: '',
att02: '',
att10: '',
att11: '',
att21: '',
att35: '',
att36: '',
att44: ''
}
rawRows.value = []
errorMessage.value = ''
openState.value = {}
activeSchema.value = storeSchemaByKey.tak
}
onMounted(() => {
loadAttributeOptions()
})
</script>
<style scoped>
.order-page {
--psq-sticky-offset: 12px;
--grp-title-w: 44px;
--psq-header-h: 56px;
--psq-col-adet: calc(var(--col-adet) + var(--beden-w));
--psq-l1-lift: 42px;
}
.order-grid-header {
top: calc(var(--header-h) + var(--filter-h) + var(--save-h) + var(--psq-sticky-offset)) !important;
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count)))
var(--psq-col-adet) !important;
}
.order-grid-header .col-fixed,
.order-grid-header .total-cell {
writing-mode: horizontal-tb !important;
transform: none !important;
height: var(--psq-header-h) !important;
font-size: 10px !important;
line-height: 1 !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px !important;
}
.order-grid-header .beden-block {
height: var(--psq-header-h) !important;
}
.order-grid-header .grp-row {
height: var(--psq-header-h) !important;
align-items: center;
}
.order-grid-header .grp-title {
width: var(--grp-title-w) !important;
text-align: center !important;
padding-right: 0 !important;
font-size: 10px !important;
}
.order-grid-header .grp-cell.hdr {
height: var(--psq-header-h) !important;
font-size: 10px !important;
}
.order-grid-header .total-row {
grid-column: 7 / -1 !important;
}
.order-grid-header .total-cell {
width: var(--psq-col-adet) !important;
}
.order-grid-header .total-cell:last-child {
width: var(--psq-col-adet) !important;
}
.order-sub-header {
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count)))
var(--psq-col-adet) !important;
top: calc(
var(--header-h)
+ var(--filter-h)
+ var(--save-h)
+ var(--grid-header-h)
+ var(--psq-sticky-offset)
) !important;
}
.order-sub-header.level-2 {
min-height: 82px !important;
height: 82px !important;
background: #fff9c4 !important;
border-top: 1px solid #d4c79f !important;
border-bottom: 1px solid #d4c79f !important;
}
.order-sub-header.level-1 {
min-height: 84px !important;
height: 84px !important;
top: calc(
var(--header-h)
+ var(--filter-h)
+ var(--save-h)
+ var(--grid-header-h)
+ var(--psq-sticky-offset)
- var(--psq-l1-lift)
) !important;
background: var(--q-primary, #1976d2) !important;
border-top: 1px solid #145ea8 !important;
border-bottom: 1px solid #145ea8 !important;
color: #fff !important;
}
.order-sub-header.level-1 .sub-col.level1-merged {
grid-column: 1 / 6;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
padding: 0 10px;
border-right: 1px solid rgba(255, 255, 255, 0.45);
min-width: 0;
}
.order-sub-header.level-1 .sub-col.level1-merged .text-caption {
color: #fff !important;
opacity: 0.95;
}
.order-sub-header.level-1 .sub-center.level1-center {
grid-column: 6;
display: grid;
grid-template-rows: 42px 42px;
align-items: stretch;
height: 100%;
overflow: hidden;
padding-left: calc(var(--grp-title-w) + var(--grp-title-gap));
margin-left: 0;
}
.order-sub-header.level-1 .beden-row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
height: 42px;
min-height: 42px;
}
.order-sub-header.level-1 .beden-row .beden-cell {
display: flex;
align-items: center;
justify-content: center;
height: 42px;
min-height: 42px;
background: var(--q-primary, #1976d2) !important;
color: #fff !important;
border-right: 1px solid rgba(255, 255, 255, 0.45);
border-bottom: 1px solid rgba(255, 255, 255, 0.45);
border-top: none;
border-left: none;
font-size: 12px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
overflow: hidden;
}
.order-sub-header.level-1 .beden-row.values-top .beden-cell {
background: #fff9c4 !important;
color: var(--q-primary, #1976d2) !important;
font-weight: 700;
}
.order-sub-header.level-1 .beden-row.headers .beden-cell {
font-weight: 500;
color: #fff !important;
}
.order-sub-header.level-1 .sub-right.level1-right {
grid-column: 7 / 8;
display: grid;
grid-template-columns: var(--psq-col-adet);
grid-template-rows: 30px 30px 24px;
align-items: stretch;
justify-items: stretch;
height: 100%;
overflow: hidden;
padding: 0 8px 0 6px;
border-left: 1px solid rgba(255, 255, 255, 0.45);
color: #fff;
}
.order-sub-header.level-1 .sub-right .top-total {
grid-column: 1;
grid-row: 1;
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
background: #fff9c4 !important;
color: var(--q-primary, #1976d2) !important;
padding: 0 6px;
border-radius: 4px;
}
.order-sub-header.level-1 .sub-right .bottom-label {
font-size: 12px;
font-weight: 700;
}
.order-sub-header.level-1 .sub-right .bottom-row {
grid-column: 1;
grid-row: 2;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
white-space: nowrap;
overflow: hidden;
}
.order-sub-header.level-1 .sub-right .icon-row {
grid-column: 1;
grid-row: 3;
display: flex;
align-items: center;
justify-content: flex-end;
}
.order-sub-header.level-2 .sub-col {
display: flex;
align-items: center;
padding: 0 8px;
font-size: 12px;
font-weight: 500;
color: #111;
min-width: 0;
border-right: 1px solid #d4c79f;
white-space: normal;
overflow: visible;
text-overflow: clip;
line-height: 1.2;
word-break: break-word;
overflow-wrap: anywhere;
}
.order-sub-header.level-2 .sub-col.model { grid-column: 1; }
.order-sub-header.level-2 .sub-col.renk { grid-column: 2; }
.order-sub-header.level-2 .sub-col.ana { grid-column: 3; }
.order-sub-header.level-2 .sub-col.alt { grid-column: 4; }
.order-sub-header.level-2 .sub-col.aciklama { grid-column: 5; }
.order-sub-header.level-2 .sub-col.model,
.order-sub-header.level-2 .sub-col.renk,
.order-sub-header.level-2 .sub-col.ana,
.order-sub-header.level-2 .sub-col.alt {
justify-content: center;
text-align: center;
}
.order-sub-header.level-2 .sub-col.renk {
flex-direction: column;
gap: 2px;
line-height: 1.1;
}
.order-sub-header.level-2 .sub-col.renk .renk-kodu {
font-weight: 700;
}
.order-sub-header.level-2 .sub-col.renk .renk-aciklama {
font-size: 11px;
opacity: 0.9;
}
.order-sub-header.level-2 .sub-col.aciklama {
justify-content: flex-start;
text-align: left;
}
.order-sub-header.level-2 .sub-center.level2-center {
grid-column: 6;
display: grid;
grid-template-rows: 1fr 1fr;
align-items: stretch;
height: 100%;
padding-left: var(--grp-title-w);
margin-left: var(--grp-title-gap);
}
.order-sub-header.level-2 .beden-row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
}
.order-sub-header.level-2 .beden-row.values-top .beden-cell {
border-right: 1px solid #d4c79f;
background: transparent;
font-size: 13px;
font-weight: 600;
color: #1f1f1f;
line-height: 1;
}
.order-sub-header.level-2 .beden-row.headers .beden-cell {
border-top: 1px solid #d4c79f;
border-right: 1px solid #d4c79f;
border-bottom: none;
background: var(--q-primary, #1976d2);
font-size: 12px;
font-weight: 700;
color: #fff;
line-height: 1;
}
.order-sub-header.level-2 .sub-right.level2-right {
grid-column: 7 / 8;
display: grid;
grid-template-columns: var(--psq-col-adet);
grid-template-rows: 1fr 1fr;
align-items: center;
justify-items: start;
padding-left: 8px;
padding-right: 0;
transform: none !important;
border-left: 1px solid #d4c79f;
}
.order-sub-header.level-2 .sub-right .top-total {
grid-column: 1 / 2;
grid-row: 1;
font-size: 14px;
font-weight: 700;
line-height: 1;
justify-self: start;
text-align: left;
}
.order-sub-header.level-2 .sub-right .bottom-label {
font-size: 12px;
font-weight: 700;
}
.order-sub-header.level-2 .sub-right .bottom-row {
grid-column: 1;
grid-row: 2;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
}
.order-grid-body .summary-row {
display: grid !important;
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
var(--psq-col-adet) !important;
min-height: 56px;
height: 56px;
background: #fff;
border-top: 1px solid #d4c79f;
border-bottom: 1px solid #d4c79f;
align-items: stretch;
}
.order-grid-body .summary-row .cell {
min-height: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid #d4c79f;
font-size: 12px;
overflow: hidden !important;
}
.order-grid-body .summary-row .cell:last-child {
border-right: none;
}
.order-grid-body .summary-row .grp-area {
display: grid !important;
grid-template-columns: var(--grp-title-w) var(--grp-title-gap) 1fr;
width: 100% !important;
height: 56px;
padding-left: 0 !important;
transform: none !important;
border-right: 1px solid #d4c79f;
}
.order-grid-body .summary-row .grp-row {
grid-column: 3;
width: 100%;
height: 56px;
margin-left: 0 !important;
justify-content: start !important;
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
}
.order-grid-body .summary-row .grp-row .cell.beden {
width: var(--beden-w);
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
margin: 0 !important;
border-right: 1px solid #d4c79f;
border-left: none !important;
border-top: none !important;
border-bottom: none !important;
min-height: 56px;
height: 56px;
box-sizing: border-box;
}
.order-grid-body .summary-row .grp-row .cell.beden:first-child {
border-left: 1px solid #d4c79f !important;
}
.order-grid-body .summary-row .cell.model,
.order-grid-body .summary-row .cell.renk,
.order-grid-body .summary-row .cell.ana,
.order-grid-body .summary-row .cell.alt {
justify-content: center;
text-align: center;
}
.order-grid-body .summary-row .cell.aciklama {
grid-column: 5 / 6 !important;
position: static !important;
width: var(--col-aciklama) !important;
margin-right: 0 !important;
min-height: 56px !important;
z-index: auto !important;
background: #fff !important;
box-sizing: border-box !important;
border-right: 1px solid #d4c79f !important;
justify-content: flex-start !important;
text-align: left !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
line-height: 1 !important;
padding-left: 6px !important;
padding-right: 6px !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
}
.order-grid-body .summary-row .cell.depo-merged {
grid-column: 1 / 6 !important;
justify-content: center !important;
text-align: center !important;
font-weight: 600;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.order-grid-body .summary-row .cell.adet,
.order-grid-body .summary-row .cell.termin {
justify-content: flex-end;
text-align: right;
padding-right: 10px !important;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.order-grid-body {
margin-top: 0 !important;
padding-top: 0 !important;
}
.order-grid-body > .summary-group,
.order-grid-body > .summary-group:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
}
.detail-table-wrap {
padding: 8px 0 12px 0;
background: #fff;
}
.detail-table :deep(th),
.detail-table :deep(td) {
white-space: nowrap;
}
</style>

View File

@@ -297,6 +297,12 @@ const routes = [
component: () => import('pages/ProductStockQuery.vue'), component: () => import('pages/ProductStockQuery.vue'),
meta: { permission: 'order:view' } meta: { permission: 'order:view' }
}, },
{
path: 'product-stock-by-attributes',
name: 'product-stock-by-attributes',
component: () => import('pages/ProductStockByAttributes.vue'),
meta: { permission: 'order:view' }
},
/* ================= PASSWORD ================= */ /* ================= PASSWORD ================= */

View File

@@ -2,6 +2,67 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import api from 'src/services/api' 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 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
if (data && typeof data === 'object') {
const msg = String(data.message || '').trim()
const step = String(data.step || '').trim()
const detail = String(data.detail || '').trim()
const parts = [msg]
if (step) parts.push(`step=${step}`)
if (detail) parts.push(detail)
const merged = parts.filter(Boolean).join(' | ')
if (merged) return merged
}
return err?.message || fallback
}
function logApiError (action, err, payload = null) {
const status = err?.response?.status
const data = err?.response?.data
console.error(`[OrderProductionItemStore] ${action} failed`, {
status,
payload,
data,
message: err?.message
})
}
export const useOrderProductionItemStore = defineStore('orderproductionitems', { export const useOrderProductionItemStore = defineStore('orderproductionitems', {
state: () => ({ state: () => ({
items: [], items: [],
@@ -118,7 +179,8 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
) )
return res?.data || { missingCount: 0, missing: [] } return res?.data || { missingCount: 0, missing: [] }
} catch (err) { } catch (err) {
this.error = err?.response?.data || err?.message || 'Kontrol basarisiz' logApiError('validateUpdates', err, { orderHeaderID, lineCount: lines?.length || 0 })
this.error = extractApiErrorMessage(err, 'Kontrol basarisiz')
throw err throw err
} finally { } finally {
this.saving = false this.saving = false
@@ -137,7 +199,8 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
) )
return res?.data || { updated: 0, inserted: 0 } return res?.data || { updated: 0, inserted: 0 }
} catch (err) { } catch (err) {
this.error = err?.response?.data || err?.message || 'Guncelleme basarisiz' logApiError('applyUpdates', err, { orderHeaderID, lineCount: lines?.length || 0, insertMissing })
this.error = extractApiErrorMessage(err, 'Guncelleme basarisiz')
throw err throw err
} finally { } finally {
this.saving = false this.saving = false

View File

@@ -3438,17 +3438,28 @@ export function normalizeBeden(v) {
- Core logic aligned with backend detectBedenGroupGo - Core logic aligned with backend detectBedenGroupGo
- Keeps frontend aksbir bucket for accessory lines - Keeps frontend aksbir bucket for accessory lines
=========================================================== */ =========================================================== */
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') { export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '') {
const list = Array.isArray(bedenList) && bedenList.length > 0 const list = Array.isArray(bedenList) && bedenList.length > 0
? bedenList.map(v => (v || '').toString().trim().toUpperCase()) ? bedenList.map(v => (v || '').toString().trim().toUpperCase())
: [' '] : [' ']
const rawAna = (urunAnaGrubu || '').toString().toUpperCase() const rawAna = normalizeTextForMatch(urunAnaGrubu || '')
const rawKat = (urunKategori || '').toString().toUpperCase() const rawKat = normalizeTextForMatch(urunKategori || '')
const hasGarson = rawAna.includes('GARSON') || rawKat.includes('GARSON') || const rawYetiskinGarson = normalizeTextForMatch(yetiskinGarson || '')
rawAna.includes('GARSON') || rawKat.includes('GARSON')
const hasAyakkabi = rawAna.includes('AYAKKABI') || rawKat.includes('AYAKKABI') || // Ozel kural:
rawAna.includes('AYAKKABI') || rawKat.includes('AYAKKABI') // YETISKIN/GARSON = GARSON ve URUN ANA GRUBU "GOMLEK ATA YAKA" veya "GOMLEK KLASIK" ise
// sonuc "yas" olmalidir.
const isGarsonGomlekAnaGrubu =
rawAna.includes('GOMLEK ATA YAKA') ||
rawAna.includes('GOMLEK KLASIK')
const hasGarsonSignal = rawAna.includes('GARSON') || rawKat.includes('GARSON') || rawYetiskinGarson.includes('GARSON')
if (isGarsonGomlekAnaGrubu && (rawKat.includes('GARSON') || rawYetiskinGarson.includes('GARSON'))) {
return 'yas'
}
const hasGarson = hasGarsonSignal
const hasAyakkabi = rawAna.includes('AYAKKABI') || rawKat.includes('AYAKKABI')
if (hasGarson && hasAyakkabi) return 'ayk_garson' if (hasGarson && hasAyakkabi) return 'ayk_garson'
if (hasGarson) return 'yas' if (hasGarson) return 'yas'
@@ -3457,28 +3468,27 @@ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = ''
const harfliBedenler = ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] const harfliBedenler = ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL']
if (list.some(b => harfliBedenler.includes(b))) return 'gom' if (list.some(b => harfliBedenler.includes(b))) return 'gom'
const ana = (urunAnaGrubu || '') const ana = normalizeTextForMatch(urunAnaGrubu || '')
.toUpperCase()
.trim() .trim()
.replace(/\(.*?\)/g, '') .replace(/\(.*?\)/g, '')
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '') .replace(/[^A-Z0-9\s]/g, '')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
const kat = (urunKategori || '').toUpperCase().trim() const kat = normalizeTextForMatch(urunKategori || '').trim()
// 🔸 Aksesuar ise "aksbir" // 🔸 Aksesuar ise "aksbir"
const aksesuarGruplari = [ const aksesuarGruplari = [
'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP','ÇORAP', 'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP',
'FULAR','MENDIL','MENDİL','KASKOL','ASKI', 'FULAR','MENDIL','KASKOL','ASKI',
'YAKA','KOL DUGMESI','KOL DÜĞMESİ' 'YAKA','KOL DUGMESI'
] ]
const giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT'] const giyimGruplari = ['GOMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TISORT']
// 🔸 Pantolon özel durumu // 🔸 Pantolon özel durumu
if ( if (
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) && aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
!giyimGruplari.some(g => ana.includes(g)) !giyimGruplari.some(g => ana.includes(g))
) return 'aksbir' ) return 'aksbir'
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan' if (ana.includes('PANTOLON') && kat.includes('YETISKIN')) return 'pan'
// 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı // 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı
const allNumeric = list.every(v => /^\d+$/.test(v)) const allNumeric = list.every(v => /^\d+$/.test(v))
if (allNumeric) { if (allNumeric) {
@@ -3488,7 +3498,7 @@ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = ''
} }
// 🔸 Yaş grubu (çocuk/garson) // 🔸 Yaş grubu (çocuk/garson)
if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas' if (kat.includes('GARSON') || kat.includes('COCUK')) return 'yas'
// 🔸 Varsayılan: takım elbise // 🔸 Varsayılan: takım elbise
return 'tak' return 'tak'