From 4dc0415546c0b39ab6af06e4eb6f8d4526202ad7 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Tue, 3 Mar 2026 23:28:43 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/main.go | 18 + svc/queries/productstockquery.go | 201 +++ svc/routes/product_images.go | 155 +++ svc/routes/product_stock_query.go | 85 ++ ...g.js.temporary.compiled.1772552660663.mjs} | 0 ui/src/layouts/MainLayout.vue | 13 + ui/src/pages/ProductStockQuery.vue | 1078 +++++++++++++++++ ui/src/router/routes.js | 8 + 8 files changed, 1558 insertions(+) create mode 100644 svc/queries/productstockquery.go create mode 100644 svc/routes/product_images.go create mode 100644 svc/routes/product_stock_query.go rename ui/{quasar.config.js.temporary.compiled.1772532850970.mjs => quasar.config.js.temporary.compiled.1772552660663.mjs} (100%) create mode 100644 ui/src/pages/ProductStockQuery.vue diff --git a/svc/main.go b/svc/main.go index f99ad74..c9f1376 100644 --- a/svc/main.go +++ b/svc/main.go @@ -555,6 +555,24 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)), ) + bindV3(r, pgDB, + "/api/product-stock-query", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductStockQueryHandler)), + ) + + bindV3(r, pgDB, + "/api/product-images", "GET", + "order", "view", + wrapV3(routes.GetProductImagesHandler(pgDB)), + ) + + bindV3(r, pgDB, + "/api/product-images/{id}/content", "GET", + "order", "view", + wrapV3(routes.GetProductImageContentHandler(pgDB)), + ) + // ============================================================ // ROLE MANAGEMENT // ============================================================ diff --git a/svc/queries/productstockquery.go b/svc/queries/productstockquery.go new file mode 100644 index 0000000..d84f27b --- /dev/null +++ b/svc/queries/productstockquery.go @@ -0,0 +1,201 @@ +package queries + +// GetProductStockQuery: +// Ürün kodu bazlı, optimize stok + attribute sorgusu. +const GetProductStockQuery = ` +DECLARE @ProductCode NVARCHAR(50) = @p1; + +;WITH INV AS +( + SELECT + CompanyCode, + OfficeCode, + StoreTypeCode, + StoreCode, + WarehouseCode, + ItemTypeCode, + ItemCode, + ColorCode, + ItemDim1Code, + ItemDim2Code, + ItemDim3Code, + SUM(PickingQty1) AS PickingQty1, + SUM(ReserveQty1) AS ReserveQty1, + 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 + AND ItemCode = @ProductCode + AND LEN(ItemCode) = 13 + AND LEN(@ProductCode) = 13 + ORDER BY PriceDate DESC +) +SELECT + I.WarehouseCode AS Depo_Kodu, + W.WarehouseDescription AS Depo_Adi, + IT.ItemTypeDescription AS InventoryType, + I.ItemCode AS Urun_Kodu, + A.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, + A.ProductAtt01Desc AS URUN_ANA_GRUBU, + A.ProductAtt02Desc AS URUN_ALT_GRUBU, + A.ProductAtt10Desc AS MARKA, + A.ProductAtt11Desc AS DR, + A.ProductAtt21Desc AS KALIP, + A.ProductAtt22Desc AS IKINCI_PARCA_KALIP, + A.ProductAtt23Desc AS PACA_GENISLIGI, + A.ProductAtt24Desc AS UCUNCU_PARCA_KALIP, + A.ProductAtt25Desc AS UCUNCU_PARCA_MODEL, + A.ProductAtt26Desc AS BIRINCI_PARCA_KUMAS, + A.ProductAtt27Desc AS IKINCI_PARCA_KUMAS, + A.ProductAtt28Desc AS UCUNCU_PARCA_KUMAS, + A.ProductAtt29Desc AS BIRINCI_PARCA_KARISIM, + A.ProductAtt30Desc AS IKINCI_PARCA_KARISIM, + A.ProductAtt31Desc AS UCUNCU_PARCA_KARISIM, + A.ProductAtt32Desc AS YAKA_TIPI, + A.ProductAtt33Desc AS DUGME, + A.ProductAtt34Desc AS YIRTMAC, + A.ProductAtt35Desc AS SEZON_YILI, + A.ProductAtt36Desc AS MEVSIM, + A.ProductAtt37Desc AS TABAN, + A.ProductAtt38Desc AS BIRINCI_PARCA_FIT, + A.ProductAtt39Desc AS IKINCI_PARCA_FIT, + A.ProductAtt40Desc AS BOS2, + A.ProductAtt41Desc AS KISA_KAR, + A.ProductAtt42Desc AS SERI_FASON, + A.ProductAtt43Desc AS STOK_GIRIS_YONTEMI, + A.ProductAtt44Desc AS YETISKIN_GARSON, + A.ProductAtt45Desc AS ASKILI_YAN, + A.ProductAtt46Desc AS BOS3, + P.Price AS Fiyat +FROM INV I +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' +CROSS JOIN Attr A +OUTER APPLY (SELECT TOP 1 Price FROM Price) P +WHERE + I.ItemTypeCode = 1 + AND I.ItemCode = @ProductCode + AND LEN(I.ItemCode) = 13 + AND LEN(@ProductCode) = 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' + ); +` diff --git a/svc/routes/product_images.go b/svc/routes/product_images.go new file mode 100644 index 0000000..94aad07 --- /dev/null +++ b/svc/routes/product_images.go @@ -0,0 +1,155 @@ +package routes + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/gorilla/mux" +) + +type ProductImageItem struct { + ID int64 `json:"id"` + FileName string `json:"file_name"` + FileSize int64 `json:"file_size"` + Storage string `json:"storage_path"` + ContentURL string `json:"content_url"` +} + +// GET /api/product-images?code=...&color=... +func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + code := strings.TrimSpace(r.URL.Query().Get("code")) + color := strings.TrimSpace(r.URL.Query().Get("color")) + + if code == "" { + http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest) + return + } + + query := ` +SELECT + b.id, + b.file_name, + COALESCE(b.file_size, 0) AS file_size, + COALESCE(b.storage_path, '') AS storage_path +FROM dfblob b +JOIN mmitem i + ON i.id = b.src_id +WHERE b.typ = 'img' + AND b.src_table = 'mmitem' + AND i.code = $1 + AND ($2 = '' OR b.file_name ILIKE '%' || '-' || $2 || '-%') +ORDER BY COALESCE(b.sort_order, 999999), b.zlins_dttm DESC, b.id DESC +` + + rows, err := pg.Query(query, code, color) + if err != nil { + http.Error(w, "Görsel sorgu hatası: "+err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + items := make([]ProductImageItem, 0, 16) + for rows.Next() { + var it ProductImageItem + if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil { + continue + } + it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) + items = append(items, it) + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(items) + } +} + +// GET /api/product-images/{id}/content +func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := mux.Vars(r)["id"] + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || id <= 0 { + http.Error(w, "Geçersiz görsel id", http.StatusBadRequest) + return + } + + var ( + fileName string + storagePath string + storedInDB bool + binData []byte + ) + + err = pg.QueryRow(` +SELECT + COALESCE(file_name, ''), + COALESCE(storage_path, ''), + COALESCE(stored_in_db, false), + bin +FROM dfblob +WHERE id = $1 + AND typ = 'img' +`, id).Scan(&fileName, &storagePath, &storedInDB, &binData) + if err != nil { + if err == sql.ErrNoRows { + http.NotFound(w, r) + return + } + http.Error(w, "Görsel okunamadı: "+err.Error(), http.StatusInternalServerError) + return + } + + if storedInDB && len(binData) > 0 { + w.Header().Set("Content-Type", http.DetectContentType(binData)) + w.Header().Set("Cache-Control", "public, max-age=3600") + _, _ = w.Write(binData) + return + } + + resolved := resolveStoragePath(storagePath) + if resolved == "" { + http.NotFound(w, r) + return + } + + w.Header().Set("Cache-Control", "public, max-age=3600") + http.ServeFile(w, r, resolved) + } +} + +func resolveStoragePath(storagePath string) string { + raw := strings.TrimSpace(storagePath) + if raw == "" { + return "" + } + + raw = strings.TrimPrefix(raw, "./") + candidates := []string{ + filepath.Clean(storagePath), + filepath.Clean(raw), + filepath.Join(".", raw), + filepath.Join("..", raw), + } + + if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" { + candidates = append(candidates, filepath.Join(root, raw)) + } + + for _, p := range candidates { + if p == "" { + continue + } + if st, err := os.Stat(p); err == nil && !st.IsDir() { + return p + } + } + + return "" +} diff --git a/svc/routes/product_stock_query.go b/svc/routes/product_stock_query.go new file mode 100644 index 0000000..8d7d658 --- /dev/null +++ b/svc/routes/product_stock_query.go @@ -0,0 +1,85 @@ +package routes + +import ( + "bssapp-backend/db" + "bssapp-backend/queries" + "context" + "encoding/json" + "log" + "net/http" + "strconv" + "strings" + "time" +) + +// GetProductStockQueryHandler +// GET /api/product-stock-query?code=... +func GetProductStockQueryHandler(w http.ResponseWriter, r *http.Request) { + code := strings.TrimSpace(r.URL.Query().Get("code")) + if code == "" { + http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + rows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductStockQuery, code) + if err != nil { + log.Printf("❌ [PRODUCT-STOCK-QUERY] SQL hatası: %v", err) + http.Error(w, "SQL hatası: "+err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + http.Error(w, "Kolon bilgisi alınamadı", http.StatusInternalServerError) + return + } + + result := make([]map[string]interface{}, 0, 128) + 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 { + log.Printf("⚠️ [PRODUCT-STOCK-QUERY] scan hatası: %v", err) + 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 { + http.Error(w, "Satır okuma hatası: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(result) +} + +func normalizeSQLValue(v interface{}) interface{} { + switch val := v.(type) { + case nil: + return "" + case []byte: + s := strings.TrimSpace(string(val)) + if s == "" { + return "" + } + if n, err := strconv.ParseFloat(strings.ReplaceAll(s, ",", "."), 64); err == nil { + return n + } + return s + default: + return val + } +} diff --git a/ui/quasar.config.js.temporary.compiled.1772532850970.mjs b/ui/quasar.config.js.temporary.compiled.1772552660663.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1772532850970.mjs rename to ui/quasar.config.js.temporary.compiled.1772552660663.mjs diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 752232c..06a05c8 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -261,6 +261,19 @@ const menuItems = [ ] }, + { + label: 'Ürün', + icon: 'inventory_2', + + children: [ + { + label: 'Ürün Kodundan Stok Sorgula', + to: '/app/product-stock-query', + permission: 'order:view' + } + ] + }, + { label: 'Sistem', icon: 'settings', diff --git a/ui/src/pages/ProductStockQuery.vue b/ui/src/pages/ProductStockQuery.vue new file mode 100644 index 0000000..983dbd6 --- /dev/null +++ b/ui/src/pages/ProductStockQuery.vue @@ -0,0 +1,1078 @@ + + + + + + + + + + + diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 8e197cf..a76935f 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -290,6 +290,14 @@ const routes = [ meta: { permission: 'order:export' } }, + /* ================= PRODUCTS ================= */ + { + path: 'product-stock-query', + name: 'product-stock-query', + component: () => import('pages/ProductStockQuery.vue'), + meta: { permission: 'order:view' } + }, + /* ================= PASSWORD ================= */