Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-18 17:49:30 +03:00
parent 149cea778e
commit d1fbe60aeb
5 changed files with 216 additions and 105 deletions

View File

@@ -672,6 +672,20 @@ func cleanProductPricingFilterValues(values []string) []string {
return clean return clean
} }
func isProductCodeOnlyPricingFilter(filters ProductPricingFilters) bool {
return len(cleanProductPricingFilterValues(filters.ProductCode)) > 0 &&
strings.TrimSpace(filters.Search) == "" &&
len(cleanProductPricingFilterValues(filters.BrandGroup)) == 0 &&
len(cleanProductPricingFilterValues(filters.AskiliYan)) == 0 &&
len(cleanProductPricingFilterValues(filters.Kategori)) == 0 &&
len(cleanProductPricingFilterValues(filters.UrunIlkGrubu)) == 0 &&
len(cleanProductPricingFilterValues(filters.UrunAnaGrubu)) == 0 &&
len(cleanProductPricingFilterValues(filters.UrunAltGrubu)) == 0 &&
len(cleanProductPricingFilterValues(filters.Icerik)) == 0 &&
len(cleanProductPricingFilterValues(filters.Karisim)) == 0 &&
len(cleanProductPricingFilterValues(filters.Marka)) == 0
}
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) { func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) {
result := ProductPricingPage{ result := ProductPricingPage{
Rows: []models.ProductPricing{}, Rows: []models.ProductPricing{},
@@ -751,8 +765,28 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
}, " OR ")+")") }, " OR ")+")")
} }
whereSQL := strings.Join(whereParts, " AND ") whereSQL := strings.Join(whereParts, " AND ")
cleanProductCodes := cleanProductPricingFilterValues(filters.ProductCode)
productCodeOnlyFilter := isProductCodeOnlyPricingFilter(filters)
if includeTotal { if includeTotal && productCodeOnlyFilter {
result.TotalCount = len(cleanProductCodes)
if result.TotalCount == 0 {
result.TotalPages = 0
result.Page = 1
return result, nil
}
totalPages := int(math.Ceil(float64(result.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
} else if includeTotal {
countQuery := ` countQuery := `
SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode))) SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode)))
FROM ProductFilterWithDescription('TR') FROM ProductFilterWithDescription('TR')
@@ -789,7 +823,7 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
// Stage 1: fetch only paged products first. Exact product-code filters do not // Stage 1: fetch only paged products first. Exact product-code filters do not
// need the stock sort temp table here; detailed metrics are fetched below. // need the stock sort temp table here; detailed metrics are fetched below.
productCodeFastPath := len(cleanProductPricingFilterValues(filters.ProductCode)) > 0 productCodeFastPath := len(cleanProductCodes) > 0
sortBy = strings.TrimSpace(sortBy) sortBy = strings.TrimSpace(sortBy)
orderDir := "DESC" orderDir := "DESC"
if !descending { if !descending {
@@ -876,7 +910,72 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
OFFSET ` + strconv.Itoa(offset) + ` ROWS OFFSET ` + strconv.Itoa(offset) + ` ROWS
FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY; FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY;
` `
if productCodeFastPath { if productCodeOnlyFilter {
valueRows := make([]string, 0, len(cleanProductCodes))
directArgs := make([]any, 0, len(cleanProductCodes))
for i, code := range cleanProductCodes {
paramName := "@p" + strconv.Itoa(i+1)
valueRows = append(valueRows, "("+paramName+")")
directArgs = append(directArgs, code)
}
args = directArgs
productQuery = `
WITH req_codes AS (
SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode)
WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0
),
attr AS (
SELECT
LTRIM(RTRIM(a.ItemCode)) AS ProductCode,
MAX(CASE WHEN a.AttributeTypeCode = 45 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS AskiliYan,
MAX(CASE WHEN a.AttributeTypeCode = 44 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Kategori,
MAX(CASE WHEN a.AttributeTypeCode = 42 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS UrunIlkGrubuCode,
MAX(CASE WHEN a.AttributeTypeCode = 42 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunIlkGrubu,
MAX(CASE WHEN a.AttributeTypeCode = 1 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunAnaGrubu,
MAX(CASE WHEN a.AttributeTypeCode = 2 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunAltGrubu,
MAX(CASE WHEN a.AttributeTypeCode = 41 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Icerik,
MAX(CASE WHEN a.AttributeTypeCode = 29 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Karisim,
MAX(CASE WHEN a.AttributeTypeCode = 10 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Marka,
MAX(CASE WHEN a.AttributeTypeCode = 10 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS BrandCode
FROM dbo.prItemAttribute a WITH(NOLOCK)
INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(a.ItemCode))
LEFT JOIN dbo.cdItemAttributeDesc d WITH(NOLOCK)
ON d.ItemTypeCode = a.ItemTypeCode
AND d.AttributeTypeCode = a.AttributeTypeCode
AND d.AttributeCode = a.AttributeCode
AND d.LangCode = 'TR'
WHERE a.ItemTypeCode = 1
AND a.AttributeTypeCode IN (1,2,10,29,41,42,44,45)
GROUP BY LTRIM(RTRIM(a.ItemCode))
)
SELECT
rc.ProductCode,
CAST('' AS NVARCHAR(100)) AS BrandGroupSec,
COALESCE(attr.AskiliYan, '') AS AskiliYan,
COALESCE(attr.Kategori, '') AS Kategori,
COALESCE(attr.UrunIlkGrubu, '') AS UrunIlkGrubu,
COALESCE(attr.UrunAnaGrubu, '') AS UrunAnaGrubu,
COALESCE(attr.UrunAltGrubu, '') AS UrunAltGrubu,
COALESCE(attr.Icerik, '') AS Icerik,
COALESCE(attr.Karisim, '') AS Karisim,
COALESCE(attr.Marka, '') AS Marka,
COALESCE(attr.BrandCode, '') AS BrandCode
FROM req_codes rc
INNER JOIN dbo.cdItem ci WITH(NOLOCK)
ON ci.ItemTypeCode = 1
AND LTRIM(RTRIM(ci.ItemCode)) = rc.ProductCode
LEFT JOIN attr
ON attr.ProductCode = rc.ProductCode
WHERE ISNULL(ci.IsBlocked, 0) = 0
AND LEN(LTRIM(RTRIM(ci.ItemCode))) = 13
AND COALESCE(attr.UrunIlkGrubuCode, '') IN ('SERI', 'AKSESUAR')
ORDER BY rc.ProductCode ASC
OFFSET ` + strconv.Itoa(offset) + ` ROWS
FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY;
`
} else if productCodeFastPath {
productQuery = ` productQuery = `
IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes; IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes;

View File

@@ -258,8 +258,9 @@ norm AS (
COALESCE(price, 0) AS price COALESCE(price, 0) AS price
FROM input FROM input
), ),
-- Prefer PG's authoritative variant dimension table (mmitem_dim). Fall back to cache table if needed. -- Only PG's authoritative variant dimension table may drive delta writes.
dims_mmitem_dim AS ( -- Do not union sdprc/cache dimensions; stale rows there can re-create wrong variant keys.
dims AS (
SELECT SELECT
norm.product_code AS product_code, norm.product_code AS product_code,
md.val1::bigint AS dim1, md.val1::bigint AS dim1,
@@ -275,41 +276,7 @@ norm AS (
AND COALESCE(md.is_active, TRUE) = TRUE AND COALESCE(md.is_active, TRUE) = TRUE
WHERE md.val1 IS NOT NULL WHERE md.val1 IS NOT NULL
AND md.val1 > 0 AND md.val1 > 0
GROUP BY norm.product_code, md.val1, md.val2 GROUP BY norm.product_code, md.val1, md.val3
),
dims_cache_table AS (
SELECT
NULLIF(BTRIM(c.product_code), '') AS product_code,
c.dim1::bigint AS dim1,
c.dim3::bigint AS dim3
FROM mk_mmitem_dim_combo c
JOIN norm
ON norm.product_code = c.product_code
WHERE c.dim1 IS NOT NULL
),
dims_cache AS (
SELECT product_code, dim1, dim3 FROM dims_mmitem_dim
UNION
SELECT product_code, dim1, dim3 FROM dims_cache_table
),
dims_sdprc AS (
SELECT
norm.product_code AS product_code,
s.dim1 AS dim1,
s.dim3 AS dim3
FROM norm
JOIN mmitem mm
ON mm.code = norm.product_code
JOIN sdprc s
ON s.mmitem_id = mm.id
WHERE s.dim1 IS NOT NULL
AND s.dim1 > 0
GROUP BY norm.product_code, s.dim1, s.dim3
),
dims AS (
SELECT product_code, dim1, dim3 FROM dims_cache
UNION
SELECT product_code, dim1, dim3 FROM dims_sdprc
), ),
mapped AS ( mapped AS (
SELECT SELECT

View File

@@ -431,37 +431,6 @@ WHERE is_active = TRUE
return out return out
} }
loadDimCombosFromCache := func(productCode string) ([]dimCombo, error) {
productCode = strings.TrimSpace(productCode)
if productCode == "" {
return nil, nil
}
rows, err := pgTx.QueryContext(ctx, `
SELECT dim1, dim3
FROM mk_mmitem_dim_combo
WHERE product_code = $1
ORDER BY dim1, dim3_key
`, productCode)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]dimCombo, 0, 32)
for rows.Next() {
var d1 int64
var d3 sql.NullInt64
if err := rows.Scan(&d1, &d3); err != nil {
return nil, err
}
if d1 <= 0 {
continue
}
out = append(out, dimCombo{Dim1: d1, Dim3: d3})
}
return out, rows.Err()
}
parseDimID := func(s string) (int64, bool) { parseDimID := func(s string) (int64, bool) {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if s == "" { if s == "" {
@@ -812,7 +781,7 @@ filtered AS (
AND price > 0 AND price > 0
), ),
grouped AS ( grouped AS (
-- Ensure one row per business key to avoid unique violations under strict constraints (e.g. uq_sdprc_3). -- Ensure one row per business key to avoid unique violations under strict constraints.
SELECT SELECT
sdprcgrp_id, sdprcgrp_id,
currency AS crn, currency AS crn,
@@ -822,16 +791,35 @@ grouped AS (
FROM filtered FROM filtered
GROUP BY sdprcgrp_id, currency, dim1, dim3 GROUP BY sdprcgrp_id, currency, dim1, dim3
), ),
upserted AS ( updated AS (
UPDATE sdprc s
SET prc = g.prc,
zlins_dttm = now()
FROM grouped g
WHERE s.mmitem_id = $2::bigint
AND s.sdprcgrp_id = g.sdprcgrp_id
AND s.crn = g.crn
AND s.dim1 = g.dim1
AND COALESCE(s.dim3, 0) = COALESCE(g.dim3, 0)
AND s.prc IS DISTINCT FROM g.prc
RETURNING 1
),
inserted AS (
INSERT INTO sdprc (mmitem_id, sdprcgrp_id, crn, dim1, dim3, prc, zlins_dttm) INSERT INTO sdprc (mmitem_id, sdprcgrp_id, crn, dim1, dim3, prc, zlins_dttm)
SELECT $2::bigint, g.sdprcgrp_id, g.crn, g.dim1, g.dim3, g.prc, now() SELECT $2::bigint, g.sdprcgrp_id, g.crn, g.dim1, g.dim3, g.prc, now()
FROM grouped g FROM grouped g
ON CONFLICT ON CONSTRAINT uq_sdprc_3 WHERE NOT EXISTS (
DO UPDATE SET prc = EXCLUDED.prc, zlins_dttm = EXCLUDED.zlins_dttm SELECT 1
WHERE sdprc.prc IS DISTINCT FROM EXCLUDED.prc FROM sdprc s
WHERE s.mmitem_id = $2::bigint
AND s.sdprcgrp_id = g.sdprcgrp_id
AND s.crn = g.crn
AND s.dim1 = g.dim1
AND COALESCE(s.dim3, 0) = COALESCE(g.dim3, 0)
)
RETURNING 1 RETURNING 1
) )
SELECT COUNT(*)::int FROM upserted; SELECT ((SELECT COUNT(*) FROM updated) + (SELECT COUNT(*) FROM inserted))::int;
` `
var inserted int var inserted int
if err := pgTx.QueryRowContext(ctx, q, raw, mmItemID).Scan(&inserted); err != nil { if err := pgTx.QueryRowContext(ctx, q, raw, mmItemID).Scan(&inserted); err != nil {
@@ -1083,28 +1071,8 @@ VALUES (
_ = upsertDimCombosCache(code, dims) // best-effort cache fill _ = upsertDimCombosCache(code, dims) // best-effort cache fill
} }
// 2) Cache fallback (fast). // 2) Last resort: MSSQL stock tokens, then seed mmitem_dim. Do not use
cacheStarted := time.Now() // mk_mmitem_dim_combo as a write source; stale cache rows can create wrong keys.
if len(dims) == 0 {
cached, cacheErr := loadDimCombosFromCache(code)
if cacheErr == nil && len(cached) > 0 {
dims = cached
logger.Info("save:pg:dims:cache:hit",
"product_code", code,
"dims", len(dims),
"duration_ms", time.Since(cacheStarted).Milliseconds(),
)
} else if cacheErr != nil {
logger.Error("save:pg:dims:cache-load:error", "product_code", code, "err", cacheErr)
} else {
logger.Info("save:pg:dims:cache:miss",
"product_code", code,
"duration_ms", time.Since(cacheStarted).Milliseconds(),
)
}
}
// 3) Last resort: MSSQL stock tokens (legacy).
if len(dims) == 0 { if len(dims) == 0 {
d, err := loadDimsFromMssqlStock(code) d, err := loadDimsFromMssqlStock(code)
if err != nil { if err != nil {

View File

@@ -3,6 +3,18 @@
<q-inner-loading :showing="pageBusy"> <q-inner-loading :showing="pageBusy">
<q-spinner-gears size="52px" color="primary" /> <q-spinner-gears size="52px" color="primary" />
</q-inner-loading> </q-inner-loading>
<div
v-if="pageBusy"
class="page-busy-overlay"
@click.stop
@mousedown.stop
@mouseup.stop
@touchstart.stop
@wheel.stop
>
<q-spinner-gears size="56px" color="primary" />
<div class="page-busy-label">Yukleniyor...</div>
</div>
<div class="top-bar row items-center justify-between q-mb-xs"> <div class="top-bar row items-center justify-between q-mb-xs">
<div class="text-subtitle1 text-weight-bold">Urun Fiyatlandirma</div> <div class="text-subtitle1 text-weight-bold">Urun Fiyatlandirma</div>
@@ -69,7 +81,7 @@
:disable="pageBusy" :disable="pageBusy"
@click="leftDetailsExpanded = !leftDetailsExpanded" @click="leftDetailsExpanded = !leftDetailsExpanded"
/> />
<q-btn-dropdown dense color="secondary" outline icon="view_module" label="Doviz Gorunumu" :auto-close="false"> <q-btn-dropdown dense color="secondary" outline icon="view_module" label="Doviz Gorunumu" :auto-close="false" :disable="pageBusy">
<q-list dense class="currency-menu-list"> <q-list dense class="currency-menu-list">
<q-item clickable @click="selectAllCurrencies"> <q-item clickable @click="selectAllCurrencies">
<q-item-section>Tumunu Sec</q-item-section> <q-item-section>Tumunu Sec</q-item-section>
@@ -126,7 +138,7 @@
</div> </div>
<div class="toolbar-group"> <div class="toolbar-group">
<q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :auto-close="true"> <q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :auto-close="true" :disable="pageBusy">
<q-list dense style="min-width: 260px;"> <q-list dense style="min-width: 260px;">
<q-item clickable :disable="filteredRows.length === 0" @click="exportCurrentView"> <q-item clickable :disable="filteredRows.length === 0" @click="exportCurrentView">
<q-item-section avatar><q-icon name="grid_on" /></q-item-section> <q-item-section avatar><q-icon name="grid_on" /></q-item-section>
@@ -2503,7 +2515,12 @@ async function reloadData ({ page = 1, useCache = true } = {}) {
await bindHorizontalScrollSync() await bindHorizontalScrollSync()
// Let the table render before we re-enable actions (prevents double-submits while the UI is still updating). // Let the table render before we re-enable actions (prevents double-submits while the UI is still updating).
await nextTick() await nextTick()
await new Promise((resolve) => setTimeout(resolve, 0)) const remainingBusyMs = Math.max(0, 350 - (Date.now() - startedAt))
await new Promise((resolve) => setTimeout(resolve, remainingBusyMs))
console.info('[product-pricing][ui] render:done', {
duration_ms: Date.now() - startedAt,
row_count: Array.isArray(store.rows) ? store.rows.length : 0
})
isReloading.value = false isReloading.value = false
} }
} }
@@ -2574,6 +2591,27 @@ onBeforeUnmount(() => {
min-width: 170px; min-width: 170px;
} }
.page-busy-overlay {
position: fixed;
inset: 0;
z-index: 9000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(1px);
cursor: wait;
pointer-events: all;
}
.page-busy-label {
color: #1f2937;
font-size: 13px;
font-weight: 700;
}
.top-actions { .top-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -3,6 +3,18 @@
<q-inner-loading :showing="pageBusy"> <q-inner-loading :showing="pageBusy">
<q-spinner-gears size="52px" color="primary" /> <q-spinner-gears size="52px" color="primary" />
</q-inner-loading> </q-inner-loading>
<div
v-if="pageBusy"
class="page-busy-overlay"
@click.stop
@mousedown.stop
@mouseup.stop
@touchstart.stop
@wheel.stop
>
<q-spinner-gears size="56px" color="primary" />
<div class="page-busy-label">Yukleniyor...</div>
</div>
<div class="top-bar row items-center justify-between q-mb-xs"> <div class="top-bar row items-center justify-between q-mb-xs">
<div class="text-subtitle1 text-weight-bold">Toptan Kampanya Yonetimi</div> <div class="text-subtitle1 text-weight-bold">Toptan Kampanya Yonetimi</div>
@@ -69,7 +81,7 @@
:disable="pageBusy" :disable="pageBusy"
@click="leftDetailsExpanded = !leftDetailsExpanded" @click="leftDetailsExpanded = !leftDetailsExpanded"
/> />
<q-btn-dropdown dense color="secondary" outline icon="view_module" label="Gosterge Fiyat Sec" :auto-close="false"> <q-btn-dropdown dense color="secondary" outline icon="view_module" label="Gosterge Fiyat Sec" :auto-close="false" :disable="pageBusy">
<q-list dense class="currency-menu-list"> <q-list dense class="currency-menu-list">
<q-item clickable @click="selectAllPriceOptions"> <q-item clickable @click="selectAllPriceOptions">
<q-item-section>Tumunu Sec</q-item-section> <q-item-section>Tumunu Sec</q-item-section>
@@ -130,7 +142,7 @@
</div> </div>
<div class="toolbar-group"> <div class="toolbar-group">
<q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :auto-close="true"> <q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :auto-close="true" :disable="pageBusy">
<q-list dense style="min-width: 260px;"> <q-list dense style="min-width: 260px;">
<q-item clickable :disable="filteredRows.length === 0" @click="exportCurrentView"> <q-item clickable :disable="filteredRows.length === 0" @click="exportCurrentView">
<q-item-section avatar><q-icon name="grid_on" /></q-item-section> <q-item-section avatar><q-icon name="grid_on" /></q-item-section>
@@ -2713,7 +2725,13 @@ async function reloadData ({ page = 1, useCache = true } = {}) {
}) })
await bindHorizontalScrollSync() await bindHorizontalScrollSync()
await nextTick() await nextTick()
await new Promise((resolve) => setTimeout(resolve, 0)) const remainingBusyMs = Math.max(0, 350 - (Date.now() - startedAt))
await new Promise((resolve) => setTimeout(resolve, remainingBusyMs))
console.info('[product-pricing][ui] render:done', {
duration_ms: Date.now() - startedAt,
row_count: Array.isArray(store.rows) ? store.rows.length : 0,
variant_row_count: Array.isArray(variantRows.value) ? variantRows.value.length : 0
})
isReloading.value = false isReloading.value = false
} }
} }
@@ -2787,6 +2805,27 @@ onBeforeUnmount(() => {
min-width: 170px; min-width: 170px;
} }
.page-busy-overlay {
position: fixed;
inset: 0;
z-index: 9000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(1px);
cursor: wait;
pointer-events: all;
}
.page-busy-label {
color: #1f2937;
font-size: 13px;
font-weight: 700;
}
.top-actions { .top-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;