Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -6,21 +6,162 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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 {
|
if limit <= 0 {
|
||||||
limit = 500
|
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).
|
// Stage 1: fetch only paged products first (fast path).
|
||||||
productQuery := `
|
productQuery := `
|
||||||
SELECT TOP (` + strconv.Itoa(limit) + `)
|
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,
|
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||||
|
` + brandGroupExpr + ` AS BrandGroupSec,
|
||||||
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
|
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
|
||||||
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
|
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
|
||||||
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
|
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
|
||||||
@@ -30,11 +171,44 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
|||||||
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
|
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
|
||||||
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
|
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka
|
||||||
FROM ProductFilterWithDescription('TR')
|
FROM ProductFilterWithDescription('TR')
|
||||||
WHERE ProductAtt42 IN ('SERI', 'AKSESUAR')
|
WHERE ` + whereSQL + `
|
||||||
AND IsBlocked = 0
|
) f
|
||||||
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
GROUP BY f.ProductCode;
|
||||||
AND (@p1 = '' OR LTRIM(RTRIM(ProductCode)) > @p1)
|
|
||||||
ORDER BY LTRIM(RTRIM(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 (
|
var (
|
||||||
@@ -43,7 +217,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
|||||||
)
|
)
|
||||||
for attempt := 1; attempt <= 3; attempt++ {
|
for attempt := 1; attempt <= 3; attempt++ {
|
||||||
var err error
|
var err error
|
||||||
rows, err = db.MssqlDB.QueryContext(ctx, productQuery, afterProductCode)
|
rows, err = db.MssqlDB.QueryContext(ctx, productQuery, args...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rowsErr = nil
|
rowsErr = nil
|
||||||
break
|
break
|
||||||
@@ -60,7 +234,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rowsErr != nil {
|
if rowsErr != nil {
|
||||||
return nil, rowsErr
|
return result, rowsErr
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
@@ -69,6 +243,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
|||||||
var item models.ProductPricing
|
var item models.ProductPricing
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&item.ProductCode,
|
&item.ProductCode,
|
||||||
|
&item.BrandGroupSec,
|
||||||
&item.AskiliYan,
|
&item.AskiliYan,
|
||||||
&item.Kategori,
|
&item.Kategori,
|
||||||
&item.UrunIlkGrubu,
|
&item.UrunIlkGrubu,
|
||||||
@@ -78,15 +253,16 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
|||||||
&item.Karisim,
|
&item.Karisim,
|
||||||
&item.Marka,
|
&item.Marka,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return result, err
|
||||||
}
|
}
|
||||||
out = append(out, item)
|
out = append(out, item)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
return result, err
|
||||||
}
|
}
|
||||||
if len(out) == 0 {
|
if len(out) == 0 {
|
||||||
return out, nil
|
result.Rows = out
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 2: fetch metrics only for paged product codes.
|
// 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
|
WHERE s.ItemTypeCode = 1
|
||||||
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||||
AND s.In_Qty1 > 0
|
AND s.In_Qty1 > 0
|
||||||
|
AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP'
|
||||||
AND LTRIM(RTRIM(s.WarehouseCode)) IN (
|
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-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
|
||||||
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
|
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
|
||||||
@@ -214,7 +391,7 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
|||||||
|
|
||||||
metricsRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...)
|
metricsRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("metrics query failed: %w", err)
|
return result, fmt.Errorf("metrics query failed: %w", err)
|
||||||
}
|
}
|
||||||
defer metricsRows.Close()
|
defer metricsRows.Close()
|
||||||
|
|
||||||
@@ -237,12 +414,12 @@ func GetProductPricingList(ctx context.Context, limit int, afterProductCode stri
|
|||||||
&m.StockEntryDate,
|
&m.StockEntryDate,
|
||||||
&m.LastPricingDate,
|
&m.LastPricingDate,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return result, err
|
||||||
}
|
}
|
||||||
metricsByCode[strings.TrimSpace(code)] = m
|
metricsByCode[strings.TrimSpace(code)] = m
|
||||||
}
|
}
|
||||||
if err := metricsRows.Err(); err != nil {
|
if err := metricsRows.Err(); err != nil {
|
||||||
return nil, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range out {
|
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 {
|
func isTransientMSSQLNetworkError(err error) bool {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/lib/pq"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -497,25 +498,65 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
// ROLES
|
// ROLES
|
||||||
for _, role := range payload.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
|
// DEPARTMENTS
|
||||||
for _, d := range payload.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
|
// PIYASALAR
|
||||||
for _, p := range payload.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
|
// NEBIM
|
||||||
for _, n := range payload.NebimUsers {
|
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 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)
|
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,31 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
limit := 500
|
limit := 500
|
||||||
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
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
|
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 err != nil {
|
||||||
if isPricingTimeoutLike(err, ctx.Err()) {
|
if isPricingTimeoutLike(err, ctx.Err()) {
|
||||||
log.Printf(
|
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)
|
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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(
|
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,
|
traceID,
|
||||||
claims.Username,
|
claims.Username,
|
||||||
claims.ID,
|
claims.ID,
|
||||||
|
pageResult.Page,
|
||||||
limit,
|
limit,
|
||||||
afterProductCode,
|
len(pageResult.Rows),
|
||||||
len(rows),
|
pageResult.TotalCount,
|
||||||
hasMore,
|
pageResult.TotalPages,
|
||||||
nextCursor,
|
|
||||||
time.Since(started).Milliseconds(),
|
time.Since(started).Milliseconds(),
|
||||||
)
|
)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
if hasMore {
|
w.Header().Set("X-Total-Count", strconv.Itoa(pageResult.TotalCount))
|
||||||
w.Header().Set("X-Has-More", "true")
|
w.Header().Set("X-Total-Pages", strconv.Itoa(pageResult.TotalPages))
|
||||||
} else {
|
w.Header().Set("X-Page", strconv.Itoa(pageResult.Page))
|
||||||
w.Header().Set("X-Has-More", "false")
|
_ = json.NewEncoder(w).Encode(pageResult.Rows)
|
||||||
}
|
|
||||||
if nextCursor != "" {
|
|
||||||
w.Header().Set("X-Next-Cursor", nextCursor)
|
|
||||||
}
|
|
||||||
_ = json.NewEncoder(w).Encode(rows)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPricingTraceID(r *http.Request) string {
|
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, "no connection could be made") ||
|
||||||
strings.Contains(e, "failed to respond")
|
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 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" 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -44,19 +64,15 @@
|
|||||||
class="pane-table pricing-table"
|
class="pane-table pricing-table"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
row-key="id"
|
row-key="productCode"
|
||||||
:rows="filteredRows"
|
:rows="filteredRows"
|
||||||
:columns="visibleColumns"
|
:columns="visibleColumns"
|
||||||
:loading="store.loading"
|
:loading="tableLoading"
|
||||||
virtual-scroll
|
|
||||||
:virtual-scroll-item-size="rowHeight"
|
|
||||||
:virtual-scroll-sticky-size-start="headerHeight"
|
|
||||||
:virtual-scroll-slice-size="36"
|
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
v-model:pagination="tablePagination"
|
:pagination="tablePagination"
|
||||||
hide-bottom
|
hide-bottom
|
||||||
:table-style="tableStyle"
|
:table-style="tableStyle"
|
||||||
@virtual-scroll="onTableVirtualScroll"
|
@update:pagination="onPaginationChange"
|
||||||
>
|
>
|
||||||
<template #header="props">
|
<template #header="props">
|
||||||
<q-tr :props="props" class="header-row-fixed">
|
<q-tr :props="props" class="header-row-fixed">
|
||||||
@@ -139,6 +155,54 @@
|
|||||||
Sonuc yok
|
Sonuc yok
|
||||||
</div>
|
</div>
|
||||||
</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 v-else-if="isNumberRangeFilterField(col.field)" class="excel-filter-menu">
|
||||||
<div class="range-filter-grid">
|
<div class="range-filter-grid">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -216,8 +280,25 @@
|
|||||||
<q-checkbox
|
<q-checkbox
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
:model-value="!!selectedMap[props.row.id]"
|
:model-value="isRowSelected(props.row.productCode)"
|
||||||
@update:model-value="(val) => toggleRowSelection(props.row.id, val)"
|
@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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
@@ -306,17 +387,52 @@
|
|||||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-xs">
|
<q-banner v-if="store.error" class="bg-red text-white q-mt-xs">
|
||||||
Hata: {{ store.error }}
|
Hata: {{ store.error }}
|
||||||
</q-banner>
|
</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>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||||
|
|
||||||
const store = useProductPricingStore()
|
const store = useProductPricingStore()
|
||||||
const FETCH_LIMIT = 500
|
const PAGE_LIMIT = 500
|
||||||
const nextCursor = ref('')
|
const currentPage = ref(1)
|
||||||
const loadingMore = ref(false)
|
let reloadTimer = null
|
||||||
|
|
||||||
const usdToTry = 38.25
|
const usdToTry = 38.25
|
||||||
const eurToTry = 41.6
|
const eurToTry = 41.6
|
||||||
@@ -338,6 +454,8 @@ const currencyOptions = [
|
|||||||
|
|
||||||
const multiFilterColumns = [
|
const multiFilterColumns = [
|
||||||
{ field: 'productCode', label: 'Urun Kodu' },
|
{ field: 'productCode', label: 'Urun Kodu' },
|
||||||
|
{ field: 'brandGroupSelection', label: 'Marka Grubu Secimi' },
|
||||||
|
{ field: 'marka', label: 'Marka' },
|
||||||
{ field: 'askiliYan', label: 'Askili Yan' },
|
{ field: 'askiliYan', label: 'Askili Yan' },
|
||||||
{ field: 'kategori', label: 'Kategori' },
|
{ field: 'kategori', label: 'Kategori' },
|
||||||
{ field: 'urunIlkGrubu', label: 'Urun Ilk Grubu' },
|
{ field: 'urunIlkGrubu', label: 'Urun Ilk Grubu' },
|
||||||
@@ -346,10 +464,48 @@ const multiFilterColumns = [
|
|||||||
{ field: 'icerik', label: 'Icerik' },
|
{ field: 'icerik', label: 'Icerik' },
|
||||||
{ field: 'karisim', label: 'Karisim' }
|
{ field: 'karisim', label: 'Karisim' }
|
||||||
]
|
]
|
||||||
|
const serverBackedMultiFilterFields = new Set([
|
||||||
|
'productCode',
|
||||||
|
'brandGroupSelection',
|
||||||
|
'marka',
|
||||||
|
'askiliYan',
|
||||||
|
'kategori',
|
||||||
|
'urunIlkGrubu',
|
||||||
|
'urunAnaGrubu',
|
||||||
|
'urunAltGrubu',
|
||||||
|
'icerik',
|
||||||
|
'karisim'
|
||||||
|
])
|
||||||
const numberRangeFilterFields = ['stockQty']
|
const numberRangeFilterFields = ['stockQty']
|
||||||
const dateRangeFilterFields = ['stockEntryDate', 'lastPricingDate']
|
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({
|
const columnFilters = ref({
|
||||||
productCode: [],
|
productCode: [],
|
||||||
|
brandGroupSelection: [],
|
||||||
|
marka: [],
|
||||||
askiliYan: [],
|
askiliYan: [],
|
||||||
kategori: [],
|
kategori: [],
|
||||||
urunIlkGrubu: [],
|
urunIlkGrubu: [],
|
||||||
@@ -360,6 +516,8 @@ const columnFilters = ref({
|
|||||||
})
|
})
|
||||||
const columnFilterSearch = ref({
|
const columnFilterSearch = ref({
|
||||||
productCode: '',
|
productCode: '',
|
||||||
|
brandGroupSelection: '',
|
||||||
|
marka: '',
|
||||||
askiliYan: '',
|
askiliYan: '',
|
||||||
kategori: '',
|
kategori: '',
|
||||||
urunIlkGrubu: '',
|
urunIlkGrubu: '',
|
||||||
@@ -375,23 +533,30 @@ const dateRangeFilters = ref({
|
|||||||
stockEntryDate: { from: '', to: '' },
|
stockEntryDate: { from: '', to: '' },
|
||||||
lastPricingDate: { 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 multiSelectFilterFieldSet = new Set(multiFilterColumns.map((x) => x.field))
|
||||||
const numberRangeFilterFieldSet = new Set(numberRangeFilterFields)
|
const numberRangeFilterFieldSet = new Set(numberRangeFilterFields)
|
||||||
const dateRangeFilterFieldSet = new Set(dateRangeFilterFields)
|
const dateRangeFilterFieldSet = new Set(dateRangeFilterFields)
|
||||||
|
const valueSelectFilterFieldSet = new Set(valueFilterFields)
|
||||||
const headerFilterFieldSet = new Set([
|
const headerFilterFieldSet = new Set([
|
||||||
...multiFilterColumns.map((x) => x.field),
|
...multiFilterColumns.map((x) => x.field),
|
||||||
...numberRangeFilterFields,
|
...numberRangeFilterFields,
|
||||||
...dateRangeFilterFields
|
...dateRangeFilterFields,
|
||||||
|
...valueFilterFields
|
||||||
])
|
])
|
||||||
|
|
||||||
const mainTableRef = ref(null)
|
const mainTableRef = ref(null)
|
||||||
const tablePagination = ref({
|
const tablePagination = ref({
|
||||||
page: 1,
|
page: 1, // server-side paging var; q-table local paging kapali
|
||||||
rowsPerPage: 0,
|
rowsPerPage: 0,
|
||||||
sortBy: 'productCode',
|
sortBy: 'stockQty',
|
||||||
descending: false
|
descending: true
|
||||||
})
|
})
|
||||||
const selectedMap = ref({})
|
const selectedMap = ref({})
|
||||||
|
const bulkDialogOpen = ref(false)
|
||||||
|
const bulkField = ref('expenseForBasePrice')
|
||||||
|
const bulkValue = ref('')
|
||||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||||
const showSelectedOnly = ref(false)
|
const showSelectedOnly = ref(false)
|
||||||
|
|
||||||
@@ -437,7 +602,10 @@ function col (name, label, field, width, extra = {}) {
|
|||||||
|
|
||||||
const allColumns = [
|
const allColumns = [
|
||||||
col('select', '', 'select', 40, { align: 'center', classes: 'text-center selection-col' }),
|
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('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('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('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' }),
|
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('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||||
col('icerik', 'ICERIK', 'icerik', 62, { 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('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('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('expenseForBasePrice', 'TABAN FIYAT MASRAF', 'expenseForBasePrice', 86, { align: 'right', classes: 'usd-col' }),
|
||||||
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 74, { align: 'right', classes: 'usd-col' }),
|
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 74, { align: 'right', classes: 'usd-col' }),
|
||||||
@@ -476,7 +642,10 @@ const allColumns = [
|
|||||||
|
|
||||||
const stickyColumnNames = [
|
const stickyColumnNames = [
|
||||||
'select',
|
'select',
|
||||||
|
'brandGroupSelection',
|
||||||
|
'marka',
|
||||||
'productCode',
|
'productCode',
|
||||||
|
'calcAction',
|
||||||
'stockQty',
|
'stockQty',
|
||||||
'stockEntryDate',
|
'stockEntryDate',
|
||||||
'lastPricingDate',
|
'lastPricingDate',
|
||||||
@@ -487,8 +656,6 @@ const stickyColumnNames = [
|
|||||||
'urunAltGrubu',
|
'urunAltGrubu',
|
||||||
'icerik',
|
'icerik',
|
||||||
'karisim',
|
'karisim',
|
||||||
'marka',
|
|
||||||
'brandGroupSelection',
|
|
||||||
'costPrice',
|
'costPrice',
|
||||||
'expenseForBasePrice',
|
'expenseForBasePrice',
|
||||||
'basePriceUsd',
|
'basePriceUsd',
|
||||||
@@ -531,6 +698,17 @@ const tableStyle = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const rows = computed(() => store.rows || [])
|
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 multiFilterOptionMap = computed(() => {
|
||||||
const map = {}
|
const map = {}
|
||||||
multiFilterColumns.forEach(({ field }) => {
|
multiFilterColumns.forEach(({ field }) => {
|
||||||
@@ -556,13 +734,52 @@ const filteredFilterOptionMap = computed(() => {
|
|||||||
})
|
})
|
||||||
return map
|
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(() => {
|
const filteredRows = computed(() => {
|
||||||
return rows.value.filter((row) => {
|
return rows.value.filter((row) => {
|
||||||
if (showSelectedOnly.value && !selectedMap.value[row.id]) return false
|
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
||||||
for (const mf of multiFilterColumns) {
|
for (const { field } of multiFilterColumns) {
|
||||||
const selected = columnFilters.value[mf.field] || []
|
// Server-backed filters already reload full dataset (all pages) from backend.
|
||||||
if (selected.length > 0 && !selected.includes(String(row?.[mf.field] ?? '').trim())) return false
|
// 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 stockQtyMin = parseNullableNumber(numberRangeFilters.value.stockQty?.min)
|
||||||
const stockQtyMax = parseNullableNumber(numberRangeFilters.value.stockQty?.max)
|
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 selectedRowCount = computed(() => Object.values(selectedMap.value).filter(Boolean).length)
|
||||||
const selectedVisibleCount = computed(() => visibleRowIds.value.filter((id) => !!selectedMap.value[id]).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 allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
||||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||||
const hasMoreRows = computed(() => Boolean(store.hasMore))
|
|
||||||
|
|
||||||
function isHeaderFilterField (field) {
|
function isHeaderFilterField (field) {
|
||||||
return headerFilterFieldSet.has(field)
|
return headerFilterFieldSet.has(field)
|
||||||
@@ -598,8 +814,13 @@ function isDateRangeFilterField (field) {
|
|||||||
return dateRangeFilterFieldSet.has(field)
|
return dateRangeFilterFieldSet.has(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValueSelectFilterField (field) {
|
||||||
|
return valueSelectFilterFieldSet.has(field)
|
||||||
|
}
|
||||||
|
|
||||||
function hasFilter (field) {
|
function hasFilter (field) {
|
||||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
|
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
|
||||||
|
if (isValueSelectFilterField(field)) return (valueFilters.value[field] || []).length > 0
|
||||||
if (isNumberRangeFilterField(field)) {
|
if (isNumberRangeFilterField(field)) {
|
||||||
const filter = numberRangeFilters.value[field]
|
const filter = numberRangeFilters.value[field]
|
||||||
return !!String(filter?.min || '').trim() || !!String(filter?.max || '').trim()
|
return !!String(filter?.min || '').trim() || !!String(filter?.max || '').trim()
|
||||||
@@ -613,6 +834,7 @@ function hasFilter (field) {
|
|||||||
|
|
||||||
function getFilterBadgeValue (field) {
|
function getFilterBadgeValue (field) {
|
||||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
|
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
|
||||||
|
if (isValueSelectFilterField(field)) return (valueFilters.value[field] || []).length
|
||||||
if (isNumberRangeFilterField(field)) {
|
if (isNumberRangeFilterField(field)) {
|
||||||
const filter = numberRangeFilters.value[field]
|
const filter = numberRangeFilters.value[field]
|
||||||
return [filter?.min, filter?.max].filter((x) => String(x || '').trim()).length
|
return [filter?.min, filter?.max].filter((x) => String(x || '').trim()).length
|
||||||
@@ -625,11 +847,19 @@ function getFilterBadgeValue (field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearColumnFilter (field) {
|
function clearColumnFilter (field) {
|
||||||
if (!isMultiSelectFilterField(field)) return
|
if (isMultiSelectFilterField(field)) {
|
||||||
columnFilters.value = {
|
columnFilters.value = {
|
||||||
...columnFilters.value,
|
...columnFilters.value,
|
||||||
[field]: []
|
[field]: []
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isValueSelectFilterField(field)) {
|
||||||
|
valueFilters.value = {
|
||||||
|
...valueFilters.value,
|
||||||
|
[field]: []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearRangeFilter (field) {
|
function clearRangeFilter (field) {
|
||||||
@@ -649,17 +879,27 @@ function clearRangeFilter (field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFilterOptionsForField (field) {
|
function getFilterOptionsForField (field) {
|
||||||
|
if (isValueSelectFilterField(field)) return filteredValueFilterOptionMap.value[field] || []
|
||||||
return filteredFilterOptionMap.value[field] || []
|
return filteredFilterOptionMap.value[field] || []
|
||||||
}
|
}
|
||||||
|
|
||||||
function isColumnFilterValueSelected (field, value) {
|
function isColumnFilterValueSelected (field, value) {
|
||||||
|
if (isValueSelectFilterField(field)) return (valueFilters.value[field] || []).includes(value)
|
||||||
return (columnFilters.value[field] || []).includes(value)
|
return (columnFilters.value[field] || []).includes(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleColumnFilterValue (field, 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)
|
if (current.has(value)) current.delete(value)
|
||||||
else current.add(value)
|
else current.add(value)
|
||||||
|
if (isValueSelectFilterField(field)) {
|
||||||
|
valueFilters.value = {
|
||||||
|
...valueFilters.value,
|
||||||
|
[field]: Array.from(current)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
columnFilters.value = {
|
columnFilters.value = {
|
||||||
...columnFilters.value,
|
...columnFilters.value,
|
||||||
[field]: Array.from(current)
|
[field]: Array.from(current)
|
||||||
@@ -668,6 +908,13 @@ function toggleColumnFilterValue (field, value) {
|
|||||||
|
|
||||||
function selectAllColumnFilterOptions (field) {
|
function selectAllColumnFilterOptions (field) {
|
||||||
const options = getFilterOptionsForField(field)
|
const options = getFilterOptionsForField(field)
|
||||||
|
if (isValueSelectFilterField(field)) {
|
||||||
|
valueFilters.value = {
|
||||||
|
...valueFilters.value,
|
||||||
|
[field]: options.map((option) => option.value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
columnFilters.value = {
|
columnFilters.value = {
|
||||||
...columnFilters.value,
|
...columnFilters.value,
|
||||||
[field]: options.map((option) => option.value)
|
[field]: options.map((option) => option.value)
|
||||||
@@ -701,6 +948,10 @@ function round2 (value) {
|
|||||||
return Number(Number(value || 0).toFixed(2))
|
return Number(Number(value || 0).toFixed(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toValueFilterKey (value) {
|
||||||
|
return round2(parseNumber(value)).toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
function parseNumber (val) {
|
function parseNumber (val) {
|
||||||
if (typeof val === 'number') return Number.isFinite(val) ? val : 0
|
if (typeof val === 'number') return Number.isFinite(val) ? val : 0
|
||||||
const text = String(val ?? '').trim().replace(/\s/g, '')
|
const text = String(val ?? '').trim().replace(/\s/g, '')
|
||||||
@@ -789,12 +1040,27 @@ function onEditableCellChange (row, field, val) {
|
|||||||
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') recalcByBasePrice(row)
|
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') recalcByBasePrice(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateRow (row) {
|
||||||
|
if (!row) return
|
||||||
|
recalcByBasePrice(row)
|
||||||
|
toggleRowSelection(rowSelectionKey(row), true)
|
||||||
|
}
|
||||||
|
|
||||||
function onBrandGroupSelectionChange (row, val) {
|
function onBrandGroupSelectionChange (row, val) {
|
||||||
store.updateBrandGroupSelection(row, val)
|
store.updateBrandGroupSelection(row, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleRowSelection (rowId, val) {
|
function isRowSelected (rowKey) {
|
||||||
selectedMap.value = { ...selectedMap.value, [rowId]: !!val }
|
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) {
|
function toggleSelectAllVisible (val) {
|
||||||
@@ -803,9 +1069,25 @@ function toggleSelectAllVisible (val) {
|
|||||||
selectedMap.value = next
|
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 () {
|
function resetAll () {
|
||||||
columnFilters.value = {
|
columnFilters.value = {
|
||||||
productCode: [],
|
productCode: [],
|
||||||
|
brandGroupSelection: [],
|
||||||
|
marka: [],
|
||||||
askiliYan: [],
|
askiliYan: [],
|
||||||
kategori: [],
|
kategori: [],
|
||||||
urunIlkGrubu: [],
|
urunIlkGrubu: [],
|
||||||
@@ -816,6 +1098,8 @@ function resetAll () {
|
|||||||
}
|
}
|
||||||
columnFilterSearch.value = {
|
columnFilterSearch.value = {
|
||||||
productCode: '',
|
productCode: '',
|
||||||
|
brandGroupSelection: '',
|
||||||
|
marka: '',
|
||||||
askiliYan: '',
|
askiliYan: '',
|
||||||
kategori: '',
|
kategori: '',
|
||||||
urunIlkGrubu: '',
|
urunIlkGrubu: '',
|
||||||
@@ -824,6 +1108,8 @@ function resetAll () {
|
|||||||
icerik: '',
|
icerik: '',
|
||||||
karisim: ''
|
karisim: ''
|
||||||
}
|
}
|
||||||
|
valueFilters.value = Object.fromEntries(valueFilterFields.map((field) => [field, []]))
|
||||||
|
valueFilterSearch.value = Object.fromEntries(valueFilterFields.map((field) => [field, '']))
|
||||||
numberRangeFilters.value = {
|
numberRangeFilters.value = {
|
||||||
stockQty: { min: '', max: '' }
|
stockQty: { min: '', max: '' }
|
||||||
}
|
}
|
||||||
@@ -863,53 +1149,57 @@ function clearAllCurrencies () {
|
|||||||
selectedCurrencies.value = []
|
selectedCurrencies.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchChunk ({ reset = false } = {}) {
|
function onPaginationChange (next) {
|
||||||
const afterProductCode = reset ? '' : nextCursor.value
|
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({
|
const result = await store.fetchRows({
|
||||||
limit: FETCH_LIMIT,
|
limit: PAGE_LIMIT,
|
||||||
afterProductCode,
|
page,
|
||||||
append: !reset
|
append: false,
|
||||||
|
silent: false,
|
||||||
|
filters: buildServerFilters()
|
||||||
})
|
})
|
||||||
const fetched = Number(result?.fetched) || 0
|
currentPage.value = Number(result?.page) || page
|
||||||
nextCursor.value = String(result?.nextCursor || '')
|
return Number(result?.fetched) || 0
|
||||||
return fetched
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMoreRows () {
|
async function reloadData ({ page = 1 } = {}) {
|
||||||
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 () {
|
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
console.info('[product-pricing][ui] reload:start', {
|
console.info('[product-pricing][ui] reload:start', {
|
||||||
at: new Date(startedAt).toISOString()
|
at: new Date(startedAt).toISOString()
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
nextCursor.value = ''
|
await fetchChunk({ page })
|
||||||
await fetchChunk({ reset: true })
|
|
||||||
await ensureEnoughVisibleRows(120, 6)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[product-pricing][ui] reload:error', {
|
console.error('[product-pricing][ui] reload:error', {
|
||||||
duration_ms: Date.now() - startedAt,
|
duration_ms: Date.now() - startedAt,
|
||||||
@@ -924,20 +1214,28 @@ async function reloadData () {
|
|||||||
selectedMap.value = {}
|
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 () => {
|
onMounted(async () => {
|
||||||
await reloadData()
|
await reloadData({ page: currentPage.value })
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (reloadTimer) {
|
||||||
|
clearTimeout(reloadTimer)
|
||||||
|
reloadTimer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
[columnFilters],
|
||||||
columnFilters,
|
() => { scheduleReload() },
|
||||||
numberRangeFilters,
|
|
||||||
dateRangeFilters,
|
|
||||||
showSelectedOnly,
|
|
||||||
() => tablePagination.value.sortBy,
|
|
||||||
() => tablePagination.value.descending
|
|
||||||
],
|
|
||||||
() => { void ensureEnoughVisibleRows(80, 4) },
|
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
</script>
|
</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', {
|
export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
rows: [],
|
rows: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: '',
|
error: '',
|
||||||
hasMore: true
|
hasMore: true,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
totalCount: 0,
|
||||||
|
pageCache: {},
|
||||||
|
cacheOrder: [],
|
||||||
|
prefetchInFlight: {}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
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 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 = {}) {
|
async fetchRows (options = {}) {
|
||||||
|
const silent = Boolean(options?.silent)
|
||||||
|
if (!silent) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
}
|
||||||
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
|
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 append = Boolean(options?.append)
|
const append = Boolean(options?.append)
|
||||||
const baseIndex = append ? this.rows.length : 0
|
const baseIndex = append ? this.rows.length : 0
|
||||||
|
const filters = normalizeFilters(options?.filters || {})
|
||||||
|
const cacheKey = makeCacheKey(limit, page, filters)
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
console.info('[product-pricing][frontend] request:start', {
|
console.info('[product-pricing][frontend] request:start', {
|
||||||
at: new Date(startedAt).toISOString(),
|
at: new Date(startedAt).toISOString(),
|
||||||
timeout_ms: 180000,
|
timeout_ms: 180000,
|
||||||
limit,
|
limit,
|
||||||
after_product_code: afterProductCode || null,
|
page,
|
||||||
append
|
append
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const params = { limit }
|
if (options?.useCache !== false) {
|
||||||
if (afterProductCode) params.after_product_code = afterProductCode
|
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({
|
const res = await api.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/pricing/products',
|
url: '/pricing/products',
|
||||||
@@ -111,44 +253,48 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
|||||||
timeout: 180000
|
timeout: 180000
|
||||||
})
|
})
|
||||||
const traceId = res?.headers?.['x-trace-id'] || null
|
const traceId = res?.headers?.['x-trace-id'] || null
|
||||||
const hasMoreHeader = String(res?.headers?.['x-has-more'] || '').toLowerCase()
|
const totalCount = Number(res?.headers?.['x-total-count'] || 0)
|
||||||
const nextCursorHeader = toText(res?.headers?.['x-next-cursor'])
|
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 data = Array.isArray(res?.data) ? res.data : []
|
||||||
const mapped = data.map((x, i) => mapRow(x, i, baseIndex))
|
const mapped = data.map((x, i) => mapRow(x, i, baseIndex))
|
||||||
const fallbackNextCursor = mapped.length > 0
|
const payload = {
|
||||||
? toText(mapped[mapped.length - 1]?.productCode)
|
rows: mapped,
|
||||||
: ''
|
totalCount: Number.isFinite(totalCount) ? totalCount : 0,
|
||||||
const nextCursor = nextCursorHeader || fallbackNextCursor
|
totalPages: Number.isFinite(totalPages) ? totalPages : 1,
|
||||||
if (append) {
|
page: Number.isFinite(currentPage) ? currentPage : page
|
||||||
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
|
this.cachePut(cacheKey, payload)
|
||||||
} else {
|
this.applyPageResult(payload, page)
|
||||||
this.rows = mapped
|
|
||||||
|
// 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', {
|
console.info('[product-pricing][frontend] request:success', {
|
||||||
trace_id: traceId,
|
trace_id: traceId,
|
||||||
duration_ms: Date.now() - startedAt,
|
duration_ms: Date.now() - startedAt,
|
||||||
row_count: this.rows.length,
|
row_count: this.rows.length,
|
||||||
fetched_count: mapped.length,
|
fetched_count: mapped.length,
|
||||||
has_more: this.hasMore,
|
has_more: this.hasMore,
|
||||||
next_cursor: nextCursor || null
|
page: this.page,
|
||||||
|
total_pages: this.totalPages,
|
||||||
|
total_count: this.totalCount
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
traceId,
|
traceId,
|
||||||
fetched: mapped.length,
|
fetched: mapped.length,
|
||||||
hasMore: this.hasMore,
|
hasMore: this.hasMore,
|
||||||
nextCursor
|
page: this.page,
|
||||||
|
totalPages: this.totalPages,
|
||||||
|
totalCount: this.totalCount
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!append) this.rows = []
|
this.rows = []
|
||||||
this.hasMore = false
|
this.hasMore = false
|
||||||
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||||
this.error = toText(msg)
|
this.error = toText(msg)
|
||||||
@@ -161,7 +307,7 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
|||||||
})
|
})
|
||||||
throw err
|
throw err
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
if (!silent) this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user