Merge remote-tracking branch 'origin/master'
This commit is contained in:
18
svc/main.go
18
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
|
||||
// ============================================================
|
||||
|
||||
201
svc/queries/productstockquery.go
Normal file
201
svc/queries/productstockquery.go
Normal file
@@ -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'
|
||||
);
|
||||
`
|
||||
155
svc/routes/product_images.go
Normal file
155
svc/routes/product_images.go
Normal file
@@ -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 ""
|
||||
}
|
||||
85
svc/routes/product_stock_query.go
Normal file
85
svc/routes/product_stock_query.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user