@@ -8,6 +8,36 @@
</div>
</div>
<div class="col-12 col-md-auto row items-center q-gutter-sm">
<q-option-group
v-model="selectedPeriods"
:options="periodOptions"
type="checkbox"
inline
dense
class="period-selector bg-white q-px-sm q-py-xs"
/>
<q-btn-dropdown
dense
outline
color="primary"
:icon="allProductGroupsExpanded ? ' unfold _less ' : ' unfold _more '"
:label="allProductGroupsExpanded ? ' Detayları Kapat ' : ' Tüm Detayları Aç '"
@click="toggleAllProductGroups"
>
<div class="detail-level-menu">
<q-option-group
v-model="activeSelectedExpandLevelKeys"
:options="expandLevelOptions"
type="checkbox"
dense
/>
<q-separator class="q-my-sm" />
<div class="row items-center justify-end q-gutter-xs">
<q-btn dense flat color="grey-7" label="Kapat" @click="expandedGroups = {}" />
<q-btn dense color="primary" label="Seçileni Aç" @click="expandSelectedProductGroups" />
</div>
</div>
</q-btn-dropdown>
<q-btn outline color="secondary" icon="refresh" label="Yenile" :loading="loading" @click="reload" />
</div>
</div>
@@ -32,13 +62,13 @@
align="left"
>
<q-tab name="products" icon="inventory_2" label="Ürün KPI" />
<q-tab name="general" icon="analytics" label="KPI Genel " />
<q-tab name="order_product_customers" icon="assignment_ind" label="Ürün > Müşteri Sipariş" />
<q-tab name="order_market_details" icon="account_tree" label="Piyasa > Sipariş Detay" />
<q-tab name="sales_color_yaka_market_customer" icon="palette" label="Renk > Yaka > Piyasa > Müşteri " />
<q-tab name="idle" icon="warning" label="Atı l Stok / Maliyet" />
<q-tab name="markets" icon="query_stats" label="Piyasa Grup Kı yası " />
<q-tab name="countries " icon="public " label="Ülke / Segment " />
<q-tab name="customers " icon="groups " label="Müşteri Kı rı lı mı " />
<q-tab name="sales_product_country_segment_market_customer" icon="account_tree" label="Ürün > Ülke > Segment > Piyasa > Müşteri " />
<q-tab name="sales_market_customer_product " icon="groups " label="Piyasa > Müşteri > Ürün Satış KPI " />
<q-tab name="sales_country_segment_market_customer_product " icon="public " label="Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI " />
<q-tab name="order_product_customers" icon="assignment_ind" label="Ürün > Piyasa > Müşteri Sipariş" />
<q-tab name="order_market_details" icon="receipt_long" label="Piyasa > Müşteri > Ürün Sipariş" />
</q-tabs>
<q-table
@@ -370,9 +400,9 @@
flat
bordered
row-key="row_key"
class="performance-table bg-white"
class="performance-table product-breakdown-table bg-white"
:rows="productTableRows"
:columns="c olumns"
:columns="productC olumns"
:loading="loading"
:pagination="pagination"
@request="onRequest"
@@ -455,8 +485,23 @@
:class="[' group - row ', `group-row-level-${Math.min(Number(props.row.level || 0), 5)}`]"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
<template v-if="col.name === ' image ' && groupShowsImage(props.row)">
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
<q-img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
fit="contain"
class="product-thumb"
no-spinner
/>
<div v-else class="product-thumb-placeholder">
<q-icon name="image" size="22px" />
</div>
<q-tooltip>Foto</q-tooltip>
</button>
</template>
<div
v-if="col.name === groupLabelColumnName(props.row)"
v-else- if="col.name === groupLabelColumnName(props.row)"
class="group-cell-label"
:style="{ paddingLeft: `${props.row.level * 18}px` }"
>
@@ -710,6 +755,111 @@
</template>
</q-table>
<q-table
v-else-if="salesBreakdownTabKeys.includes(activeTab)"
flat
bordered
row-key="row_key"
class="performance-table bg-white"
:rows="filteredActiveSalesBreakdownRows"
:columns="visibleSalesBreakdownColumns"
:loading="loading"
:pagination="{ rowsPerPage: 100, sortBy: ' performance _score ', descending: true }"
>
<template #header-cell="props">
<q-th :props="props" class="filterable-header-cell">
<div class="header-with-filter">
<span class="header-label" :title="props.col.label">{{ props.col.label }}</span>
<q-btn
v-if="isColumnFilterable(props.col.name)"
dense
flat
round
size="8px"
icon="filter_alt"
:color="hasColumnFilter(activeTab, props.col.name) ? ' primary ' : ' grey - 7 '"
class="header-filter-btn"
@click.stop
>
<q-badge v-if="hasColumnFilter(activeTab, props.col.name)" color="primary" floating rounded>
{{ getColumnFilterBadgeValue(activeTab, 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(activeTab, props.col.name)]"
dense
outlined
clearable
class="excel-filter-select"
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(activeTab, props.col.name)" />
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(activeTab, props.col.name)" />
</div>
<q-virtual-scroll
v-if="columnFilterOptions(activeTab, props.col.name).length > 0"
class="excel-filter-options"
:items="columnFilterOptions(activeTab, props.col.name)"
:virtual-scroll-item-size="32"
separator
>
<template #default="{ item: option }">
<q-item
:key="`${props.col.name}-${option.value}`"
dense
clickable
class="excel-filter-option"
@click="toggleColumnFilterValue(activeTab, props.col.name, option.value)"
>
<q-item-section avatar>
<q-checkbox
dense
size="sm"
:model-value="isColumnFilterValueSelected(activeTab, props.col.name, option.value)"
@update:model-value="() => toggleColumnFilterValue(activeTab, props.col.name, option.value)"
@click.stop
/>
</q-item-section>
<q-item-section>
<q-item-label>{{ option.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-virtual-scroll>
<div v-else class="excel-filter-empty">Sonuç yok</div>
</div>
</q-menu>
</q-btn>
<q-btn v-else dense flat round size="8px" icon="filter_alt" class="header-filter-btn header-filter-ghost" tabindex="-1" />
</div>
</q-th>
</template>
<template #body-cell-sales_usd_90d="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.sales_usd_90d, ' USD ') }}
</q-td>
</template>
<template #body-cell-sales_usd_365d="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.sales_usd_365d, ' USD ') }}
</q-td>
</template>
<template #body-cell-avg_price_usd_90d="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.avg_price_usd_90d, ' USD ') }}
</q-td>
</template>
<template #body-cell-performance_bucket="props">
<q-td :props="props">
<q-badge :color="bucketColor(props.row.performance_bucket)">
{{ bucketLabel(props.row.performance_bucket) }}
</q-badge>
</q-td>
</template>
</q-table>
<div v-else-if="activeTab === ' customers '">
<q-card flat bordered class="q-mb-sm">
<q-card-section class="row q-col-gutter-sm items-center">
@@ -1034,9 +1184,16 @@ const orderAnalysisMode = ref('product')
const marketRows = ref([])
const countryRows = ref([])
const customerRows = ref([])
const salesBreakdownRows = reactive({
sales_color_yaka_market_customer: [],
sales_product_country_segment_market_customer: [],
sales_market_customer_product: [],
sales_country_segment_market_customer_product: []
})
const summary = ref({})
const activeTab = ref(' products ')
const customerBreakdown = ref(' market _customer ')
const selectedPeriods = ref([' 90 ', ' 180 ', ' 360 ', ' total '])
const imageUrlByKey = ref({})
const imageListByKey = ref({})
const imageDialog = ref(false)
@@ -1055,6 +1212,16 @@ const detailStockSizes = ref([])
const columnFilters = reactive({})
const columnFilterSearch = reactive({})
const expandedGroups = ref({})
const selectedExpandLevelKeysByTab = reactive({
products: [' kategori ', ' askili _yan ', ' urun _ilk _grubu ', ' urun _ana _grubu ', ' urun _alt _grubu ', ' market _key '],
sales_color_yaka_market_customer: [' color _code ', ' yaka _kodu ', ' market _key ', ' customer _code '],
idle: [' kategori ', ' askili _yan ', ' urun _ilk _grubu ', ' urun _ana _grubu ', ' urun _alt _grubu ', ' product _code '],
sales_product_country_segment_market_customer: [' product _code ', ' country ', ' customer _segment ', ' market _key ', ' customer _code '],
sales_market_customer_product: [' market _key ', ' customer _code ', ' product _code ', ' color _code ', ' yaka _kodu '],
sales_country_segment_market_customer_product: [' country ', ' customer _segment ', ' market _key ', ' customer _code ', ' product _code ', ' color _code ', ' yaka _kodu '],
order_product_customers: [' product _code ', ' market _key ', ' customer _code '],
order_market_details: [' market _key ', ' customer _code ', ' product _code ', ' color _code ', ' yaka _kodu ']
})
const productKpiFetchLimit = 50000
const pagination = ref({
@@ -1071,15 +1238,32 @@ const bucketOptions = [
{ label: ' Stok Riski ', value: ' STOK _RISKI ' },
{ label: ' Fiyat Baskı sı ', value: ' FIYAT _BASKISI ' },
{ label: ' Fiyat Fı rsatı ', value: ' FIYAT _FIRSATI ' },
{ label: ' Maliyet Yok ', value: ' MALIYET _YOK ' },
{ label: ' Takip ', value: ' TAKIP ' }
]
const periodOptions = [
{ label: ' 90 G ', value: ' 90 ' },
{ label: ' 180 G ', value: ' 180 ' },
{ label: ' 360 G ', value: ' 360 ' },
{ label: ' Genel ', value: ' total ' }
]
const customerBreakdownOptions = [
{ label: ' Piyasa > Müşteri ', value: ' market _customer ' },
{ label: ' Ülke > Müşteri ', value: ' country _customer ' },
{ label: ' Piyasa > Ülke > Müşteri ', value: ' market _country _customer ' }
]
const salesBreakdownModes = {
sales_color_yaka_market_customer: ' color _yaka _market _customer ',
sales_product_country_segment_market_customer: ' product _country _segment _market _customer ',
sales_market_customer_product: ' market _customer _product ',
sales_country_segment_market_customer_product: ' country _segment _market _customer _product '
}
const salesBreakdownTabKeys = Object.keys(salesBreakdownModes)
const detailStockColumns = [
{ name: ' size _code ', label: ' Beden ', field: ' size _code ', align: ' left ', sortable: true },
{ name: ' stock _qty ', label: ' Stok ', field: row => formatNumber(row.stock_qty, 0), align: ' right ', sortable: true }
@@ -1088,7 +1272,7 @@ const detailStockColumns = [
const detailSalesColumns = [
{ name: ' sales _date ', label: ' Tarih ', field: ' sales _date ', align: ' left ', sortable: true },
{ name: ' ref _number ', label: ' Ref ', field: ' ref _number ', align: ' left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' country ', label: ' Ülke ', field: ' country ', align: ' left ', sortable: true },
{ name: ' customer _code ', label: ' Müşteri Kodu ', field: ' customer _code ', align: ' left ', sortable: true },
{ name: ' customer _name ', label: ' Müşteri ', field: ' customer _name ', align: ' left ', sortable: true },
@@ -1104,40 +1288,75 @@ const columns = [
{ name: ' yaka _kodu ', label: ' Yaka ', field: ' yaka _kodu ', align: ' left ' },
{ name: ' item _description ', label: ' Açıklama ', field: ' item _description ', align: ' left ' },
{ name: ' kategori ', label: ' Kategori ', field: ' kategori ', align: ' left ' },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ' },
{ name: ' askili _yan ', label: ' Askı lı / Yan ', field: ' askili _yan ', align: ' left ' },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ' },
{ name: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ', field: row => row.urun_ana_grubu || row.seri || ' ', align: ' left ' },
{ name: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ', field: ' urun _alt _grubu ', align: ' left ' },
{ name: ' market _key ', label: ' Piyasa ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' stock _qty ', label: ' Stok ', field: row => formatNumber(row.stock_qty, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _90d ', label: ' 90 G Satış ', field: row => formatNumber(row.sales_qty_90d, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _180d ', label: ' 180 G Satış ', field: row => formatNumber(row.sales_qty_180d, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _365d ', label: ' 360 G Satış ', field: row => formatNumber(row.sales_qty_365d, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _total ', label: ' Genel Satış ', field: row => formatNumber(row.sales_qty_total, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _90d ', label: ' 90 G USD ', field: ' sales _usd _90d ', align: ' right ', sortable: true },
{ name: ' sales _usd _180d ', label: ' 180 G USD ', field: ' sales _usd _180d ', align: ' right ', sortable: true },
{ name: ' sales _usd _365d ', label: ' 360 G USD ', field: ' sales _usd _365d ', align: ' right ', sortable: true },
{ name: ' sales _usd _total ', label: ' Genel USD ', field: ' sales _usd _total ', align: ' right ', sortable: true },
{ name: ' stock _days _90d ', label: ' Stok Gün ', field: row => formatNumber(row.stock_days_90d, 1), align: ' right ', sortable: true },
{ name: ' stock _days _180d ', label: ' 180 G Stok Gün ', field: row => formatNumber(row.stock_days_180d, 1), align: ' right ', sortable: true },
{ name: ' stock _days _total ', label: ' Genel Stok Gün ', field: row => formatNumber(row.stock_days_total, 1), align: ' right ', sortable: true },
{ name: ' avg _price _usd _90d ', label: ' Ort . USD ', field: row => formatMoney(row.avg_price_usd_90d, ' USD '), align: ' right ' },
{ name: ' avg _price _usd _180d ', label: ' 180 G Ort . USD ', field: row => formatMoney(row.avg_price_usd_180d, ' USD '), align: ' right ' },
{ name: ' avg _price _usd _365d ', label: ' 360 G Ort . USD ', field: row => formatMoney(row.avg_price_usd_365d, ' USD '), align: ' right ' },
{ name: ' avg _price _usd _total ', label: ' Genel Ort . USD ', field: row => formatMoney(row.avg_price_usd_total, ' USD '), align: ' right ' },
{ name: ' base _price _usd ', label: ' Ort . USD Taban Maliyeti ', field: row => formatMoney(row.base_price_usd, ' USD '), align: ' right ', sortable: true },
{ name: ' cost _price _usd ', label: ' Ort . USD Çıplak Maliyeti ', field: row => formatMoney(row.cost_price_usd, ' USD '), align: ' right ', sortable: true },
{ name: ' unit _profit _base _90d ', label: ' 90 G P . Başı Taban K / Z ', field: row => formatMoney(row.unit_profit_base_90d, ' USD '), align: ' right ', sortable: true },
{ name: ' unit _profit _cost _90d ', label: ' 90 G P . Başı Çıplak K / Z ', field: row => formatMoney(row.unit_profit_cost_90d, ' USD '), align: ' right ', sortable: true },
{ name: ' unit _profit _base _180d ', label: ' 180 G P . Başı Taban K / Z ', field: row => formatMoney(row.unit_profit_base_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' unit _profit _cost _180d ', label: ' 180 G P . Başı Çıplak K / Z ', field: row => formatMoney(row.unit_profit_cost_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' unit _profit _base _total ', label: ' Genel P . Başı Taban K / Z ', field: row => formatMoney(row.unit_profit_base_total, ' USD '), align: ' right ', sortable: true },
{ name: ' unit _profit _cost _total ', label: ' Genel P . Başı Çıplak K / Z ', field: row => formatMoney(row.unit_profit_cost_total, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _usd _90d ', label: ' 90 G Toplam K / Z ', field: row => formatMoney(row.gross_profit_usd_90d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _usd _180d ', label: ' 180 G Toplam K / Z ', field: row => formatMoney(row.gross_profit_usd_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _usd _total ', label: ' Genel Toplam K / Z ', field: row => formatMoney(row.gross_profit_usd_total, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _margin _base _90d ', label: ' 90 G Taban Marj ', field: row => formatPercent(row.gross_margin_base_90d), align: ' right ', sortable: true },
{ name: ' gross _margin _cost _90d ', label: ' 90 G Çıplak Marj ', field: row => formatPercent(row.gross_margin_cost_90d), align: ' right ', sortable: true },
{ name: ' gross _margin _base _180d ', label: ' 180 G Taban Marj ', field: row => formatPercent(row.gross_margin_base_180d), align: ' right ', sortable: true },
{ name: ' gross _margin _cost _180d ', label: ' 180 G Çıplak Marj ', field: row => formatPercent(row.gross_margin_cost_180d), align: ' right ', sortable: true },
{ name: ' gross _margin _base _total ', label: ' Genel Taban Marj ', field: row => formatPercent(row.gross_margin_base_total), align: ' right ', sortable: true },
{ name: ' gross _margin _cost _total ', label: ' Genel Çıplak Marj ', field: row => formatPercent(row.gross_margin_cost_total), align: ' right ', sortable: true },
{ name: ' market _count _90d ', label: ' 90 G Piyasa ', field: row => formatNumber(row.market_count_90d, 0), align: ' right ', sortable: true },
{ name: ' customer _count _90d ', label: ' 90 G Müşteri ', field: row => formatNumber(row.customer_count_90d, 0), align: ' right ', sortable: true },
{ name: ' market _count _total ', label: ' Genel Piyasa ', field: row => formatNumber(row.market_count_total, 0), align: ' right ', sortable: true },
{ name: ' customer _count _total ', label: ' Genel Müşteri ', field: row => formatNumber(row.customer_count_total, 0), align: ' right ', sortable: true },
{ name: ' sales _index _90d ', label: ' Piyasa End . ', field: row => formatNumber(row.sales_index_90d, 2), align: ' right ', sortable: true },
{ name: ' sales _index _total ', label: ' Genel Endeks ', field: row => formatNumber(row.sales_index_total, 2), align: ' right ', sortable: true },
{ name: ' performance _score ', label: ' Skor ', field: row => formatNumber(row.performance_score, 1), align: ' right ', sortable: true },
{ name: ' performance _bucket ', label: ' Durum ', field: ' performance _bucket ', align: ' left ' },
{ name: ' recommendation ', label: ' Öneri ', field: ' recommendation ', align: ' left ' }
]
const periodColumnRules = [
{ period: ' 90 ', match: name => name.includes(' _90d ') },
{ period: ' 180 ', match: name => name.includes(' _180d ') },
{ period: ' 360 ', match: name => name.includes(' _365d ') },
{ period: ' total ', match: name => name.includes(' _total ') }
]
const selectedPeriodSet = computed(() => new Set(selectedPeriods.value))
function columnPeriod (name) {
return periodColumnRules.find(rule => rule.match(name))?.period || ' '
}
function visiblePeriodColumn (col) {
const period = columnPeriod(col.name)
return !period || selectedPeriodSet.value.has(period)
}
const productColumns = computed(() => columns.filter(visiblePeriodColumn))
const generalColumns = [
{ name: ' image ', label: ' Foto ', field: ' image ', align: ' center ' },
{ name: ' product _code ', label: ' Ürün ', field: ' product _code ', align: ' left ', sortable: true },
@@ -1145,11 +1364,11 @@ const generalColumns = [
{ name: ' yaka _kodu ', label: ' Yaka ', field: ' yaka _kodu ', align: ' left ', sortable: true },
{ name: ' item _description ', label: ' Açıklama ', field: ' item _description ', align: ' left ', sortable: true },
{ name: ' kategori ', label: ' Kategori ', field: ' kategori ', align: ' left ', sortable: true },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ', sortable: true },
{ name: ' askili _yan ', label: ' Askı lı / Yan ', field: ' askili _yan ', align: ' left ', sortable: true },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ', sortable: true },
{ name: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ', field: row => row.urun_ana_grubu || row.seri || ' ', align: ' left ', sortable: true },
{ name: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ', field: ' urun _alt _grubu ', align: ' left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' period _start ', label: ' Başlangıç ', field: ' period _start ', align: ' left ', sortable: true },
{ name: ' period _end ', label: ' Bitiş ', field: ' period _end ', align: ' left ', sortable: true },
{ name: ' stock _qty ', label: ' Stok ', field: row => formatNumber(row.stock_qty, 0), align: ' right ', sortable: true },
@@ -1183,11 +1402,11 @@ const orderAnalysisColumns = [
{ name: ' yaka _kodu ', label: ' Yaka ', field: ' yaka _kodu ', align: ' left ', sortable: true },
{ name: ' item _description ', label: ' Açıklama ', field: ' item _description ', align: ' left ', sortable: true },
{ name: ' kategori ', label: ' Kategori ', field: ' kategori ', align: ' left ', sortable: true },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ', sortable: true },
{ name: ' askili _yan ', label: ' Askı lı / Yan ', field: ' askili _yan ', align: ' left ', sortable: true },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ', sortable: true },
{ name: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ', field: row => row.urun_ana_grubu || row.seri || ' ', align: ' left ', sortable: true },
{ name: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ', field: ' urun _alt _grubu ', align: ' left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' order _qty ', label: ' Açık Sipariş ', field: row => formatNumber(row.order_qty, 0), align: ' right ', sortable: true },
{ name: ' order _usd ', label: ' Sipariş USD ', field: row => formatMoney(row.order_usd, ' USD '), align: ' right ', sortable: true },
{ name: ' avg _order _price _usd ', label: ' Ort . Sipariş USD ', field: row => formatMoney(row.avg_order_price_usd, ' USD '), align: ' right ', sortable: true },
@@ -1212,7 +1431,7 @@ const orderAnalysisColumns = [
const orderGroupColumns = [
{ name: ' group _key ', label: ' Kı rı lı m ', field: row => orderGroupLabel(row), align: ' left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' customer _code ', label: ' Müşteri Kodu ', field: ' customer _code ', align: ' left ', sortable: true },
{ name: ' customer _name ', label: ' Müşteri ', field: ' customer _name ', align: ' left ', sortable: true },
{ name: ' product _count ', label: ' Ürün ', field: row => formatNumber(row.product_count, 0), align: ' right ', sortable: true },
@@ -1242,7 +1461,7 @@ const orderProductCustomerColumns = [
{ name: ' color _code ', label: ' Renk ', field: ' color _code ', align: ' left ', sortable: true },
{ name: ' yaka _kodu ', label: ' Yaka ', field: ' yaka _kodu ', align: ' left ', sortable: true },
{ name: ' item _description ', label: ' Açıklama ', field: ' item _description ', align: ' left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' customer _code ', label: ' Müşteri Kodu ', field: ' customer _code ', align: ' left ', sortable: true },
{ name: ' customer _name ', label: ' Müşteri ', field: ' customer _name ', align: ' left ', sortable: true },
{ name: ' order _qty ', label: ' Açık Sipariş ', field: row => formatNumber(row.order_qty, 0), align: ' right ', sortable: true },
@@ -1264,7 +1483,7 @@ const orderProductCustomerColumns = [
]
const orderMarketDetailColumns = [
{ name: ' market _key ', label: ' Piyasa ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' customer _code ', label: ' Müşteri Kodu ', field: ' customer _code ', align: ' left ', sortable: true },
{ name: ' customer _name ', label: ' Müşteri ', field: ' customer _name ', align: ' left ', sortable: true },
{ name: ' order _number ', label: ' Sipariş No ', field: ' order _number ', align: ' left ', sortable: true },
@@ -1295,8 +1514,8 @@ const idleColumns = [
{ name: ' yaka _kodu ', label: ' Yaka ', field: ' yaka _kodu ', align: ' left ' },
{ name: ' item _description ', label: ' Açıklama ', field: ' item _description ', align: ' left ' },
{ name: ' kategori ', label: ' Kategori ', field: ' kategori ', align: ' left ' },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ' },
{ name: ' askili _yan ', label: ' Askı lı / Yan ', field: ' askili _yan ', align: ' left ' },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ' },
{ name: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ', field: row => row.urun_ana_grubu || row.seri || ' ', align: ' left ' },
{ name: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ', field: ' urun _alt _grubu ', align: ' left ' },
{ name: ' stock _qty ', label: ' Stok ', field: row => formatNumber(row.stock_qty, 0), align: ' right ', sortable: true },
@@ -1309,10 +1528,10 @@ const idleColumns = [
]
const marketColumns = [
{ name: ' market _key ', label: ' Piyasa Grubu ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' kategori ', label: ' Kategori ', field: ' kategori ', align: ' left ' },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ' },
{ name: ' askili _yan ', label: ' Askı lı / Yan ', field: ' askili _yan ', align: ' left ' },
{ name: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', field: row => row.urun_ilk_grubu || row.yas_grubu || ' ', align: ' left ' },
{ name: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ', field: row => row.urun_ana_grubu || row.seri || ' ', align: ' left ' },
{ name: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ', field: ' urun _alt _grubu ', align: ' left ' },
{ name: ' product _count ', label: ' Ürün ', field: row => formatNumber(row.product_count, 0), align: ' right ', sortable: true },
@@ -1330,19 +1549,19 @@ const marketColumns = [
const countryColumns = [
{ name: ' country ', label: ' Ülke ', field: ' country ', align: ' left ', sortable: true },
{ name: ' customer _segment ', label: ' Müşteri Segmenti ', field: ' customer _segment ', align: ' left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa Grubu ', field: ' market _key ' , align: 'left ' },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ' },
{ name: ' product _count ', label: ' Ürün ', field: row => formatNumber(row.product_count, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _90d ', label: ' 90 G Adet ', field: row => formatNumber(row.sales_qty_90d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _90d ', label: ' 90 G USD ', field: ' sales _usd _90d ', align: ' right ', sortable: true },
{ name: ' avg _price _usd _90d ', label: ' Ort . USD ', field: ' avg _price _usd _90d ', align: ' right ', sortable: true },
{ name: ' sales _usd _90d ', label: ' 90 G USD ', field: row => formatMoney(row.sales_usd_90d, ' USD ') , align: ' right ', sortable: true },
{ name: ' avg _price _usd _90d ', label: ' Ort . USD ', field: row => formatMoney(row.avg_price_usd_90d, ' USD ') , align: ' right ', sortable: true },
{ name: ' customer _count _90d ', label: ' Müşteri ', field: row => formatNumber(row.customer_count_90d, 0), align: ' right ', sortable: true },
{ name: ' invoice _count _90d ', label: ' Fatura ', field: row => formatNumber(row.invoice_count_90d, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _365d ', label: ' 365 G Adet ', field: row => formatNumber(row.sales_qty_365d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _365d ', label: ' 365 G USD ', field: row => formatMoney(row.sales_usd_365d, ' USD '), align: ' right ', sortable: true }
{ name: ' sales _qty _365d ', label: ' 360 G Adet ', field: row => formatNumber(row.sales_qty_365d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _365d ', label: ' 360 G USD ', field: row => formatMoney(row.sales_usd_365d, ' USD '), align: ' right ', sortable: true }
]
const customerColumns = [
{ name: ' market _key ', label: ' Piyasa Grubu ', field: ' market _key ' , align: 'left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key) , align: ' left ', sortable: true },
{ name: ' country ', label: ' Ülke ', field: ' country ', align: ' left ', sortable: true },
{ name: ' customer _segment ', label: ' Segment ', field: ' customer _segment ', align: ' left ', sortable: true },
{ name: ' customer _code ', label: ' Müşteri Kodu ', field: ' customer _code ', align: ' left ', sortable: true },
@@ -1351,12 +1570,74 @@ const customerColumns = [
{ name: ' sales _qty _90d ', label: ' 90 G Adet ', field: row => formatNumber(row.sales_qty_90d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _90d ', label: ' 90 G USD ', field: ' sales _usd _90d ', align: ' right ', sortable: true },
{ name: ' avg _price _usd _90d ', label: ' Ort . USD ', field: ' avg _price _usd _90d ', align: ' right ', sortable: true },
{ name: ' base _price _usd _90d ', label: ' 90 G Taban ', field: row => formatMoney(row.base_price_usd_90d, ' USD '), align: ' right ', sortable: true },
{ name: ' cost _price _usd _90d ', label: ' 90 G Çıplak ', field: row => formatMoney(row.cost_price_usd_90d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _base _usd _90d ', label: ' 90 G Taban K / Z ', field: row => formatMoney(row.gross_profit_base_usd_90d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _cost _usd _90d ', label: ' 90 G Çıplak K / Z ', field: row => formatMoney(row.gross_profit_cost_usd_90d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _margin _base _90d ', label: ' 90 G Taban Marj ', field: row => formatPercent(row.gross_margin_base_90d), align: ' right ', sortable: true },
{ name: ' gross _margin _cost _90d ', label: ' 90 G Çıplak Marj ', field: row => formatPercent(row.gross_margin_cost_90d), align: ' right ', sortable: true },
{ name: ' invoice _count _90d ', label: ' Fatura ', field: row => formatNumber(row.invoice_count_90d, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _365d ', label: ' 365 G Adet ', field: row => formatNumber(row.sales_qty_365d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _365d ', label: ' 365 G USD ', field: ' sales _usd _365d ', align: ' right ', sortable: true },
{ name: ' sales _qty _365d ', label: ' 360 G Adet ', field: row => formatNumber(row.sales_qty_365d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _365d ', label: ' 360 G USD ', field: ' sales _usd _365d ', align: ' right ', sortable: true },
{ name: ' last _sale _date ', label: ' Son Satış ', field: ' last _sale _date ', align: ' left ', sortable: true }
]
const salesBreakdownColumns = [
{ name: ' product _code ', label: ' Ürün ', field: ' product _code ', align: ' left ', sortable: true },
{ name: ' color _code ', label: ' Renk ', field: ' color _code ', align: ' left ', sortable: true },
{ name: ' yaka _kodu ', label: ' Yaka ', field: ' yaka _kodu ', align: ' left ', sortable: true },
{ name: ' item _description ', label: ' Açıklama ', field: ' item _description ', align: ' left ', sortable: true },
{ name: ' country ', label: ' Ülke ', field: ' country ', align: ' left ', sortable: true },
{ name: ' customer _segment ', label: ' Segment ', field: ' customer _segment ', align: ' left ', sortable: true },
{ name: ' market _key ', label: ' Piyasa ', field: row => displayMarketName(row.market_key), align: ' left ', sortable: true },
{ name: ' customer _code ', label: ' Müşteri Kodu ', field: ' customer _code ', align: ' left ', sortable: true },
{ name: ' customer _name ', label: ' Müşteri ', field: ' customer _name ', align: ' left ', sortable: true },
{ name: ' product _count ', label: ' Ürün Sayı sı ', field: row => formatNumber(row.product_count, 0), align: ' right ', sortable: true },
{ name: ' customer _count _90d ', label: ' 90 G Müşteri ', field: row => formatNumber(row.customer_count_90d, 0), align: ' right ', sortable: true },
{ name: ' invoice _count _90d ', label: ' 90 G Fatura ', field: row => formatNumber(row.invoice_count_90d, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _90d ', label: ' 90 G Adet ', field: row => formatNumber(row.sales_qty_90d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _90d ', label: ' 90 G USD ', field: ' sales _usd _90d ', align: ' right ', sortable: true },
{ name: ' avg _price _usd _90d ', label: ' Ort . USD ', field: ' avg _price _usd _90d ', align: ' right ', sortable: true },
{ name: ' customer _count _180d ', label: ' 180 G Müşteri ', field: row => formatNumber(row.customer_count_180d, 0), align: ' right ', sortable: true },
{ name: ' invoice _count _180d ', label: ' 180 G Fatura ', field: row => formatNumber(row.invoice_count_180d, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _180d ', label: ' 180 G Adet ', field: row => formatNumber(row.sales_qty_180d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _180d ', label: ' 180 G USD ', field: row => formatMoney(row.sales_usd_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' avg _price _usd _180d ', label: ' 180 G Ort . USD ', field: row => formatMoney(row.avg_price_usd_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' base _price _usd _180d ', label: ' 180 G Taban ', field: row => formatMoney(row.base_price_usd_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' cost _price _usd _180d ', label: ' 180 G Çıplak ', field: row => formatMoney(row.cost_price_usd_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _base _usd _180d ', label: ' 180 G Taban K / Z ', field: row => formatMoney(row.gross_profit_base_usd_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _cost _usd _180d ', label: ' 180 G Çıplak K / Z ', field: row => formatMoney(row.gross_profit_cost_usd_180d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _margin _base _180d ', label: ' 180 G Taban Marj ', field: row => formatPercent(row.gross_margin_base_180d), align: ' right ', sortable: true },
{ name: ' gross _margin _cost _180d ', label: ' 180 G Çıplak Marj ', field: row => formatPercent(row.gross_margin_cost_180d), align: ' right ', sortable: true },
{ name: ' sales _qty _365d ', label: ' 360 G Adet ', field: row => formatNumber(row.sales_qty_365d, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _365d ', label: ' 360 G USD ', field: row => formatMoney(row.sales_usd_365d, ' USD '), align: ' right ', sortable: true },
{ name: ' avg _price _usd _365d ', label: ' 360 G Ort . USD ', field: row => formatMoney(row.avg_price_usd_365d, ' USD '), align: ' right ', sortable: true },
{ name: ' base _price _usd _365d ', label: ' 360 G Taban ', field: row => formatMoney(row.base_price_usd_365d, ' USD '), align: ' right ', sortable: true },
{ name: ' cost _price _usd _365d ', label: ' 360 G Çıplak ', field: row => formatMoney(row.cost_price_usd_365d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _base _usd _365d ', label: ' 360 G Taban K / Z ', field: row => formatMoney(row.gross_profit_base_usd_365d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _cost _usd _365d ', label: ' 360 G Çıplak K / Z ', field: row => formatMoney(row.gross_profit_cost_usd_365d, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _margin _base _365d ', label: ' 360 G Taban Marj ', field: row => formatPercent(row.gross_margin_base_365d), align: ' right ', sortable: true },
{ name: ' gross _margin _cost _365d ', label: ' 360 G Çıplak Marj ', field: row => formatPercent(row.gross_margin_cost_365d), align: ' right ', sortable: true },
{ name: ' customer _count _total ', label: ' Genel Müşteri ', field: row => formatNumber(row.customer_count_total, 0), align: ' right ', sortable: true },
{ name: ' invoice _count _total ', label: ' Genel Fatura ', field: row => formatNumber(row.invoice_count_total, 0), align: ' right ', sortable: true },
{ name: ' sales _qty _total ', label: ' Genel Adet ', field: row => formatNumber(row.sales_qty_total, 0), align: ' right ', sortable: true },
{ name: ' sales _usd _total ', label: ' Genel USD ', field: row => formatMoney(row.sales_usd_total, ' USD '), align: ' right ', sortable: true },
{ name: ' avg _price _usd _total ', label: ' Genel Ort . USD ', field: row => formatMoney(row.avg_price_usd_total, ' USD '), align: ' right ', sortable: true },
{ name: ' base _price _usd _total ', label: ' Genel Taban ', field: row => formatMoney(row.base_price_usd_total, ' USD '), align: ' right ', sortable: true },
{ name: ' cost _price _usd _total ', label: ' Genel Çıplak ', field: row => formatMoney(row.cost_price_usd_total, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _base _usd _total ', label: ' Genel Taban K / Z ', field: row => formatMoney(row.gross_profit_base_usd_total, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _profit _cost _usd _total ', label: ' Genel Çıplak K / Z ', field: row => formatMoney(row.gross_profit_cost_usd_total, ' USD '), align: ' right ', sortable: true },
{ name: ' gross _margin _base _total ', label: ' Genel Taban Marj ', field: row => formatPercent(row.gross_margin_base_total), align: ' right ', sortable: true },
{ name: ' gross _margin _cost _total ', label: ' Genel Çıplak Marj ', field: row => formatPercent(row.gross_margin_cost_total), align: ' right ', sortable: true },
{ name: ' sales _index _90d ', label: ' Endeks ', field: row => formatNumber(row.sales_index_90d, 2), align: ' right ', sortable: true },
{ name: ' performance _score ', label: ' Skor ', field: row => formatNumber(row.performance_score, 1), align: ' right ', sortable: true },
{ name: ' performance _bucket ', label: ' Durum ', field: ' performance _bucket ', align: ' left ', sortable: true },
{ name: ' last _sale _date ', label: ' Son Satış ', field: ' last _sale _date ', align: ' left ', sortable: true },
{ name: ' recommendation ', label: ' Öneri ', field: ' recommendation ', align: ' left ' }
]
const visibleSalesBreakdownColumns = computed(() => salesBreakdownColumns.filter(visiblePeriodColumn))
const summaryCards = computed(() => [
{ key: ' date ', label: ' KPI Tarihi ', value: summary.value.kpi_date || ' - ' },
{ key: ' rows ', label: ' Satı r ', value: formatNumber(summary.value.total_rows, 0) },
@@ -1448,26 +1729,110 @@ const performanceCards = computed(() => {
const groupLevels = [
{ key: ' kategori ', label: ' Kategori ' },
{ key: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', fallback: ' yas _grubu ' },
{ key: ' askili _yan ', label: ' Askı lı / Yan ' },
{ key: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', fallback: ' yas _grubu ' },
{ key: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ', fallback: ' seri ' },
{ key: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ' },
{ key: ' product _code ', label: ' Ürün Kodu ' },
{ key: ' color _code ', label: ' Renk ' }
{ key: ' market _key ', label: ' Piyasa ' },
{ key: ' product _code ', label: ' Ürün Kodu ' }
]
const tabGroupLevels = {
products: groupLevels,
sales_color_yaka_market_customer: [
{ key: ' color _code ', label: ' Renk ' },
{ key: ' yaka _kodu ', label: ' Yaka ' },
{ key: ' market _key ', label: ' Piyasa ' },
{ key: ' customer _code ', label: ' Müşteri ' }
],
idle: [
{ key: ' kategori ', label: ' Kategori ' },
{ key: ' askili _yan ', label: ' Askı lı / Yan ' },
{ key: ' urun _ilk _grubu ', label: ' Ürün İlk Grubu ', fallback: ' yas _grubu ' },
{ key: ' urun _ana _grubu ', label: ' Ürün Ana Grubu ', fallback: ' seri ' },
{ key: ' urun _alt _grubu ', label: ' Ürün Alt Grubu ' },
{ key: ' product _code ', label: ' Ürün Kodu ' }
],
sales_product_country_segment_market_customer: [
{ key: ' product _code ', label: ' Ürün Kodu ' },
{ key: ' country ', label: ' Ülke ' },
{ key: ' customer _segment ', label: ' Segment ' },
{ key: ' market _key ', label: ' Piyasa ' },
{ key: ' customer _code ', label: ' Müşteri ' }
],
sales_market_customer_product: [
{ key: ' market _key ', label: ' Piyasa ' },
{ key: ' customer _code ', label: ' Müşteri ' },
{ key: ' product _code ', label: ' Ürün Kodu ' },
{ key: ' color _code ', label: ' Renk ' },
{ key: ' yaka _kodu ', label: ' Yaka ' }
],
sales_country_segment_market_customer_product: [
{ key: ' country ', label: ' Ülke ' },
{ key: ' customer _segment ', label: ' Segment ' },
{ key: ' market _key ', label: ' Piyasa ' },
{ key: ' customer _code ', label: ' Müşteri ' },
{ key: ' product _code ', label: ' Ürün Kodu ' },
{ key: ' color _code ', label: ' Renk ' },
{ key: ' yaka _kodu ', label: ' Yaka ' }
],
order_product_customers: [
{ key: ' product _code ', label: ' Ürün Kodu ' },
{ key: ' market _key ', label: ' Piyasa ' },
{ key: ' customer _code ', label: ' Müşteri ' }
],
order_market_details: [
{ key: ' market _key ', label: ' Piyasa ' },
{ key: ' customer _code ', label: ' Müşteri ' },
{ key: ' product _code ', label: ' Ürün Kodu ' },
{ key: ' color _code ', label: ' Renk ' },
{ key: ' yaka _kodu ', label: ' Yaka ' }
]
}
const activeGroupLevels = computed(() => tabGroupLevels[activeTab.value] || groupLevels)
const activeSelectedExpandLevelKeys = computed({
get: () => selectedExpandLevelKeysByTab[activeTab.value] || [],
set: value => {
selectedExpandLevelKeysByTab[activeTab.value] = Array.isArray(value) ? value : []
}
})
const expandLevelOptions = computed(() => activeGroupLevels.value.map(level => ({
label: level.label,
value: level.key
})))
const selectedExpandThroughLevel = computed(() => {
const selected = new Set(activeSelectedExpandLevelKeys.value)
const indexes = activeGroupLevels.value
.map((level, index) => selected.has(level.key) ? index : -1)
.filter(index => index >= 0)
return indexes.length ? Math.max(...indexes) : -1
})
const autoExpandThroughLevel = computed(() => {
const selectedLevel = selectedExpandThroughLevel.value
return selectedLevel >= 0 ? selectedLevel : activeGroupLevels.value.length - 1
})
const tableFilterSourceMap = computed(() => ({
products: { rows: rows.value, columns },
products: { rows: rows.value, columns: productColumns.value },
general: { rows: generalRows.value, columns: generalColumns },
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
idle: { rows: idleRows.value, columns: idleColumns },
markets: { rows: marketRows.value, columns: marketColumns },
countries: { rows: countryRows.value, columns: countryColumns },
customers: { rows: customerRows.value, columns: customerColumns }
customers: { rows: customerRows.value, columns: customerColumns },
sales_color_yaka_market_customer: { rows: salesBreakdownRows.sales_color_yaka_market_customer, columns: visibleSalesBreakdownColumns.value },
sales_product_country_segment_market_customer: { rows: salesBreakdownRows.sales_product_country_segment_market_customer, columns: visibleSalesBreakdownColumns.value },
sales_market_customer_product: { rows: salesBreakdownRows.sales_market_customer_product, columns: visibleSalesBreakdownColumns.value },
sales_country_segment_market_customer_product: { rows: salesBreakdownRows.sales_country_segment_market_customer_product, columns: visibleSalesBreakdownColumns.value }
}))
const filteredProductRows = computed(() => filterRowsForTable(' products ', rows.value, columns ))
const filteredProductRows = computed(() => filterRowsForTable(' products ', rows.value, productColumns.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))
@@ -1475,6 +1840,7 @@ const filteredIdleRows = computed(() => filterRowsForTable('idle', idleRows.valu
const filteredMarketRows = computed(() => filterRowsForTable(' markets ', marketRows.value, marketColumns))
const filteredCountryRows = computed(() => filterRowsForTable(' countries ', countryRows.value, countryColumns))
const filteredCustomerRows = computed(() => filterRowsForTable(' customers ', customerRows.value, customerColumns))
const filteredActiveSalesBreakdownRows = computed(() => filterRowsForTable(activeTab.value, salesBreakdownRows[activeTab.value] || [], visibleSalesBreakdownColumns.value))
const columnFilterOptionMap = computed(() => {
const out = {}
@@ -1502,17 +1868,34 @@ const columnFilterOptionMap = computed(() => {
const productTableRows = computed(() => {
const out = []
appendGroupRows(out, filteredProductRows.value, 0, [])
appendGroupRows(out, filteredProductRows.value, 0, [], groupLevels )
return out
})
function appendGroupRows (out, sourceRows, level, parentKeys) {
if (level >= groupLevels.length) {
out.push(...sourceRow s)
const productGroupKeys = computed(() => {
const keys = []
collectGroupKeys(keys, filteredProductRows.value, 0, [], groupLevel s)
return keys
})
const productAutoExpandKeys = computed(() => {
const keys = []
collectGroupKeys(keys, filteredProductRows.value, 0, [], activeGroupLevels.value, autoExpandThroughLevel.value)
return keys
})
const allProductGroupsExpanded = computed(() => {
const keys = productAutoExpandKeys.value
return keys.length > 0 && keys.every(key => expandedGroups.value[key] === true)
})
function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLevels) {
if (level >= levels.length) {
out.push(...sortProductLeafRows(sourceRows))
return
}
const groupDef = groupL evels[level]
const groupDef = l evels[level]
const grouped = new Map()
for (const row of sourceRows) {
const value = normalizeGroupValue(rawProductCellValue(row, groupDef.key) || row[groupDef.fallback])
@@ -1526,12 +1909,45 @@ function appendGroupRows (out, sourceRows, level, parentKeys) {
const key = [...parentKeys, `${groupDef.key}:${value}`].join(' | ')
out.push(makeGroupRow(key, level, groupDef, value, groupRows))
if (isGroupExpanded(key)) {
appendGroupRows(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`])
appendGroupRows(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`], levels )
}
})
}
function collectGroupKeys (out, sourceRows, level, parentKeys, levels = groupLevels, maxLevel = levels.length - 1) {
if (level >= levels.length) return
const groupDef = levels[level]
const grouped = new Map()
for (const row of sourceRows) {
const value = normalizeGroupValue(rawProductCellValue(row, groupDef.key) || row[groupDef.fallback])
if (!grouped.has(value)) grouped.set(value, [])
grouped.get(value).push(row)
}
Array.from(grouped.entries())
.sort((a, b) => a[0].localeCompare(b[0], ' tr '))
.forEach(([value, groupRows]) => {
const key = [...parentKeys, `${groupDef.key}:${value}`].join(' | ')
out.push(key)
if (level < maxLevel) {
collectGroupKeys(out, groupRows, level + 1, [...parentKeys, `${groupDef.key}:${value}`], levels, maxLevel)
}
})
}
function sortProductLeafRows (sourceRows) {
return [...sourceRows].sort((a, b) => {
const colorCmp = String(a?.color_code || ' ').localeCompare(String(b?.color_code || ' '), ' tr ', { numeric: true })
if (colorCmp !== 0) return colorCmp
const yakaCmp = String(a?.yaka_kodu || ' ').localeCompare(String(b?.yaka_kodu || ' '), ' tr ', { numeric: true })
if (yakaCmp !== 0) return yakaCmp
return String(a?.item_description || ' ').localeCompare(String(b?.item_description || ' '), ' tr ', { numeric: true })
})
}
function makeGroupRow (key, level, groupDef, value, groupRows) {
const imageSource = groupRows.find(row => row?.product_code) || {}
return {
__group: true,
row_key: `group|${key}`,
@@ -1539,8 +1955,11 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
level,
group_field: groupDef.key,
group_value: value,
label: `${groupDef.label}: ${ value}` ,
label: value,
count: groupRows.length,
image_product_code: imageSource.product_code || ' ',
image_color_code: imageSource.color_code || ' ',
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 '),
@@ -1550,33 +1969,48 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
askili_yan: groupDef.key === ' askili _yan ' ? value : distinctSummary(groupRows, ' askili _yan '),
urun_ana_grubu: groupDef.key === ' urun _ana _grubu ' ? value : distinctSummary(groupRows, ' urun _ana _grubu ', ' seri '),
urun_alt_grubu: groupDef.key === ' urun _alt _grubu ' ? value : distinctSummary(groupRows, ' urun _alt _grubu '),
market_key: distinctSummary(groupRows, ' market _key '),
market_key: groupDef.key === ' market _key ' ? value : distinctSummary(groupRows, 'market _key '),
stock_qty: sumRows(groupRows, ' stock _qty '),
sales_qty_90d: sumRows(groupRows, ' sales _qty _90d '),
sales_qty_180d: sumRows(groupRows, ' sales _qty _180d '),
sales_qty_365d: sumRows(groupRows, ' sales _qty _365d '),
sales_qty_total: sumRows(groupRows, ' sales _qty _total '),
sales_usd_90d: sumRows(groupRows, ' sales _usd _90d '),
sales_usd_180d: sumRows(groupRows, ' sales _usd _180d '),
sales_usd_365d: sumRows(groupRows, ' sales _usd _365d '),
sales_usd_total: sumRows(groupRows, ' sales _usd _total '),
gross_profit_usd_90d: sumRows(groupRows, ' gross _profit _usd _90d '),
gross_profit_usd_180d: sumRows(groupRows, ' gross _profit _usd _180d '),
gross_profit_usd_total: sumRows(groupRows, ' gross _profit _usd _total '),
market_count_90d: distinctCount(groupRows, ' market _key '),
customer_count_90d: sumRows(groupRows, ' customer _count _90d '),
market_count_total: sumRows(groupRows, ' market _count _total '),
customer_count_total: sumRows(groupRows, ' customer _count _total '),
avg_price_usd_90d: weightedAverage(groupRows, ' sales _usd _90d ', ' sales _qty _90d '),
avg_price_usd_180d: weightedAverage(groupRows, ' sales _usd _180d ', ' sales _qty _180d '),
avg_price_usd_365d: weightedAverage(groupRows, ' sales _usd _365d ', ' sales _qty _365d '),
avg_price_usd_total: weightedAverage(groupRows, ' sales _usd _total ', ' sales _qty _total '),
base_price_usd: weightedAverageOrAverage(groupRows, ' base _price _usd ', ' sales _qty _90d '),
cost_price_usd: weightedAverageOrAverage(groupRows, ' cost _price _usd ', ' sales _qty _90d '),
unit_profit_base_90d: weightedAverageOrAverage(groupRows, ' unit _profit _base _90d ', ' sales _qty _90d '),
unit_profit_cost_90d: weightedAverageOrAverage(groupRows, ' unit _profit _cost _90d ', ' sales _qty _90d '),
unit_profit_base_180d: weightedAverageOrAverage(groupRows, ' unit _profit _base _180d ', ' sales _qty _180d '),
unit_profit_cost_180d: weightedAverageOrAverage(groupRows, ' unit _profit _cost _180d ', ' sales _qty _180d '),
unit_profit_base_total: weightedAverageOrAverage(groupRows, ' unit _profit _base _total ', ' sales _qty _total '),
unit_profit_cost_total: weightedAverageOrAverage(groupRows, ' unit _profit _cost _total ', ' sales _qty _total '),
gross_margin_base_90d: marginFromRows(groupRows, ' sales _usd _90d ', ' sales _qty _90d ', ' base _price _usd '),
gross_margin_cost_90d: marginFromRows(groupRows, ' sales _usd _90d ', ' sales _qty _90d ', ' cost _price _usd '),
gross_margin_base_180d: marginFromRows(groupRows, ' sales _usd _180d ', ' sales _qty _180d ', ' base _price _usd '),
gross_margin_cost_180d: marginFromRows(groupRows, ' sales _usd _180d ', ' sales _qty _180d ', ' cost _price _usd '),
gross_margin_base_total: marginFromRows(groupRows, ' sales _usd _total ', ' sales _qty _total ', ' base _price _usd '),
gross_margin_cost_total: marginFromRows(groupRows, ' sales _usd _total ', ' sales _qty _total ', ' cost _price _usd '),
gross_margin_90d: ratio(sumRows(groupRows, ' gross _profit _usd _90d '), sumRows(groupRows, ' sales _usd _90d ')),
gross_margin_180d: ratio(sumRows(groupRows, ' gross _profit _usd _180d '), sumRows(groupRows, ' sales _usd _180d ')),
stock_days_90d: weightedAverageOrAverage(groupRows, ' stock _days _90d ', ' stock _qty '),
stock_days_180d: weightedAverageOrAverage(groupRows, ' stock _days _180d ', ' stock _qty '),
stock_days_total: weightedAverageOrAverage(groupRows, ' stock _days _total ', ' stock _qty '),
sales_index_90d: averageRows(groupRows, ' sales _index _90d '),
sales_index_total: averageRows(groupRows, ' sales _index _total '),
performance_score: weightedAverageOrAverage(groupRows, ' performance _score ', ' sales _qty _90d '),
performance_bucket: dominantValue(groupRows, ' performance _bucket '),
recommendation: `${formatNumber(groupRows.length, 0)} satı r`
@@ -1688,12 +2122,33 @@ function toggleGroup (key) {
}
}
function toggleAllProductGroups () {
const keys = productAutoExpandKeys.value
if (allProductGroupsExpanded.value) {
expandedGroups.value = {}
return
}
expandSelectedProductGroups()
}
function expandSelectedProductGroups () {
const keys = productAutoExpandKeys.value
const next = { ...expandedGroups.value }
for (const key of keys) next[key] = true
expandedGroups.value = next
}
function groupLabelColumnName (row) {
return row?.group_field || ' product _code '
}
function groupShowsImage (row) {
return Number(row?.level ?? 0) < autoExpandThroughLevel.value
}
function groupCellClass (name) {
return [' stock _qty ', ' sales _qty _90d ', ' sales _qty _180d ', ' sales _usd _90d ', ' sales _usd _180d ', ' stock _days _90d ', ' stock _days _180d ', ' avg _price _usd _90d ', ' avg _price _usd _180d ', ' base _price _usd ', ' cost _price _usd ', ' unit _profit _base _90d ', ' unit _profit _cost _90d ', ' unit _profit _base _180d ', ' unit _profit _cost _180d ', ' gross _profit _usd _90d ', ' gross _profit _usd _180d ', ' gross _margin _base _90d ', ' gross _margin _cost _90d ', ' gross _margin _base _180d ', ' gross _margin _cost _180d ', ' gross _margin _90d ', ' gross _margin _180d ', ' market _count _90d ', ' customer _count _90d ', ' sales _index _90d ', ' performance _score '].includes(name)
if (name === ' image ') return ' product - image - cell '
return [' stock _qty ', ' sales _qty _90d ', ' sales _qty _180d ', ' sales _qty _365d ', ' sales _qty _total ', ' sales _usd _90d ', ' sales _usd _180d ', ' sales _usd _365d ', ' sales _usd _total ', ' stock _days _90d ', ' stock _days _180d ', ' stock _days _total ', ' avg _price _usd _90d ', ' avg _price _usd _180d ', ' avg _price _usd _365d ', ' avg _price _usd _total ', ' base _price _usd ', ' cost _price _usd ', ' unit _profit _base _90d ', ' unit _profit _cost _90d ', ' unit _profit _base _180d ', ' unit _profit _cost _180d ', ' unit _profit _base _total ', ' unit _profit _cost _total ', ' gross _profit _usd _90d ', ' gross _profit _usd _180d ', ' gross _profit _usd _total ', ' gross _margin _base _90d ', ' gross _margin _cost _90d ', ' gross _margin _base _180d ', ' gross _margin _cost _180d ', ' gross _margin _base _total ', ' gross _margin _cost _total ', ' gross _margin _90d ', ' gross _margin _180d ', ' market _count _90d ', ' customer _count _90d ', ' market _count _total ', ' customer _count _total ', ' sales _index _90d ', ' sales _index _total ', ' performance _score '].includes(name)
? ' text - right '
: ' '
}
@@ -1707,6 +2162,7 @@ function formatGroupCell (row, name) {
function withProductMargins (row) {
return {
...row,
avg_price_usd_365d: Number(row?.sales_qty_365d || 0) > 0 ? Number(row?.sales_usd_365d || 0) / Number(row?.sales_qty_365d || 0) : 0,
gross_margin_base_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.base_price_usd),
gross_margin_cost_90d: marginFromSalesCost(row?.sales_usd_90d, row?.sales_qty_90d, row?.cost_price_usd),
gross_margin_base_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.base_price_usd),
@@ -1793,6 +2249,7 @@ function rawProductCellValue (row, name) {
switch (name) {
case ' urun _ilk _grubu ': return row.urun_ilk_grubu || row.yas_grubu || ' '
case ' urun _ana _grubu ': return row.urun_ana_grubu || row.seri || ' '
case ' market _key ': return displayMarketName(row.market_key)
case ' performance _bucket ': return bucketLabel(row.performance_bucket)
default: return row[name]
}
@@ -1803,6 +2260,7 @@ function rawColumnCellValue (row, col) {
switch (name) {
case ' urun _ilk _grubu ': return row.urun_ilk_grubu || row.yas_grubu || ' '
case ' urun _ana _grubu ': return row.urun_ana_grubu || row.seri || ' '
case ' market _key ': return displayMarketName(row.market_key)
case ' performance _bucket ': return bucketLabel(row.performance_bucket)
case ' is _overdue ': return row.is_overdue ? ' Evet ' : ' Hayı r '
default:
@@ -1823,26 +2281,39 @@ function formatProductCell (row, name) {
case ' stock _qty ':
case ' sales _qty _90d ':
case ' sales _qty _180d ':
case ' sales _qty _365d ':
case ' sales _qty _total ':
case ' market _count _90d ':
case ' customer _count _90d ':
case ' market _count _total ':
case ' customer _count _total ':
return formatNumber(row[name], 0)
case ' stock _days _90d ':
case ' stock _days _180d ':
case ' stock _days _total ':
case ' sales _index _90d ':
case ' sales _index _total ':
case ' performance _score ':
return formatNumber(row[name], name === ' performance _score ' ? 1 : 2)
case ' sales _usd _90d ':
case ' sales _usd _180d ':
case ' sales _usd _365d ':
case ' sales _usd _total ':
case ' avg _price _usd _90d ':
case ' avg _price _usd _180d ':
case ' avg _price _usd _365d ':
case ' avg _price _usd _total ':
case ' base _price _usd ':
case ' cost _price _usd ':
case ' unit _profit _base _90d ':
case ' unit _profit _cost _90d ':
case ' unit _profit _base _180d ':
case ' unit _profit _cost _180d ':
case ' unit _profit _base _total ':
case ' unit _profit _cost _total ':
case ' gross _profit _usd _90d ':
case ' gross _profit _usd _180d ':
case ' gross _profit _usd _total ':
return formatMoney(row[name], ' USD ')
case ' gross _margin _base _90d ':
case ' gross _margin _cost _90d ':
@@ -1857,6 +2328,8 @@ function formatProductCell (row, name) {
return row.urun_ilk_grubu || row.yas_grubu || ' '
case ' urun _ana _grubu ':
return row.urun_ana_grubu || row.seri || ' '
case ' market _key ':
return displayMarketName(row.market_key)
default:
return rawProductCellValue(row, name) || ' '
}
@@ -1873,6 +2346,35 @@ function normalizeRow (row) {
})
}
function performanceVariantKey (row) {
return [
String(row?.product_code || ' ').trim(),
String(row?.color_code || ' ').trim(),
String(row?.yaka_kodu || ' ').trim(),
displayMarketName(row?.market_key)
].join(' | ')
}
function generalMetricFields (row) {
return {
period_start: row?.period_start || ' ',
period_end: row?.period_end || ' ',
sales_qty_total: Number(row?.sales_qty_total || 0),
sales_usd_total: Number(row?.sales_usd_total || 0),
avg_daily_sales_total: Number(row?.avg_daily_sales_total || 0),
stock_days_total: Number(row?.stock_days_total || 0),
avg_price_usd_total: Number(row?.avg_price_usd_total || 0),
gross_profit_usd_total: Number(row?.gross_profit_usd_total || 0),
gross_margin_total: Number(row?.gross_margin_total || 0),
unit_profit_cost_total: Number(row?.unit_profit_cost_total || 0),
unit_profit_base_total: Number(row?.unit_profit_base_total || 0),
market_count_total: Number(row?.market_count_total || 0),
customer_count_total: Number(row?.customer_count_total || 0),
invoice_count_total: Number(row?.invoice_count_total || 0),
sales_index_total: Number(row?.sales_index_total || 0)
}
}
function normalizeGeneralRow (row) {
const product = String(row?.product_code || ' ').trim()
const color = String(row?.color_code || ' ').trim()
@@ -1923,6 +2425,13 @@ function normalizeOrderMarketDetailRow (row) {
}
}
function normalizeSalesBreakdownRow (row) {
return {
...row,
row_key: `sales-breakdown|${row?.breakdown || ' '}|${row?.product_code || ' '}|${row?.color_code || ' '}|${row?.yaka_kodu || ' '}|${row?.country || ' '}|${row?.customer_segment || ' '}|${row?.market_key || ' '}|${row?.customer_code || ' '}`
}
}
function orderGroupLabel (row) {
if (row?.breakdown === ' customer ') {
return [row.customer_code, row.customer_name].filter(Boolean).join(' - ') || ' - '
@@ -1931,12 +2440,17 @@ function orderGroupLabel (row) {
}
function productImageKey (row) {
const product = String(row?.product_code || ' ').trim()
const color = String(row?.color_code || ' ').trim()
const yaka = String(row?.yaka_kodu || ' ').trim()
const product = String(row?.image_product_code || row?.product_code || ' ').trim()
const color = String(row?.image_color_code || row?.color_code || ' ').trim()
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || ' ').trim()
return `${product}|${color}|${yaka}`
}
function displayMarketName (value) {
const parts = String(value || ' ').split(' | ').map(part => part.trim()).filter(Boolean)
return parts.length ? parts[parts.length - 1] : ' '
}
function normalizeUploadsPath (storagePath) {
const raw = String(storagePath || ' ').trim()
if (!raw) return ' '
@@ -1976,11 +2490,11 @@ async function fetchProductImagesForRow (row) {
return imageListByKey.value[key]
}
const code = String(row?.product_code || ' ').trim()
const code = String(row?.image_product_code || row?.product_code || ' ').trim()
if (!code) return []
const color = String(row?.color_code || ' ').trim()
const yaka = String(row?.yaka_kodu || ' ').trim()
const color = String(row?.image_color_code || row?.color_code || ' ').trim()
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || ' ').trim()
try {
const resp = await api.get(' / product - images ', {
@@ -2024,11 +2538,11 @@ async function openProductImageDialog (row) {
const urls = await fetchProductImagesForRow(row)
imageDialogUrls.value = urls
imageSlide.value = 0
imageDialogTitle.value = String(row?.product_code || ' - ')
imageDialogTitle.value = String(row?.image_product_code || row?.product_code || ' - ')
imageDialogSubtitle.value = [
row?.item_description,
row?.color_code ? `Renk: ${ row.color_code}` : ' ' ,
row?.yaka_kodu ? `Yaka: ${ row.yaka_kodu}` : ' '
row?.image_ color_code || row? .color_code,
row?.image_ yaka_kodu || row? .yaka_kodu
].filter(Boolean).join(' | ')
imageDialog.value = true
}
@@ -2038,9 +2552,9 @@ async function openPerformanceDialog (row) {
performanceDialogTitle.value = String(row?.product_code || ' - ')
performanceDialogSubtitle.value = [
row?.item_description,
row?.color_code ? `Renk: ${row.color_code}` : ' ' ,
row?.yaka_kodu ? `Yaka: ${row.yaka_kodu}` : ' ' ,
row?.market_key ? `Piyasa: ${row.market_key}` : ' '
row?.color_code,
row?.yaka_kodu,
row?.market_key
].filter(Boolean).join(' | ')
performanceDialog.value = true
detailLoading.value = true
@@ -2071,7 +2585,13 @@ async function openPerformanceDialog (row) {
async function reload () {
loading.value = true
try {
const [summaryResp, listResp, generalResp, marketResp, countryResp, customerResp] = await Promise.all([
const salesBreakdownRequests = salesBreakdownTabKeys.map(tabKey => {
return api.get(' / pricing / product - performance / sales - breakdown ', {
params: { limit: 800, mode: salesBreakdownModes[tabKey] },
timeout: 60000
})
})
const [summaryResp, listResp, generalResp, marketResp, countryResp, customerResp, ...salesBreakdownResp] = await Promise.all([
api.get(' / pricing / product - performance / summary ', { timeout: 60000 }),
api.get(' / pricing / product - performance ', {
params: {
@@ -2088,11 +2608,17 @@ async function reload () {
api.get(' / pricing / product - performance / customers ', {
params: { limit: 500, breakdown: customerBreakdown.value },
timeout: 60000
})
}),
...salesBreakdownRequests
])
summary.value = summaryResp?.data || {}
rows.value = (Array.isArray(list Resp?.data?.rows) ? list Resp.data.rows : []).map(normalizeRow)
generalRows.value = (Array.isArray(generalResp?.data) ? generalResp.data : []).map(normalizeGeneralRow )
const normalizedGeneralRows = (Array.isArray(general Resp?.data) ? general Resp.data : []).map(normalizeGeneral Row)
const generalByVariant = new Map(normalizedGeneralRows.map(row => [performanceVariantKey(row), generalMetricFields(row)]) )
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(row => {
const generalMetrics = generalByVariant.get(performanceVariantKey(row)) || {}
return normalizeRow({ ...row, ...generalMetrics })
})
generalRows.value = normalizedGeneralRows
marketRows.value = Array.isArray(marketResp?.data) ? marketResp.data : []
countryRows.value = (Array.isArray(countryResp?.data) ? countryResp.data : []).map(row => ({
...row,
@@ -2102,6 +2628,9 @@ async function reload () {
...row,
customer_key: `${row.breakdown || ' - '}|${row.market_key || ' - '}|${row.country || ' - '}|${row.customer_segment || ' - '}|${row.customer_code || ' - '}`
}))
salesBreakdownTabKeys.forEach((tabKey, index) => {
salesBreakdownRows[tabKey] = (Array.isArray(salesBreakdownResp[index]?.data) ? salesBreakdownResp[index].data : []).map(normalizeSalesBreakdownRow)
})
pagination.value.rowsNumber = Number(listResp?.data?.total_count || 0)
if (activeTab.value === ' order _product _customers ') {
await loadOrderProductCustomers(false)
@@ -2109,7 +2638,7 @@ async function reload () {
if (activeTab.value === ' order _market _details ') {
await loadOrderMarketDetails(false)
}
void primeProductImages([... rows.value, ...generalRows.value] )
void primeProductImages(rows.value)
} catch (err) {
Notify.create({ type: ' negative ', message: err?.response?.data || err?.message || ' Ürün performans verisi alı namadı ' })
} finally {
@@ -2180,6 +2709,7 @@ function bucketColor (value) {
case ' STOK _RISKI ': return ' orange - 8 '
case ' FIYAT _BASKISI ': return ' red - 7 '
case ' FIYAT _FIRSATI ': return ' teal - 7 '
case ' MALIYET _YOK ': return ' blue - grey - 7 '
default: return ' grey - 7 '
}
}
@@ -2193,6 +2723,14 @@ watch(activeTab, tab => {
}
})
watch(productTableRows, tableRows => {
if (activeTab.value !== ' products ' ) return
const imageRows = tableRows
. filter ( row => ! row . _ _group || groupShowsImage ( row ) )
. slice ( 0 , 240 )
void primeProductImages ( imageRows )
} , { immediate : true } )
onMounted ( reload )
< / script >
@@ -2212,6 +2750,11 @@ onMounted(reload)
margin - top : 4 px ;
}
. detail - level - menu {
width : 260 px ;
padding : 10 px ;
}
. performance - table {
max - height : calc ( 100 vh - 260 px ) ;
}
@@ -2229,6 +2772,20 @@ onMounted(reload)
. performance - table : deep ( . q - table td ) {
border - right : 1 px solid # d5dce5 ;
border - bottom : 1 px solid # d5dce5 ;
vertical - align : middle ;
white - space : normal ;
}
. performance - table : deep ( . q - table tbody td ) {
max - width : 220 px ;
}
. performance - table : deep ( . q - table tbody td span : not ( . q - badge ) : not ( . q - icon ) ) {
display : - webkit - box ;
- webkit - line - clamp : 4 ;
- webkit - box - orient : vertical ;
overflow : hidden ;
line - height : 1.25 ;
}
. performance - table : deep ( . q - table th : first - child ) ,
@@ -2244,6 +2801,113 @@ onMounted(reload)
border - top : 1 px solid # d5dce5 ;
}
. product - breakdown - table : deep ( . q - table ) {
min - width : 2050 px ;
table - layout : fixed ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( - n + 10 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( - n + 10 ) ) {
position : sticky ;
z - index : 2 ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( - n + 10 ) ) {
z - index : 4 ;
background : # f8fbff ;
}
. product - breakdown - table : deep ( . q - table tbody tr : not ( . group - row ) td : nth - child ( - n + 10 ) ) {
background : # fff ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 1 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 1 ) ) {
left : 0 ;
width : 80 px ;
min - width : 80 px ;
max - width : 80 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 2 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 2 ) ) {
left : 80 px ;
width : 96 px ;
min - width : 96 px ;
max - width : 96 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 3 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 3 ) ) {
left : 176 px ;
width : 48 px ;
min - width : 48 px ;
max - width : 48 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 4 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 4 ) ) {
left : 224 px ;
width : 48 px ;
min - width : 48 px ;
max - width : 48 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 5 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 5 ) ) {
left : 272 px ;
width : 150 px ;
min - width : 150 px ;
max - width : 150 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 6 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 6 ) ) {
left : 422 px ;
width : 72 px ;
min - width : 72 px ;
max - width : 72 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 7 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 7 ) ) {
left : 494 px ;
width : 72 px ;
min - width : 72 px ;
max - width : 72 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 8 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 8 ) ) {
left : 566 px ;
width : 92 px ;
min - width : 92 px ;
max - width : 92 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 9 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 9 ) ) {
left : 658 px ;
width : 96 px ;
min - width : 96 px ;
max - width : 96 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 10 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 10 ) ) {
left : 754 px ;
width : 96 px ;
min - width : 96 px ;
max - width : 96 px ;
box - shadow : 8 px 0 10 px - 10 px rgba ( 17 , 24 , 39 , 0.45 ) ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 11 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 11 ) ) {
width : 180 px ;
min - width : 140 px ;
}
. filterable - header - cell {
vertical - align : middle ;
}
@@ -2253,7 +2917,7 @@ onMounted(reload)
grid - template - columns : minmax ( 0 , 1 fr ) 20 px ;
align - items : center ;
gap : 2 px ;
min - width : 78 px ;
min - width : 42 px ;
}
. header - with - filter > span {
@@ -2305,47 +2969,47 @@ onMounted(reload)
}
. group - row td {
background : # eaf4ff ;
background : color - mix ( in srgb , var ( -- q - primary ) 8 % , # ffffff ) ;
font - weight : 600 ;
color : # 17324 f ;
border - color : # c3d4e6 ;
color : color - mix ( in srgb , var ( -- q - primary ) 55 % , # 1 f2937 ) ;
border - color : color - mix ( in srgb , var ( -- q - primary ) 18 % , # d5dce5 ) ;
white - space : nowrap ;
}
. group - row - level - 0 td {
background : # 1565 c0 ;
background : color - mix ( in srgb , var ( -- q - primary ) 82 % , # 1 f2937 ) ;
color : # fff ;
border - color : # 0 d47a1 ;
border - color : color - mix ( in srgb , var ( -- q - primary ) 75 % , # 111827 ) ;
}
. group - row - level - 1 td {
background : # 2 f7fd0 ;
background : color - mix ( in srgb , var ( -- q - primary ) 68 % , # ffffff ) ;
color : # fff ;
border - color : # 1565 c0 ;
border - color : color - mix ( in srgb , var ( -- q - primary ) 58 % , # d5dce5 ) ;
}
. group - row - level - 2 td {
background : # 6 da9df ;
color : # 0 c2f52 ;
border - color : # 3 c83c7 ;
background : color - mix ( in srgb , var ( -- q - primary ) 36 % , # ffffff ) ;
color : color - mix ( in srgb , var ( -- q - primary ) 65 % , # 111827 ) ;
border - color : color - mix ( in srgb , var ( -- q - primary ) 34 % , # d5dce5 ) ;
}
. group - row - level - 3 td {
background : # a9cdef ;
color : # 12385 c ;
border - color : # 78 addb ;
background : color - mix ( in srgb , var ( -- q - primary ) 22 % , # ffffff ) ;
color : color - mix ( in srgb , var ( -- q - primary ) 58 % , # 1 f2937 ) ;
border - color : color - mix ( in srgb , var ( -- q - primary ) 24 % , # d5dce5 ) ;
}
. group - row - level - 4 td {
background : # d1e5f8 ;
color : # 17324 f ;
border - color : # 9 fc5e8 ;
background : color - mix ( in srgb , var ( -- q - primary ) 14 % , # ffffff ) ;
color : color - mix ( in srgb , var ( -- q - primary ) 52 % , # 1 f2937 ) ;
border - color : color - mix ( in srgb , var ( -- q - primary ) 18 % , # d5dce5 ) ;
}
. group - row - level - 5 td {
background : # eaf4ff ;
color : # 17324 f ;
border - color : # c3d4e6 ;
background : color - mix ( in srgb , var ( -- q - primary ) 8 % , # ffffff ) ;
color : color - mix ( in srgb , var ( -- q - primary ) 48 % , # 1 f2937 ) ;
border - color : color - mix ( in srgb , var ( -- q - primary ) 14 % , # d5dce5 ) ;
}
. group - cell - label {
@@ -2360,24 +3024,27 @@ onMounted(reload)
}
. product - image - cell {
width : 72 px ;
min - width : 72 px ;
width : 80 px ;
min - width : 80 px ;
}
. product - thumb - button {
width : 56 px ;
height : 72 px ;
width : 68 px ;
height : 92 px ;
border : 1 px solid # d9dde3 ;
border - radius : 6 px ;
background : # fff ;
padding : 2 px ;
padding : 4 px ;
cursor : pointer ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. product - thumb ,
. product - thumb - placeholder {
width : 50 px ;
height : 66 px ;
width : 58 px ;
height : 82 px ;
}
. product - thumb - placeholder {