Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -6,35 +6,209 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetProductPricingList(ctx context.Context, limit int, afterProductCode string) ([]models.ProductPricing, error) {
|
||||
type ProductPricingFilters struct {
|
||||
Search string
|
||||
ProductCode []string
|
||||
BrandGroup []string
|
||||
AskiliYan []string
|
||||
Kategori []string
|
||||
UrunIlkGrubu []string
|
||||
UrunAnaGrubu []string
|
||||
UrunAltGrubu []string
|
||||
Icerik []string
|
||||
Karisim []string
|
||||
Marka []string
|
||||
}
|
||||
|
||||
type ProductPricingPage struct {
|
||||
Rows []models.ProductPricing
|
||||
TotalCount int
|
||||
TotalPages int
|
||||
Page int
|
||||
Limit int
|
||||
}
|
||||
|
||||
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters) (ProductPricingPage, error) {
|
||||
result := ProductPricingPage{
|
||||
Rows: []models.ProductPricing{},
|
||||
TotalCount: 0,
|
||||
TotalPages: 0,
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
afterProductCode = strings.TrimSpace(afterProductCode)
|
||||
offset := (page - 1) * limit
|
||||
|
||||
paramIndex := 1
|
||||
args := make([]any, 0, 64)
|
||||
nextParam := func() string {
|
||||
name := "@p" + strconv.Itoa(paramIndex)
|
||||
paramIndex++
|
||||
return name
|
||||
}
|
||||
whereParts := []string{
|
||||
"ProductAtt42 IN ('SERI', 'AKSESUAR')",
|
||||
"IsBlocked = 0",
|
||||
"LEN(LTRIM(RTRIM(ProductCode))) = 13",
|
||||
}
|
||||
addInFilter := func(expr string, values []string) {
|
||||
clean := make([]string, 0, len(values))
|
||||
for _, v := range values {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
clean = append(clean, v)
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return
|
||||
}
|
||||
ors := make([]string, 0, len(clean))
|
||||
for _, v := range clean {
|
||||
p := nextParam()
|
||||
ors = append(ors, expr+" = "+p)
|
||||
args = append(args, v)
|
||||
}
|
||||
whereParts = append(whereParts, "("+strings.Join(ors, " OR ")+")")
|
||||
}
|
||||
brandGroupExpr := `CASE ABS(CHECKSUM(LTRIM(RTRIM(ProductCode)))) % 3
|
||||
WHEN 0 THEN 'MARKA GRUBU A'
|
||||
WHEN 1 THEN 'MARKA GRUBU B'
|
||||
ELSE 'MARKA GRUBU C'
|
||||
END`
|
||||
addInFilter("LTRIM(RTRIM(ProductCode))", filters.ProductCode)
|
||||
addInFilter(brandGroupExpr, filters.BrandGroup)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '')", filters.AskiliYan)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '')", filters.Kategori)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '')", filters.UrunIlkGrubu)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '')", filters.UrunAnaGrubu)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '')", filters.UrunAltGrubu)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '')", filters.Icerik)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '')", filters.Karisim)
|
||||
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '')", filters.Marka)
|
||||
if q := strings.TrimSpace(filters.Search); q != "" {
|
||||
p := nextParam()
|
||||
args = append(args, "%"+q+"%")
|
||||
whereParts = append(whereParts, "("+strings.Join([]string{
|
||||
"LTRIM(RTRIM(ProductCode)) LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') LIKE " + p,
|
||||
"COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') LIKE " + p,
|
||||
}, " OR ")+")")
|
||||
}
|
||||
whereSQL := strings.Join(whereParts, " AND ")
|
||||
|
||||
countQuery := `
|
||||
SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode)))
|
||||
FROM ProductFilterWithDescription('TR')
|
||||
WHERE ` + whereSQL + `;
|
||||
`
|
||||
var totalCount int
|
||||
if err := db.MssqlDB.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.TotalCount = totalCount
|
||||
if totalCount == 0 {
|
||||
result.TotalPages = 0
|
||||
result.Page = 1
|
||||
return result, nil
|
||||
}
|
||||
totalPages := int(math.Ceil(float64(totalCount) / float64(limit)))
|
||||
if totalPages <= 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
if page > totalPages {
|
||||
page = totalPages
|
||||
offset = (page - 1) * limit
|
||||
}
|
||||
result.Page = page
|
||||
result.Limit = limit
|
||||
result.TotalPages = totalPages
|
||||
|
||||
// Stage 1: fetch only paged products first (fast path).
|
||||
productQuery := `
|
||||
SELECT TOP (` + strconv.Itoa(limit) + `)
|
||||
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
|
||||
FROM ProductFilterWithDescription('TR')
|
||||
WHERE ProductAtt42 IN ('SERI', 'AKSESUAR')
|
||||
AND IsBlocked = 0
|
||||
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
||||
AND (@p1 = '' OR LTRIM(RTRIM(ProductCode)) > @p1)
|
||||
ORDER BY LTRIM(RTRIM(ProductCode));
|
||||
IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes;
|
||||
IF OBJECT_ID('tempdb..#stock_base') IS NOT NULL DROP TABLE #stock_base;
|
||||
|
||||
SELECT
|
||||
f.ProductCode,
|
||||
MAX(f.BrandGroupSec) AS BrandGroupSec,
|
||||
MAX(f.AskiliYan) AS AskiliYan,
|
||||
MAX(f.Kategori) AS Kategori,
|
||||
MAX(f.UrunIlkGrubu) AS UrunIlkGrubu,
|
||||
MAX(f.UrunAnaGrubu) AS UrunAnaGrubu,
|
||||
MAX(f.UrunAltGrubu) AS UrunAltGrubu,
|
||||
MAX(f.Icerik) AS Icerik,
|
||||
MAX(f.Karisim) AS Karisim,
|
||||
MAX(f.Marka) AS Marka
|
||||
INTO #req_codes
|
||||
FROM (
|
||||
SELECT
|
||||
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||
` + brandGroupExpr + ` AS BrandGroupSec,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
|
||||
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
|
||||
FROM ProductFilterWithDescription('TR')
|
||||
WHERE ` + whereSQL + `
|
||||
) f
|
||||
GROUP BY f.ProductCode;
|
||||
|
||||
CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode);
|
||||
|
||||
SELECT
|
||||
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
|
||||
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
|
||||
INTO #stock_base
|
||||
FROM trStock s WITH(NOLOCK)
|
||||
INNER JOIN #req_codes rc
|
||||
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||
WHERE s.ItemTypeCode = 1
|
||||
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||
GROUP BY LTRIM(RTRIM(s.ItemCode));
|
||||
|
||||
CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode);
|
||||
|
||||
SELECT
|
||||
rc.ProductCode,
|
||||
rc.BrandGroupSec,
|
||||
rc.AskiliYan,
|
||||
rc.Kategori,
|
||||
rc.UrunIlkGrubu,
|
||||
rc.UrunAnaGrubu,
|
||||
rc.UrunAltGrubu,
|
||||
rc.Icerik,
|
||||
rc.Karisim,
|
||||
rc.Marka
|
||||
FROM #req_codes rc
|
||||
LEFT JOIN #stock_base sb
|
||||
ON sb.ItemCode = rc.ProductCode
|
||||
ORDER BY
|
||||
CAST(ROUND(ISNULL(sb.InventoryQty1, 0), 2) AS DECIMAL(18, 2)) DESC,
|
||||
rc.ProductCode ASC
|
||||
OFFSET ` + strconv.Itoa(offset) + ` ROWS
|
||||
FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY;
|
||||
`
|
||||
|
||||
var (
|
||||
@@ -43,7 +217,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
)
|
||||
for attempt := 1; attempt <= 3; attempt++ {
|
||||
var err error
|
||||
rows, err = db.MssqlDB.QueryContext(ctx, productQuery, afterProductCode)
|
||||
rows, err = db.MssqlDB.QueryContext(ctx, productQuery, args...)
|
||||
if err == nil {
|
||||
rowsErr = nil
|
||||
break
|
||||
@@ -60,7 +234,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
}
|
||||
}
|
||||
if rowsErr != nil {
|
||||
return nil, rowsErr
|
||||
return result, rowsErr
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -69,6 +243,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
var item models.ProductPricing
|
||||
if err := rows.Scan(
|
||||
&item.ProductCode,
|
||||
&item.BrandGroupSec,
|
||||
&item.AskiliYan,
|
||||
&item.Kategori,
|
||||
&item.UrunIlkGrubu,
|
||||
@@ -78,15 +253,16 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
&item.Karisim,
|
||||
&item.Marka,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
return result, err
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
return result, err
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return out, nil
|
||||
result.Rows = out
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Stage 2: fetch metrics only for paged product codes.
|
||||
@@ -134,6 +310,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
WHERE s.ItemTypeCode = 1
|
||||
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||
AND s.In_Qty1 > 0
|
||||
AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP'
|
||||
AND LTRIM(RTRIM(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-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
|
||||
@@ -214,7 +391,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
|
||||
metricsRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metrics query failed: %w", err)
|
||||
return result, fmt.Errorf("metrics query failed: %w", err)
|
||||
}
|
||||
defer metricsRows.Close()
|
||||
|
||||
@@ -237,12 +414,12 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
&m.StockEntryDate,
|
||||
&m.LastPricingDate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
return result, err
|
||||
}
|
||||
metricsByCode[strings.TrimSpace(code)] = m
|
||||
}
|
||||
if err := metricsRows.Err(); err != nil {
|
||||
return nil, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
for i := range out {
|
||||
@@ -254,7 +431,8 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
result.Rows = out
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isTransientMSSQLNetworkError(err error) bool {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/lib/pq"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -497,25 +498,65 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
||||
|
||||
// ROLES
|
||||
for _, role := range payload.Roles {
|
||||
_, _ = tx.Exec(queries.InsertUserRole, newID, role)
|
||||
role = strings.TrimSpace(role)
|
||||
if role == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(queries.InsertUserRole, newID, role); err != nil {
|
||||
log.Printf("USER ROLE INSERT ERROR user_id=%d role=%q err=%v", newID, role, err)
|
||||
http.Error(w, "Rol eklenemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// DEPARTMENTS
|
||||
for _, d := range payload.Departments {
|
||||
_, _ = tx.Exec(queries.InsertUserDepartment, newID, d.Code)
|
||||
code := strings.TrimSpace(d.Code)
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(queries.InsertUserDepartment, newID, code); err != nil {
|
||||
log.Printf("USER DEPARTMENT INSERT ERROR user_id=%d department=%q err=%v", newID, code, err)
|
||||
http.Error(w, "Departman eklenemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// PIYASALAR
|
||||
for _, p := range payload.Piyasalar {
|
||||
_, _ = tx.Exec(queries.InsertUserPiyasa, newID, p.Code)
|
||||
code := strings.TrimSpace(p.Code)
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(queries.InsertUserPiyasa, newID, code); err != nil {
|
||||
log.Printf("USER PIYASA INSERT ERROR user_id=%d piyasa=%q err=%v", newID, code, err)
|
||||
http.Error(w, "Piyasa eklenemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// NEBIM
|
||||
for _, n := range payload.NebimUsers {
|
||||
_, _ = tx.Exec(queries.InsertUserNebim, newID, n.Username)
|
||||
username := strings.TrimSpace(n.Username)
|
||||
if username == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(queries.InsertUserNebim, newID, username); err != nil {
|
||||
log.Printf("USER NEBIM INSERT ERROR user_id=%d username=%q err=%v", newID, username, err)
|
||||
http.Error(w, "Nebim kullanıcısı eklenemedi", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
if pe, ok := err.(*pq.Error); ok {
|
||||
log.Printf(
|
||||
"USER CREATE COMMIT ERROR user_id=%d code=%s detail=%s constraint=%s table=%s err=%v",
|
||||
newID, pe.Code, pe.Detail, pe.Constraint, pe.Table, err,
|
||||
)
|
||||
} else {
|
||||
log.Printf("USER CREATE COMMIT ERROR user_id=%d err=%v", newID, err)
|
||||
}
|
||||
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,13 +32,31 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
limit := 500
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 10000 {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 500 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
afterProductCode := strings.TrimSpace(r.URL.Query().Get("after_product_code"))
|
||||
page := 1
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("page")); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
|
||||
page = parsed
|
||||
}
|
||||
}
|
||||
filters := queries.ProductPricingFilters{
|
||||
Search: strings.TrimSpace(r.URL.Query().Get("q")),
|
||||
ProductCode: splitCSVParam(r.URL.Query().Get("product_code")),
|
||||
BrandGroup: splitCSVParam(r.URL.Query().Get("brand_group_selection")),
|
||||
AskiliYan: splitCSVParam(r.URL.Query().Get("askili_yan")),
|
||||
Kategori: splitCSVParam(r.URL.Query().Get("kategori")),
|
||||
UrunIlkGrubu: splitCSVParam(r.URL.Query().Get("urun_ilk_grubu")),
|
||||
UrunAnaGrubu: splitCSVParam(r.URL.Query().Get("urun_ana_grubu")),
|
||||
UrunAltGrubu: splitCSVParam(r.URL.Query().Get("urun_alt_grubu")),
|
||||
Icerik: splitCSVParam(r.URL.Query().Get("icerik")),
|
||||
Karisim: splitCSVParam(r.URL.Query().Get("karisim")),
|
||||
Marka: splitCSVParam(r.URL.Query().Get("marka")),
|
||||
}
|
||||
|
||||
rows, err := queries.GetProductPricingList(ctx, limit+1, afterProductCode)
|
||||
pageResult, err := queries.GetProductPricingPage(ctx, page, limit, filters)
|
||||
if err != nil {
|
||||
if isPricingTimeoutLike(err, ctx.Err()) {
|
||||
log.Printf(
|
||||
@@ -63,38 +81,24 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
hasMore := len(rows) > limit
|
||||
if hasMore {
|
||||
rows = rows[:limit]
|
||||
}
|
||||
nextCursor := ""
|
||||
if hasMore && len(rows) > 0 {
|
||||
nextCursor = strings.TrimSpace(rows[len(rows)-1].ProductCode)
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[ProductPricing] trace=%s success user=%s id=%d limit=%d after=%q count=%d has_more=%t next=%q duration_ms=%d",
|
||||
"[ProductPricing] trace=%s success user=%s id=%d page=%d limit=%d count=%d total=%d total_pages=%d duration_ms=%d",
|
||||
traceID,
|
||||
claims.Username,
|
||||
claims.ID,
|
||||
pageResult.Page,
|
||||
limit,
|
||||
afterProductCode,
|
||||
len(rows),
|
||||
hasMore,
|
||||
nextCursor,
|
||||
len(pageResult.Rows),
|
||||
pageResult.TotalCount,
|
||||
pageResult.TotalPages,
|
||||
time.Since(started).Milliseconds(),
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if hasMore {
|
||||
w.Header().Set("X-Has-More", "true")
|
||||
} else {
|
||||
w.Header().Set("X-Has-More", "false")
|
||||
}
|
||||
if nextCursor != "" {
|
||||
w.Header().Set("X-Next-Cursor", nextCursor)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
w.Header().Set("X-Total-Count", strconv.Itoa(pageResult.TotalCount))
|
||||
w.Header().Set("X-Total-Pages", strconv.Itoa(pageResult.TotalPages))
|
||||
w.Header().Set("X-Page", strconv.Itoa(pageResult.Page))
|
||||
_ = json.NewEncoder(w).Encode(pageResult.Rows)
|
||||
}
|
||||
|
||||
func buildPricingTraceID(r *http.Request) string {
|
||||
@@ -124,3 +128,20 @@ func isPricingTimeoutLike(err error, ctxErr error) bool {
|
||||
strings.Contains(e, "no connection could be made") ||
|
||||
strings.Contains(e, "failed to respond")
|
||||
}
|
||||
|
||||
func splitCSVParam(raw string) []string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,158 +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'),
|
||||
|
||||
import(/* webpackMode: "eager" */ 'boot/locale'),
|
||||
|
||||
import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard')
|
||||
|
||||
]).then(bootFiles => {
|
||||
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
|
||||
start(app, boot)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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} }
|
||||
|
||||
@@ -35,6 +35,26 @@
|
||||
/>
|
||||
<q-btn flat color="grey-7" icon="restart_alt" label="Filtreleri Sifirla" @click="resetAll" />
|
||||
<q-btn color="primary" icon="refresh" label="Veriyi Yenile" :loading="store.loading" @click="reloadData" />
|
||||
<q-btn
|
||||
color="primary"
|
||||
outline
|
||||
icon="edit_note"
|
||||
label="Secili Olanlari Toplu Degistir"
|
||||
:disable="selectedRowCount === 0"
|
||||
@click="bulkDialogOpen = true"
|
||||
/>
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
color="primary"
|
||||
:max="Math.max(1, store.totalPages || 1)"
|
||||
:max-pages="8"
|
||||
boundary-links
|
||||
direction-links
|
||||
@update:model-value="onPageChange"
|
||||
/>
|
||||
<div class="text-caption text-grey-8">
|
||||
Sayfa {{ currentPage }} / {{ Math.max(1, store.totalPages || 1) }} - Toplam {{ store.totalCount || 0 }} urun kodu
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,19 +64,15 @@
|
||||
class="pane-table pricing-table"
|
||||
flat
|
||||
dense
|
||||
row-key="id"
|
||||
row-key="productCode"
|
||||
:rows="filteredRows"
|
||||
:columns="visibleColumns"
|
||||
:loading="store.loading"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="rowHeight"
|
||||
:virtual-scroll-sticky-size-start="headerHeight"
|
||||
:virtual-scroll-slice-size="36"
|
||||
:loading="tableLoading"
|
||||
:rows-per-page-options="[0]"
|
||||
v-model:pagination="tablePagination"
|
||||
:pagination="tablePagination"
|
||||
hide-bottom
|
||||
:table-style="tableStyle"
|
||||
@virtual-scroll="onTableVirtualScroll"
|
||||
@update:pagination="onPaginationChange"
|
||||
>
|
||||
<template #header="props">
|
||||
<q-tr :props="props" class="header-row-fixed">
|
||||
@@ -139,6 +155,54 @@
|
||||
Sonuc yok
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isValueSelectFilterField(col.field)" class="excel-filter-menu">
|
||||
<q-input
|
||||
v-model="valueFilterSearch[col.field]"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
class="excel-filter-select"
|
||||
placeholder="Deger ara"
|
||||
/>
|
||||
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
||||
<q-btn flat dense size="sm" label="Tumunu Sec" @click="selectAllColumnFilterOptions(col.field)" />
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(col.field)" />
|
||||
</div>
|
||||
<q-virtual-scroll
|
||||
v-if="getFilterOptionsForField(col.field).length > 0"
|
||||
class="excel-filter-options"
|
||||
:items="getFilterOptionsForField(col.field)"
|
||||
:virtual-scroll-item-size="32"
|
||||
separator
|
||||
>
|
||||
<template #default="{ item: option }">
|
||||
<q-item
|
||||
:key="`${col.field}-${option.value}`"
|
||||
dense
|
||||
clickable
|
||||
class="excel-filter-option"
|
||||
@click="toggleColumnFilterValue(col.field, option.value)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
dense
|
||||
size="sm"
|
||||
:model-value="isColumnFilterValueSelected(col.field, option.value)"
|
||||
@update:model-value="() => toggleColumnFilterValue(col.field, option.value)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ option.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
<div v-else class="excel-filter-empty">
|
||||
Sonuc yok
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isNumberRangeFilterField(col.field)" class="excel-filter-menu">
|
||||
<div class="range-filter-grid">
|
||||
<q-input
|
||||
@@ -216,8 +280,25 @@
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="primary"
|
||||
:model-value="!!selectedMap[props.row.id]"
|
||||
@update:model-value="(val) => toggleRowSelection(props.row.id, val)"
|
||||
:model-value="isRowSelected(props.row.productCode)"
|
||||
@update:model-value="(val) => onRowCheckboxChange(props.row, val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-calcAction="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
size="sm"
|
||||
color="primary"
|
||||
label="Hesapla"
|
||||
@click="calculateRow(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
@@ -306,17 +387,52 @@
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-xs">
|
||||
Hata: {{ store.error }}
|
||||
</q-banner>
|
||||
|
||||
<q-dialog v-model="bulkDialogOpen">
|
||||
<q-card style="min-width: 420px; max-width: 95vw;">
|
||||
<q-card-section class="text-subtitle1 text-weight-bold">
|
||||
Secili Olanlari Toplu Degistir
|
||||
</q-card-section>
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<q-select
|
||||
v-model="bulkField"
|
||||
:options="bulkFieldOptions"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
outlined
|
||||
label="Alan"
|
||||
/>
|
||||
<q-input
|
||||
v-model="bulkValue"
|
||||
dense
|
||||
outlined
|
||||
label="Deger"
|
||||
inputmode="decimal"
|
||||
/>
|
||||
<div class="text-caption text-grey-8">
|
||||
Uygulanacak satir sayisi: {{ selectedRowCount }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Iptal" v-close-popup />
|
||||
<q-btn color="primary" label="Uygula" @click="applyBulkUpdate" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||
|
||||
const store = useProductPricingStore()
|
||||
const FETCH_LIMIT = 500
|
||||
const nextCursor = ref('')
|
||||
const loadingMore = ref(false)
|
||||
const PAGE_LIMIT = 500
|
||||
const currentPage = ref(1)
|
||||
let reloadTimer = null
|
||||
|
||||
const usdToTry = 38.25
|
||||
const eurToTry = 41.6
|
||||
@@ -338,6 +454,8 @@ const currencyOptions = [
|
||||
|
||||
const multiFilterColumns = [
|
||||
{ field: 'productCode', label: 'Urun Kodu' },
|
||||
{ field: 'brandGroupSelection', label: 'Marka Grubu Secimi' },
|
||||
{ field: 'marka', label: 'Marka' },
|
||||
{ field: 'askiliYan', label: 'Askili Yan' },
|
||||
{ field: 'kategori', label: 'Kategori' },
|
||||
{ field: 'urunIlkGrubu', label: 'Urun Ilk Grubu' },
|
||||
@@ -346,10 +464,48 @@ const multiFilterColumns = [
|
||||
{ field: 'icerik', label: 'Icerik' },
|
||||
{ field: 'karisim', label: 'Karisim' }
|
||||
]
|
||||
const serverBackedMultiFilterFields = new Set([
|
||||
'productCode',
|
||||
'brandGroupSelection',
|
||||
'marka',
|
||||
'askiliYan',
|
||||
'kategori',
|
||||
'urunIlkGrubu',
|
||||
'urunAnaGrubu',
|
||||
'urunAltGrubu',
|
||||
'icerik',
|
||||
'karisim'
|
||||
])
|
||||
const numberRangeFilterFields = ['stockQty']
|
||||
const dateRangeFilterFields = ['stockEntryDate', 'lastPricingDate']
|
||||
const valueFilterFields = [
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry',
|
||||
'usd1',
|
||||
'usd2',
|
||||
'usd3',
|
||||
'usd4',
|
||||
'usd5',
|
||||
'usd6',
|
||||
'eur1',
|
||||
'eur2',
|
||||
'eur3',
|
||||
'eur4',
|
||||
'eur5',
|
||||
'eur6',
|
||||
'try1',
|
||||
'try2',
|
||||
'try3',
|
||||
'try4',
|
||||
'try5',
|
||||
'try6'
|
||||
]
|
||||
const columnFilters = ref({
|
||||
productCode: [],
|
||||
brandGroupSelection: [],
|
||||
marka: [],
|
||||
askiliYan: [],
|
||||
kategori: [],
|
||||
urunIlkGrubu: [],
|
||||
@@ -360,6 +516,8 @@ const columnFilters = ref({
|
||||
})
|
||||
const columnFilterSearch = ref({
|
||||
productCode: '',
|
||||
brandGroupSelection: '',
|
||||
marka: '',
|
||||
askiliYan: '',
|
||||
kategori: '',
|
||||
urunIlkGrubu: '',
|
||||
@@ -375,23 +533,30 @@ const dateRangeFilters = ref({
|
||||
stockEntryDate: { from: '', to: '' },
|
||||
lastPricingDate: { from: '', to: '' }
|
||||
})
|
||||
const valueFilters = ref(Object.fromEntries(valueFilterFields.map((field) => [field, []])))
|
||||
const valueFilterSearch = ref(Object.fromEntries(valueFilterFields.map((field) => [field, ''])))
|
||||
const multiSelectFilterFieldSet = new Set(multiFilterColumns.map((x) => x.field))
|
||||
const numberRangeFilterFieldSet = new Set(numberRangeFilterFields)
|
||||
const dateRangeFilterFieldSet = new Set(dateRangeFilterFields)
|
||||
const valueSelectFilterFieldSet = new Set(valueFilterFields)
|
||||
const headerFilterFieldSet = new Set([
|
||||
...multiFilterColumns.map((x) => x.field),
|
||||
...numberRangeFilterFields,
|
||||
...dateRangeFilterFields
|
||||
...dateRangeFilterFields,
|
||||
...valueFilterFields
|
||||
])
|
||||
|
||||
const mainTableRef = ref(null)
|
||||
const tablePagination = ref({
|
||||
page: 1,
|
||||
page: 1, // server-side paging var; q-table local paging kapali
|
||||
rowsPerPage: 0,
|
||||
sortBy: 'productCode',
|
||||
descending: false
|
||||
sortBy: 'stockQty',
|
||||
descending: true
|
||||
})
|
||||
const selectedMap = ref({})
|
||||
const bulkDialogOpen = ref(false)
|
||||
const bulkField = ref('expenseForBasePrice')
|
||||
const bulkValue = ref('')
|
||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||
const showSelectedOnly = ref(false)
|
||||
|
||||
@@ -437,7 +602,10 @@ function col (name, label, field, width, extra = {}) {
|
||||
|
||||
const allColumns = [
|
||||
col('select', '', 'select', 40, { align: 'center', classes: 'text-center selection-col' }),
|
||||
col('brandGroupSelection', 'MARKA GRUBU SECIMI', 'brandGroupSelection', 76),
|
||||
col('marka', 'MARKA', 'marka', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('productCode', 'URUN KODU', 'productCode', 108, { sortable: true, classes: 'ps-col product-code-col' }),
|
||||
col('calcAction', 'HESAPLA', 'calcAction', 72, { align: 'center', classes: 'ps-col' }),
|
||||
col('stockQty', 'STOK ADET', 'stockQty', 72, { align: 'right', sortable: true, classes: 'ps-col stock-col' }),
|
||||
col('stockEntryDate', 'STOK GIRIS TARIHI', 'stockEntryDate', 92, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('lastPricingDate', 'SON FIYATLANDIRMA TARIHI', 'lastPricingDate', 108, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
@@ -448,8 +616,6 @@ const allColumns = [
|
||||
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('karisim', 'KARISIM', 'karisim', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('marka', 'MARKA', 'marka', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('brandGroupSelection', 'MARKA GRUBU SECIMI', 'brandGroupSelection', 76),
|
||||
col('costPrice', 'MALIYET FIYATI', 'costPrice', 74, { align: 'right', sortable: true, classes: 'usd-col' }),
|
||||
col('expenseForBasePrice', 'TABAN FIYAT MASRAF', 'expenseForBasePrice', 86, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 74, { align: 'right', classes: 'usd-col' }),
|
||||
@@ -476,7 +642,10 @@ const allColumns = [
|
||||
|
||||
const stickyColumnNames = [
|
||||
'select',
|
||||
'brandGroupSelection',
|
||||
'marka',
|
||||
'productCode',
|
||||
'calcAction',
|
||||
'stockQty',
|
||||
'stockEntryDate',
|
||||
'lastPricingDate',
|
||||
@@ -487,8 +656,6 @@ const stickyColumnNames = [
|
||||
'urunAltGrubu',
|
||||
'icerik',
|
||||
'karisim',
|
||||
'marka',
|
||||
'brandGroupSelection',
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
@@ -531,6 +698,17 @@ const tableStyle = computed(() => ({
|
||||
}))
|
||||
|
||||
const rows = computed(() => store.rows || [])
|
||||
const tableLoading = computed(() => Boolean(store.loading) && rows.value.length === 0)
|
||||
const bulkFieldOptions = computed(() => {
|
||||
return editableColumns
|
||||
.map((name) => {
|
||||
const colDef = allColumns.find((c) => c.field === name)
|
||||
return {
|
||||
value: name,
|
||||
label: colDef?.label || name
|
||||
}
|
||||
})
|
||||
})
|
||||
const multiFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
multiFilterColumns.forEach(({ field }) => {
|
||||
@@ -556,13 +734,52 @@ const filteredFilterOptionMap = computed(() => {
|
||||
})
|
||||
return map
|
||||
})
|
||||
const valueFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
valueFilterFields.forEach((field) => {
|
||||
const uniq = new Set()
|
||||
rows.value.forEach((row) => {
|
||||
uniq.add(toValueFilterKey(row?.[field]))
|
||||
})
|
||||
map[field] = Array.from(uniq)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.map((v) => ({ label: formatPrice(v), value: v }))
|
||||
})
|
||||
return map
|
||||
})
|
||||
const filteredValueFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
valueFilterFields.forEach((field) => {
|
||||
const search = String(valueFilterSearch.value[field] || '').trim().toLocaleLowerCase('tr')
|
||||
const options = valueFilterOptionMap.value[field] || []
|
||||
map[field] = search
|
||||
? options.filter((option) => option.label.toLocaleLowerCase('tr').includes(search))
|
||||
: options
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
function rowSelectionKey (row) {
|
||||
const code = String(row?.productCode ?? '').trim()
|
||||
if (code) return code
|
||||
return String(row?.id ?? '')
|
||||
}
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter((row) => {
|
||||
if (showSelectedOnly.value && !selectedMap.value[row.id]) return false
|
||||
for (const mf of multiFilterColumns) {
|
||||
const selected = columnFilters.value[mf.field] || []
|
||||
if (selected.length > 0 && !selected.includes(String(row?.[mf.field] ?? '').trim())) return false
|
||||
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
||||
for (const { field } of multiFilterColumns) {
|
||||
// Server-backed filters already reload full dataset (all pages) from backend.
|
||||
// Keep only non-server multi filters (e.g. brandGroupSelection) as local page filter.
|
||||
if (serverBackedMultiFilterFields.has(field)) continue
|
||||
const selected = columnFilters.value[field] || []
|
||||
if (selected.length <= 0) continue
|
||||
const rowVal = String(row?.[field] ?? '').trim()
|
||||
if (!selected.includes(rowVal)) return false
|
||||
}
|
||||
for (const field of valueFilterFields) {
|
||||
const selected = valueFilters.value[field] || []
|
||||
if (selected.length > 0 && !selected.includes(toValueFilterKey(row?.[field]))) return false
|
||||
}
|
||||
const stockQtyMin = parseNullableNumber(numberRangeFilters.value.stockQty?.min)
|
||||
const stockQtyMax = parseNullableNumber(numberRangeFilters.value.stockQty?.max)
|
||||
@@ -575,12 +792,11 @@ const filteredRows = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const visibleRowIds = computed(() => filteredRows.value.map((row) => row.id))
|
||||
const visibleRowIds = computed(() => filteredRows.value.map((row) => rowSelectionKey(row)))
|
||||
const selectedRowCount = computed(() => Object.values(selectedMap.value).filter(Boolean).length)
|
||||
const selectedVisibleCount = computed(() => visibleRowIds.value.filter((id) => !!selectedMap.value[id]).length)
|
||||
const allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||
const hasMoreRows = computed(() => Boolean(store.hasMore))
|
||||
|
||||
function isHeaderFilterField (field) {
|
||||
return headerFilterFieldSet.has(field)
|
||||
@@ -598,8 +814,13 @@ function isDateRangeFilterField (field) {
|
||||
return dateRangeFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function isValueSelectFilterField (field) {
|
||||
return valueSelectFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function hasFilter (field) {
|
||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
|
||||
if (isValueSelectFilterField(field)) return (valueFilters.value[field] || []).length > 0
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
const filter = numberRangeFilters.value[field]
|
||||
return !!String(filter?.min || '').trim() || !!String(filter?.max || '').trim()
|
||||
@@ -613,6 +834,7 @@ function hasFilter (field) {
|
||||
|
||||
function getFilterBadgeValue (field) {
|
||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
|
||||
if (isValueSelectFilterField(field)) return (valueFilters.value[field] || []).length
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
const filter = numberRangeFilters.value[field]
|
||||
return [filter?.min, filter?.max].filter((x) => String(x || '').trim()).length
|
||||
@@ -625,10 +847,18 @@ function getFilterBadgeValue (field) {
|
||||
}
|
||||
|
||||
function clearColumnFilter (field) {
|
||||
if (!isMultiSelectFilterField(field)) return
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: []
|
||||
if (isMultiSelectFilterField(field)) {
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: []
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isValueSelectFilterField(field)) {
|
||||
valueFilters.value = {
|
||||
...valueFilters.value,
|
||||
[field]: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,17 +879,27 @@ function clearRangeFilter (field) {
|
||||
}
|
||||
|
||||
function getFilterOptionsForField (field) {
|
||||
if (isValueSelectFilterField(field)) return filteredValueFilterOptionMap.value[field] || []
|
||||
return filteredFilterOptionMap.value[field] || []
|
||||
}
|
||||
|
||||
function isColumnFilterValueSelected (field, value) {
|
||||
if (isValueSelectFilterField(field)) return (valueFilters.value[field] || []).includes(value)
|
||||
return (columnFilters.value[field] || []).includes(value)
|
||||
}
|
||||
|
||||
function toggleColumnFilterValue (field, value) {
|
||||
const current = new Set(columnFilters.value[field] || [])
|
||||
const target = isValueSelectFilterField(field) ? valueFilters.value : columnFilters.value
|
||||
const current = new Set(target[field] || [])
|
||||
if (current.has(value)) current.delete(value)
|
||||
else current.add(value)
|
||||
if (isValueSelectFilterField(field)) {
|
||||
valueFilters.value = {
|
||||
...valueFilters.value,
|
||||
[field]: Array.from(current)
|
||||
}
|
||||
return
|
||||
}
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: Array.from(current)
|
||||
@@ -668,6 +908,13 @@ function toggleColumnFilterValue (field, value) {
|
||||
|
||||
function selectAllColumnFilterOptions (field) {
|
||||
const options = getFilterOptionsForField(field)
|
||||
if (isValueSelectFilterField(field)) {
|
||||
valueFilters.value = {
|
||||
...valueFilters.value,
|
||||
[field]: options.map((option) => option.value)
|
||||
}
|
||||
return
|
||||
}
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: options.map((option) => option.value)
|
||||
@@ -701,6 +948,10 @@ function round2 (value) {
|
||||
return Number(Number(value || 0).toFixed(2))
|
||||
}
|
||||
|
||||
function toValueFilterKey (value) {
|
||||
return round2(parseNumber(value)).toFixed(2)
|
||||
}
|
||||
|
||||
function parseNumber (val) {
|
||||
if (typeof val === 'number') return Number.isFinite(val) ? val : 0
|
||||
const text = String(val ?? '').trim().replace(/\s/g, '')
|
||||
@@ -789,12 +1040,27 @@ function onEditableCellChange (row, field, val) {
|
||||
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') recalcByBasePrice(row)
|
||||
}
|
||||
|
||||
function calculateRow (row) {
|
||||
if (!row) return
|
||||
recalcByBasePrice(row)
|
||||
toggleRowSelection(rowSelectionKey(row), true)
|
||||
}
|
||||
|
||||
function onBrandGroupSelectionChange (row, val) {
|
||||
store.updateBrandGroupSelection(row, val)
|
||||
}
|
||||
|
||||
function toggleRowSelection (rowId, val) {
|
||||
selectedMap.value = { ...selectedMap.value, [rowId]: !!val }
|
||||
function isRowSelected (rowKey) {
|
||||
return !!selectedMap.value[rowKey]
|
||||
}
|
||||
|
||||
function onRowCheckboxChange (row, val) {
|
||||
if (!row) return
|
||||
toggleRowSelection(rowSelectionKey(row), val)
|
||||
}
|
||||
|
||||
function toggleRowSelection (rowKey, val) {
|
||||
selectedMap.value = { ...selectedMap.value, [rowKey]: !!val }
|
||||
}
|
||||
|
||||
function toggleSelectAllVisible (val) {
|
||||
@@ -803,9 +1069,25 @@ function toggleSelectAllVisible (val) {
|
||||
selectedMap.value = next
|
||||
}
|
||||
|
||||
function applyBulkUpdate () {
|
||||
const field = String(bulkField.value || '').trim()
|
||||
if (!field || !editableColumnSet.has(field)) return
|
||||
const parsed = parseNumber(bulkValue.value)
|
||||
rows.value.forEach((row) => {
|
||||
if (!isRowSelected(rowSelectionKey(row))) return
|
||||
store.updateCell(row, field, parsed)
|
||||
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') {
|
||||
recalcByBasePrice(row)
|
||||
}
|
||||
})
|
||||
bulkDialogOpen.value = false
|
||||
}
|
||||
|
||||
function resetAll () {
|
||||
columnFilters.value = {
|
||||
productCode: [],
|
||||
brandGroupSelection: [],
|
||||
marka: [],
|
||||
askiliYan: [],
|
||||
kategori: [],
|
||||
urunIlkGrubu: [],
|
||||
@@ -816,6 +1098,8 @@ function resetAll () {
|
||||
}
|
||||
columnFilterSearch.value = {
|
||||
productCode: '',
|
||||
brandGroupSelection: '',
|
||||
marka: '',
|
||||
askiliYan: '',
|
||||
kategori: '',
|
||||
urunIlkGrubu: '',
|
||||
@@ -824,6 +1108,8 @@ function resetAll () {
|
||||
icerik: '',
|
||||
karisim: ''
|
||||
}
|
||||
valueFilters.value = Object.fromEntries(valueFilterFields.map((field) => [field, []]))
|
||||
valueFilterSearch.value = Object.fromEntries(valueFilterFields.map((field) => [field, '']))
|
||||
numberRangeFilters.value = {
|
||||
stockQty: { min: '', max: '' }
|
||||
}
|
||||
@@ -863,53 +1149,57 @@ function clearAllCurrencies () {
|
||||
selectedCurrencies.value = []
|
||||
}
|
||||
|
||||
async function fetchChunk ({ reset = false } = {}) {
|
||||
const afterProductCode = reset ? '' : nextCursor.value
|
||||
function onPaginationChange (next) {
|
||||
tablePagination.value = {
|
||||
...tablePagination.value,
|
||||
...(next || {}),
|
||||
page: 1,
|
||||
rowsPerPage: 0
|
||||
}
|
||||
}
|
||||
|
||||
function buildServerFilters () {
|
||||
return {
|
||||
product_code: columnFilters.value.productCode || [],
|
||||
brand_group_selection: columnFilters.value.brandGroupSelection || [],
|
||||
marka: columnFilters.value.marka || [],
|
||||
askili_yan: columnFilters.value.askiliYan || [],
|
||||
kategori: columnFilters.value.kategori || [],
|
||||
urun_ilk_grubu: columnFilters.value.urunIlkGrubu || [],
|
||||
urun_ana_grubu: columnFilters.value.urunAnaGrubu || [],
|
||||
urun_alt_grubu: columnFilters.value.urunAltGrubu || [],
|
||||
icerik: columnFilters.value.icerik || [],
|
||||
karisim: columnFilters.value.karisim || []
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReload () {
|
||||
if (reloadTimer) clearTimeout(reloadTimer)
|
||||
reloadTimer = setTimeout(() => {
|
||||
reloadTimer = null
|
||||
void reloadData({ page: 1 })
|
||||
}, 180)
|
||||
}
|
||||
|
||||
async function fetchChunk ({ page = 1 } = {}) {
|
||||
const result = await store.fetchRows({
|
||||
limit: FETCH_LIMIT,
|
||||
afterProductCode,
|
||||
append: !reset
|
||||
limit: PAGE_LIMIT,
|
||||
page,
|
||||
append: false,
|
||||
silent: false,
|
||||
filters: buildServerFilters()
|
||||
})
|
||||
const fetched = Number(result?.fetched) || 0
|
||||
nextCursor.value = String(result?.nextCursor || '')
|
||||
return fetched
|
||||
currentPage.value = Number(result?.page) || page
|
||||
return Number(result?.fetched) || 0
|
||||
}
|
||||
|
||||
async function loadMoreRows () {
|
||||
if (loadingMore.value || store.loading || !hasMoreRows.value) return
|
||||
loadingMore.value = true
|
||||
try {
|
||||
await fetchChunk({ reset: false })
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onTableVirtualScroll (details) {
|
||||
const to = Number(details?.to || 0)
|
||||
if (!Number.isFinite(to)) return
|
||||
if (to >= filteredRows.value.length - 25) {
|
||||
void loadMoreRows()
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureEnoughVisibleRows (minRows = 80, maxBatches = 4) {
|
||||
let guard = 0
|
||||
while (hasMoreRows.value && filteredRows.value.length < minRows && guard < maxBatches) {
|
||||
await loadMoreRows()
|
||||
guard++
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData () {
|
||||
async function reloadData ({ page = 1 } = {}) {
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][ui] reload:start', {
|
||||
at: new Date(startedAt).toISOString()
|
||||
})
|
||||
try {
|
||||
nextCursor.value = ''
|
||||
await fetchChunk({ reset: true })
|
||||
await ensureEnoughVisibleRows(120, 6)
|
||||
await fetchChunk({ page })
|
||||
} catch (err) {
|
||||
console.error('[product-pricing][ui] reload:error', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
@@ -924,20 +1214,28 @@ async function reloadData () {
|
||||
selectedMap.value = {}
|
||||
}
|
||||
|
||||
function onPageChange (page) {
|
||||
const p = Number(page) > 0 ? Number(page) : 1
|
||||
if (store.loading) return
|
||||
if (p === currentPage.value && p === (store.page || 1)) return
|
||||
currentPage.value = p
|
||||
void reloadData({ page: p })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await reloadData()
|
||||
await reloadData({ page: currentPage.value })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (reloadTimer) {
|
||||
clearTimeout(reloadTimer)
|
||||
reloadTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
columnFilters,
|
||||
numberRangeFilters,
|
||||
dateRangeFilters,
|
||||
showSelectedOnly,
|
||||
() => tablePagination.value.sortBy,
|
||||
() => tablePagination.value.descending
|
||||
],
|
||||
() => { void ensureEnoughVisibleRows(80, 4) },
|
||||
[columnFilters],
|
||||
() => { scheduleReload() },
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -77,33 +77,175 @@ function mapRow (raw, index, baseIndex = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
function cloneRows (rows = []) {
|
||||
return rows.map((r) => ({ ...r }))
|
||||
}
|
||||
|
||||
function normalizeFilterList (list) {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map((x) => toText(x)).filter(Boolean).sort()
|
||||
}
|
||||
|
||||
function normalizeFilters (filters = {}) {
|
||||
const keys = ['product_code', 'brand_group_selection', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'karisim', 'marka']
|
||||
const out = {}
|
||||
for (const key of keys) out[key] = normalizeFilterList(filters[key])
|
||||
const q = toText(filters.q)
|
||||
if (q) out.q = q
|
||||
return out
|
||||
}
|
||||
|
||||
function makeCacheKey (limit, page, filters) {
|
||||
return JSON.stringify({
|
||||
limit: Number(limit) || 500,
|
||||
page: Number(page) || 1,
|
||||
filters: normalizeFilters(filters)
|
||||
})
|
||||
}
|
||||
|
||||
export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
state: () => ({
|
||||
rows: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
hasMore: true
|
||||
hasMore: true,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
totalCount: 0,
|
||||
pageCache: {},
|
||||
cacheOrder: [],
|
||||
prefetchInFlight: {}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchRows (options = {}) {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
cachePut (key, value) {
|
||||
this.pageCache[key] = value
|
||||
this.cacheOrder = this.cacheOrder.filter((x) => x !== key)
|
||||
this.cacheOrder.push(key)
|
||||
while (this.cacheOrder.length > 24) {
|
||||
const oldest = this.cacheOrder.shift()
|
||||
if (oldest) delete this.pageCache[oldest]
|
||||
}
|
||||
},
|
||||
|
||||
cacheGet (key) {
|
||||
return this.pageCache[key] || null
|
||||
},
|
||||
|
||||
applyPageResult (payload = {}, requestedPage = 1) {
|
||||
const data = Array.isArray(payload?.rows) ? payload.rows : []
|
||||
this.rows = cloneRows(data)
|
||||
this.totalCount = Number.isFinite(payload?.totalCount) ? payload.totalCount : 0
|
||||
this.totalPages = Math.max(1, Number(payload?.totalPages || 1))
|
||||
this.page = Math.max(1, Number(payload?.page || requestedPage))
|
||||
this.hasMore = this.page < this.totalPages
|
||||
},
|
||||
|
||||
async prefetchPage (options = {}) {
|
||||
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
|
||||
const afterProductCode = toText(options?.afterProductCode)
|
||||
const page = Number(options?.page) > 0 ? Number(options.page) : 1
|
||||
const filters = normalizeFilters(options?.filters || {})
|
||||
const key = makeCacheKey(limit, page, filters)
|
||||
if (this.pageCache[key]) return
|
||||
if (this.prefetchInFlight[key]) {
|
||||
await this.prefetchInFlight[key]
|
||||
return
|
||||
}
|
||||
const run = async () => {
|
||||
try {
|
||||
const params = { limit, page }
|
||||
for (const k of Object.keys(filters)) {
|
||||
if (k === 'q') {
|
||||
params.q = filters.q
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(filters[k]) && filters[k].length > 0) {
|
||||
params[k] = filters[k].join(',')
|
||||
}
|
||||
}
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
url: '/pricing/products',
|
||||
params,
|
||||
timeout: 180000
|
||||
})
|
||||
const totalCount = Number(res?.headers?.['x-total-count'] || 0)
|
||||
const totalPages = Math.max(1, Number(res?.headers?.['x-total-pages'] || 1))
|
||||
const currentPage = Math.max(1, Number(res?.headers?.['x-page'] || page))
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
const mapped = data.map((x, i) => mapRow(x, i, 0))
|
||||
this.cachePut(key, {
|
||||
rows: mapped,
|
||||
totalCount: Number.isFinite(totalCount) ? totalCount : 0,
|
||||
totalPages: Number.isFinite(totalPages) ? totalPages : 1,
|
||||
page: currentPage
|
||||
})
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
this.prefetchInFlight[key] = run()
|
||||
try {
|
||||
await this.prefetchInFlight[key]
|
||||
} finally {
|
||||
delete this.prefetchInFlight[key]
|
||||
}
|
||||
},
|
||||
|
||||
async fetchRows (options = {}) {
|
||||
const silent = Boolean(options?.silent)
|
||||
if (!silent) {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
}
|
||||
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
|
||||
const page = Number(options?.page) > 0 ? Number(options.page) : 1
|
||||
const append = Boolean(options?.append)
|
||||
const baseIndex = append ? this.rows.length : 0
|
||||
const filters = normalizeFilters(options?.filters || {})
|
||||
const cacheKey = makeCacheKey(limit, page, filters)
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][frontend] request:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
timeout_ms: 180000,
|
||||
limit,
|
||||
after_product_code: afterProductCode || null,
|
||||
page,
|
||||
append
|
||||
})
|
||||
try {
|
||||
const params = { limit }
|
||||
if (afterProductCode) params.after_product_code = afterProductCode
|
||||
if (options?.useCache !== false) {
|
||||
const inFlight = this.prefetchInFlight[cacheKey]
|
||||
if (inFlight) {
|
||||
await inFlight
|
||||
}
|
||||
const cached = this.cacheGet(cacheKey)
|
||||
if (cached) {
|
||||
this.applyPageResult(cached, page)
|
||||
console.info('[product-pricing][frontend] request:cache-hit', {
|
||||
page: this.page,
|
||||
total_pages: this.totalPages,
|
||||
row_count: this.rows.length,
|
||||
duration_ms: Date.now() - startedAt
|
||||
})
|
||||
return {
|
||||
traceId: null,
|
||||
fetched: this.rows.length,
|
||||
hasMore: this.hasMore,
|
||||
page: this.page,
|
||||
totalPages: this.totalPages,
|
||||
totalCount: this.totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const params = { limit, page }
|
||||
for (const key of Object.keys(filters)) {
|
||||
if (key === 'q') {
|
||||
params.q = filters.q
|
||||
continue
|
||||
}
|
||||
const list = filters[key]
|
||||
if (Array.isArray(list) && list.length > 0) params[key] = list.join(',')
|
||||
}
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
url: '/pricing/products',
|
||||
@@ -111,44 +253,48 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
timeout: 180000
|
||||
})
|
||||
const traceId = res?.headers?.['x-trace-id'] || null
|
||||
const hasMoreHeader = String(res?.headers?.['x-has-more'] || '').toLowerCase()
|
||||
const nextCursorHeader = toText(res?.headers?.['x-next-cursor'])
|
||||
const totalCount = Number(res?.headers?.['x-total-count'] || 0)
|
||||
const totalPages = Math.max(1, Number(res?.headers?.['x-total-pages'] || 1))
|
||||
const currentPage = Math.max(1, Number(res?.headers?.['x-page'] || page))
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
const mapped = data.map((x, i) => mapRow(x, i, baseIndex))
|
||||
const fallbackNextCursor = mapped.length > 0
|
||||
? toText(mapped[mapped.length - 1]?.productCode)
|
||||
: ''
|
||||
const nextCursor = nextCursorHeader || fallbackNextCursor
|
||||
if (append) {
|
||||
const merged = [...this.rows]
|
||||
const seen = new Set(this.rows.map((x) => x?.productCode))
|
||||
for (const row of mapped) {
|
||||
const key = row?.productCode
|
||||
if (key && seen.has(key)) continue
|
||||
merged.push(row)
|
||||
if (key) seen.add(key)
|
||||
}
|
||||
this.rows = merged
|
||||
} else {
|
||||
this.rows = mapped
|
||||
const payload = {
|
||||
rows: mapped,
|
||||
totalCount: Number.isFinite(totalCount) ? totalCount : 0,
|
||||
totalPages: Number.isFinite(totalPages) ? totalPages : 1,
|
||||
page: Number.isFinite(currentPage) ? currentPage : page
|
||||
}
|
||||
this.cachePut(cacheKey, payload)
|
||||
this.applyPageResult(payload, page)
|
||||
|
||||
// Background prefetch for next page to reduce perceived wait on page change.
|
||||
if (this.page < this.totalPages) {
|
||||
void this.prefetchPage({
|
||||
limit,
|
||||
page: this.page + 1,
|
||||
filters
|
||||
})
|
||||
}
|
||||
this.hasMore = hasMoreHeader ? hasMoreHeader === 'true' : mapped.length === limit
|
||||
console.info('[product-pricing][frontend] request:success', {
|
||||
trace_id: traceId,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
row_count: this.rows.length,
|
||||
fetched_count: mapped.length,
|
||||
has_more: this.hasMore,
|
||||
next_cursor: nextCursor || null
|
||||
page: this.page,
|
||||
total_pages: this.totalPages,
|
||||
total_count: this.totalCount
|
||||
})
|
||||
return {
|
||||
traceId,
|
||||
fetched: mapped.length,
|
||||
hasMore: this.hasMore,
|
||||
nextCursor
|
||||
page: this.page,
|
||||
totalPages: this.totalPages,
|
||||
totalCount: this.totalCount
|
||||
}
|
||||
} catch (err) {
|
||||
if (!append) this.rows = []
|
||||
this.rows = []
|
||||
this.hasMore = false
|
||||
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||
this.error = toText(msg)
|
||||
@@ -161,7 +307,7 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
})
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
if (!silent) this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user