Compare commits
2
Commits
a45478d15f
...
9e22cc5ec5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e22cc5ec5 | ||
|
|
5e2a85b98e |
@@ -3123,6 +3123,7 @@ type ProductPerformanceGroupedRequest struct {
|
|||||||
GroupLevels []string
|
GroupLevels []string
|
||||||
ExpandedKeys map[string]bool
|
ExpandedKeys map[string]bool
|
||||||
Limit int
|
Limit int
|
||||||
|
MainGroup string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
||||||
@@ -3154,17 +3155,26 @@ type productPerformanceSQLGroupFilter struct {
|
|||||||
|
|
||||||
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
||||||
mode := strings.TrimSpace(req.Mode)
|
mode := strings.TrimSpace(req.Mode)
|
||||||
if mode != "products" && mode != "idle" {
|
if mode != "products" && mode != "product_detail" && mode != "idle" {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
if err := EnsureProductPerformanceTables(pg); err != nil {
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
||||||
return nil, true, err
|
return nil, true, err
|
||||||
}
|
}
|
||||||
out := make([]map[string]any, 0, 512)
|
out := make([]map[string]any, 0, 512)
|
||||||
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, nil, req.ExpandedKeys, req.Limit)
|
filters := productPerformanceGroupedBaseFilters(req)
|
||||||
|
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.Limit)
|
||||||
return out, true, err
|
return out, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
|
||||||
|
mainGroup := strings.TrimSpace(req.MainGroup)
|
||||||
|
if strings.TrimSpace(req.Mode) != "product_detail" || mainGroup == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []productPerformanceSQLGroupFilter{{Field: "urun_ana_grubu", Value: mainGroup}}
|
||||||
|
}
|
||||||
|
|
||||||
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, limit int) error {
|
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, limit int) error {
|
||||||
if level >= len(levels) {
|
if level >= len(levels) {
|
||||||
rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit)
|
rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit)
|
||||||
@@ -3948,6 +3958,8 @@ func defaultProductPerformanceGroupLevels(mode string) []string {
|
|||||||
switch mode {
|
switch mode {
|
||||||
case "sales_color_yaka_market_customer":
|
case "sales_color_yaka_market_customer":
|
||||||
return []string{"color_code", "yaka_kodu", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
return []string{"color_code", "yaka_kodu", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
||||||
|
case "product_detail":
|
||||||
|
return []string{"urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu", "market_key"}
|
||||||
case "idle":
|
case "idle":
|
||||||
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu"}
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu"}
|
||||||
case "sales_product_country_segment_market_customer":
|
case "sales_product_country_segment_market_customer":
|
||||||
|
|||||||
@@ -509,6 +509,9 @@ Scope AS (
|
|||||||
PriceDim AS (
|
PriceDim AS (
|
||||||
SELECT
|
SELECT
|
||||||
product_code,
|
product_code,
|
||||||
|
COALESCE(urun_ilk_grubu, '') AS urun_ilk_grubu,
|
||||||
|
COALESCE(urun_ana_grubu, '') AS urun_ana_grubu,
|
||||||
|
COALESCE(urun_alt_grubu, '') AS urun_alt_grubu,
|
||||||
CASE
|
CASE
|
||||||
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN base_price_usd
|
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN base_price_usd
|
||||||
ELSE cost_price_usd
|
ELSE cost_price_usd
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
|
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
|
||||||
|
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
|
||||||
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
|
||||||
limit := intQuery(r, "limit", 50000)
|
limit := intQuery(r, "limit", 50000)
|
||||||
expanded := map[string]bool{}
|
expanded := map[string]bool{}
|
||||||
@@ -278,6 +279,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
GroupLevels []string `json:"group_levels"`
|
GroupLevels []string `json:"group_levels"`
|
||||||
ExpandedKeys []string `json:"expanded_keys"`
|
ExpandedKeys []string `json:"expanded_keys"`
|
||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
|
MainGroup string `json:"urun_ana_grubu"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
|
||||||
@@ -286,6 +288,9 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
if strings.TrimSpace(body.Mode) != "" {
|
if strings.TrimSpace(body.Mode) != "" {
|
||||||
mode = strings.TrimSpace(body.Mode)
|
mode = strings.TrimSpace(body.Mode)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(body.MainGroup) != "" {
|
||||||
|
mainGroup = strings.TrimSpace(body.MainGroup)
|
||||||
|
}
|
||||||
if len(body.GroupLevels) > 0 {
|
if len(body.GroupLevels) > 0 {
|
||||||
groupLevels = body.GroupLevels
|
groupLevels = body.GroupLevels
|
||||||
}
|
}
|
||||||
@@ -312,6 +317,7 @@ func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
GroupLevels: groupLevels,
|
GroupLevels: groupLevels,
|
||||||
ExpandedKeys: expanded,
|
ExpandedKeys: expanded,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
|
MainGroup: mainGroup,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -86,6 +86,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'product_detail'" class="main-group-bar q-mb-xs">
|
||||||
|
<button
|
||||||
|
v-for="option in detailMainGroupOptions"
|
||||||
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
:class="['main-group-button', { active: selectedDetailMainGroup === option.value }]"
|
||||||
|
:disabled="pageBusy"
|
||||||
|
@click="selectedDetailMainGroup = option.value"
|
||||||
|
>
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
<small>{{ formatNumber(option.stock_qty, 0) }} stok</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
v-if="activeTab === 'general'"
|
v-if="activeTab === 'general'"
|
||||||
flat
|
flat
|
||||||
@@ -525,13 +539,13 @@
|
|||||||
</q-table>
|
</q-table>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
v-else-if="activeTab === 'products'"
|
v-else-if="isProductKpiTab"
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
row-key="row_key"
|
row-key="row_key"
|
||||||
class="performance-table product-breakdown-table bg-white"
|
class="performance-table product-breakdown-table bg-white"
|
||||||
:rows="displayProductTableRows"
|
:rows="displayProductKpiTableRows"
|
||||||
:columns="productColumns"
|
:columns="activeProductColumns"
|
||||||
:loading="loading || backendGroupedLoading"
|
:loading="loading || backendGroupedLoading"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
:sort-method="sortProductGroupedTableRows"
|
:sort-method="sortProductGroupedTableRows"
|
||||||
@@ -549,17 +563,17 @@
|
|||||||
round
|
round
|
||||||
size="8px"
|
size="8px"
|
||||||
icon="filter_alt"
|
icon="filter_alt"
|
||||||
:color="hasColumnFilter('products', props.col.name) ? 'primary' : 'grey-7'"
|
:color="hasColumnFilter(activeProductTableKey, props.col.name) ? 'primary' : 'grey-7'"
|
||||||
class="header-filter-btn"
|
class="header-filter-btn"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<q-badge v-if="hasColumnFilter('products', props.col.name)" color="primary" floating rounded>
|
<q-badge v-if="hasColumnFilter(activeProductTableKey, props.col.name)" color="primary" floating rounded>
|
||||||
{{ getColumnFilterBadgeValue('products', props.col.name) }}
|
{{ getColumnFilterBadgeValue(activeProductTableKey, props.col.name) }}
|
||||||
</q-badge>
|
</q-badge>
|
||||||
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||||
<div class="excel-filter-menu">
|
<div class="excel-filter-menu">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="columnFilterSearch[filterKey('products', props.col.name)]"
|
v-model="columnFilterSearch[filterKey(activeProductTableKey, props.col.name)]"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
clearable
|
clearable
|
||||||
@@ -567,13 +581,13 @@
|
|||||||
placeholder="Ara"
|
placeholder="Ara"
|
||||||
/>
|
/>
|
||||||
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
||||||
<q-btn flat dense size="sm" label="Tümünü Seç" @click="selectAllColumnFilterOptions('products', props.col.name)" />
|
<q-btn flat dense size="sm" label="Tümünü Seç" @click="selectAllColumnFilterOptions(activeProductTableKey, props.col.name)" />
|
||||||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('products', props.col.name)" />
|
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(activeProductTableKey, props.col.name)" />
|
||||||
</div>
|
</div>
|
||||||
<q-virtual-scroll
|
<q-virtual-scroll
|
||||||
v-if="columnFilterOptions('products', props.col.name).length > 0"
|
v-if="columnFilterOptions(activeProductTableKey, props.col.name).length > 0"
|
||||||
class="excel-filter-options"
|
class="excel-filter-options"
|
||||||
:items="columnFilterOptions('products', props.col.name)"
|
:items="columnFilterOptions(activeProductTableKey, props.col.name)"
|
||||||
:virtual-scroll-item-size="32"
|
:virtual-scroll-item-size="32"
|
||||||
separator
|
separator
|
||||||
>
|
>
|
||||||
@@ -583,14 +597,14 @@
|
|||||||
dense
|
dense
|
||||||
clickable
|
clickable
|
||||||
class="excel-filter-option"
|
class="excel-filter-option"
|
||||||
@click="toggleColumnFilterValue('products', props.col.name, option.value)"
|
@click="toggleColumnFilterValue(activeProductTableKey, props.col.name, option.value)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="isColumnFilterValueSelected('products', props.col.name, option.value)"
|
:model-value="isColumnFilterValueSelected(activeProductTableKey, props.col.name, option.value)"
|
||||||
@update:model-value="() => toggleColumnFilterValue('products', props.col.name, option.value)"
|
@update:model-value="() => toggleColumnFilterValue(activeProductTableKey, props.col.name, option.value)"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
@@ -1521,6 +1535,7 @@ const summary = ref({})
|
|||||||
const activeTab = ref('products')
|
const activeTab = ref('products')
|
||||||
const customerBreakdown = ref('market_customer')
|
const customerBreakdown = ref('market_customer')
|
||||||
const selectedPeriods = ref(['90', '180', '360', 'total'])
|
const selectedPeriods = ref(['90', '180', '360', 'total'])
|
||||||
|
const selectedDetailMainGroup = ref('')
|
||||||
const imageUrlByKey = ref({})
|
const imageUrlByKey = ref({})
|
||||||
const imageListByKey = ref({})
|
const imageListByKey = ref({})
|
||||||
const imageDialog = ref(false)
|
const imageDialog = ref(false)
|
||||||
@@ -1549,6 +1564,7 @@ const expandedGroups = ref({})
|
|||||||
const lastManualExpandedGroupKey = ref('')
|
const lastManualExpandedGroupKey = ref('')
|
||||||
const selectedExpandLevelKeysByTab = reactive({
|
const selectedExpandLevelKeysByTab = reactive({
|
||||||
products: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
products: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
||||||
|
product_detail: ['urun_ana_grubu', 'urun_alt_grubu', 'product_code', 'color_code', 'yaka_kodu'],
|
||||||
sales_color_yaka_market_customer: ['color_code', 'yaka_kodu', 'urun_ilk_grubu', 'askili_yan'],
|
sales_color_yaka_market_customer: ['color_code', 'yaka_kodu', 'urun_ilk_grubu', 'askili_yan'],
|
||||||
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori', 'urun_ana_grubu', 'urun_alt_grubu'],
|
||||||
sales_product_country_segment_market_customer: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
sales_product_country_segment_market_customer: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
||||||
@@ -1561,7 +1577,8 @@ const productKpiFetchLimit = 50000
|
|||||||
const reportFetchLimit = 50000
|
const reportFetchLimit = 50000
|
||||||
const maxBulkExpandKeys = 1200
|
const maxBulkExpandKeys = 1200
|
||||||
const performanceTabs = [
|
const performanceTabs = [
|
||||||
{ name: 'products', icon: 'inventory_2', label: 'Ürün KPI' },
|
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
||||||
|
{ name: 'product_detail', icon: 'category', label: 'Detay KPI' },
|
||||||
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk/Yaka > Piyasa > Müşteri' },
|
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk/Yaka > Piyasa > Müşteri' },
|
||||||
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
||||||
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
|
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
|
||||||
@@ -1842,6 +1859,10 @@ function reorderColumnsInPlace (targetColumns) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productColumns = computed(() => orderedMetricColumns(columns.filter(visiblePeriodColumn)))
|
const productColumns = computed(() => orderedMetricColumns(columns.filter(visiblePeriodColumn)))
|
||||||
|
const productDetailColumns = computed(() => orderedMetricColumns(columns
|
||||||
|
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
|
||||||
|
.filter(visiblePeriodColumn)))
|
||||||
|
const activeProductColumns = computed(() => activeTab.value === 'product_detail' ? productDetailColumns.value : productColumns.value)
|
||||||
|
|
||||||
const generalColumns = [
|
const generalColumns = [
|
||||||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||||||
@@ -2386,6 +2407,14 @@ const groupLevels = [
|
|||||||
|
|
||||||
const tabGroupLevels = {
|
const tabGroupLevels = {
|
||||||
products: groupLevels,
|
products: groupLevels,
|
||||||
|
product_detail: [
|
||||||
|
{ key: 'urun_ana_grubu', label: 'Ürün Ana Grubu' },
|
||||||
|
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||||||
|
{ key: 'product_code', label: 'Ürün' },
|
||||||
|
{ key: 'color_code', label: 'Renk' },
|
||||||
|
{ key: 'yaka_kodu', label: 'Yaka' },
|
||||||
|
{ key: 'market_key', label: 'Piyasa' }
|
||||||
|
],
|
||||||
sales_color_yaka_market_customer: [
|
sales_color_yaka_market_customer: [
|
||||||
{ key: 'color_code', label: 'Renk' },
|
{ key: 'color_code', label: 'Renk' },
|
||||||
{ key: 'yaka_kodu', label: 'Yaka' },
|
{ key: 'yaka_kodu', label: 'Yaka' },
|
||||||
@@ -2507,6 +2536,7 @@ const autoExpandThroughLevel = computed(() => {
|
|||||||
|
|
||||||
const tableFilterSourceMap = computed(() => ({
|
const tableFilterSourceMap = computed(() => ({
|
||||||
products: { rows: rows.value, columns: productColumns.value },
|
products: { rows: rows.value, columns: productColumns.value },
|
||||||
|
product_detail: { rows: detailProductRows.value, columns: productDetailColumns.value },
|
||||||
general: { rows: generalRows.value, columns: generalColumns },
|
general: { rows: generalRows.value, columns: generalColumns },
|
||||||
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
|
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
|
||||||
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
|
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
|
||||||
@@ -2521,6 +2551,14 @@ const tableFilterSourceMap = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, productColumns.value))
|
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, productColumns.value))
|
||||||
|
const isProductKpiTab = computed(() => activeTab.value === 'products' || activeTab.value === 'product_detail')
|
||||||
|
const activeProductTableKey = computed(() => activeTab.value === 'product_detail' ? 'product_detail' : 'products')
|
||||||
|
const detailProductRows = computed(() => {
|
||||||
|
const selected = String(selectedDetailMainGroup.value || '').trim()
|
||||||
|
if (!selected) return rows.value
|
||||||
|
return rows.value.filter(row => productMainGroupValue(row) === selected)
|
||||||
|
})
|
||||||
|
const filteredDetailProductRows = computed(() => filterRowsForTable('product_detail', detailProductRows.value, productDetailColumns.value))
|
||||||
const filteredGeneralRows = computed(() => filterRowsForTable('general', generalRows.value, generalColumns))
|
const filteredGeneralRows = computed(() => filterRowsForTable('general', generalRows.value, generalColumns))
|
||||||
const filteredOrderProductCustomerRows = computed(() => filterRowsForTable('order_product_customers', orderProductCustomerRows.value, orderProductCustomerColumns))
|
const filteredOrderProductCustomerRows = computed(() => filterRowsForTable('order_product_customers', orderProductCustomerRows.value, orderProductCustomerColumns))
|
||||||
const filteredOrderMarketDetailRows = computed(() => filterRowsForTable('order_market_details', orderMarketDetailRows.value, orderMarketDetailColumns))
|
const filteredOrderMarketDetailRows = computed(() => filterRowsForTable('order_market_details', orderMarketDetailRows.value, orderMarketDetailColumns))
|
||||||
@@ -2531,6 +2569,7 @@ const filteredCustomerRows = computed(() => filterRowsForTable('customers', cust
|
|||||||
const filteredActiveSalesBreakdownRows = computed(() => filterRowsForTable(activeTab.value, salesBreakdownRows[activeTab.value] || [], visibleSalesBreakdownColumns.value))
|
const filteredActiveSalesBreakdownRows = computed(() => filterRowsForTable(activeTab.value, salesBreakdownRows[activeTab.value] || [], visibleSalesBreakdownColumns.value))
|
||||||
const activeGroupSourceRows = computed(() => {
|
const activeGroupSourceRows = computed(() => {
|
||||||
if (activeTab.value === 'products') return filteredProductRows.value
|
if (activeTab.value === 'products') return filteredProductRows.value
|
||||||
|
if (activeTab.value === 'product_detail') return filteredDetailProductRows.value
|
||||||
if (activeTab.value === 'idle') return filteredIdleRows.value
|
if (activeTab.value === 'idle') return filteredIdleRows.value
|
||||||
if (activeTab.value === 'order_product_customers') return filteredOrderProductCustomerRows.value
|
if (activeTab.value === 'order_product_customers') return filteredOrderProductCustomerRows.value
|
||||||
if (activeTab.value === 'order_market_details') return filteredOrderMarketDetailRows.value
|
if (activeTab.value === 'order_market_details') return filteredOrderMarketDetailRows.value
|
||||||
@@ -2567,17 +2606,34 @@ const productTableRows = computed(() => {
|
|||||||
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
||||||
return out
|
return out
|
||||||
})
|
})
|
||||||
|
const detailProductTableRows = computed(() => buildGroupedTableRows('product_detail', filteredDetailProductRows.value))
|
||||||
|
|
||||||
const idleTableRows = computed(() => buildGroupedTableRows('idle', filteredIdleRows.value))
|
const idleTableRows = computed(() => buildGroupedTableRows('idle', filteredIdleRows.value))
|
||||||
const orderProductCustomerTableRows = computed(() => buildGroupedTableRows('order_product_customers', filteredOrderProductCustomerRows.value))
|
const orderProductCustomerTableRows = computed(() => buildGroupedTableRows('order_product_customers', filteredOrderProductCustomerRows.value))
|
||||||
const orderMarketDetailTableRows = computed(() => buildGroupedTableRows('order_market_details', filteredOrderMarketDetailRows.value))
|
const orderMarketDetailTableRows = computed(() => buildGroupedTableRows('order_market_details', filteredOrderMarketDetailRows.value))
|
||||||
const activeSalesBreakdownTableRows = computed(() => buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
|
const activeSalesBreakdownTableRows = computed(() => buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
|
||||||
const displayProductTableRows = computed(() => backendRowsForTab('products', productTableRows.value))
|
const displayProductTableRows = computed(() => backendRowsForTab('products', productTableRows.value))
|
||||||
|
const displayDetailProductTableRows = computed(() => backendRowsForTab('product_detail', detailProductTableRows.value))
|
||||||
|
const displayProductKpiTableRows = computed(() => activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : displayProductTableRows.value)
|
||||||
const displayIdleTableRows = computed(() => backendRowsForTab('idle', idleTableRows.value))
|
const displayIdleTableRows = computed(() => backendRowsForTab('idle', idleTableRows.value))
|
||||||
const displayOrderProductCustomerTableRows = computed(() => backendRowsForTab('order_product_customers', orderProductCustomerTableRows.value))
|
const displayOrderProductCustomerTableRows = computed(() => backendRowsForTab('order_product_customers', orderProductCustomerTableRows.value))
|
||||||
const displayOrderMarketDetailTableRows = computed(() => backendRowsForTab('order_market_details', orderMarketDetailTableRows.value))
|
const displayOrderMarketDetailTableRows = computed(() => backendRowsForTab('order_market_details', orderMarketDetailTableRows.value))
|
||||||
const displayActiveSalesBreakdownTableRows = computed(() => backendRowsForTab(activeTab.value, activeSalesBreakdownTableRows.value))
|
const displayActiveSalesBreakdownTableRows = computed(() => backendRowsForTab(activeTab.value, activeSalesBreakdownTableRows.value))
|
||||||
|
|
||||||
|
const detailMainGroupOptions = computed(() => {
|
||||||
|
const grouped = new Map()
|
||||||
|
for (const row of rows.value) {
|
||||||
|
const value = productMainGroupValue(row)
|
||||||
|
if (!value) continue
|
||||||
|
const current = grouped.get(value) || { value, label: value, stock_qty: 0, sales_qty_90d: 0 }
|
||||||
|
current.stock_qty += Number(row?.stock_qty || 0)
|
||||||
|
current.sales_qty_90d += Number(row?.sales_qty_90d || 0)
|
||||||
|
grouped.set(value, current)
|
||||||
|
}
|
||||||
|
return Array.from(grouped.values())
|
||||||
|
.sort((a, b) => Number(b.sales_qty_90d || 0) - Number(a.sales_qty_90d || 0) || a.label.localeCompare(b.label, 'tr'))
|
||||||
|
})
|
||||||
|
|
||||||
const productGroupKeys = computed(() => {
|
const productGroupKeys = computed(() => {
|
||||||
const keys = []
|
const keys = []
|
||||||
collectGroupKeys(keys, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
collectGroupKeys(keys, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
||||||
@@ -4000,6 +4056,7 @@ async function loadProductDetailRows (row) {
|
|||||||
|
|
||||||
function backendGroupedSupportedTab (tabKey = activeTab.value) {
|
function backendGroupedSupportedTab (tabKey = activeTab.value) {
|
||||||
return tabKey === 'products' ||
|
return tabKey === 'products' ||
|
||||||
|
tabKey === 'product_detail' ||
|
||||||
tabKey === 'idle' ||
|
tabKey === 'idle' ||
|
||||||
tabKey === 'order_product_customers' ||
|
tabKey === 'order_product_customers' ||
|
||||||
tabKey === 'order_market_details' ||
|
tabKey === 'order_market_details' ||
|
||||||
@@ -4059,7 +4116,10 @@ async function loadBackendGroupedRows () {
|
|||||||
mode: tabKey,
|
mode: tabKey,
|
||||||
groupLevels: activeGroupLevels.value.map(level => level.key),
|
groupLevels: activeGroupLevels.value.map(level => level.key),
|
||||||
expandedKeys: activeExpandedGroupKeys(),
|
expandedKeys: activeExpandedGroupKeys(),
|
||||||
limit: tabKey === 'products' || tabKey === 'idle' ? productKpiFetchLimit : 50000
|
limit: tabKey === 'products' || tabKey === 'product_detail' || tabKey === 'idle' ? productKpiFetchLimit : 50000
|
||||||
|
}
|
||||||
|
if (tabKey === 'product_detail') {
|
||||||
|
params.urunAnaGrubu = selectedDetailMainGroup.value
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const startedAt = performance.now()
|
const startedAt = performance.now()
|
||||||
@@ -4088,7 +4148,7 @@ function normalizeGroupedDisplayRow (tabKey, row) {
|
|||||||
return withOrderPeriodScores(nextRow)
|
return withOrderPeriodScores(nextRow)
|
||||||
}
|
}
|
||||||
const next = withPeriodScores(nextRow)
|
const next = withPeriodScores(nextRow)
|
||||||
if (tabKey !== 'products' && tabKey !== 'idle') {
|
if (tabKey !== 'products' && tabKey !== 'product_detail' && tabKey !== 'idle') {
|
||||||
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
||||||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||||||
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
||||||
@@ -4276,9 +4336,33 @@ watch(activeSelectedExpandLevelKeys, () => {
|
|||||||
scheduleLoadBackendGroupedRows()
|
scheduleLoadBackendGroupedRows()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(detailMainGroupOptions, options => {
|
||||||
|
if (!options.length) {
|
||||||
|
selectedDetailMainGroup.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedDetailMainGroup.value || !options.some(option => option.value === selectedDetailMainGroup.value)) {
|
||||||
|
selectedDetailMainGroup.value = options[0].value
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(selectedDetailMainGroup, () => {
|
||||||
|
if (activeTab.value === 'product_detail') {
|
||||||
|
expandedGroups.value = Object.fromEntries(
|
||||||
|
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith('tab:product_detail|'))
|
||||||
|
)
|
||||||
|
backendGroupedRows.value = {
|
||||||
|
...backendGroupedRows.value,
|
||||||
|
product_detail: []
|
||||||
|
}
|
||||||
|
scheduleLoadBackendGroupedRows()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(productTableRows, tableRows => {
|
watch(productTableRows, tableRows => {
|
||||||
if (activeTab.value !== 'products') return
|
if (!isProductKpiTab.value) return
|
||||||
const imageRows = tableRows
|
const sourceRows = activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : tableRows
|
||||||
|
const imageRows = sourceRows
|
||||||
.filter(row => !row.__group || groupShowsImage(row))
|
.filter(row => !row.__group || groupShowsImage(row))
|
||||||
.slice(0, 240)
|
.slice(0, 240)
|
||||||
void primeProductImages(imageRows)
|
void primeProductImages(imageRows)
|
||||||
@@ -4411,6 +4495,49 @@ onMounted(reload)
|
|||||||
opacity: 0.62;
|
opacity: 0.62;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-group-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #d7dde5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid #c9d2dd;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f7f9fb;
|
||||||
|
color: #26384c;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button small {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button.active {
|
||||||
|
border-color: #1d4ed8;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #173a75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-group-button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
.metric-card {
|
.metric-card {
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
groupedCacheKey (params = {}) {
|
groupedCacheKey (params = {}) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
mode: String(params.mode || ''),
|
mode: String(params.mode || ''),
|
||||||
|
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
|
||||||
groupLevels: normalizedList(params.groupLevels),
|
groupLevels: normalizedList(params.groupLevels),
|
||||||
expandedKeys: normalizedList(params.expandedKeys, true),
|
expandedKeys: normalizedList(params.expandedKeys, true),
|
||||||
limit: Number(params.limit || 0)
|
limit: Number(params.limit || 0)
|
||||||
@@ -104,13 +105,15 @@ export const useProductPerformanceStore = defineStore('product-performance-store
|
|||||||
})
|
})
|
||||||
const request = api.post('/pricing/product-performance/grouped', {
|
const request = api.post('/pricing/product-performance/grouped', {
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
|
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
|
||||||
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
|
||||||
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
|
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
|
||||||
limit: params.limit
|
limit: params.limit
|
||||||
}, {
|
}, {
|
||||||
params: {
|
params: {
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
limit: params.limit
|
limit: params.limit,
|
||||||
|
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || ''
|
||||||
},
|
},
|
||||||
timeout: 90000
|
timeout: 90000
|
||||||
}).then(resp => {
|
}).then(resp => {
|
||||||
|
|||||||
Reference in New Issue
Block a user