ui: update ProductPerformanceProfitability page to enhance table views and tabs management
This commit is contained in:
@@ -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="columns"
|
||||
:columns="productColumns"
|
||||
: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: '90G', value: '90' },
|
||||
{ label: '180G', value: '180' },
|
||||
{ label: '360G', 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: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_180d', label: '180G Satış', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_365d', label: '360G 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: '90G USD', field: 'sales_usd_90d', align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_180d', label: '180G USD', field: 'sales_usd_180d', align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_365d', label: '360G 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: '180G 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: '180G Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right' },
|
||||
{ name: 'avg_price_usd_365d', label: '360G 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: '90G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_cost_90d', label: '90G P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_base_180d', label: '180G P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'unit_profit_cost_180d', label: '180G 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: '90G Toplam K/Z', field: row => formatMoney(row.gross_profit_usd_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_usd_180d', label: '180G 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: '90G Taban Marj', field: row => formatPercent(row.gross_margin_base_90d), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_cost_90d', label: '90G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_90d), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_base_180d', label: '180G Taban Marj', field: row => formatPercent(row.gross_margin_base_180d), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_cost_180d', label: '180G Çı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: '90G Piyasa', field: row => formatNumber(row.market_count_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'customer_count_90d', label: '90G 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: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_90d', label: '90G 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: '90G 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: '365G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_365d', label: '365G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true }
|
||||
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_365d', label: '360G 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: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_90d', label: '90G 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: '90G Taban', field: row => formatMoney(row.base_price_usd_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'cost_price_usd_90d', label: '90G Çıplak', field: row => formatMoney(row.cost_price_usd_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_base_usd_90d', label: '90G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_cost_usd_90d', label: '90G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_90d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_base_90d', label: '90G Taban Marj', field: row => formatPercent(row.gross_margin_base_90d), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_cost_90d', label: '90G Çı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: '365G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_365d', label: '365G USD', field: 'sales_usd_365d', align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_365d', label: '360G 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: '90G Müşteri', field: row => formatNumber(row.customer_count_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'invoice_count_90d', label: '90G Fatura', field: row => formatNumber(row.invoice_count_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_90d', label: '90G Adet', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_90d', label: '90G 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: '180G Müşteri', field: row => formatNumber(row.customer_count_180d, 0), align: 'right', sortable: true },
|
||||
{ name: 'invoice_count_180d', label: '180G Fatura', field: row => formatNumber(row.invoice_count_180d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_180d', label: '180G Adet', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_180d', label: '180G USD', field: row => formatMoney(row.sales_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'avg_price_usd_180d', label: '180G Ort. USD', field: row => formatMoney(row.avg_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'base_price_usd_180d', label: '180G Taban', field: row => formatMoney(row.base_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'cost_price_usd_180d', label: '180G Çıplak', field: row => formatMoney(row.cost_price_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_base_usd_180d', label: '180G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_cost_usd_180d', label: '180G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_180d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_base_180d', label: '180G Taban Marj', field: row => formatPercent(row.gross_margin_base_180d), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_cost_180d', label: '180G Çıplak Marj', field: row => formatPercent(row.gross_margin_cost_180d), align: 'right', sortable: true },
|
||||
{ name: 'sales_qty_365d', label: '360G Adet', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||||
{ name: 'sales_usd_365d', label: '360G USD', field: row => formatMoney(row.sales_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'avg_price_usd_365d', label: '360G Ort. USD', field: row => formatMoney(row.avg_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'base_price_usd_365d', label: '360G Taban', field: row => formatMoney(row.base_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'cost_price_usd_365d', label: '360G Çıplak', field: row => formatMoney(row.cost_price_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_base_usd_365d', label: '360G Taban K/Z', field: row => formatMoney(row.gross_profit_base_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_profit_cost_usd_365d', label: '360G Çıplak K/Z', field: row => formatMoney(row.gross_profit_cost_usd_365d, 'USD'), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_base_365d', label: '360G Taban Marj', field: row => formatPercent(row.gross_margin_base_365d), align: 'right', sortable: true },
|
||||
{ name: 'gross_margin_cost_365d', label: '360G Çı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(...sourceRows)
|
||||
const productGroupKeys = computed(() => {
|
||||
const keys = []
|
||||
collectGroupKeys(keys, filteredProductRows.value, 0, [], groupLevels)
|
||||
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 = groupLevels[level]
|
||||
const groupDef = levels[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(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow)
|
||||
generalRows.value = (Array.isArray(generalResp?.data) ? generalResp.data : []).map(normalizeGeneralRow)
|
||||
const normalizedGeneralRows = (Array.isArray(generalResp?.data) ? generalResp.data : []).map(normalizeGeneralRow)
|
||||
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: 4px;
|
||||
}
|
||||
|
||||
.detail-level-menu {
|
||||
width: 260px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.performance-table {
|
||||
max-height: calc(100vh - 260px);
|
||||
}
|
||||
@@ -2229,6 +2772,20 @@ onMounted(reload)
|
||||
.performance-table :deep(.q-table td) {
|
||||
border-right: 1px solid #d5dce5;
|
||||
border-bottom: 1px solid #d5dce5;
|
||||
vertical-align: middle;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table tbody td) {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.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: 1px solid #d5dce5;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table) {
|
||||
min-width: 2050px;
|
||||
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: 80px;
|
||||
min-width: 80px;
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(2)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(2)) {
|
||||
left: 80px;
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
max-width: 96px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(3)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(3)) {
|
||||
left: 176px;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
max-width: 48px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(4)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(4)) {
|
||||
left: 224px;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
max-width: 48px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(5)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(5)) {
|
||||
left: 272px;
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(6)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(6)) {
|
||||
left: 422px;
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
max-width: 72px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(7)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(7)) {
|
||||
left: 494px;
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
max-width: 72px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(8)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(8)) {
|
||||
left: 566px;
|
||||
width: 92px;
|
||||
min-width: 92px;
|
||||
max-width: 92px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(9)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(9)) {
|
||||
left: 658px;
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
max-width: 96px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(10)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(10)) {
|
||||
left: 754px;
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
max-width: 96px;
|
||||
box-shadow: 8px 0 10px -10px 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: 180px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.filterable-header-cell {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -2253,7 +2917,7 @@ onMounted(reload)
|
||||
grid-template-columns: minmax(0, 1fr) 20px;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 78px;
|
||||
min-width: 42px;
|
||||
}
|
||||
|
||||
.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: #17324f;
|
||||
border-color: #c3d4e6;
|
||||
color: color-mix(in srgb, var(--q-primary) 55%, #1f2937);
|
||||
border-color: color-mix(in srgb, var(--q-primary) 18%, #d5dce5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group-row-level-0 td {
|
||||
background: #1565c0;
|
||||
background: color-mix(in srgb, var(--q-primary) 82%, #1f2937);
|
||||
color: #fff;
|
||||
border-color: #0d47a1;
|
||||
border-color: color-mix(in srgb, var(--q-primary) 75%, #111827);
|
||||
}
|
||||
|
||||
.group-row-level-1 td {
|
||||
background: #2f7fd0;
|
||||
background: color-mix(in srgb, var(--q-primary) 68%, #ffffff);
|
||||
color: #fff;
|
||||
border-color: #1565c0;
|
||||
border-color: color-mix(in srgb, var(--q-primary) 58%, #d5dce5);
|
||||
}
|
||||
|
||||
.group-row-level-2 td {
|
||||
background: #6da9df;
|
||||
color: #0c2f52;
|
||||
border-color: #3c83c7;
|
||||
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: #12385c;
|
||||
border-color: #78addb;
|
||||
background: color-mix(in srgb, var(--q-primary) 22%, #ffffff);
|
||||
color: color-mix(in srgb, var(--q-primary) 58%, #1f2937);
|
||||
border-color: color-mix(in srgb, var(--q-primary) 24%, #d5dce5);
|
||||
}
|
||||
|
||||
.group-row-level-4 td {
|
||||
background: #d1e5f8;
|
||||
color: #17324f;
|
||||
border-color: #9fc5e8;
|
||||
background: color-mix(in srgb, var(--q-primary) 14%, #ffffff);
|
||||
color: color-mix(in srgb, var(--q-primary) 52%, #1f2937);
|
||||
border-color: color-mix(in srgb, var(--q-primary) 18%, #d5dce5);
|
||||
}
|
||||
|
||||
.group-row-level-5 td {
|
||||
background: #eaf4ff;
|
||||
color: #17324f;
|
||||
border-color: #c3d4e6;
|
||||
background: color-mix(in srgb, var(--q-primary) 8%, #ffffff);
|
||||
color: color-mix(in srgb, var(--q-primary) 48%, #1f2937);
|
||||
border-color: color-mix(in srgb, var(--q-primary) 14%, #d5dce5);
|
||||
}
|
||||
|
||||
.group-cell-label {
|
||||
@@ -2360,24 +3024,27 @@ onMounted(reload)
|
||||
}
|
||||
|
||||
.product-image-cell {
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.product-thumb-button {
|
||||
width: 56px;
|
||||
height: 72px;
|
||||
width: 68px;
|
||||
height: 92px;
|
||||
border: 1px solid #d9dde3;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
padding: 2px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.product-thumb,
|
||||
.product-thumb-placeholder {
|
||||
width: 50px;
|
||||
height: 66px;
|
||||
width: 58px;
|
||||
height: 82px;
|
||||
}
|
||||
|
||||
.product-thumb-placeholder {
|
||||
|
||||
Reference in New Issue
Block a user