@@ -86,6 +86,20 @@
</button>
</div>
<div v-if="activeTab === ' product _detail '" class="main-group-bar q-mb-xs">
<button
v-for="option in detailMainGroupOptions"
:key="option.value"
type="button"
:class="[' main - group - button ', { active: selectedDetailMainGroup === option.value }]"
:disabled="pageBusy"
@click="selectedDetailMainGroup = option.value"
>
<span>{{ option.label }}</span>
<small>{{ formatNumber(option.stock_qty, 0) }} stok</small>
</button>
</div>
<q-table
v-if="activeTab === ' general '"
flat
@@ -525,13 +539,13 @@
</q-table>
<q-table
v-else-if="activeTab === ' products ' "
v-else-if="isProductKpiTab "
flat
bordered
row-key="row_key"
class="performance-table product-breakdown-table bg-white"
:rows="displayProductTableRows"
:columns="p roductColumns"
:rows="displayProductKpi TableRows"
:columns="activeP roductColumns"
:loading="loading || backendGroupedLoading"
:pagination="pagination"
:sort-method="sortProductGroupedTableRows"
@@ -549,17 +563,17 @@
round
size="8px"
icon="filter_alt"
:color="hasColumnFilter(' products ' , props.col.name) ? 'primary ' : ' grey - 7 '"
:color="hasColumnFilter(activeProductTableKey , props.col.name) ? ' primary ' : ' grey - 7 '"
class="header-filter-btn"
@click.stop
>
<q-badge v-if="hasColumnFilter(' products ' , props.col.name)" color="primary" floating rounded>
{{ getColumnFilterBadgeValue(' products ' , props.col.name) }}
<q-badge v-if="hasColumnFilter(activeProductTableKey , props.col.name)" color="primary" floating rounded>
{{ getColumnFilterBadgeValue(activeProductTableKey , props.col.name) }}
</q-badge>
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
<div class="excel-filter-menu">
<q-input
v-model="columnFilterSearch[filterKey(' products ' , props.col.name)]"
v-model="columnFilterSearch[filterKey(activeProductTableKey , props.col.name)]"
dense
outlined
clearable
@@ -567,13 +581,13 @@
placeholder="Ara"
/>
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
<q-btn flat dense size="sm" label="Tümünü Seç" @click="selectAllColumnFilterOptions(' products ' , props.col.name)" />
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(' products ' , props.col.name)" />
<q-btn flat dense size="sm" label="Tümünü Seç" @click="selectAllColumnFilterOptions(activeProductTableKey , props.col.name)" />
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(activeProductTableKey , props.col.name)" />
</div>
<q-virtual-scroll
v-if="columnFilterOptions(' products ' , props.col.name).length > 0"
v-if="columnFilterOptions(activeProductTableKey , props.col.name).length > 0"
class="excel-filter-options"
:items="columnFilterOptions(' products ' , props.col.name)"
:items="columnFilterOptions(activeProductTableKey , props.col.name)"
:virtual-scroll-item-size="32"
separator
>
@@ -583,14 +597,14 @@
dense
clickable
class="excel-filter-option"
@click="toggleColumnFilterValue(' products ' , props.col.name, option.value)"
@click="toggleColumnFilterValue(activeProductTableKey , props.col.name, option.value)"
>
<q-item-section avatar>
<q-checkbox
dense
size="sm"
:model-value="isColumnFilterValueSelected(' products ' , props.col.name, option.value)"
@update:model-value="() => toggleColumnFilterValue(' products ' , props.col.name, option.value)"
:model-value="isColumnFilterValueSelected(activeProductTableKey , props.col.name, option.value)"
@update:model-value="() => toggleColumnFilterValue(activeProductTableKey , props.col.name, option.value)"
@click.stop
/>
</q-item-section>
@@ -1521,6 +1535,7 @@ const summary = ref({})
const activeTab = ref(' products ')
const customerBreakdown = ref(' market _customer ')
const selectedPeriods = ref([' 90 ', ' 180 ', ' 360 ', ' total '])
const selectedDetailMainGroup = ref(' ')
const imageUrlByKey = ref({})
const imageListByKey = ref({})
const imageDialog = ref(false)
@@ -1549,6 +1564,7 @@ const expandedGroups = ref({})
const lastManualExpandedGroupKey = ref(' ')
const selectedExpandLevelKeysByTab = reactive({
products: [' urun _ilk _grubu ', ' askili _yan ', ' kategori ', ' urun _ana _grubu ', ' urun _alt _grubu '],
product_detail: [' urun _ana _grubu ', ' urun _alt _grubu ', ' product _code ', ' color _code ', ' yaka _kodu '],
sales_color_yaka_market_customer: [' color _code ', ' yaka _kodu ', ' urun _ilk _grubu ', ' askili _yan '],
idle: [' urun _ilk _grubu ', ' askili _yan ', ' kategori ', ' urun _ana _grubu ', ' urun _alt _grubu '],
sales_product_country_segment_market_customer: [' market _key ', ' customer _code ', ' customer _name ', ' urun _ilk _grubu ', ' askili _yan '],
@@ -1561,7 +1577,8 @@ const productKpiFetchLimit = 50000
const reportFetchLimit = 50000
const maxBulkExpandKeys = 1200
const performanceTabs = [
{ name: ' products ', icon: ' inventory _2 ', label: ' Ürün KPI ' },
{ name: ' products ', icon: ' dashboard ', label: ' Genel Özet KPI ' },
{ name: ' product _detail ', icon: ' category ', label: ' Detay KPI ' },
{ name: ' sales _color _yaka _market _customer ', icon: ' palette ', label: ' Renk / Yaka > Piyasa > Müşteri ' },
{ name: ' idle ', icon: ' warning ', label: ' Atı l Stok / Maliyet ' },
{ name: ' sales _product _country _segment _market _customer ', icon: ' account _tree ', label: ' Ürün > Ülke > Segment > Piyasa > Müşteri ' },
@@ -1842,6 +1859,10 @@ function reorderColumnsInPlace (targetColumns) {
}
const productColumns = computed(() => orderedMetricColumns(columns.filter(visiblePeriodColumn)))
const productDetailColumns = computed(() => orderedMetricColumns(columns
.filter(col => ![' urun _ilk _grubu ', ' askili _yan ', ' kategori '].includes(col.name))
.filter(visiblePeriodColumn)))
const activeProductColumns = computed(() => activeTab.value === ' product _detail ' ? productDetailColumns.value : productColumns.value)
const generalColumns = [
{ name: ' image ', label: ' Foto ', field: ' image ', align: ' center ' },
@@ -2386,6 +2407,14 @@ const groupLevels = [
const tabGroupLevels = {
products: groupLevels,
product_detail: [
{ key: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ' },
{ key: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ' },
{ key: ' product _code ', label: ' Ürün ' },
{ key: ' color _code ', label: ' Renk ' },
{ key: ' yaka _kodu ', label: ' Yaka ' },
{ key: ' market _key ', label: ' Piyasa ' }
],
sales_color_yaka_market_customer: [
{ key: ' color _code ', label: ' Renk ' },
{ key: ' yaka _kodu ', label: ' Yaka ' },
@@ -2507,6 +2536,7 @@ const autoExpandThroughLevel = computed(() => {
const tableFilterSourceMap = computed(() => ({
products: { rows: rows.value, columns: productColumns.value },
product_detail: { rows: detailProductRows.value, columns: productDetailColumns.value },
general: { rows: generalRows.value, columns: generalColumns },
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
@@ -2521,6 +2551,14 @@ const tableFilterSourceMap = computed(() => ({
}))
const filteredProductRows = computed(() => filterRowsForTable(' products ', rows.value, productColumns.value))
const isProductKpiTab = computed(() => activeTab.value === ' products ' || activeTab.value === ' product _detail ')
const activeProductTableKey = computed(() => activeTab.value === ' product _detail ' ? ' product _detail ' : ' products ')
const detailProductRows = computed(() => {
const selected = String(selectedDetailMainGroup.value || ' ').trim()
if (!selected) return rows.value
return rows.value.filter(row => productMainGroupValue(row) === selected)
})
const filteredDetailProductRows = computed(() => filterRowsForTable(' product _detail ', detailProductRows.value, productDetailColumns.value))
const filteredGeneralRows = computed(() => filterRowsForTable(' general ', generalRows.value, generalColumns))
const filteredOrderProductCustomerRows = computed(() => filterRowsForTable(' order _product _customers ', orderProductCustomerRows.value, orderProductCustomerColumns))
const filteredOrderMarketDetailRows = computed(() => filterRowsForTable(' order _market _details ', orderMarketDetailRows.value, orderMarketDetailColumns))
@@ -2531,6 +2569,7 @@ const filteredCustomerRows = computed(() => filterRowsForTable('customers', cust
const filteredActiveSalesBreakdownRows = computed(() => filterRowsForTable(activeTab.value, salesBreakdownRows[activeTab.value] || [], visibleSalesBreakdownColumns.value))
const activeGroupSourceRows = computed(() => {
if (activeTab.value === ' products ') return filteredProductRows.value
if (activeTab.value === ' product _detail ') return filteredDetailProductRows.value
if (activeTab.value === ' idle ') return filteredIdleRows.value
if (activeTab.value === ' order _product _customers ') return filteredOrderProductCustomerRows.value
if (activeTab.value === ' order _market _details ') return filteredOrderMarketDetailRows.value
@@ -2567,17 +2606,34 @@ const productTableRows = computed(() => {
appendGroupRows(out, filteredProductRows.value, 0, [' tab : products '], groupLevels)
return out
})
const detailProductTableRows = computed(() => buildGroupedTableRows(' product _detail ', filteredDetailProductRows.value))
const idleTableRows = computed(() => buildGroupedTableRows(' idle ', filteredIdleRows.value))
const orderProductCustomerTableRows = computed(() => buildGroupedTableRows(' order _product _customers ', filteredOrderProductCustomerRows.value))
const orderMarketDetailTableRows = computed(() => buildGroupedTableRows(' order _market _details ', filteredOrderMarketDetailRows.value))
const activeSalesBreakdownTableRows = computed(() => buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
const displayProductTableRows = computed(() => backendRowsForTab(' products ', productTableRows.value))
const displayDetailProductTableRows = computed(() => backendRowsForTab(' product _detail ', detailProductTableRows.value))
const displayProductKpiTableRows = computed(() => activeTab.value === ' product _detail ' ? displayDetailProductTableRows.value : displayProductTableRows.value)
const displayIdleTableRows = computed(() => backendRowsForTab(' idle ', idleTableRows.value))
const displayOrderProductCustomerTableRows = computed(() => backendRowsForTab(' order _product _customers ', orderProductCustomerTableRows.value))
const displayOrderMarketDetailTableRows = computed(() => backendRowsForTab(' order _market _details ', orderMarketDetailTableRows.value))
const displayActiveSalesBreakdownTableRows = computed(() => backendRowsForTab(activeTab.value, activeSalesBreakdownTableRows.value))
const detailMainGroupOptions = computed(() => {
const grouped = new Map()
for (const row of rows.value) {
const value = productMainGroupValue(row)
if (!value) continue
const current = grouped.get(value) || { value, label: value, stock_qty: 0, sales_qty_90d: 0 }
current.stock_qty += Number(row?.stock_qty || 0)
current.sales_qty_90d += Number(row?.sales_qty_90d || 0)
grouped.set(value, current)
}
return Array.from(grouped.values())
.sort((a, b) => Number(b.sales_qty_90d || 0) - Number(a.sales_qty_90d || 0) || a.label.localeCompare(b.label, ' tr '))
})
const productGroupKeys = computed(() => {
const keys = []
collectGroupKeys(keys, filteredProductRows.value, 0, [' tab : products '], groupLevels)
@@ -4000,6 +4056,7 @@ async function loadProductDetailRows (row) {
function backendGroupedSupportedTab (tabKey = activeTab.value) {
return tabKey === ' products ' ||
tabKey === ' product _detail ' ||
tabKey === ' idle ' ||
tabKey === ' order _product _customers ' ||
tabKey === ' order _market _details ' ||
@@ -4059,7 +4116,10 @@ async function loadBackendGroupedRows () {
mode: tabKey,
groupLevels: activeGroupLevels.value.map(level => level.key),
expandedKeys: activeExpandedGroupKeys(),
limit: tabKey === ' products ' || tabKey === ' idle ' ? productKpiFetchLimit : 50000
limit: tabKey === ' products ' || tabKey === ' product _detail ' || tabKey === 'idle ' ? productKpiFetchLimit : 50000
}
if (tabKey === ' product _detail ') {
params.urunAnaGrubu = selectedDetailMainGroup.value
}
try {
const startedAt = performance.now()
@@ -4088,7 +4148,7 @@ function normalizeGroupedDisplayRow (tabKey, row) {
return withOrderPeriodScores(nextRow)
}
const next = withPeriodScores(nextRow)
if (tabKey !== ' products ' && tabKey !== ' idle ') {
if (tabKey !== ' products ' && tabKey !== ' product _detail ' && tabKey !== 'idle ') {
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, ' 90 d ')))
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, ' 180 d ')))
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, ' 365 d ')))
@@ -4276,9 +4336,33 @@ watch(activeSelectedExpandLevelKeys, () => {
scheduleLoadBackendGroupedRows()
})
watch(detailMainGroupOptions, options => {
if (!options.length) {
selectedDetailMainGroup.value = ' '
return
}
if (!selectedDetailMainGroup.value || !options.some(option => option.value === selectedDetailMainGroup.value)) {
selectedDetailMainGroup.value = options[0].value
}
}, { immediate: true })
watch(selectedDetailMainGroup, () => {
if (activeTab.value === ' product _detail ') {
expandedGroups.value = Object.fromEntries(
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith(' tab : product _detail | '))
)
backendGroupedRows.value = {
...backendGroupedRows.value,
product_detail: []
}
scheduleLoadBackendGroupedRows()
}
})
watch(productTableRows, tableRows => {
if (activeTab.value !== ' products ' ) return
const imageRows = tableRows
if (!isProductKpiTab.value) return
const sourceRows = activeTab.value === ' product _detail ' ? displayDetailProductTableRows . value : tableRows
const imageRows = sourceRows
. filter ( row => ! row . _ _group || groupShowsImage ( row ) )
. slice ( 0 , 240 )
void primeProductImages ( imageRows )
@@ -4411,6 +4495,49 @@ onMounted(reload)
opacity : 0.62 ;
}
. main - group - bar {
display : flex ;
gap : 6 px ;
overflow - x : auto ;
padding : 6 px ;
border : 1 px solid # d7dde5 ;
border - radius : 6 px ;
background : # ffffff ;
}
. main - group - button {
flex : 0 0 auto ;
display : inline - flex ;
align - items : center ;
gap : 6 px ;
min - height : 30 px ;
padding : 4 px 10 px ;
border : 1 px solid # c9d2dd ;
border - radius : 6 px ;
background : # f7f9fb ;
color : # 26384 c ;
font - size : 12 px ;
font - weight : 700 ;
cursor : pointer ;
}
. main - group - button small {
color : # 64748 b ;
font - size : 10 px ;
font - weight : 600 ;
}
. main - group - button . active {
border - color : # 1 d4ed8 ;
background : # dbeafe ;
color : # 173 a75 ;
}
. main - group - button : disabled {
cursor : wait ;
opacity : 0.62 ;
}
. metric - card {
min - height : 38 px ;
}