@@ -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 , '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' ) ) )
@@ -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 ;
}