product performance grouped kpi improvements
This commit is contained in:
@@ -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
|
||||
@@ -95,7 +109,7 @@
|
||||
:rows="filteredGeneralRows"
|
||||
:columns="generalColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score_total', descending: true }"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
>
|
||||
@@ -206,7 +220,7 @@
|
||||
:rows="displayOrderProductCustomerTableRows"
|
||||
:columns="orderProductCustomerColumns"
|
||||
:loading="loading || backendGroupedLoading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_usd', descending: true }"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'order_usd', descending: true }"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
@@ -354,7 +368,7 @@
|
||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||
@click.stop="toggleGroup(props.row.key)"
|
||||
/>
|
||||
<span class="group-title">{{ props.row.label }}</span>
|
||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||
</div>
|
||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||
</q-td>
|
||||
@@ -396,7 +410,7 @@
|
||||
:rows="displayOrderMarketDetailTableRows"
|
||||
:columns="orderMarketDetailColumns"
|
||||
:loading="loading || backendGroupedLoading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_date', descending: true }"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'order_date', descending: true }"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
@@ -508,7 +522,7 @@
|
||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||
@click.stop="toggleGroup(props.row.key)"
|
||||
/>
|
||||
<span class="group-title">{{ props.row.label }}</span>
|
||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||
</div>
|
||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||
</q-td>
|
||||
@@ -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="productColumns"
|
||||
:rows="displayProductKpiTableRows"
|
||||
:columns="activeProductColumns"
|
||||
: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>
|
||||
@@ -646,7 +660,7 @@
|
||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||
@click.stop="toggleGroup(props.row.key)"
|
||||
/>
|
||||
<span class="group-title">{{ props.row.label }}</span>
|
||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||
</div>
|
||||
<span v-else>{{ formatGroupCell(props.row, col.name) }}</span>
|
||||
</q-td>
|
||||
@@ -688,7 +702,7 @@
|
||||
:rows="displayIdleTableRows"
|
||||
:columns="idleColumns"
|
||||
:loading="loading || backendGroupedLoading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
@@ -832,7 +846,7 @@
|
||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||
@click.stop="toggleGroup(props.row.key)"
|
||||
/>
|
||||
<span class="group-title">{{ props.row.label }}</span>
|
||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||
</div>
|
||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||
</q-td>
|
||||
@@ -874,7 +888,7 @@
|
||||
:rows="filteredMarketRows"
|
||||
:columns="marketColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
>
|
||||
@@ -974,7 +988,7 @@
|
||||
:rows="displayActiveSalesBreakdownTableRows"
|
||||
:columns="visibleSalesBreakdownColumns"
|
||||
:loading="loading || backendGroupedLoading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score_total', descending: true }"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
@@ -1091,7 +1105,7 @@
|
||||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||||
@click.stop="toggleGroup(props.row.key)"
|
||||
/>
|
||||
<span class="group-title">{{ props.row.label }}</span>
|
||||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||||
</div>
|
||||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||||
</q-td>
|
||||
@@ -1132,7 +1146,7 @@
|
||||
:rows="filteredCustomerRows"
|
||||
:columns="customerColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
>
|
||||
@@ -1233,7 +1247,7 @@
|
||||
:rows="filteredCountryRows"
|
||||
:columns="countryColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
>
|
||||
@@ -1476,10 +1490,26 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { useProductPerformanceStore } from 'src/stores/productPerformanceStore'
|
||||
import { formatMoney, formatNumber, formatPercent } from 'src/utils/formatters'
|
||||
|
||||
const performanceStore = useProductPerformanceStore()
|
||||
|
||||
function formatNumber (value, fraction = 2) {
|
||||
const n = Number(value || 0)
|
||||
const digits = Math.max(0, Math.min(2, Number(fraction || 0)))
|
||||
return n.toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits
|
||||
})
|
||||
}
|
||||
|
||||
function formatMoney (value, currency = 'USD') {
|
||||
return `${formatNumber(value, 2)} ${currency}`
|
||||
}
|
||||
|
||||
function formatPercent (value) {
|
||||
return `${formatNumber(Number(value || 0) * 100, 2)}%`
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const generalRows = ref([])
|
||||
@@ -1505,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)
|
||||
@@ -1533,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'],
|
||||
@@ -1542,10 +1574,12 @@ const selectedExpandLevelKeysByTab = reactive({
|
||||
order_market_details: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan']
|
||||
})
|
||||
const productKpiFetchLimit = 50000
|
||||
const reportFetchLimit = 50000
|
||||
const maxBulkExpandKeys = 1200
|
||||
const performanceTabs = [
|
||||
{ name: 'products', icon: 'inventory_2', label: 'Ürün KPI' },
|
||||
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk > Yaka > Piyasa > Müşteri' },
|
||||
{ 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' },
|
||||
{ name: 'sales_country_segment_market_customer_product', icon: 'public', label: 'Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI' },
|
||||
@@ -1556,7 +1590,7 @@ const pageBusy = computed(() => loading.value || backendGroupedLoading.value ||
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 100,
|
||||
rowsPerPage: 0,
|
||||
sortBy: 'performance_score_total',
|
||||
descending: true
|
||||
})
|
||||
@@ -1691,6 +1725,7 @@ const periodOrder = {
|
||||
const hiddenTableColumns = new Set(['item_description', 'performance_score'])
|
||||
const dimensionColumnNames = [
|
||||
'product_code',
|
||||
'color_yaka',
|
||||
'color_code',
|
||||
'yaka_kodu',
|
||||
'item_description',
|
||||
@@ -1824,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' },
|
||||
@@ -2158,6 +2197,36 @@ function removeColumnsInPlace (targetColumns, names) {
|
||||
return targetColumns
|
||||
}
|
||||
|
||||
function ensureColorYakaColumn (targetColumns) {
|
||||
const colorIndex = targetColumns.findIndex(col => col.name === 'color_code')
|
||||
const yakaIndex = targetColumns.findIndex(col => col.name === 'yaka_kodu')
|
||||
const insertAt = colorIndex >= 0 ? colorIndex : yakaIndex
|
||||
if (insertAt >= 0 && !targetColumns.some(col => col.name === 'color_yaka')) {
|
||||
targetColumns.splice(insertAt, 0, {
|
||||
name: 'color_yaka',
|
||||
label: 'Renk/Yaka',
|
||||
field: row => colorYakaValue(row),
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
classes: 'color-yaka-cell',
|
||||
headerClasses: 'color-yaka-cell'
|
||||
})
|
||||
}
|
||||
removeColumnsInPlace(targetColumns, ['color_code', 'yaka_kodu'])
|
||||
return targetColumns
|
||||
}
|
||||
|
||||
;[
|
||||
columns,
|
||||
generalColumns,
|
||||
orderAnalysisColumns,
|
||||
orderGroupColumns,
|
||||
orderProductCustomerColumns,
|
||||
orderMarketDetailColumns,
|
||||
idleColumns,
|
||||
salesBreakdownColumns
|
||||
].forEach(ensureColorYakaColumn)
|
||||
|
||||
;[
|
||||
columns,
|
||||
generalColumns,
|
||||
@@ -2338,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' },
|
||||
@@ -2459,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 },
|
||||
@@ -2473,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))
|
||||
@@ -2483,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
|
||||
@@ -2519,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)
|
||||
@@ -2725,6 +2829,9 @@ function sortValueForColumnName (row, columnName) {
|
||||
function makeGroupRow (key, level, groupDef, value, groupRows) {
|
||||
const imageSource = groupRows.find(row => row?.product_code) || {}
|
||||
const aggregate = aggregateGroupFields(groupRows, groupDef.key)
|
||||
const colorSummary = groupDef.key === 'color_code' ? value : distinctSummary(groupRows, 'color_code')
|
||||
const yakaSummary = groupDef.key === 'yaka_kodu' ? value : distinctSummary(groupRows, 'yaka_kodu')
|
||||
const colorDescriptionSummary = distinctSummary(groupRows, 'color_description')
|
||||
return {
|
||||
__group: true,
|
||||
row_key: `group|${key}`,
|
||||
@@ -2732,14 +2839,16 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
|
||||
level,
|
||||
group_field: groupDef.key,
|
||||
group_value: value,
|
||||
label: value,
|
||||
label: colorYakaGroupLabel(groupDef.key, value, colorSummary, yakaSummary),
|
||||
count: groupRows.length,
|
||||
image_product_code: imageSource.product_code || '',
|
||||
image_color_code: imageSource.color_code || '',
|
||||
image_color_description: imageSource.color_description || '',
|
||||
image_yaka_kodu: imageSource.yaka_kodu || '',
|
||||
product_code: groupDef.key === 'product_code' ? value : distinctSummary(groupRows, 'product_code'),
|
||||
color_code: groupDef.key === 'color_code' ? value : distinctSummary(groupRows, 'color_code'),
|
||||
yaka_kodu: groupDef.key === 'yaka_kodu' ? value : distinctSummary(groupRows, 'yaka_kodu'),
|
||||
color_code: colorSummary,
|
||||
color_description: colorDescriptionSummary,
|
||||
yaka_kodu: yakaSummary,
|
||||
item_description: distinctSummary(groupRows, 'item_description'),
|
||||
kategori: groupDef.key === 'kategori' ? value : distinctSummary(groupRows, 'kategori'),
|
||||
urun_ilk_grubu: groupDef.key === 'urun_ilk_grubu' ? value : distinctSummary(groupRows, 'urun_ilk_grubu'),
|
||||
@@ -3170,7 +3279,17 @@ function expandSelectedProductGroups () {
|
||||
}
|
||||
|
||||
function groupLabelColumnName (row) {
|
||||
return row?.group_field || 'product_code'
|
||||
const field = String(row?.group_field || '')
|
||||
if (field === 'color_code' || field === 'yaka_kodu') return 'color_yaka'
|
||||
return field || 'product_code'
|
||||
}
|
||||
|
||||
function groupDisplayLabel (row) {
|
||||
const field = String(row?.group_field || '')
|
||||
if (field === 'color_code' || field === 'yaka_kodu') {
|
||||
return colorYakaValue(row) || row?.label || row?.group_value || ''
|
||||
}
|
||||
return row?.label || row?.group_value || ''
|
||||
}
|
||||
|
||||
function groupIndentPx (row) {
|
||||
@@ -3209,6 +3328,7 @@ function formatGroupCell (row, colOrName) {
|
||||
const name = colOrName?.name || colOrName
|
||||
if (name === 'image') return ''
|
||||
if (name === 'recommendation') return row.recommendation || ''
|
||||
if (name === 'color_yaka' && ['color_code', 'yaka_kodu'].includes(String(row?.group_field || ''))) return colorYakaValue(row)
|
||||
if (groupDimensionFields.has(name) && name !== row?.group_field) return ''
|
||||
return formatTableCell(row, colOrName)
|
||||
}
|
||||
@@ -3347,6 +3467,42 @@ function columnFilterLabel (row, col) {
|
||||
return formattedColumnCellValue(row, col) || columnFilterValue(row, col)
|
||||
}
|
||||
|
||||
function colorYakaValue (row) {
|
||||
const groupField = String(row?.group_field || '')
|
||||
const groupValue = String(row?.group_value || row?.label || '').trim()
|
||||
const color = String(row?.image_color_code || row?.color_code || '').trim()
|
||||
const colorDescription = String(row?.image_color_description || row?.color_description || '').trim()
|
||||
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
|
||||
const colorLabel = colorDisplayLabel(groupField === 'color_code' ? groupValue || color : color, colorDescription)
|
||||
if (row?.__group && groupField === 'color_code') return colorLabel
|
||||
if (row?.__group && groupField === 'yaka_kodu') {
|
||||
const parentColor = colorLabel && colorLabel !== '-' ? colorLabel : ''
|
||||
const groupYaka = groupValue && groupValue !== '-' ? groupValue : yaka
|
||||
return [parentColor, groupYaka].filter(Boolean).join('/')
|
||||
}
|
||||
return [colorLabel, yaka].filter(value => value && value !== '-').join('/')
|
||||
}
|
||||
|
||||
function colorDisplayLabel (code, description) {
|
||||
const colorCode = String(code || '').trim()
|
||||
const desc = String(description || '').trim()
|
||||
if (!colorCode) return ''
|
||||
if (!desc) return colorCode
|
||||
return `${colorCode}-${desc.toLocaleUpperCase('tr-TR')}`
|
||||
}
|
||||
|
||||
function colorYakaGroupLabel (groupField, groupValue, colorSummary, yakaSummary) {
|
||||
const field = String(groupField || '')
|
||||
const value = String(groupValue || '').trim()
|
||||
if (field === 'color_code') return value
|
||||
if (field === 'yaka_kodu') {
|
||||
const color = String(colorSummary || '').trim()
|
||||
const yaka = value || String(yakaSummary || '').trim()
|
||||
return [color, yaka].filter(part => part && part !== '-').join('/')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function productCategoryValue (row) {
|
||||
return row?.kategori || ''
|
||||
}
|
||||
@@ -3380,6 +3536,7 @@ function cleanProductFirstGroupValue (value) {
|
||||
|
||||
function rawProductCellValue (row, name) {
|
||||
switch (name) {
|
||||
case 'color_yaka': return colorYakaValue(row)
|
||||
case 'kategori': return productCategoryValue(row)
|
||||
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
||||
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
||||
@@ -3393,6 +3550,7 @@ function rawProductCellValue (row, name) {
|
||||
function rawColumnCellValue (row, col) {
|
||||
const name = col?.name || col
|
||||
switch (name) {
|
||||
case 'color_yaka': return colorYakaValue(row)
|
||||
case 'kategori': return productCategoryValue(row)
|
||||
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
||||
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
||||
@@ -3469,6 +3627,7 @@ function formatTableCell (row, colOrName) {
|
||||
const col = typeof colOrName === 'object' ? colOrName : null
|
||||
const name = col?.name || colOrName
|
||||
if (name === 'image') return ''
|
||||
if (name === 'color_yaka') return colorYakaValue(row)
|
||||
if (['kategori', 'urun_ilk_grubu', 'urun_ana_grubu'].includes(name)) return rawProductCellValue(row, name) || ''
|
||||
if (name === 'market_key') return displayMarketName(row.market_key)
|
||||
if (name === 'performance_bucket') return bucketLabel(row.performance_bucket)
|
||||
@@ -3897,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' ||
|
||||
@@ -3956,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()
|
||||
@@ -3985,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, '90d')))
|
||||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||||
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
||||
@@ -4042,7 +4205,7 @@ async function reload () {
|
||||
try {
|
||||
const salesBreakdownRequests = salesBreakdownTabKeys.map(tabKey => {
|
||||
return timedProductPerformanceGet(`sales-breakdown:${tabKey}`, '/pricing/product-performance/sales-breakdown', {
|
||||
params: { limit: 800, mode: salesBreakdownModes[tabKey] },
|
||||
params: { limit: reportFetchLimit, mode: salesBreakdownModes[tabKey] },
|
||||
timeout: 60000
|
||||
})
|
||||
})
|
||||
@@ -4055,11 +4218,11 @@ async function reload () {
|
||||
},
|
||||
timeout: 60000
|
||||
}),
|
||||
timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: 500 }, timeout: 60000 }),
|
||||
timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: 500 }, timeout: 60000 }),
|
||||
timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: 500 }, timeout: 60000 }),
|
||||
timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: reportFetchLimit }, timeout: 60000 }),
|
||||
timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: reportFetchLimit }, timeout: 60000 }),
|
||||
timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: reportFetchLimit }, timeout: 60000 }),
|
||||
timedProductPerformanceGet('customers', '/pricing/product-performance/customers', {
|
||||
params: { limit: 500, breakdown: customerBreakdown.value },
|
||||
params: { limit: reportFetchLimit, breakdown: customerBreakdown.value },
|
||||
timeout: 60000
|
||||
}),
|
||||
...salesBreakdownRequests
|
||||
@@ -4101,7 +4264,7 @@ async function reload () {
|
||||
async function loadOrderProductCustomers (showLoading = true) {
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: 500 }, timeout: 90000 })
|
||||
const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: reportFetchLimit }, timeout: 90000 })
|
||||
orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
|
||||
orderProductCustomersLoaded.value = true
|
||||
} catch (err) {
|
||||
@@ -4114,7 +4277,7 @@ async function loadOrderProductCustomers (showLoading = true) {
|
||||
async function loadOrderMarketDetails (showLoading = true) {
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: 800 }, timeout: 90000 })
|
||||
const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: reportFetchLimit }, timeout: 90000 })
|
||||
orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
|
||||
orderMarketDetailsLoaded.value = true
|
||||
} catch (err) {
|
||||
@@ -4128,9 +4291,9 @@ async function loadOrderAnalysis (showLoading = true) {
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
const [productResp, marketResp, customerResp] = await Promise.all([
|
||||
timedProductPerformanceGet('orders:products', '/pricing/product-performance/orders', { params: { limit: 500 }, timeout: 90000 }),
|
||||
timedProductPerformanceGet('orders:groups:market', '/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'market' }, timeout: 90000 }),
|
||||
timedProductPerformanceGet('orders:groups:customer', '/pricing/product-performance/orders/groups', { params: { limit: 500, breakdown: 'customer' }, timeout: 90000 })
|
||||
timedProductPerformanceGet('orders:products', '/pricing/product-performance/orders', { params: { limit: reportFetchLimit }, timeout: 90000 }),
|
||||
timedProductPerformanceGet('orders:groups:market', '/pricing/product-performance/orders/groups', { params: { limit: reportFetchLimit, breakdown: 'market' }, timeout: 90000 }),
|
||||
timedProductPerformanceGet('orders:groups:customer', '/pricing/product-performance/orders/groups', { params: { limit: reportFetchLimit, breakdown: 'customer' }, timeout: 90000 })
|
||||
])
|
||||
orderAnalysisRows.value = (Array.isArray(productResp?.data) ? productResp.data : []).map(normalizeOrderAnalysisRow)
|
||||
orderMarketRows.value = (Array.isArray(marketResp?.data) ? marketResp.data : []).map(normalizeOrderGroupRow)
|
||||
@@ -4173,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)
|
||||
@@ -4308,6 +4495,49 @@ onMounted(reload)
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.main-group-bar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding: 6px;
|
||||
border: 1px solid #d7dde5;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.main-group-button {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 30px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #c9d2dd;
|
||||
border-radius: 6px;
|
||||
background: #f7f9fb;
|
||||
color: #26384c;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.main-group-button small {
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.main-group-button.active {
|
||||
border-color: #1d4ed8;
|
||||
background: #dbeafe;
|
||||
color: #173a75;
|
||||
}
|
||||
|
||||
.main-group-button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
min-height: 38px;
|
||||
}
|
||||
@@ -4417,18 +4647,26 @@ onMounted(reload)
|
||||
.performance-table :deep(.q-table tbody td .q-field),
|
||||
.performance-table :deep(.q-table tbody td input),
|
||||
.performance-table :deep(.q-table tbody td textarea) {
|
||||
min-width: 72px;
|
||||
min-width: 54px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody td .q-field__control),
|
||||
.performance-table :deep(.q-table tbody td .q-field__native),
|
||||
.performance-table :deep(.q-table tbody td .q-field__input) {
|
||||
min-height: 26px;
|
||||
font-size: 11px;
|
||||
min-height: 24px;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table th.color-yaka-cell),
|
||||
.performance-table :deep(.q-table td.color-yaka-cell) {
|
||||
width: 132px;
|
||||
min-width: 132px;
|
||||
max-width: 150px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table th:first-child),
|
||||
.performance-table :deep(.q-table td:first-child) {
|
||||
border-left: 1px solid #d5dce5;
|
||||
@@ -4778,39 +5016,39 @@ onMounted(reload)
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-0 td) {
|
||||
background: #2f5f8f;
|
||||
background: #123b63;
|
||||
color: #fff;
|
||||
border-color: #244b72;
|
||||
border-color: #0b2845;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-1 td) {
|
||||
background: #4f7fad;
|
||||
background: #1f6f54;
|
||||
color: #fff;
|
||||
border-color: #426d96;
|
||||
border-color: #18563f;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-2 td) {
|
||||
background: #d8e6f4;
|
||||
background: #ffe2a8;
|
||||
color: #1f2937;
|
||||
border-color: #b8cce0;
|
||||
border-color: #d7a84d;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-3 td) {
|
||||
background: #e6eef7;
|
||||
background: #d8f0dc;
|
||||
color: #1f2937;
|
||||
border-color: #c4d2e2;
|
||||
border-color: #9bc7a3;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-4 td) {
|
||||
background: #eef4fa;
|
||||
background: #e7ecff;
|
||||
color: #1f2937;
|
||||
border-color: #cbd8e6;
|
||||
border-color: #b8c2ef;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-5 td) {
|
||||
background: #f4f8fc;
|
||||
background: #f2f3f5;
|
||||
color: #1f2937;
|
||||
border-color: #d5dce5;
|
||||
border-color: #c4c9d1;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody tr.group-row td span),
|
||||
|
||||
@@ -65,6 +65,7 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
groupedCacheKey (params = {}) {
|
||||
return JSON.stringify({
|
||||
mode: String(params.mode || ''),
|
||||
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
|
||||
groupLevels: normalizedList(params.groupLevels),
|
||||
expandedKeys: normalizedList(params.expandedKeys, true),
|
||||
limit: Number(params.limit || 0)
|
||||
@@ -104,13 +105,15 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
||||
})
|
||||
const request = api.post('/pricing/product-performance/grouped', {
|
||||
mode: params.mode,
|
||||
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
|
||||
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
||||
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
|
||||
limit: params.limit
|
||||
}, {
|
||||
params: {
|
||||
mode: params.mode,
|
||||
limit: params.limit
|
||||
limit: params.limit,
|
||||
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || ''
|
||||
},
|
||||
timeout: 90000
|
||||
}).then(resp => {
|
||||
|
||||
Reference in New Issue
Block a user