diff --git a/ui/src/pages/OrderPriceList.vue b/ui/src/pages/OrderPriceList.vue
index 08cc20b..6cbc8c1 100644
--- a/ui/src/pages/OrderPriceList.vue
+++ b/ui/src/pages/OrderPriceList.vue
@@ -51,22 +51,6 @@
@filter="onTopFilterSearchUrunAnaGrubu"
@update:model-value="onTopUrunAnaGrubuChange"
/>
-
+
+
+
+
+
+
+
+ {{ props.value }}
+
+
+
-
+
-
+
-
+
+
+ {{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }}
+
+
+
-
+
{{ formatPrice(props.row[name]) }}
@@ -221,6 +400,8 @@ const priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived])
const topUrunIlkGrubu = ref(null)
const topUrunAnaGrubu = ref(null)
const selectedProductCodes = ref([])
+const selectedCampaignLabels = ref([])
+const campaignFilterSearch = ref('')
const selectedPriceOptions = ref(priceOptions.map((x) => x.value))
const leftDetailsExpanded = ref(true)
@@ -237,8 +418,13 @@ const serverFilterLoading = ref({})
const serverFilterLastQuery = ref({})
const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' })
const imageCache = new Map()
+const mainTableRef = ref(null)
+const topScrollRef = ref(null)
+let syncingScroll = false
const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || []))
+const selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || []))
+const selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || []))
const pageBusy = computed(() => loading.value || renderPending.value)
const canFetch = computed(() => Boolean(topUrunIlkGrubu.value || topUrunAnaGrubu.value || selectedProductCodes.value.length > 0))
const showGuidanceOverlay = computed(() => !loading.value && rows.value.length === 0 && error.value === GUIDANCE_MSG)
@@ -246,6 +432,21 @@ const showGuidanceOverlay = computed(() => !loading.value && rows.value.length =
const topUrunIlkGrubuOptions = computed(() => serverFilterOptionMap.value.urunIlkGrubu || [])
const topUrunAnaGrubuOptions = computed(() => serverFilterOptionMap.value.urunAnaGrubu || [])
const productCodeOptions = computed(() => serverFilterOptionMap.value.productCode || [])
+const campaignOptions = computed(() => {
+ const uniq = new Set()
+ for (const row of rows.value || []) {
+ const val = toText(row?.campaignLabel)
+ if (val) uniq.add(val)
+ }
+ return Array.from(uniq)
+ .sort((a, b) => a.localeCompare(b, 'tr'))
+ .map((value) => ({ label: value, value }))
+})
+const filteredCampaignOptions = computed(() => {
+ const q = toText(campaignFilterSearch.value).toLocaleLowerCase('tr')
+ const list = campaignOptions.value
+ return q ? list.filter((x) => x.label.toLocaleLowerCase('tr').includes(q)) : list
+})
function toText (value) {
return String(value ?? '').trim()
@@ -398,11 +599,56 @@ function onTopFilterSearchUrunAnaGrubu (val, update) {
})
}
-function onProductCodeSearch (val, update) {
- update(() => {
- filterSearch.value.productCode = toText(val)
- void fetchServerFilterOptions('productCode', val)
- })
+function onProductCodeMenuShow () {
+ void fetchServerFilterOptions('productCode', filterSearch.value.productCode)
+}
+
+function onProductCodeSearchText (val) {
+ void fetchServerFilterOptions('productCode', val)
+}
+
+function toggleProductCodeValue (value) {
+ const v = toText(value)
+ if (!v) return
+ const set = new Set(selectedProductCodes.value || [])
+ if (set.has(v)) set.delete(v)
+ else set.add(v)
+ selectedProductCodes.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
+}
+
+function selectAllProductCodeOptions () {
+ const set = new Set(selectedProductCodes.value || [])
+ for (const option of productCodeOptions.value) {
+ const v = toText(option.value)
+ if (v) set.add(v)
+ }
+ selectedProductCodes.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
+}
+
+function clearProductCodeOptions () {
+ selectedProductCodes.value = []
+}
+
+function toggleCampaignValue (value) {
+ const v = toText(value)
+ if (!v) return
+ const set = new Set(selectedCampaignLabels.value || [])
+ if (set.has(v)) set.delete(v)
+ else set.add(v)
+ selectedCampaignLabels.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
+}
+
+function selectAllCampaignOptions () {
+ const set = new Set(selectedCampaignLabels.value || [])
+ for (const option of filteredCampaignOptions.value) {
+ const v = toText(option.value)
+ if (v) set.add(v)
+ }
+ selectedCampaignLabels.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
+}
+
+function clearCampaignOptions () {
+ selectedCampaignLabels.value = []
}
function onTopUrunIlkGrubuChange () {
@@ -591,14 +837,58 @@ const visibleColumns = computed(() => allColumns.filter((c) => {
return true
}))
-const filteredRows = computed(() => rows.value || [])
+const filteredRows = computed(() => {
+ const campaignSet = selectedCampaignLabelSet.value
+ if (campaignSet.size === 0) return rows.value || []
+ return (rows.value || []).filter((row) => campaignSet.has(toText(row?.campaignLabel)))
+})
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
const tableStyle = computed(() => ({
width: `${tableMinWidth.value}px`,
minWidth: `${tableMinWidth.value}px`,
tableLayout: 'fixed'
}))
-const stickyScrollComp = computed(() => 650)
+const stickyColumnNames = computed(() => {
+ const visible = new Set(visibleColumns.value.map((x) => x.name))
+ return ['image', 'brandGroupSelection', 'marka', 'productCode', 'variantCodes', 'variantStocks', 'campaignLabel', 'campaignRate'].filter((x) => visible.has(x))
+})
+const stickyBoundaryColumnName = 'campaignRate'
+const stickyColumnNameSet = computed(() => new Set(stickyColumnNames.value))
+const stickyLeftMap = computed(() => {
+ const map = {}
+ let left = 0
+ for (const name of stickyColumnNames.value) {
+ const colDef = allColumns.find((x) => x.name === name)
+ if (!colDef) continue
+ map[name] = left
+ left += extractWidth(colDef.style)
+ }
+ return map
+})
+const stickyScrollComp = computed(() => {
+ const boundaryCol = allColumns.find((x) => x.name === stickyBoundaryColumnName)
+ return ((stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)) * 1.2
+})
+
+function isStickyCol (name) {
+ return stickyColumnNameSet.value.has(name)
+}
+
+function isStickyBoundary (name) {
+ return name === stickyBoundaryColumnName
+}
+
+function getHeaderCellStyle (col) {
+ const base = col.headerStyle || col.style || ''
+ if (!isStickyCol(col.name)) return base
+ return `${base};left:${stickyLeftMap.value[col.name] || 0}px;`
+}
+
+function getBodyCellStyle (col) {
+ const base = col.style || ''
+ if (!isStickyCol(col.name)) return base
+ return `${base};left:${stickyLeftMap.value[col.name] || 0}px;`
+}
function extractWidth (style) {
const m = String(style || '').match(/width:(\d+)px/)
@@ -654,6 +944,34 @@ function printVisibleRows () {
win.document.close()
}
+function getTableMiddleEl () {
+ return mainTableRef.value?.$el?.querySelector?.('.q-table__middle') || null
+}
+
+function onTopScroll () {
+ if (syncingScroll) return
+ const middle = getTableMiddleEl()
+ const top = topScrollRef.value
+ if (!middle || !top) return
+ syncingScroll = true
+ middle.scrollLeft = top.scrollLeft
+ requestAnimationFrame(() => { syncingScroll = false })
+}
+
+function bindTableScrollSync () {
+ const middle = getTableMiddleEl()
+ if (!middle || middle.__orderPriceListScrollBound) return
+ middle.__orderPriceListScrollBound = true
+ middle.addEventListener('scroll', () => {
+ if (syncingScroll) return
+ const top = topScrollRef.value
+ if (!top) return
+ syncingScroll = true
+ top.scrollLeft = middle.scrollLeft
+ requestAnimationFrame(() => { syncingScroll = false })
+ }, { passive: true })
+}
+
function escapeHtml (value) {
return String(value ?? '')
.replace(/&/g, '&')
@@ -669,16 +987,28 @@ watch(selectedProductCodes, (list) => {
}
})
+watch([tableMinWidth, rows], async () => {
+ await nextTick()
+ bindTableScrollSync()
+})
+
onMounted(() => {
void fetchServerFilterOptions('urunIlkGrubu', '')
void fetchServerFilterOptions('urunAnaGrubu', '')
void fetchServerFilterOptions('productCode', '')
+ void nextTick(bindTableScrollSync)
})