Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-18 16:31:30 +03:00
parent 9f4ed539ff
commit 44476e03bc
3 changed files with 60 additions and 45 deletions

View File

@@ -811,43 +811,27 @@ filtered AS (
AND dim1 > 0 AND dim1 > 0
AND price > 0 AND price > 0
), ),
latest AS ( grouped AS (
SELECT DISTINCT ON (s.sdprcgrp_id, s.crn, s.dim1, COALESCE(s.dim3, 0)) -- Ensure one row per business key to avoid unique violations under strict constraints (e.g. uq_sdprc_3).
s.sdprcgrp_id,
s.crn,
s.dim1,
s.dim3,
s.prc
FROM sdprc s
WHERE s.mmitem_id = $2
AND (s.sdprcgrp_id, s.crn, s.dim1, COALESCE(s.dim3, 0)) IN (
SELECT sdprcgrp_id, currency, dim1, COALESCE(dim3, 0) FROM filtered
)
ORDER BY s.sdprcgrp_id, s.crn, s.dim1, COALESCE(s.dim3, 0), s.zlins_dttm DESC, s.id DESC
),
to_insert AS (
SELECT SELECT
$2::bigint AS mmitem_id, sdprcgrp_id,
f.sdprcgrp_id, currency AS crn,
f.currency AS crn, dim1,
f.dim1, dim3,
f.dim3, MAX(price) AS prc
f.price AS prc FROM filtered
FROM filtered f GROUP BY sdprcgrp_id, currency, dim1, dim3
LEFT JOIN latest l
ON l.sdprcgrp_id = f.sdprcgrp_id
AND l.crn = f.currency
AND l.dim1 = f.dim1
AND ((l.dim3 IS NULL AND f.dim3 IS NULL) OR l.dim3 = f.dim3)
WHERE l.prc IS NULL OR l.prc IS DISTINCT FROM f.price
), ),
ins AS ( upserted 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 mmitem_id, sdprcgrp_id, crn, dim1, dim3, prc, now() SELECT $2::bigint, g.sdprcgrp_id, g.crn, g.dim1, g.dim3, g.prc, now()
FROM to_insert FROM grouped g
ON CONFLICT ON CONSTRAINT uq_sdprc_3
DO UPDATE SET prc = EXCLUDED.prc, zlins_dttm = EXCLUDED.zlins_dttm
WHERE sdprc.prc IS DISTINCT FROM EXCLUDED.prc
RETURNING 1 RETURNING 1
) )
SELECT COUNT(*)::int FROM ins; SELECT COUNT(*)::int FROM upserted;
` `
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 {

View File

@@ -1,5 +1,9 @@
<template> <template>
<q-page class="q-pa-xs pricing-page"> <q-page class="q-pa-xs pricing-page">
<q-inner-loading :showing="pageBusy">
<q-spinner-gears size="52px" color="primary" />
</q-inner-loading>
<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>
<div class="top-actions"> <div class="top-actions">
@@ -13,6 +17,7 @@
map-options map-options
:options="topUrunIlkGrubuOptions" :options="topUrunIlkGrubuOptions"
:loading="Boolean(serverFilterLoading.urunIlkGrubu)" :loading="Boolean(serverFilterLoading.urunIlkGrubu)"
:disable="pageBusy"
label="Urun Ilk Grubu" label="Urun Ilk Grubu"
style="min-width: 220px" style="min-width: 220px"
@filter="onTopFilterSearchUrunIlkGrubu" @filter="onTopFilterSearchUrunIlkGrubu"
@@ -29,6 +34,7 @@
map-options map-options
:options="topUrunAnaGrubuOptions" :options="topUrunAnaGrubuOptions"
:loading="Boolean(serverFilterLoading.urunAnaGrubu)" :loading="Boolean(serverFilterLoading.urunAnaGrubu)"
:disable="pageBusy"
label="Urun Ana Grubu (max 3)" label="Urun Ana Grubu (max 3)"
style="min-width: 260px" style="min-width: 260px"
@filter="onTopFilterSearchUrunAnaGrubu" @filter="onTopFilterSearchUrunAnaGrubu"
@@ -38,7 +44,7 @@
color="primary" color="primary"
icon="filter_alt" icon="filter_alt"
label="Gruplari Getir" label="Gruplari Getir"
:disable="!canFetchByGroup" :disable="pageBusy || !canFetchByGroup"
:loading="store.loading" :loading="store.loading"
@click="reloadData({ page: 1 })" @click="reloadData({ page: 1 })"
/> />
@@ -47,6 +53,7 @@
color="grey-7" color="grey-7"
icon="restart_alt" icon="restart_alt"
label="Secimleri Sifirla" label="Secimleri Sifirla"
:disable="pageBusy"
@click="resetGroupSelections" @click="resetGroupSelections"
/> />
</div> </div>
@@ -59,6 +66,7 @@
color="grey-8" color="grey-8"
icon="view_sidebar" icon="view_sidebar"
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'" :label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
: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">
@@ -75,6 +83,7 @@
<q-checkbox <q-checkbox
:model-value="isCurrencySelected(option.value)" :model-value="isCurrencySelected(option.value)"
dense dense
:disable="pageBusy"
@update:model-value="(val) => toggleCurrency(option.value, val)" @update:model-value="(val) => toggleCurrency(option.value, val)"
@click.stop @click.stop
/> />
@@ -92,7 +101,7 @@
:color="showSelectedOnly ? 'primary' : 'grey-7'" :color="showSelectedOnly ? 'primary' : 'grey-7'"
:icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'" :icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'"
:label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'" :label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'"
:disable="!showSelectedOnly && selectedRowCount === 0" :disable="pageBusy || (!showSelectedOnly && selectedRowCount === 0)"
@click="toggleShowSelectedOnly" @click="toggleShowSelectedOnly"
/> />
<q-btn <q-btn
@@ -101,7 +110,7 @@
outline outline
icon="calculate" icon="calculate"
label="Secilileri Hesapla" label="Secilileri Hesapla"
:disable="selectedRowCount === 0 || bulkCalcLoading" :disable="pageBusy || selectedRowCount === 0 || bulkCalcLoading"
:loading="bulkCalcLoading" :loading="bulkCalcLoading"
@click="calculateSelectedRows" @click="calculateSelectedRows"
/> />
@@ -110,7 +119,7 @@
color="primary" color="primary"
icon="save" icon="save"
:label="saveButtonLabel" :label="saveButtonLabel"
:disable="selectedDirtyCount === 0 || saving" :disable="pageBusy || selectedDirtyCount === 0 || saving"
:loading="saving" :loading="saving"
@click="saveSelectedRows" @click="saveSelectedRows"
/> />
@@ -146,6 +155,7 @@
:max-pages="8" :max-pages="8"
boundary-links boundary-links
direction-links direction-links
:disable="pageBusy"
@update:model-value="onPageChange" @update:model-value="onPageChange"
/> />
<div class="text-caption text-grey-8"> <div class="text-caption text-grey-8">
@@ -157,9 +167,6 @@
</div> </div>
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }"> <div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
<q-inner-loading :showing="saving || bulkCalcLoading">
<q-spinner-gears size="46px" color="primary" />
</q-inner-loading>
<div v-if="showGuidanceOverlay" class="empty-overlay"> <div v-if="showGuidanceOverlay" class="empty-overlay">
<div class="empty-overlay-inner"> <div class="empty-overlay-inner">
<div class="text-subtitle1 text-weight-bold">Calismaya Baslamak Icin</div> <div class="text-subtitle1 text-weight-bold">Calismaya Baslamak Icin</div>
@@ -835,6 +842,9 @@ import api, { download } from 'src/services/api'
const $q = useQuasar() const $q = useQuasar()
const store = useProductPricingStore() const store = useProductPricingStore()
const isReloading = ref(false)
const pageBusy = computed(() => Boolean(isReloading.value || store.loading || saving.value || bulkCalcLoading.value || exportAllLoading.value))
const PAGE_LIMIT = 250 const PAGE_LIMIT = 250
const currentPage = ref(1) const currentPage = ref(1)
let reloadTimer = null let reloadTimer = null
@@ -2451,7 +2461,9 @@ async function fetchChunk ({ page = 1, useCache = true } = {}) {
} }
async function reloadData ({ page = 1, useCache = true } = {}) { async function reloadData ({ page = 1, useCache = true } = {}) {
if (isReloading.value) return
const startedAt = Date.now() const startedAt = Date.now()
isReloading.value = true
console.info('[product-pricing][ui] reload:start', { console.info('[product-pricing][ui] reload:start', {
at: new Date(startedAt).toISOString() at: new Date(startedAt).toISOString()
}) })
@@ -2469,6 +2481,10 @@ async function reloadData ({ page = 1, useCache = true } = {}) {
has_error: Boolean(store.error) has_error: Boolean(store.error)
}) })
await bindHorizontalScrollSync() await bindHorizontalScrollSync()
// Let the table render before we re-enable actions (prevents double-submits while the UI is still updating).
await nextTick()
await new Promise((resolve) => setTimeout(resolve, 0))
isReloading.value = false
} }
// Full "fetch all pages" is intentionally avoided; keep server-side paging for performance. // Full "fetch all pages" is intentionally avoided; keep server-side paging for performance.

View File

@@ -1,5 +1,9 @@
<template> <template>
<q-page class="q-pa-xs pricing-page"> <q-page class="q-pa-xs pricing-page">
<q-inner-loading :showing="pageBusy">
<q-spinner-gears size="52px" color="primary" />
</q-inner-loading>
<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>
<div class="top-actions"> <div class="top-actions">
@@ -13,6 +17,7 @@
map-options map-options
:options="topUrunIlkGrubuOptions" :options="topUrunIlkGrubuOptions"
:loading="Boolean(serverFilterLoading.urunIlkGrubu)" :loading="Boolean(serverFilterLoading.urunIlkGrubu)"
:disable="pageBusy"
label="Urun Ilk Grubu" label="Urun Ilk Grubu"
style="min-width: 220px" style="min-width: 220px"
@filter="onTopFilterSearchUrunIlkGrubu" @filter="onTopFilterSearchUrunIlkGrubu"
@@ -29,6 +34,7 @@
map-options map-options
:options="topUrunAnaGrubuOptions" :options="topUrunAnaGrubuOptions"
:loading="Boolean(serverFilterLoading.urunAnaGrubu)" :loading="Boolean(serverFilterLoading.urunAnaGrubu)"
:disable="pageBusy"
label="Urun Ana Grubu (max 3)" label="Urun Ana Grubu (max 3)"
style="min-width: 260px" style="min-width: 260px"
@filter="onTopFilterSearchUrunAnaGrubu" @filter="onTopFilterSearchUrunAnaGrubu"
@@ -38,7 +44,7 @@
color="primary" color="primary"
icon="filter_alt" icon="filter_alt"
label="Gruplari Getir" label="Gruplari Getir"
:disable="!canFetchByGroup" :disable="pageBusy || !canFetchByGroup"
:loading="store.loading" :loading="store.loading"
@click="reloadData({ page: 1 })" @click="reloadData({ page: 1 })"
/> />
@@ -47,6 +53,7 @@
color="grey-7" color="grey-7"
icon="restart_alt" icon="restart_alt"
label="Secimleri Sifirla" label="Secimleri Sifirla"
:disable="pageBusy"
@click="resetGroupSelections" @click="resetGroupSelections"
/> />
</div> </div>
@@ -59,6 +66,7 @@
color="grey-8" color="grey-8"
icon="view_sidebar" icon="view_sidebar"
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'" :label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
: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">
@@ -75,6 +83,7 @@
<q-checkbox <q-checkbox
:model-value="isPriceOptionSelected(option.value)" :model-value="isPriceOptionSelected(option.value)"
dense dense
:disable="pageBusy"
@update:model-value="(val) => togglePriceOption(option.value, val)" @update:model-value="(val) => togglePriceOption(option.value, val)"
@click.stop @click.stop
/> />
@@ -106,7 +115,7 @@
:color="showSelectedOnly ? 'primary' : 'grey-7'" :color="showSelectedOnly ? 'primary' : 'grey-7'"
:icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'" :icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'"
:label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'" :label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'"
:disable="!showSelectedOnly && selectedRowCount === 0" :disable="pageBusy || (!showSelectedOnly && selectedRowCount === 0)"
@click="toggleShowSelectedOnly" @click="toggleShowSelectedOnly"
/> />
<q-btn <q-btn
@@ -114,7 +123,7 @@
color="primary" color="primary"
icon="save" icon="save"
:label="saveButtonLabel" :label="saveButtonLabel"
:disable="selectedDirtyCount === 0 || saving" :disable="pageBusy || selectedDirtyCount === 0 || saving"
:loading="saving" :loading="saving"
@click="saveSelectedRows" @click="saveSelectedRows"
/> />
@@ -150,6 +159,7 @@
:max-pages="8" :max-pages="8"
boundary-links boundary-links
direction-links direction-links
:disable="pageBusy"
@update:model-value="onPageChange" @update:model-value="onPageChange"
/> />
<div class="text-caption text-grey-8"> <div class="text-caption text-grey-8">
@@ -161,9 +171,6 @@
</div> </div>
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }"> <div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
<q-inner-loading :showing="saving">
<q-spinner-gears size="46px" color="primary" />
</q-inner-loading>
<div v-if="showGuidanceOverlay" class="empty-overlay"> <div v-if="showGuidanceOverlay" class="empty-overlay">
<div class="empty-overlay-inner"> <div class="empty-overlay-inner">
<div class="text-subtitle1 text-weight-bold">Calismaya Baslamak Icin</div> <div class="text-subtitle1 text-weight-bold">Calismaya Baslamak Icin</div>
@@ -840,6 +847,9 @@ import api, { download } from 'src/services/api'
const $q = useQuasar() const $q = useQuasar()
const store = useProductPricingStore() const store = useProductPricingStore()
const isReloading = ref(false)
const pageBusy = computed(() => Boolean(isReloading.value || store.loading || variantLoading.value || saving.value || exportAllLoading.value))
// Variant rows explode product rows; keep this smaller than ProductPricing for responsiveness. // Variant rows explode product rows; keep this smaller than ProductPricing for responsiveness.
const PAGE_LIMIT = 50 const PAGE_LIMIT = 50
const currentPage = ref(1) const currentPage = ref(1)
@@ -2618,7 +2628,9 @@ async function buildVariantRowsForProductPage (baseProductRows = []) {
} }
async function reloadData ({ page = 1, useCache = true } = {}) { async function reloadData ({ page = 1, useCache = true } = {}) {
if (isReloading.value) return
const startedAt = Date.now() const startedAt = Date.now()
isReloading.value = true
console.info('[product-pricing][ui] reload:start', { console.info('[product-pricing][ui] reload:start', {
at: new Date(startedAt).toISOString() at: new Date(startedAt).toISOString()
}) })
@@ -2640,6 +2652,9 @@ async function reloadData ({ page = 1, useCache = true } = {}) {
has_error: Boolean(store.error) has_error: Boolean(store.error)
}) })
await bindHorizontalScrollSync() await bindHorizontalScrollSync()
await nextTick()
await new Promise((resolve) => setTimeout(resolve, 0))
isReloading.value = false
} }
// Full "fetch all pages" is intentionally avoided; keep server-side paging for performance. // Full "fetch all pages" is intentionally avoided; keep server-side paging for performance.