5790 lines
242 KiB
Vue
5790 lines
242 KiB
Vue
<template>
|
||
<q-page class="product-performance-page q-pa-sm">
|
||
<q-inner-loading :showing="pageBusy">
|
||
<q-spinner-gears size="52px" color="primary" />
|
||
</q-inner-loading>
|
||
<teleport to="body">
|
||
<div
|
||
v-if="pageBusy"
|
||
class="page-busy-overlay"
|
||
@click.stop
|
||
@mousedown.stop
|
||
@mouseup.stop
|
||
@touchstart.stop
|
||
@wheel.stop
|
||
>
|
||
<q-spinner-gears size="56px" color="primary" />
|
||
<div class="page-busy-label">Yükleniyor...</div>
|
||
</div>
|
||
</teleport>
|
||
<div class="performance-top-bar row items-center justify-between q-col-gutter-xs q-mb-xs">
|
||
<div class="col-12 col-md">
|
||
<div class="performance-page-title">Ürün Performans ve Karlılık Analizi</div>
|
||
<div class="performance-page-subtitle text-grey-7">
|
||
Ürün, renk, yaka, piyasa ve müşteri segmenti bazında satış, stok dönüşü ve karlılık KPI'ları.
|
||
</div>
|
||
</div>
|
||
<div class="performance-top-actions col-12 col-md-auto row items-center q-gutter-xs">
|
||
<q-option-group
|
||
v-model="selectedPeriods"
|
||
:options="periodOptions"
|
||
type="checkbox"
|
||
inline
|
||
dense
|
||
:disable="pageBusy"
|
||
class="period-selector bg-white q-px-xs q-py-none"
|
||
/>
|
||
<q-btn-dropdown
|
||
v-model="detailLevelMenuOpen"
|
||
dense
|
||
outline
|
||
color="primary"
|
||
:icon="allProductGroupsExpanded ? 'unfold_less' : 'unfold_more'"
|
||
:label="allProductGroupsExpanded ? 'Detayları Kapat' : 'Tüm Detayları Aç'"
|
||
:disable="pageBusy"
|
||
>
|
||
<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" :disable="pageBusy" @click.stop="collapseAllProductGroups" />
|
||
<q-btn dense color="primary" label="Seçileni Aç" :disable="pageBusy" @click.stop="expandSelectedProductGroups" />
|
||
</div>
|
||
</div>
|
||
</q-btn-dropdown>
|
||
<q-btn dense outline color="secondary" icon="refresh" label="Yenile" :loading="loading" :disable="pageBusy" @click="reload" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="performance-summary-row row q-col-gutter-xs q-mb-xs">
|
||
<div v-for="card in summaryCards" :key="card.key" class="col-6 col-md-2">
|
||
<q-card flat bordered class="metric-card">
|
||
<q-card-section>
|
||
<div class="text-caption text-grey-7">{{ card.label }}</div>
|
||
<div class="metric-value">{{ card.value }}</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="performance-tab-grid q-mb-xs">
|
||
<button
|
||
v-for="tab in performanceTabs"
|
||
:key="tab.name"
|
||
type="button"
|
||
:class="['performance-tab-button', { active: activeTab === tab.name }]"
|
||
:disabled="pageBusy"
|
||
@click="switchPerformanceTab(tab.name)"
|
||
>
|
||
<q-icon :name="tab.icon" size="16px" />
|
||
<span>{{ tab.label }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="activeTab === 'product_detail'" class="main-group-bar q-mb-xs">
|
||
<button
|
||
v-for="option in detailMainGroupOptions"
|
||
:key="option.value"
|
||
type="button"
|
||
:class="['main-group-button', { active: selectedDetailMainGroup === option.value }]"
|
||
:disabled="pageBusy"
|
||
@click="selectedDetailMainGroup = option.value"
|
||
>
|
||
<span>{{ option.label }}</span>
|
||
<small>{{ formatNumber(option.stock_qty, 0) }} stok</small>
|
||
</button>
|
||
</div>
|
||
|
||
<q-table
|
||
v-if="activeTab === 'general'"
|
||
flat
|
||
bordered
|
||
row-key="row_key"
|
||
class="performance-table sticky-dim-table sticky-dim-7 bg-white"
|
||
:rows="filteredGeneralRows"
|
||
:columns="generalColumns"
|
||
:loading="loading"
|
||
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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('general', props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter('general', props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue('general', 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('general', 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('general', props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('general', props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions('general', props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions('general', 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('general', props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected('general', props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue('general', 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-image="props">
|
||
<q-td :props="props" class="product-image-cell">
|
||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</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>
|
||
|
||
<q-table
|
||
v-else-if="activeTab === 'order_product_customers'"
|
||
flat
|
||
bordered
|
||
row-key="row_key"
|
||
class="performance-table sticky-dim-table sticky-dim-8 bg-white"
|
||
:rows="displayOrderProductCustomerTableRows"
|
||
:columns="orderProductCustomerColumns"
|
||
:loading="loading || backendGroupedLoading"
|
||
:pagination="{ rowsPerPage: 0, sortBy: 'order_usd', descending: true }"
|
||
:sort-method="sortProductGroupedTableRows"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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('order_product_customers', props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter('order_product_customers', props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue('order_product_customers', 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('order_product_customers', 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('order_product_customers', props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('order_product_customers', props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions('order_product_customers', props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions('order_product_customers', 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('order_product_customers', props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected('order_product_customers', props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue('order_product_customers', 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-image="props">
|
||
<q-td :props="props" class="product-image-cell">
|
||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</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>
|
||
<template #body-cell-net_stock_after_order="props">
|
||
<q-td :props="props" class="text-right" :class="Number(props.row.net_stock_after_order || 0) < 0 ? 'text-negative text-weight-bold' : ''">
|
||
{{ formatNumber(props.row.net_stock_after_order, 0) }}
|
||
</q-td>
|
||
</template>
|
||
<template #body-cell-expected_profit_cost_usd="props">
|
||
<q-td :props="props" class="text-right" :class="Number(props.row.expected_profit_cost_usd || 0) < 0 ? 'text-negative text-weight-bold' : 'text-positive'">
|
||
{{ formatMoney(props.row.expected_profit_cost_usd, 'USD') }}
|
||
</q-td>
|
||
</template>
|
||
<template #body="props">
|
||
<q-tr
|
||
v-if="props.row.__group"
|
||
:props="props"
|
||
:class="groupRowClasses(props.row)"
|
||
>
|
||
<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)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</template>
|
||
<div
|
||
v-else-if="col.name === groupLabelColumnName(props.row)"
|
||
class="group-cell-label"
|
||
:style="{ paddingLeft: groupIndentPx(props.row) }"
|
||
>
|
||
<q-btn
|
||
flat
|
||
dense
|
||
round
|
||
size="sm"
|
||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||
@click.stop="toggleGroup(props.row.key)"
|
||
/>
|
||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||
</div>
|
||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
<q-tr v-else :props="props" class="cursor-pointer" @click="openPerformanceDialog(props.row)">
|
||
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
|
||
<template v-if="col.name === 'image'">
|
||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</template>
|
||
<q-badge v-else-if="col.name === 'performance_bucket'" :color="bucketColor(props.row.performance_bucket)">
|
||
{{ bucketLabel(props.row.performance_bucket) }}
|
||
</q-badge>
|
||
<span v-else>{{ formatTableCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
|
||
<q-table
|
||
v-else-if="activeTab === 'order_market_details'"
|
||
flat
|
||
bordered
|
||
row-key="row_key"
|
||
class="performance-table sticky-dim-table sticky-dim-10 bg-white"
|
||
:rows="displayOrderMarketDetailTableRows"
|
||
:columns="orderMarketDetailColumns"
|
||
:loading="loading || backendGroupedLoading"
|
||
:pagination="{ rowsPerPage: 0, sortBy: 'order_date', descending: true }"
|
||
:sort-method="sortProductGroupedTableRows"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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('order_market_details', props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter('order_market_details', props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue('order_market_details', 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('order_market_details', 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('order_market_details', props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('order_market_details', props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions('order_market_details', props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions('order_market_details', 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('order_market_details', props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected('order_market_details', props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue('order_market_details', 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-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>
|
||
<template #body-cell-net_stock_after_order="props">
|
||
<q-td :props="props" class="text-right" :class="Number(props.row.net_stock_after_order || 0) < 0 ? 'text-negative text-weight-bold' : ''">
|
||
{{ formatNumber(props.row.net_stock_after_order, 0) }}
|
||
</q-td>
|
||
</template>
|
||
<template #body-cell-expected_profit_cost_usd="props">
|
||
<q-td :props="props" class="text-right" :class="Number(props.row.expected_profit_cost_usd || 0) < 0 ? 'text-negative text-weight-bold' : 'text-positive'">
|
||
{{ formatMoney(props.row.expected_profit_cost_usd, 'USD') }}
|
||
</q-td>
|
||
</template>
|
||
<template #body="props">
|
||
<q-tr
|
||
v-if="props.row.__group"
|
||
:props="props"
|
||
:class="groupRowClasses(props.row)"
|
||
>
|
||
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
|
||
<div
|
||
v-if="col.name === groupLabelColumnName(props.row)"
|
||
class="group-cell-label"
|
||
:style="{ paddingLeft: groupIndentPx(props.row) }"
|
||
>
|
||
<q-btn
|
||
flat
|
||
dense
|
||
round
|
||
size="sm"
|
||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||
@click.stop="toggleGroup(props.row.key)"
|
||
/>
|
||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||
</div>
|
||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
<q-tr v-else :props="props" class="cursor-pointer" @click="openPerformanceDialog(props.row)">
|
||
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
|
||
<q-badge v-if="col.name === 'performance_bucket'" :color="bucketColor(props.row.performance_bucket)">
|
||
{{ bucketLabel(props.row.performance_bucket) }}
|
||
</q-badge>
|
||
<span v-else>{{ formatTableCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
|
||
<q-table
|
||
v-else-if="isProductKpiTab"
|
||
flat
|
||
bordered
|
||
row-key="row_key"
|
||
:class="[
|
||
'performance-table',
|
||
'product-breakdown-table',
|
||
'bg-white',
|
||
activeTab === 'product_detail' ? 'product-detail-table' : 'product-summary-table'
|
||
]"
|
||
:rows="displayProductKpiTableRows"
|
||
:columns="activeProductColumns"
|
||
:loading="loading || backendGroupedLoading"
|
||
:pagination="pagination"
|
||
:sort-method="sortProductGroupedTableRows"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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(activeProductTableKey, props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter(activeProductTableKey, props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue(activeProductTableKey, props.col.name) }}
|
||
</q-badge>
|
||
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||
<div class="excel-filter-menu">
|
||
<q-input
|
||
v-model="columnFilterSearch[filterKey(activeProductTableKey, 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(activeProductTableKey, props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(activeProductTableKey, props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions(activeProductTableKey, props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions(activeProductTableKey, 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(activeProductTableKey, props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected(activeProductTableKey, props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue(activeProductTableKey, 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="props">
|
||
<q-tr
|
||
v-if="props.row.__group"
|
||
:props="props"
|
||
:class="groupRowClasses(props.row)"
|
||
>
|
||
<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)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</template>
|
||
<div
|
||
v-else-if="col.name === groupLabelColumnName(props.row)"
|
||
class="group-cell-label"
|
||
:style="{ paddingLeft: groupIndentPx(props.row) }"
|
||
>
|
||
<q-btn
|
||
flat
|
||
dense
|
||
round
|
||
size="sm"
|
||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||
@click.stop="toggleGroup(props.row.key)"
|
||
/>
|
||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||
</div>
|
||
<span v-else>{{ formatGroupCell(props.row, col.name) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
<q-tr v-else :props="props" class="cursor-pointer" @click="openPerformanceDialog(props.row)">
|
||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||
<template v-if="col.name === 'image'">
|
||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</template>
|
||
<q-badge v-else-if="col.name === 'performance_bucket'" :color="bucketColor(props.row.performance_bucket)">
|
||
{{ bucketLabel(props.row.performance_bucket) }}
|
||
</q-badge>
|
||
<span v-else>{{ formatProductCell(props.row, col.name) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
|
||
<q-table
|
||
v-else-if="activeTab === 'idle'"
|
||
flat
|
||
bordered
|
||
row-key="row_key"
|
||
class="performance-table sticky-dim-table sticky-dim-11 bg-white"
|
||
:rows="displayIdleTableRows"
|
||
:columns="idleColumns"
|
||
:loading="loading || backendGroupedLoading"
|
||
:pagination="{ rowsPerPage: 0 }"
|
||
:sort-method="sortProductGroupedTableRows"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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('idle', props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter('idle', props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue('idle', 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('idle', 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('idle', props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('idle', props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions('idle', props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions('idle', 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('idle', props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected('idle', props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue('idle', 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-image="props">
|
||
<q-td :props="props" class="product-image-cell">
|
||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-idle_cost_usd="props">
|
||
<q-td :props="props" class="text-right text-weight-bold">
|
||
{{ formatMoney(props.row.idle_cost_usd, '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>
|
||
<template #body="props">
|
||
<q-tr
|
||
v-if="props.row.__group"
|
||
:props="props"
|
||
:class="groupRowClasses(props.row)"
|
||
>
|
||
<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)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</template>
|
||
<div
|
||
v-else-if="col.name === groupLabelColumnName(props.row)"
|
||
class="group-cell-label"
|
||
:style="{ paddingLeft: groupIndentPx(props.row) }"
|
||
>
|
||
<q-btn
|
||
flat
|
||
dense
|
||
round
|
||
size="sm"
|
||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||
@click.stop="toggleGroup(props.row.key)"
|
||
/>
|
||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||
</div>
|
||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
<q-tr v-else :props="props" class="cursor-pointer" @click="openPerformanceDialog(props.row)">
|
||
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
|
||
<template v-if="col.name === 'image'">
|
||
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
|
||
<img
|
||
v-if="getCachedProductImageUrl(props.row)"
|
||
:src="getCachedProductImageUrl(props.row)"
|
||
class="product-thumb"
|
||
@error="markProductImageFailed(props.row)"
|
||
loading="lazy"
|
||
decoding="async"
|
||
alt=""
|
||
/>
|
||
<div v-else class="product-thumb-placeholder">
|
||
<q-icon name="image" size="22px" />
|
||
</div>
|
||
<q-tooltip>Foto</q-tooltip>
|
||
</button>
|
||
</template>
|
||
<q-badge v-else-if="col.name === 'performance_bucket'" :color="bucketColor(props.row.performance_bucket)">
|
||
{{ bucketLabel(props.row.performance_bucket) }}
|
||
</q-badge>
|
||
<span v-else>{{ formatTableCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
|
||
<q-table
|
||
v-else-if="activeTab === 'markets'"
|
||
flat
|
||
bordered
|
||
row-key="market_key"
|
||
class="performance-table sticky-dim-table sticky-dim-9 bg-white"
|
||
:rows="filteredMarketRows"
|
||
:columns="marketColumns"
|
||
:loading="loading"
|
||
:pagination="{ rowsPerPage: 0 }"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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('markets', props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter('markets', props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue('markets', 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('markets', 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('markets', props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('markets', props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions('markets', props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions('markets', 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('markets', props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected('markets', props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue('markets', 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-stock_cost_value_usd="props">
|
||
<q-td :props="props" class="text-right">
|
||
{{ formatMoney(props.row.stock_cost_value_usd, 'USD') }}
|
||
</q-td>
|
||
</template>
|
||
<template #body-cell-risk_stock_cost_value_usd="props">
|
||
<q-td :props="props" class="text-right text-negative text-weight-bold">
|
||
{{ formatMoney(props.row.risk_stock_cost_value_usd, 'USD') }}
|
||
</q-td>
|
||
</template>
|
||
<template #body-cell-avg_gross_margin_90d="props">
|
||
<q-td :props="props" class="text-right">
|
||
{{ formatPercent(props.row.avg_gross_margin_90d) }}
|
||
</q-td>
|
||
</template>
|
||
</q-table>
|
||
|
||
<q-table
|
||
v-else-if="salesBreakdownTabKeys.includes(activeTab)"
|
||
flat
|
||
bordered
|
||
row-key="row_key"
|
||
:class="[
|
||
'performance-table',
|
||
'sticky-dim-table',
|
||
salesBreakdownStickyClass,
|
||
activeTab === 'sales_color_yaka_market_customer' ? 'sales-color-yaka-table' : '',
|
||
'bg-white'
|
||
]"
|
||
:rows="displayActiveSalesBreakdownTableRows"
|
||
:columns="visibleSalesBreakdownColumns"
|
||
:loading="loading || backendGroupedLoading"
|
||
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||
:sort-method="sortProductGroupedTableRows"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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>
|
||
<template #body="props">
|
||
<q-tr
|
||
v-if="props.row.__group"
|
||
:props="props"
|
||
:class="groupRowClasses(props.row)"
|
||
>
|
||
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
|
||
<div
|
||
v-if="col.name === groupLabelColumnName(props.row)"
|
||
class="group-cell-label"
|
||
:style="{ paddingLeft: groupIndentPx(props.row) }"
|
||
>
|
||
<q-btn
|
||
flat
|
||
dense
|
||
round
|
||
size="sm"
|
||
:icon="isGroupExpanded(props.row.key) ? 'expand_more' : 'chevron_right'"
|
||
@click.stop="toggleGroup(props.row.key)"
|
||
/>
|
||
<span class="group-title">{{ groupDisplayLabel(props.row) }}</span>
|
||
</div>
|
||
<span v-else>{{ formatGroupCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
<q-tr v-else :props="props" class="cursor-pointer" @click="openPerformanceDialog(props.row)">
|
||
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
|
||
<q-badge v-if="col.name === 'performance_bucket'" :color="bucketColor(props.row.performance_bucket)">
|
||
{{ bucketLabel(props.row.performance_bucket) }}
|
||
</q-badge>
|
||
<span v-else>{{ formatTableCell(props.row, col) }}</span>
|
||
</q-td>
|
||
</q-tr>
|
||
</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">
|
||
<div class="col-12 col-md-4">
|
||
<q-select
|
||
v-model="customerBreakdown"
|
||
dense
|
||
outlined
|
||
emit-value
|
||
map-options
|
||
label="Kırılım"
|
||
:options="customerBreakdownOptions"
|
||
@update:model-value="reload"
|
||
/>
|
||
</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
<q-table
|
||
flat
|
||
bordered
|
||
row-key="customer_key"
|
||
class="performance-table sticky-dim-table sticky-dim-5 bg-white"
|
||
:rows="filteredCustomerRows"
|
||
:columns="customerColumns"
|
||
:loading="loading"
|
||
:pagination="{ rowsPerPage: 0 }"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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('customers', props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter('customers', props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue('customers', 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('customers', 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('customers', props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('customers', props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions('customers', props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions('customers', 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('customers', props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected('customers', props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue('customers', 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-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-sales_usd_365d="props">
|
||
<q-td :props="props" class="text-right">
|
||
{{ formatMoney(props.row.sales_usd_365d, 'USD') }}
|
||
</q-td>
|
||
</template>
|
||
</q-table>
|
||
</div>
|
||
|
||
<q-table
|
||
v-else
|
||
flat
|
||
bordered
|
||
row-key="country_key"
|
||
class="performance-table sticky-dim-table sticky-dim-3 bg-white"
|
||
:rows="filteredCountryRows"
|
||
:columns="countryColumns"
|
||
:loading="loading"
|
||
:pagination="{ rowsPerPage: 0 }"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="64"
|
||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||
>
|
||
<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('countries', props.col.name) ? 'primary' : 'grey-7'"
|
||
class="header-filter-btn"
|
||
@click.stop
|
||
>
|
||
<q-badge v-if="hasColumnFilter('countries', props.col.name)" color="primary" floating rounded>
|
||
{{ getColumnFilterBadgeValue('countries', 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('countries', 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('countries', props.col.name)" />
|
||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('countries', props.col.name)" />
|
||
</div>
|
||
<q-virtual-scroll
|
||
v-if="columnFilterOptions('countries', props.col.name).length > 0"
|
||
class="excel-filter-options"
|
||
:items="columnFilterOptions('countries', 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('countries', props.col.name, option.value)"
|
||
>
|
||
<q-item-section avatar>
|
||
<q-checkbox
|
||
dense
|
||
size="sm"
|
||
:model-value="isColumnFilterValueSelected('countries', props.col.name, option.value)"
|
||
@update:model-value="() => toggleColumnFilterValue('countries', 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-avg_price_usd_90d="props">
|
||
<q-td :props="props" class="text-right">
|
||
{{ formatMoney(props.row.avg_price_usd_90d, 'USD') }}
|
||
</q-td>
|
||
</template>
|
||
</q-table>
|
||
|
||
<q-dialog v-model="imageDialog" maximized>
|
||
<q-card class="product-image-dialog">
|
||
<q-card-section class="row items-center q-pb-sm">
|
||
<div>
|
||
<div class="text-h6">{{ imageDialogTitle }}</div>
|
||
<div class="text-caption text-grey-7">{{ imageDialogSubtitle }}</div>
|
||
</div>
|
||
<q-space />
|
||
<q-btn v-close-popup flat round dense icon="close" />
|
||
</q-card-section>
|
||
<q-separator />
|
||
<q-card-section class="product-image-dialog-body">
|
||
<div class="performance-image-layout">
|
||
<aside class="image-product-fields">
|
||
<div class="image-section-title">Ürün Bilgileri</div>
|
||
<div v-for="item in imageDialogInfoRows" :key="item.key" class="image-field-row">
|
||
<span class="k">{{ item.label }}</span>
|
||
<span class="v">{{ item.value }}</span>
|
||
</div>
|
||
|
||
<div class="image-product-section">
|
||
<div class="image-section-title">Beden Stokları</div>
|
||
<q-inner-loading :showing="detailLoading">
|
||
<q-spinner size="24px" color="primary" />
|
||
</q-inner-loading>
|
||
<div v-if="detailStockSizes.length" class="image-size-stock-grid">
|
||
<div v-for="item in detailStockSizes" :key="item.size_code" class="image-size-stock-cell">
|
||
<span class="image-size-label">{{ item.size_code || '-' }}</span>
|
||
<span class="image-size-qty">{{ formatNumber(item.stock_qty, 0) }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="!detailLoading" class="image-empty-text">Beden stoku bulunamadi</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="image-media-panel">
|
||
<q-carousel
|
||
v-if="imageDialogUrls.length"
|
||
v-model="imageSlide"
|
||
animated
|
||
arrows
|
||
navigation
|
||
infinite
|
||
swipeable
|
||
class="product-image-carousel"
|
||
>
|
||
<q-carousel-slide
|
||
v-for="(url, idx) in imageDialogUrls"
|
||
:key="`${url}-${idx}`"
|
||
:name="idx"
|
||
class="column no-wrap flex-center"
|
||
>
|
||
<q-img :src="url" fit="contain" class="product-image-large" />
|
||
</q-carousel-slide>
|
||
</q-carousel>
|
||
<div v-else class="product-image-empty">
|
||
<q-icon name="image_not_supported" size="48px" />
|
||
<div class="text-subtitle2 q-mt-sm">Foto bulunamadi</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="image-sales-panel">
|
||
<div class="row q-col-gutter-xs q-mb-sm">
|
||
<div v-for="card in imageDialogPerformanceCards" :key="card.key" class="col-6 col-md-3 col-lg-4">
|
||
<q-card flat bordered class="metric-card image-metric-card">
|
||
<q-card-section>
|
||
<div class="text-caption text-grey-7">{{ card.label }}</div>
|
||
<div class="metric-value">{{ card.value }}</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
</div>
|
||
</div>
|
||
<q-table
|
||
flat
|
||
bordered
|
||
dense
|
||
class="dialog-detail-table"
|
||
title="Satış / Müşteri Özeti"
|
||
row-key="detail_key"
|
||
:rows="detailSalesRows"
|
||
:columns="detailSalesColumns"
|
||
:loading="detailLoading"
|
||
:pagination="{ rowsPerPage: 0, sortBy: 'sales_usd', descending: true }"
|
||
/>
|
||
</section>
|
||
</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<q-dialog v-model="performanceDialog" maximized>
|
||
<q-card class="product-performance-dialog">
|
||
<q-card-section class="row items-center q-pb-sm">
|
||
<div>
|
||
<div class="text-h6">{{ performanceDialogTitle }}</div>
|
||
<div class="text-caption text-grey-7">{{ performanceDialogSubtitle }}</div>
|
||
</div>
|
||
<q-space />
|
||
<q-btn v-close-popup flat round dense icon="close" />
|
||
</q-card-section>
|
||
<q-separator />
|
||
<q-card-section class="q-pa-sm">
|
||
<div class="row q-col-gutter-sm q-mb-sm">
|
||
<div v-for="card in performanceCards" :key="card.key" class="col-6 col-md-2">
|
||
<q-card flat bordered class="metric-card">
|
||
<q-card-section>
|
||
<div class="text-caption text-grey-7">{{ card.label }}</div>
|
||
<div class="metric-value">{{ card.value }}</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
</div>
|
||
</div>
|
||
<div class="row q-col-gutter-sm">
|
||
<div class="col-12 col-md-4">
|
||
<q-table
|
||
flat
|
||
bordered
|
||
dense
|
||
class="dialog-stock-table"
|
||
title="Beden Stok"
|
||
row-key="size_code"
|
||
:rows="detailStockSizes"
|
||
:columns="detailStockColumns"
|
||
:loading="detailLoading"
|
||
:pagination="{ rowsPerPage: 0 }"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="48"
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-8">
|
||
<q-table
|
||
flat
|
||
bordered
|
||
dense
|
||
class="dialog-detail-table"
|
||
title="Satış / Müşteri Özeti"
|
||
row-key="detail_key"
|
||
:rows="detailSalesRows"
|
||
:columns="detailSalesColumns"
|
||
:loading="detailLoading"
|
||
:pagination="{ rowsPerPage: 0, sortBy: 'sales_usd', descending: true }"
|
||
virtual-scroll
|
||
:virtual-scroll-item-size="48"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</q-card-section>
|
||
</q-card>
|
||
</q-dialog>
|
||
</q-page>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||
import { Notify } from 'quasar'
|
||
import api from 'src/services/api'
|
||
import { useProductPerformanceStore } from 'src/stores/productPerformanceStore'
|
||
|
||
const performanceStore = useProductPerformanceStore()
|
||
|
||
function formatNumber (value, fraction = 2) {
|
||
const n = Number(value || 0)
|
||
const digits = Math.max(0, Math.min(2, Number(fraction || 0)))
|
||
return n.toLocaleString('tr-TR', {
|
||
minimumFractionDigits: digits,
|
||
maximumFractionDigits: digits
|
||
})
|
||
}
|
||
|
||
function formatMoney (value, currency = 'USD') {
|
||
return `${formatNumber(value, 2)} ${currency}`
|
||
}
|
||
|
||
function formatPercent (value) {
|
||
return `${formatNumber(Number(value || 0) * 100, 2)}%`
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const rows = ref([])
|
||
const generalRows = ref([])
|
||
const orderAnalysisRows = ref([])
|
||
const orderMarketRows = ref([])
|
||
const orderCustomerRows = ref([])
|
||
const orderProductCustomerRows = ref([])
|
||
const orderMarketDetailRows = ref([])
|
||
const orderAnalysisLoaded = ref(false)
|
||
const orderProductCustomersLoaded = ref(false)
|
||
const orderMarketDetailsLoaded = ref(false)
|
||
const marketRowsLoaded = ref(false)
|
||
const countryRowsLoaded = ref(false)
|
||
const customerRowsLoaded = ref(false)
|
||
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 selectedDetailMainGroup = ref('')
|
||
const imageUrlByKey = ref({})
|
||
const imageListByKey = ref({})
|
||
const imageFailedKeys = ref(new Set())
|
||
const imageDialog = ref(false)
|
||
const imageDialogUrls = ref([])
|
||
const imageDialogTitle = ref('')
|
||
const imageDialogSubtitle = ref('')
|
||
const imageSlide = ref(0)
|
||
const imageDialogRow = ref(null)
|
||
const performanceDialog = ref(false)
|
||
const performanceDialogRow = ref(null)
|
||
const performanceDialogTitle = ref('')
|
||
const performanceDialogSubtitle = ref('')
|
||
const detailLoading = ref(false)
|
||
const detailSalesRows = ref([])
|
||
const detailStockSizes = ref([])
|
||
const backendGroupedRows = ref({})
|
||
const backendGroupedLoading = ref(false)
|
||
const imagePrimeLoading = ref(false)
|
||
const filterBusy = ref(false)
|
||
const bulkExpandLoading = ref(false)
|
||
const generalRowsLoaded = ref(false)
|
||
const detailLevelMenuOpen = ref(false)
|
||
let backendGroupedTimer = null
|
||
let imagePrimeActiveRequests = 0
|
||
|
||
const columnFilters = reactive({})
|
||
const columnFilterSearch = reactive({})
|
||
const expandedGroups = ref({})
|
||
const lastManualExpandedGroupKey = ref('')
|
||
const selectedExpandLevelKeysByTab = reactive({
|
||
products: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
||
product_detail: ['urun_alt_grubu'],
|
||
sales_color_yaka_market_customer: ['color_yaka'],
|
||
idle: ['urun_ilk_grubu', 'askili_yan', 'kategori'],
|
||
sales_product_country_segment_market_customer: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
||
sales_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
||
sales_country_segment_market_customer_product: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
||
order_product_customers: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan'],
|
||
order_market_details: ['market_key', 'customer_code', 'customer_name', 'urun_ilk_grubu', 'askili_yan']
|
||
})
|
||
const productKpiFetchLimit = 50000
|
||
const reportFetchLimit = 50000
|
||
const maxBulkExpandKeys = 500
|
||
const maxBackendExpandedKeys = 120
|
||
const tableVirtualSliceSize = 160
|
||
const maxAutoExpandLevelByTab = {
|
||
products: 8,
|
||
product_detail: 4,
|
||
idle: 7,
|
||
sales_color_yaka_market_customer: 5
|
||
}
|
||
const performanceTabs = [
|
||
{ name: 'products', icon: 'dashboard', label: 'Genel Özet KPI' },
|
||
{ name: 'product_detail', icon: 'category', label: 'Detay KPI' },
|
||
{ name: 'sales_color_yaka_market_customer', icon: 'palette', label: 'Renk/Yaka > Piyasa > Müşteri' },
|
||
{ name: 'idle', icon: 'warning', label: 'Atıl Stok / Maliyet' },
|
||
{ name: 'sales_product_country_segment_market_customer', icon: 'account_tree', label: 'Ürün > Ülke > Segment > Piyasa > Müşteri' },
|
||
{ name: 'sales_country_segment_market_customer_product', icon: 'public', label: 'Ülke > Segment > Piyasa > Müşteri > Ürün Satış KPI' },
|
||
{ name: 'order_product_customers', icon: 'assignment_ind', label: 'Ürün > Piyasa > Müşteri Sipariş' },
|
||
{ name: 'order_market_details', icon: 'receipt_long', label: 'Piyasa > Müşteri > Ürün Sipariş' }
|
||
]
|
||
const pageBusy = computed(() => loading.value || filterBusy.value || bulkExpandLoading.value)
|
||
|
||
const pagination = ref({
|
||
page: 1,
|
||
rowsPerPage: 0,
|
||
sortBy: 'performance_score_total',
|
||
descending: true
|
||
})
|
||
|
||
const bucketOptions = [
|
||
{ label: 'Yıldız Ürün', value: 'YILDIZ_URUN' },
|
||
{ label: 'Stoksuz Talep', value: 'STOKSUZ_TALEP' },
|
||
{ 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_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 }
|
||
]
|
||
|
||
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: 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 },
|
||
{ name: 'sales_qty', label: 'Adet', field: row => formatNumber(row.sales_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'sales_usd', label: 'USD', field: row => formatMoney(row.sales_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'avg_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_price_usd, 'USD'), align: 'right', sortable: true }
|
||
]
|
||
|
||
const columns = [
|
||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||
{ name: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', field: row => productFirstGroupValue(row), align: 'left' },
|
||
{ name: 'askili_yan', label: 'Askılı/Yan', field: 'askili_yan', align: 'left' },
|
||
{ name: 'kategori', label: 'Kategori', field: row => productCategoryValue(row), align: 'left' },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), align: 'left' },
|
||
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left' },
|
||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
|
||
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left' },
|
||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
|
||
{ 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: 'performance_score_90d', label: '90G Skor', field: row => formatNumber(row.performance_score_90d, 2), align: 'right', sortable: true },
|
||
{ name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), align: 'right', sortable: true },
|
||
{ name: 'performance_score_180d', label: '180G Skor', field: row => formatNumber(row.performance_score_180d, 2), align: 'right', sortable: true },
|
||
{ name: 'sales_qty_180d', label: '180G Satış', field: row => formatNumber(row.sales_qty_180d, 0), align: 'right', sortable: true },
|
||
{ name: 'performance_score_365d', label: '360G Skor', field: row => formatNumber(row.performance_score_365d, 2), align: 'right', sortable: true },
|
||
{ name: 'sales_qty_365d', label: '360G Satış', field: row => formatNumber(row.sales_qty_365d, 0), align: 'right', sortable: true },
|
||
{ name: 'performance_score_total', label: 'Genel Skor', field: row => formatNumber(row.performance_score_total, 2), 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_365d', label: '360G Stok Gün', field: row => formatNumber(row.stock_days_365d, 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: 'stock_turnover_90d', label: '90G Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
||
{ name: 'stock_turnover_180d', label: '180G Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), align: 'right', sortable: true },
|
||
{ name: 'stock_turnover_365d', label: '360G Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), align: 'right', sortable: true },
|
||
{ name: 'stock_turnover_total', label: 'Genel Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), 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, 2), 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 periodOrder = {
|
||
90: 1,
|
||
180: 2,
|
||
360: 3,
|
||
total: 4
|
||
}
|
||
|
||
const hiddenTableColumns = new Set(['item_description', 'performance_score'])
|
||
const dimensionColumnNames = [
|
||
'product_code',
|
||
'color_yaka',
|
||
'color_code',
|
||
'yaka_kodu',
|
||
'item_description',
|
||
'kategori',
|
||
'askili_yan',
|
||
'urun_ilk_grubu',
|
||
'urun_ana_grubu',
|
||
'urun_alt_grubu',
|
||
'market_key',
|
||
'country',
|
||
'customer_segment',
|
||
'customer_code',
|
||
'customer_name'
|
||
]
|
||
|
||
const productHierarchyColumnOrder = [
|
||
'urun_ilk_grubu',
|
||
'askili_yan',
|
||
'kategori',
|
||
'urun_ana_grubu',
|
||
'urun_alt_grubu'
|
||
]
|
||
|
||
const metricOrderFragments = [
|
||
'customer_score',
|
||
'performance_score',
|
||
'product_count',
|
||
'market_count',
|
||
'customer_count',
|
||
'invoice_count',
|
||
'order_count',
|
||
'line_count',
|
||
'sales_qty',
|
||
'order_qty',
|
||
'sales_usd',
|
||
'order_usd',
|
||
'avg_price',
|
||
'avg_order_price',
|
||
'sales_index',
|
||
'avg_daily_sales',
|
||
'stock_days',
|
||
'stock_turnover',
|
||
'base_price',
|
||
'cost_price',
|
||
'base_cost',
|
||
'cost_amount',
|
||
'unit_profit_base',
|
||
'unit_profit_cost',
|
||
'gross_profit_usd',
|
||
'gross_profit_base',
|
||
'gross_profit_cost',
|
||
'expected_profit_base',
|
||
'expected_profit_cost',
|
||
'gross_margin_base',
|
||
'gross_margin_cost',
|
||
'expected_margin_base',
|
||
'expected_margin_cost'
|
||
]
|
||
|
||
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)
|
||
}
|
||
|
||
function metricBaseName (name) {
|
||
return String(name || '')
|
||
.replace(/_90d$|_180d$|_365d$|_total$/g, '')
|
||
}
|
||
|
||
function metricOrder (name) {
|
||
const base = metricBaseName(name)
|
||
const idx = metricOrderFragments.findIndex(fragment => base.includes(fragment))
|
||
return idx >= 0 ? idx : 999
|
||
}
|
||
|
||
function isTrailingInfoColumn (name) {
|
||
return ['performance_bucket', 'recommendation', 'last_sale_date', 'first_sale_date', 'is_overdue'].includes(name)
|
||
}
|
||
|
||
function nonPeriodColumnOrder (name) {
|
||
if (name === 'performance_score') return 2
|
||
if (name === 'stock_qty' || name === 'product_count') return 1
|
||
if (dimensionColumnNames.includes(name) || name === 'image') return 0
|
||
return 3
|
||
}
|
||
|
||
function orderedMetricColumns (sourceColumns) {
|
||
const sorted = sourceColumns.filter(col => !hiddenTableColumns.has(col.name)).sort((a, b) => {
|
||
const aPeriod = columnPeriod(a.name)
|
||
const bPeriod = columnPeriod(b.name)
|
||
const aTrailing = isTrailingInfoColumn(a.name)
|
||
const bTrailing = isTrailingInfoColumn(b.name)
|
||
if (aTrailing !== bTrailing) return aTrailing ? 1 : -1
|
||
if (!aPeriod && !bPeriod) {
|
||
return nonPeriodColumnOrder(a.name) - nonPeriodColumnOrder(b.name)
|
||
}
|
||
if (!aPeriod) return -1
|
||
if (!bPeriod) return 1
|
||
const periodDiff = periodOrder[aPeriod] - periodOrder[bPeriod]
|
||
if (periodDiff) return periodDiff
|
||
const metricDiff = metricOrder(a.name) - metricOrder(b.name)
|
||
return metricDiff || 0
|
||
})
|
||
return normalizeProductHierarchyColumnOrder(sorted)
|
||
}
|
||
|
||
function normalizeProductHierarchyColumnOrder (sourceColumns) {
|
||
const positions = sourceColumns
|
||
.map((col, index) => productHierarchyColumnOrder.includes(col.name) ? index : -1)
|
||
.filter(index => index >= 0)
|
||
if (positions.length < 2) return sourceColumns
|
||
const ordered = positions
|
||
.map(index => sourceColumns[index])
|
||
.sort((a, b) => productHierarchyColumnOrder.indexOf(a.name) - productHierarchyColumnOrder.indexOf(b.name))
|
||
const next = [...sourceColumns]
|
||
positions.forEach((position, index) => {
|
||
next[position] = ordered[index]
|
||
})
|
||
return next
|
||
}
|
||
|
||
function reorderColumnsInPlace (targetColumns) {
|
||
targetColumns.splice(0, targetColumns.length, ...orderedMetricColumns(targetColumns))
|
||
return targetColumns
|
||
}
|
||
|
||
const productColumns = computed(() => orderedMetricColumns(columns.filter(visiblePeriodColumn)))
|
||
const productDetailColumns = computed(() => orderedMetricColumns(columns
|
||
.filter(col => !['urun_ilk_grubu', 'askili_yan', 'kategori'].includes(col.name))
|
||
.filter(visiblePeriodColumn)))
|
||
const activeProductColumns = computed(() => activeTab.value === 'product_detail' ? productDetailColumns.value : productColumns.value)
|
||
|
||
const generalColumns = [
|
||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||
{ 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: 'kategori', label: 'Kategori', field: 'kategori', 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 => productFirstGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), 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: 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 },
|
||
{ name: 'sales_qty_total', label: 'Toplam Satış', field: row => formatNumber(row.sales_qty_total, 0), align: 'right', sortable: true },
|
||
{ name: 'sales_usd_total', label: 'Toplam USD', field: row => formatMoney(row.sales_usd_total, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'avg_daily_sales_total', label: 'Günlük Ort.', field: row => formatNumber(row.avg_daily_sales_total, 3), 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_total', label: 'Genel Ort. USD', field: row => formatMoney(row.avg_price_usd_total, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'unit_profit_base_total', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_total, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'unit_profit_cost_total', label: 'P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_total, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'gross_profit_usd_total', label: 'Toplam K/Z', field: row => formatMoney(row.gross_profit_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: 'market_count_total', label: 'Toplam Piyasa', field: row => formatNumber(row.market_count_total, 0), align: 'right', sortable: true },
|
||
{ name: 'customer_count_total', label: 'Toplam Müşteri', field: row => formatNumber(row.customer_count_total, 0), align: 'right', sortable: true },
|
||
{ name: 'invoice_count_total', label: 'Fatura', field: row => formatNumber(row.invoice_count_total, 0), 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: 'Genel Skor', field: row => formatNumber(row.performance_score, 2), align: 'right', sortable: true },
|
||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||
{ name: 'first_sale_date', label: 'İlk Satış', field: 'first_sale_date', 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 orderAnalysisColumns = [
|
||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||
{ 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: 'kategori', label: 'Kategori', field: 'kategori', 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 => productFirstGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), 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: 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 },
|
||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||
{ name: 'base_price_usd', label: 'USD Taban Maliyeti', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'cost_price_usd', label: 'USD Çıplak Maliyeti', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'unit_profit_base_usd', label: 'P.Başı Taban K/Z', field: row => formatMoney(row.unit_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'unit_profit_cost_usd', label: 'P.Başı Çıplak K/Z', field: row => formatMoney(row.unit_profit_cost_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'expected_profit_base_usd', label: 'Taban Toplam K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'expected_profit_cost_usd', label: 'Çıplak Toplam K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||
{ name: 'expected_margin_cost', label: 'Beklenen Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||
{ name: 'market_count', label: 'Piyasa', field: row => formatNumber(row.market_count, 0), align: 'right', sortable: true },
|
||
{ name: 'customer_count', label: 'Müşteri', field: row => formatNumber(row.customer_count, 0), align: 'right', sortable: true },
|
||
{ name: 'order_count', label: 'Sipariş', field: row => formatNumber(row.order_count, 0), align: 'right', sortable: true },
|
||
{ name: 'line_count', label: 'Satır', field: row => formatNumber(row.line_count, 0), align: 'right', sortable: true },
|
||
{ name: 'earliest_due_date', label: 'İlk Termin', field: 'earliest_due_date', align: 'left', sortable: true },
|
||
{ name: 'overdue_qty', label: 'Geciken Adet', field: row => formatNumber(row.overdue_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||
]
|
||
|
||
const orderGroupColumns = [
|
||
{ name: 'group_key', label: 'Kırılım', field: row => orderGroupLabel(row), 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 },
|
||
{ 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. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'base_cost_usd', label: 'Taban Maliyet USD', field: row => formatMoney(row.base_cost_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'cost_amount_usd', label: 'Çıplak Maliyet USD', field: row => formatMoney(row.cost_amount_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'expected_profit_cost_usd', label: 'Yakl. Net K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||
{ name: 'expected_margin_base', label: 'Brüt Marj', field: row => formatPercent(row.expected_margin_base), align: 'right', sortable: true },
|
||
{ name: 'expected_margin_cost', label: 'Yakl. Net Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||
{ name: 'market_count', label: 'Piyasa', field: row => formatNumber(row.market_count, 0), align: 'right', sortable: true },
|
||
{ name: 'customer_count', label: 'Müşteri', field: row => formatNumber(row.customer_count, 0), align: 'right', sortable: true },
|
||
{ name: 'order_count', label: 'Sipariş', field: row => formatNumber(row.order_count, 0), align: 'right', sortable: true },
|
||
{ name: 'line_count', label: 'Satır', field: row => formatNumber(row.line_count, 0), align: 'right', sortable: true },
|
||
{ name: 'overdue_qty', label: 'Geciken Adet', field: row => formatNumber(row.overdue_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||
]
|
||
|
||
const orderProductCustomerColumns = [
|
||
{ 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: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||
{ name: 'kategori', label: 'Kategori', field: 'kategori', 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 => productFirstGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
|
||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', 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: '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. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||
{ name: 'base_price_usd', label: 'Taban USD', field: row => formatMoney(row.base_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'cost_price_usd', label: 'Çıplak USD', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'expected_profit_cost_usd', label: 'Yakl. Net K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||
{ name: 'expected_margin_base', label: 'Brüt Marj', field: row => formatPercent(row.expected_margin_base), align: 'right', sortable: true },
|
||
{ name: 'expected_margin_cost', label: 'Yakl. Net Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||
{ name: 'order_count', label: 'Sipariş', field: row => formatNumber(row.order_count, 0), align: 'right', sortable: true },
|
||
{ name: 'line_count', label: 'Satır', field: row => formatNumber(row.line_count, 0), align: 'right', sortable: true },
|
||
{ name: 'overdue_qty', label: 'Geciken Adet', field: row => formatNumber(row.overdue_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||
]
|
||
|
||
const orderMarketDetailColumns = [
|
||
{ 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: 'kategori', label: 'Kategori', field: 'kategori', 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 => productFirstGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
|
||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', 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: 'order_number', label: 'Sipariş No', field: 'order_number', align: 'left', sortable: true },
|
||
{ name: 'order_date', label: 'Sipariş Tarihi', field: 'order_date', align: 'left', sortable: true },
|
||
{ name: 'due_date', label: 'Termin', field: 'due_date', align: 'left', sortable: true },
|
||
{ name: 'order_qty', label: 'Adet', field: row => formatNumber(row.order_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'order_usd', label: 'USD', field: row => formatMoney(row.order_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'avg_order_price_usd', label: 'Ort. USD', field: row => formatMoney(row.avg_order_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'net_stock_after_order', label: 'Stok - Sipariş', field: 'net_stock_after_order', align: 'right', sortable: true },
|
||
{ name: 'expected_profit_base_usd', label: 'Brüt K/Z', field: row => formatMoney(row.expected_profit_base_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'expected_profit_cost_usd', label: 'Yakl. Net K/Z', field: 'expected_profit_cost_usd', align: 'right', sortable: true },
|
||
{ name: 'expected_margin_base', label: 'Brüt Marj', field: row => formatPercent(row.expected_margin_base), align: 'right', sortable: true },
|
||
{ name: 'expected_margin_cost', label: 'Yakl. Net Marj', field: row => formatPercent(row.expected_margin_cost), align: 'right', sortable: true },
|
||
{ name: 'is_overdue', label: 'Gecikti', field: row => row.is_overdue ? 'Evet' : 'Hayır', align: 'left', sortable: true },
|
||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left', sortable: true },
|
||
{ name: 'recommendation', label: 'Yorum', field: 'recommendation', align: 'left' }
|
||
]
|
||
|
||
const idleColumns = [
|
||
{ name: 'image', label: 'Foto', field: 'image', align: 'center' },
|
||
{ name: 'kategori', label: 'Kategori', field: 'kategori', 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 => productFirstGroupValue(row), align: 'left' },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), align: 'left' },
|
||
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left' },
|
||
{ name: 'product_code', label: 'Ürün', field: 'product_code', align: 'left', sortable: true },
|
||
{ name: 'item_description', label: 'Açıklama', field: 'item_description', align: 'left' },
|
||
{ name: 'color_code', label: 'Renk', field: 'color_code', align: 'left' },
|
||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', align: 'left' },
|
||
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left' },
|
||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'cost_price_usd', label: 'Maliyet USD', field: row => formatMoney(row.cost_price_usd, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'idle_cost_usd', label: 'Stok Maliyeti USD', field: 'idle_cost_usd', align: 'right', sortable: true },
|
||
{ name: 'sales_qty_90d', label: '90G Satış', field: row => formatNumber(row.sales_qty_90d, 0), 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_turnover_90d', label: '90G Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), align: 'right', sortable: true },
|
||
{ name: 'performance_bucket', label: 'Durum', field: 'performance_bucket', align: 'left' },
|
||
{ name: 'recommendation', label: 'Öneri', field: 'recommendation', align: 'left' }
|
||
]
|
||
|
||
const marketColumns = [
|
||
{ name: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), align: 'left', sortable: true },
|
||
{ name: 'kategori', label: 'Kategori', field: 'kategori', 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 => productFirstGroupValue(row), align: 'left' },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), 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 },
|
||
{ name: 'star_count', label: 'Yıldız', field: row => formatNumber(row.star_count, 0), align: 'right', sortable: true },
|
||
{ name: 'stock_risk_count', label: 'Risk', field: row => formatNumber(row.stock_risk_count, 0), align: 'right', sortable: true },
|
||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 0), align: 'right', sortable: true },
|
||
{ name: 'stock_cost_value_usd', label: 'Stok Maliyeti', field: 'stock_cost_value_usd', align: 'right', sortable: true },
|
||
{ name: 'risk_stock_cost_value_usd', label: 'Risk Maliyeti', field: 'risk_stock_cost_value_usd', 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_usd_90d', label: '90G USD', field: row => formatMoney(row.sales_usd_90d, 'USD'), align: 'right', sortable: true },
|
||
{ name: 'avg_gross_margin_90d', label: 'Ort. Marj', field: 'avg_gross_margin_90d', align: 'right', sortable: true },
|
||
{ name: 'avg_stock_days_90d', label: 'Ort. Stok Gün', field: row => formatNumber(row.avg_stock_days_90d, 1), align: 'right', sortable: true }
|
||
]
|
||
|
||
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', 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: 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: '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', 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 },
|
||
{ 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 },
|
||
{ 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_score_90d', label: '90G Müşteri Skor', field: row => formatNumber(row.customer_score_90d, 2), 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: '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: 'color_code', label: 'Renk', field: 'color_code', align: 'left', sortable: true },
|
||
{ name: 'yaka_kodu', label: 'Yaka', field: 'yaka_kodu', 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 => productFirstGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_ana_grubu', label: 'Ürün Ana Grubu', field: row => productMainGroupValue(row), align: 'left', sortable: true },
|
||
{ name: 'urun_alt_grubu', label: 'Ürün Alt Grubu', field: 'urun_alt_grubu', align: 'left', sortable: true },
|
||
{ name: 'product_code', label: 'Ürün', field: 'product_code', 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: 'market_key', label: 'Piyasa', field: row => displayMarketName(row.market_key), 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 },
|
||
{ name: 'customer_name', label: 'Müşteri', field: 'customer_name', align: 'left', sortable: true },
|
||
{ name: 'kategori', label: 'Kategori', field: 'kategori', align: 'left', sortable: true },
|
||
{ name: 'product_count', label: 'Ürün Sayısı', field: row => formatNumber(row.product_count, 0), align: 'right', sortable: true },
|
||
{ name: 'stock_qty', label: 'Stok', field: row => formatNumber(row.stock_qty, 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: 'stock_turnover_90d', label: '90G Stok Devir', field: row => formatNumber(row.stock_turnover_90d, 2), 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: 'stock_turnover_180d', label: '180G Stok Devir', field: row => formatNumber(row.stock_turnover_180d, 2), 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: 'customer_score_180d', label: '180G Müşteri Skor', field: row => formatNumber(row.customer_score_180d, 2), 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: 'stock_turnover_365d', label: '360G Stok Devir', field: row => formatNumber(row.stock_turnover_365d, 2), 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_score_365d', label: '360G Müşteri Skor', field: row => formatNumber(row.customer_score_365d, 2), 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: 'stock_turnover_total', label: 'Genel Stok Devir', field: row => formatNumber(row.stock_turnover_total, 2), 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: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 2), 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, 2), 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 productPeriodScoreColumns = [
|
||
{ name: 'performance_score_90d', label: '90G Ürün Skor', field: row => formatNumber(row.performance_score_90d, 2), align: 'right', sortable: true },
|
||
{ name: 'performance_score_180d', label: '180G Ürün Skor', field: row => formatNumber(row.performance_score_180d, 2), align: 'right', sortable: true },
|
||
{ name: 'performance_score_365d', label: '360G Ürün Skor', field: row => formatNumber(row.performance_score_365d, 2), align: 'right', sortable: true },
|
||
{ name: 'performance_score_total', label: 'Genel Ürün Skor', field: row => formatNumber(row.performance_score_total, 2), align: 'right', sortable: true }
|
||
]
|
||
|
||
const customerPeriodScoreColumns = [
|
||
{ name: 'customer_score_90d', label: '90G Müşteri Skor', field: row => formatNumber(row.customer_score_90d, 2), align: 'right', sortable: true },
|
||
{ name: 'customer_score_180d', label: '180G Müşteri Skor', field: row => formatNumber(row.customer_score_180d, 2), align: 'right', sortable: true },
|
||
{ name: 'customer_score_365d', label: '360G Müşteri Skor', field: row => formatNumber(row.customer_score_365d, 2), align: 'right', sortable: true },
|
||
{ name: 'customer_score_total', label: 'Genel Müşteri Skor', field: row => formatNumber(row.customer_score_total, 2), align: 'right', sortable: true }
|
||
]
|
||
|
||
function ensureColumns (targetColumns, columnsToEnsure) {
|
||
const existing = new Set(targetColumns.map(col => col.name))
|
||
for (const col of columnsToEnsure) {
|
||
if (!existing.has(col.name)) targetColumns.push(col)
|
||
}
|
||
}
|
||
|
||
function removeColumnsInPlace (targetColumns, names) {
|
||
const removeSet = new Set(names)
|
||
const next = targetColumns.filter(col => !removeSet.has(col.name))
|
||
targetColumns.splice(0, targetColumns.length, ...next)
|
||
return targetColumns
|
||
}
|
||
|
||
function ensureColorYakaColumn (targetColumns) {
|
||
const colorIndex = targetColumns.findIndex(col => col.name === 'color_code')
|
||
const yakaIndex = targetColumns.findIndex(col => col.name === 'yaka_kodu')
|
||
const insertAt = colorIndex >= 0 ? colorIndex : yakaIndex
|
||
if (insertAt >= 0 && !targetColumns.some(col => col.name === 'color_yaka')) {
|
||
targetColumns.splice(insertAt, 0, {
|
||
name: 'color_yaka',
|
||
label: 'Renk/Yaka',
|
||
field: row => colorYakaValue(row),
|
||
align: 'left',
|
||
sortable: true,
|
||
classes: 'color-yaka-cell',
|
||
headerClasses: 'color-yaka-cell'
|
||
})
|
||
}
|
||
removeColumnsInPlace(targetColumns, ['color_code', 'yaka_kodu'])
|
||
return targetColumns
|
||
}
|
||
|
||
;[
|
||
columns,
|
||
generalColumns,
|
||
orderAnalysisColumns,
|
||
orderGroupColumns,
|
||
orderProductCustomerColumns,
|
||
orderMarketDetailColumns,
|
||
idleColumns,
|
||
salesBreakdownColumns
|
||
].forEach(ensureColorYakaColumn)
|
||
|
||
;[
|
||
columns,
|
||
generalColumns,
|
||
orderAnalysisColumns,
|
||
orderGroupColumns,
|
||
orderProductCustomerColumns,
|
||
orderMarketDetailColumns,
|
||
idleColumns,
|
||
marketColumns,
|
||
countryColumns,
|
||
salesBreakdownColumns
|
||
].forEach(target => ensureColumns(target, productPeriodScoreColumns))
|
||
|
||
;[
|
||
customerColumns,
|
||
salesBreakdownColumns
|
||
].forEach(target => ensureColumns(target, customerPeriodScoreColumns))
|
||
|
||
;[
|
||
generalColumns,
|
||
orderAnalysisColumns,
|
||
orderGroupColumns,
|
||
orderProductCustomerColumns,
|
||
orderMarketDetailColumns,
|
||
idleColumns,
|
||
marketColumns,
|
||
countryColumns,
|
||
customerColumns,
|
||
salesBreakdownColumns
|
||
].forEach(target => removeColumnsInPlace(target, ['image']))
|
||
|
||
;[
|
||
columns,
|
||
generalColumns,
|
||
orderAnalysisColumns,
|
||
orderGroupColumns,
|
||
orderProductCustomerColumns,
|
||
orderMarketDetailColumns,
|
||
idleColumns,
|
||
marketColumns,
|
||
countryColumns,
|
||
customerColumns,
|
||
salesBreakdownColumns
|
||
].forEach(target => {
|
||
reorderColumnsInPlace(target)
|
||
attachColumnSorts(target)
|
||
})
|
||
|
||
attachColumnSorts(detailStockColumns)
|
||
attachColumnSorts(detailSalesColumns)
|
||
|
||
const visibleSalesBreakdownColumns = computed(() => {
|
||
const base = orderedMetricColumns(salesBreakdownColumns.filter(visiblePeriodColumn))
|
||
if (activeTab.value !== 'sales_color_yaka_market_customer') return base
|
||
|
||
const priority = ['performance_bucket', 'recommendation']
|
||
const pulled = priority
|
||
.map(name => base.find(col => col.name === name))
|
||
.filter(Boolean)
|
||
const rest = base.filter(col => !priority.includes(col.name))
|
||
const insertAfter = Math.max(
|
||
rest.findIndex(col => col.name === 'item_description'),
|
||
rest.findIndex(col => col.name === 'product_code')
|
||
)
|
||
if (insertAfter < 0) return [...pulled, ...rest]
|
||
return [
|
||
...rest.slice(0, insertAfter + 1),
|
||
...pulled,
|
||
...rest.slice(insertAfter + 1)
|
||
]
|
||
})
|
||
const salesBreakdownStickyClass = computed(() => activeTab.value === 'sales_color_yaka_market_customer' ? 'sticky-dim-9' : 'sticky-dim-9')
|
||
|
||
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) },
|
||
{ key: 'star', label: 'Yıldız', value: formatNumber(summary.value.star_count, 0) },
|
||
{ key: 'risk', label: 'Stok Riski', value: formatNumber(summary.value.stock_risk, 0) },
|
||
{ key: 'markets', label: '90G Piyasa', value: formatNumber(summary.value.market_count_90d, 0) },
|
||
{ key: 'customers', label: '90G Müşteri', value: formatNumber(summary.value.customer_count_90d, 0) },
|
||
{ key: 'stock_cost', label: 'Stok Maliyeti', value: formatMoney(summary.value.stock_cost_usd, 'USD') },
|
||
{ key: 'risk_cost', label: 'Risk Maliyeti', value: formatMoney(summary.value.risk_cost_usd, 'USD') }
|
||
])
|
||
|
||
const idleRows = computed(() => {
|
||
const variants = new Map()
|
||
for (const row of rows.value) {
|
||
const key = `${row.product_code || ''}|${row.color_code || ''}|${row.yaka_kodu || ''}`
|
||
const current = variants.get(key)
|
||
if (!current) {
|
||
variants.set(key, {
|
||
...row,
|
||
market_key: row.market_key && row.market_key !== 'STOK' ? row.market_key : '',
|
||
sales_qty_90d: Number(row.sales_qty_90d || 0),
|
||
sales_usd_90d: Number(row.sales_usd_90d || 0),
|
||
gross_profit_usd_90d: Number(row.gross_profit_usd_90d || 0),
|
||
customer_count_90d: Number(row.customer_count_90d || 0),
|
||
market_count_90d: row.market_key && row.market_key !== 'STOK' ? 1 : 0,
|
||
has_stock_risk: row.performance_bucket === 'STOK_RISKI'
|
||
})
|
||
continue
|
||
}
|
||
current.market_key = distinctTextSummary(current.market_key, row.market_key)
|
||
current.sales_qty_90d += Number(row.sales_qty_90d || 0)
|
||
current.sales_usd_90d += Number(row.sales_usd_90d || 0)
|
||
current.gross_profit_usd_90d += Number(row.gross_profit_usd_90d || 0)
|
||
current.customer_count_90d += Number(row.customer_count_90d || 0)
|
||
current.market_count_90d += row.market_key && row.market_key !== 'STOK' ? 1 : 0
|
||
current.has_stock_risk = current.has_stock_risk || row.performance_bucket === 'STOK_RISKI'
|
||
}
|
||
|
||
return Array.from(variants.values())
|
||
.map(row => {
|
||
const stockQty = Number(row.stock_qty || 0)
|
||
const salesQty90 = Number(row.sales_qty_90d || 0)
|
||
const stockDays90 = salesQty90 > 0 ? stockQty / (salesQty90 / 90) : 0
|
||
return {
|
||
...row,
|
||
stock_days_90d: stockDays90,
|
||
idle_cost_usd: stockQty * Number(row.cost_price_usd || 0),
|
||
performance_bucket: row.has_stock_risk ? 'STOK_RISKI' : row.performance_bucket
|
||
}
|
||
})
|
||
.filter(row => Number(row.stock_qty || 0) > 0 && (
|
||
row.performance_bucket === 'STOK_RISKI' ||
|
||
Number(row.sales_qty_90d || 0) === 0 ||
|
||
Number(row.stock_days_90d || 0) > 180
|
||
))
|
||
.sort((a, b) => Number(b.idle_cost_usd || 0) - Number(a.idle_cost_usd || 0))
|
||
})
|
||
|
||
const visibleOrderAnalysisRows = computed(() => {
|
||
if (orderAnalysisMode.value === 'market') return orderMarketRows.value
|
||
if (orderAnalysisMode.value === 'customer') return orderCustomerRows.value
|
||
return orderAnalysisRows.value
|
||
})
|
||
|
||
const visibleOrderAnalysisColumns = computed(() => {
|
||
if (orderAnalysisMode.value === 'market' || orderAnalysisMode.value === 'customer') return orderGroupColumns
|
||
return orderAnalysisColumns
|
||
})
|
||
|
||
function performanceCardsForRow (row) {
|
||
const source = row || {}
|
||
return [
|
||
{ key: 'score', label: 'Skor', value: formatNumber(source.performance_score, 2) },
|
||
{ key: 'stock', label: 'Stok', value: formatNumber(source.stock_qty, 0) },
|
||
{ key: 'sales90', label: '90G Satış', value: formatNumber(source.sales_qty_90d, 0) },
|
||
{ key: 'sales180', label: '180G Satış', value: formatNumber(source.sales_qty_180d, 0) },
|
||
{ key: 'markets', label: '90G Piyasa', value: formatNumber(source.market_count_90d, 0) },
|
||
{ key: 'customers', label: '90G Müşteri', value: formatNumber(source.customer_count_90d, 0) },
|
||
{ key: 'avg', label: 'Ort. USD Satış', value: formatMoney(source.avg_price_usd_90d, 'USD') },
|
||
{ key: 'base', label: 'Taban Maliyet', value: formatMoney(source.base_price_usd, 'USD') },
|
||
{ key: 'cost', label: 'Çıplak Maliyet', value: formatMoney(source.cost_price_usd, 'USD') },
|
||
{ key: 'profitBase', label: 'P.Başı Taban K/Z', value: formatMoney(source.unit_profit_base_90d, 'USD') },
|
||
{ key: 'profitCost', label: 'P.Başı Çıplak K/Z', value: formatMoney(source.unit_profit_cost_90d, 'USD') },
|
||
{ key: 'marginBase', label: '90G Taban Marj', value: formatPercent(source.gross_margin_base_90d) },
|
||
{ key: 'marginCost', label: '90G Çıplak Marj', value: formatPercent(source.gross_margin_cost_90d) },
|
||
{ key: 'bucket', label: 'Durum', value: bucketLabel(source.performance_bucket) }
|
||
]
|
||
}
|
||
|
||
const performanceCards = computed(() => performanceCardsForRow(performanceDialogRow.value))
|
||
const imageDialogPerformanceCards = computed(() => performanceCardsForRow(imageDialogRow.value))
|
||
|
||
const imageDialogInfoRows = computed(() => {
|
||
const row = imageDialogRow.value || {}
|
||
const productCode = row.image_product_code || row.product_code
|
||
const colorCode = row.image_color_code || row.color_code
|
||
const yakaCode = row.image_yaka_kodu || row.yaka_kodu
|
||
return [
|
||
{ key: 'product', label: 'Ürün', value: productCode || '-' },
|
||
{ key: 'color', label: 'Renk', value: colorCode || '-' },
|
||
{ key: 'yaka', label: 'Yaka', value: yakaCode || '-' },
|
||
{ key: 'category', label: 'Kategori', value: productCategoryValue(row) || '-' },
|
||
{ key: 'hanger', label: 'Askılı/Yan', value: cleanOptionalGroupValue(row.askili_yan) },
|
||
{ key: 'first', label: 'İlk Grup', value: productFirstGroupValue(row) || '-' },
|
||
{ key: 'main', label: 'Ana Grup', value: productMainGroupValue(row) || '-' },
|
||
{ key: 'sub', label: 'Alt Grup', value: row.urun_alt_grubu || '-' },
|
||
{ key: 'market', label: 'Piyasa', value: row.market_key || '-' },
|
||
{ key: 'stock', label: 'Toplam Stok', value: formatNumber(row.stock_qty, 0) },
|
||
{ key: 'base', label: 'Taban Maliyet', value: formatMoney(row.base_price_usd, 'USD') },
|
||
{ key: 'cost', label: 'Çıplak Maliyet', value: formatMoney(row.cost_price_usd, 'USD') }
|
||
]
|
||
})
|
||
|
||
const groupLevels = [
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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_yaka', label: 'Renk/Yaka' },
|
||
{ key: 'market_key', label: 'Piyasa' }
|
||
]
|
||
|
||
const tabGroupLevels = {
|
||
products: groupLevels,
|
||
product_detail: [
|
||
{ key: 'urun_alt_grubu', label: 'Ürün Alt Grubu' },
|
||
{ key: 'product_code', label: 'Ürün' },
|
||
{ key: 'color_yaka', label: 'Renk/Yaka' },
|
||
{ key: 'market_key', label: 'Piyasa' }
|
||
],
|
||
sales_color_yaka_market_customer: [
|
||
{ key: 'color_yaka', label: 'Renk/Yaka' },
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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: 'country', label: 'Ülke' },
|
||
{ key: 'market_key', label: 'Piyasa' },
|
||
{ key: 'customer_segment', label: 'Segment' },
|
||
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
||
{ key: 'customer_name', label: 'Müşteri' }
|
||
],
|
||
idle: [
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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_yaka', label: 'Renk/Yaka' }
|
||
],
|
||
sales_product_country_segment_market_customer: [
|
||
{ key: 'market_key', label: 'Piyasa' },
|
||
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
||
{ key: 'customer_name', label: 'Müşteri' },
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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_yaka', label: 'Renk/Yaka' }
|
||
],
|
||
sales_market_customer_product: [
|
||
{ key: 'market_key', label: 'Piyasa' },
|
||
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
||
{ key: 'customer_name', label: 'Müşteri' },
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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_yaka', label: 'Renk/Yaka' }
|
||
],
|
||
sales_country_segment_market_customer_product: [
|
||
{ key: 'market_key', label: 'Piyasa' },
|
||
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
||
{ key: 'customer_name', label: 'Müşteri' },
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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_yaka', label: 'Renk/Yaka' }
|
||
],
|
||
order_product_customers: [
|
||
{ key: 'market_key', label: 'Piyasa' },
|
||
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
||
{ key: 'customer_name', label: 'Müşteri' },
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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_yaka', label: 'Renk/Yaka' }
|
||
],
|
||
order_market_details: [
|
||
{ key: 'market_key', label: 'Piyasa' },
|
||
{ key: 'customer_code', label: 'Müşteri Kodu' },
|
||
{ key: 'customer_name', label: 'Müşteri' },
|
||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu' },
|
||
{ key: 'askili_yan', label: 'Askılı/Yan' },
|
||
{ key: 'kategori', label: 'Kategori' },
|
||
{ 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_yaka', label: 'Renk/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
|
||
})
|
||
|
||
function maxAutoExpandThroughLevelForTab (tabKey = activeTab.value) {
|
||
return Number.isFinite(maxAutoExpandLevelByTab[tabKey]) ? maxAutoExpandLevelByTab[tabKey] : 1
|
||
}
|
||
|
||
function maxBackendExpandedKeysForTab (tabKey = activeTab.value) {
|
||
if (tabKey === 'product_detail') return 260
|
||
if (tabKey === 'products' || tabKey === 'idle') return 260
|
||
if (salesBreakdownTabKeys.includes(tabKey)) return 180
|
||
return 100
|
||
}
|
||
|
||
const autoExpandThroughLevel = computed(() => {
|
||
const selectedLevel = selectedExpandThroughLevel.value
|
||
const lastGroupLevel = Math.max(0, activeGroupLevels.value.length - 2)
|
||
const requestedLevel = selectedLevel >= 0 ? selectedLevel : 2
|
||
return Math.min(requestedLevel, lastGroupLevel, maxAutoExpandThroughLevelForTab())
|
||
})
|
||
|
||
const tableFilterSourceMap = computed(() => ({
|
||
products: { rows: filterSourceRowsForTab('products', rows.value), columns: productColumns.value },
|
||
product_detail: { rows: filterSourceRowsForTab('product_detail', detailProductRows.value), columns: productDetailColumns.value },
|
||
general: { rows: generalRows.value, columns: generalColumns },
|
||
order_product_customers: { rows: filterSourceRowsForTab('order_product_customers', orderProductCustomerRows.value), columns: orderProductCustomerColumns },
|
||
order_market_details: { rows: filterSourceRowsForTab('order_market_details', orderMarketDetailRows.value), columns: orderMarketDetailColumns },
|
||
idle: { rows: filterSourceRowsForTab('idle', idleRows.value), columns: idleColumns },
|
||
markets: { rows: marketRows.value, columns: marketColumns },
|
||
countries: { rows: countryRows.value, columns: countryColumns },
|
||
customers: { rows: customerRows.value, columns: customerColumns },
|
||
sales_color_yaka_market_customer: { rows: filterSourceRowsForTab('sales_color_yaka_market_customer', salesBreakdownRows.sales_color_yaka_market_customer), columns: visibleSalesBreakdownColumns.value },
|
||
sales_product_country_segment_market_customer: { rows: filterSourceRowsForTab('sales_product_country_segment_market_customer', salesBreakdownRows.sales_product_country_segment_market_customer), columns: visibleSalesBreakdownColumns.value },
|
||
sales_market_customer_product: { rows: filterSourceRowsForTab('sales_market_customer_product', salesBreakdownRows.sales_market_customer_product), columns: visibleSalesBreakdownColumns.value },
|
||
sales_country_segment_market_customer_product: { rows: filterSourceRowsForTab('sales_country_segment_market_customer_product', salesBreakdownRows.sales_country_segment_market_customer_product), columns: visibleSalesBreakdownColumns.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 filteredOrderProductCustomerRows = computed(() => filterRowsForTable('order_product_customers', orderProductCustomerRows.value, orderProductCustomerColumns))
|
||
const filteredOrderMarketDetailRows = computed(() => filterRowsForTable('order_market_details', orderMarketDetailRows.value, orderMarketDetailColumns))
|
||
const filteredIdleRows = computed(() => filterRowsForTable('idle', idleRows.value, idleColumns))
|
||
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 activeGroupSourceRows = computed(() => {
|
||
if (activeTab.value === 'products') return filteredProductRows.value
|
||
if (activeTab.value === 'product_detail') return filteredDetailProductRows.value
|
||
if (activeTab.value === 'idle') return filteredIdleRows.value
|
||
if (activeTab.value === 'order_product_customers') return filteredOrderProductCustomerRows.value
|
||
if (activeTab.value === 'order_market_details') return filteredOrderMarketDetailRows.value
|
||
if (salesBreakdownTabKeys.includes(activeTab.value)) return filteredActiveSalesBreakdownRows.value
|
||
return []
|
||
})
|
||
|
||
const columnFilterOptionMap = computed(() => {
|
||
const out = {}
|
||
for (const [tableKey, source] of Object.entries(tableFilterSourceMap.value)) {
|
||
out[tableKey] = {}
|
||
for (const col of source.columns) {
|
||
if (!isColumnFilterable(col.name)) continue
|
||
const seen = new Map()
|
||
for (const row of source.rows) {
|
||
const value = columnFilterValue(row, col)
|
||
if (!value) continue
|
||
if (!seen.has(value)) {
|
||
seen.set(value, {
|
||
label: columnFilterLabel(row, col),
|
||
value
|
||
})
|
||
}
|
||
}
|
||
out[tableKey][col.name] = Array.from(seen.values())
|
||
.sort((a, b) => String(a.label).localeCompare(String(b.label), 'tr', { numeric: true }))
|
||
}
|
||
}
|
||
return out
|
||
})
|
||
|
||
const productTableRows = computed(() => {
|
||
const out = []
|
||
appendGroupRows(out, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
||
return out
|
||
})
|
||
const detailProductTableRows = computed(() => buildGroupedTableRows('product_detail', filteredDetailProductRows.value))
|
||
|
||
const idleTableRows = computed(() => buildGroupedTableRows('idle', filteredIdleRows.value))
|
||
const orderProductCustomerTableRows = computed(() => buildGroupedTableRows('order_product_customers', filteredOrderProductCustomerRows.value))
|
||
const orderMarketDetailTableRows = computed(() => buildGroupedTableRows('order_market_details', filteredOrderMarketDetailRows.value))
|
||
const activeSalesBreakdownTableRows = computed(() => buildGroupedTableRows(activeTab.value, filteredActiveSalesBreakdownRows.value))
|
||
const displayProductTableRows = computed(() => backendRowsForTab('products', productTableRows.value))
|
||
const displayDetailProductTableRows = computed(() => backendRowsForTab('product_detail', detailProductTableRows.value))
|
||
const displayProductKpiTableRows = computed(() => activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : displayProductTableRows.value)
|
||
const displayIdleTableRows = computed(() => backendRowsForTab('idle', idleTableRows.value))
|
||
const displayOrderProductCustomerTableRows = computed(() => backendRowsForTab('order_product_customers', orderProductCustomerTableRows.value))
|
||
const displayOrderMarketDetailTableRows = computed(() => backendRowsForTab('order_market_details', orderMarketDetailTableRows.value))
|
||
const displayActiveSalesBreakdownTableRows = computed(() => backendRowsForTab(activeTab.value, activeSalesBreakdownTableRows.value))
|
||
|
||
const detailMainGroupOptions = computed(() => {
|
||
const grouped = new Map()
|
||
for (const row of rows.value) {
|
||
const value = productMainGroupValue(row)
|
||
if (!value) continue
|
||
const current = grouped.get(value) || { value, label: value, stock_qty: 0, sales_qty_90d: 0 }
|
||
current.stock_qty += Number(row?.stock_qty || 0)
|
||
current.sales_qty_90d += Number(row?.sales_qty_90d || 0)
|
||
grouped.set(value, current)
|
||
}
|
||
return Array.from(grouped.values())
|
||
.sort((a, b) => Number(b.sales_qty_90d || 0) - Number(a.sales_qty_90d || 0) || a.label.localeCompare(b.label, 'tr'))
|
||
})
|
||
|
||
function filterRowsBySelectedDetailMainGroup (sourceRows) {
|
||
const selected = String(selectedDetailMainGroup.value || '').trim()
|
||
const rows = Array.isArray(sourceRows) ? sourceRows : []
|
||
if (!selected) return []
|
||
return rows.filter(row => {
|
||
const value = productMainGroupValue(row)
|
||
if (value === selected) return true
|
||
return Boolean(row?.__group && row.group_field !== 'urun_ana_grubu' && !value)
|
||
})
|
||
}
|
||
|
||
function ensureDetailMainGroupSelection () {
|
||
const options = detailMainGroupOptions.value
|
||
if (!options.length) {
|
||
selectedDetailMainGroup.value = ''
|
||
return false
|
||
}
|
||
const preferred = options.find(option => String(option.value || '').toLocaleUpperCase('tr-TR') === 'TAKIM ELBISE')
|
||
if (!selectedDetailMainGroup.value || !options.some(option => option.value === selectedDetailMainGroup.value)) {
|
||
selectedDetailMainGroup.value = (preferred || options[0]).value
|
||
}
|
||
return true
|
||
}
|
||
|
||
const productGroupKeys = computed(() => {
|
||
const keys = []
|
||
collectGroupKeys(keys, filteredProductRows.value, 0, ['tab:products'], groupLevels)
|
||
return keys
|
||
})
|
||
|
||
const productAutoExpandKeys = computed(() => {
|
||
const keys = []
|
||
collectGroupKeys(keys, activeGroupSourceRows.value, 0, [`tab:${activeTab.value}`], 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) {
|
||
appendGroupRowsAt(out, sourceRows, level, 0, parentKeys, levels)
|
||
}
|
||
|
||
function appendGroupRowsAt (out, sourceRows, level, visualLevel, parentKeys, levels = groupLevels) {
|
||
if (level >= levels.length) {
|
||
out.push(...sortProductLeafRows(sourceRows))
|
||
return
|
||
}
|
||
|
||
const groupDef = levels[level]
|
||
const grouped = new Map()
|
||
for (const row of sourceRows) {
|
||
const value = normalizeGroupValue(rawProductCellValue(row, groupDef.key))
|
||
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]) => {
|
||
if (shouldSkipGroupValue(groupDef.key, value)) {
|
||
appendGroupRowsAt(out, groupRows, level + 1, visualLevel, parentKeys, levels)
|
||
return
|
||
}
|
||
const key = [...parentKeys, `${groupDef.key}:${value}`].join('|')
|
||
out.push(makeGroupRow(key, visualLevel, groupDef, value, groupRows))
|
||
if (isGroupExpanded(key)) {
|
||
appendGroupRowsAt(out, groupRows, level + 1, visualLevel + 1, [...parentKeys, `${groupDef.key}:${value}`], levels)
|
||
}
|
||
})
|
||
}
|
||
|
||
function buildGroupedTableRows (tableKey, sourceRows) {
|
||
const levels = tabGroupLevels[tableKey] || groupLevels
|
||
const out = []
|
||
appendGroupRows(out, sourceRows, 0, [`tab:${tableKey}`], levels)
|
||
return out
|
||
}
|
||
|
||
function backendRowsForTab (tabKey, fallbackRows) {
|
||
const rows = backendGroupedRows.value[tabKey]
|
||
const backendRows = Array.isArray(rows) && rows.length ? rows : []
|
||
const sourceRows = tabKey === 'product_detail'
|
||
? filterRowsBySelectedDetailMainGroup(backendRows.length ? backendRows : fallbackRows)
|
||
: (backendRows.length ? backendRows : fallbackRows)
|
||
if (hasActiveTableFilters(tabKey)) {
|
||
return filterRowsForTable(tabKey, sourceRows, columnsForTableKey(tabKey))
|
||
}
|
||
return sourceRows
|
||
}
|
||
|
||
function filterSourceRowsForTab (tabKey, fallbackRows) {
|
||
const rows = backendGroupedRows.value[tabKey]
|
||
return Array.isArray(rows) && rows.length ? rows : fallbackRows
|
||
}
|
||
|
||
function columnsForTableKey (tabKey) {
|
||
if (tabKey === 'products') return productColumns.value
|
||
if (tabKey === 'product_detail') return productDetailColumns.value
|
||
if (tabKey === 'general') return generalColumns
|
||
if (tabKey === 'order_product_customers') return orderProductCustomerColumns
|
||
if (tabKey === 'order_market_details') return orderMarketDetailColumns
|
||
if (tabKey === 'idle') return idleColumns
|
||
if (tabKey === 'markets') return marketColumns
|
||
if (tabKey === 'countries') return countryColumns
|
||
if (tabKey === 'customers') return customerColumns
|
||
if (salesBreakdownTabKeys.includes(tabKey)) return visibleSalesBreakdownColumns.value
|
||
return productColumns.value
|
||
}
|
||
|
||
function collectGroupKeys (out, sourceRows, level, parentKeys, levels = groupLevels, maxLevel = levels.length - 1) {
|
||
if (out.length >= maxBulkExpandKeys) return
|
||
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))
|
||
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]) => {
|
||
if (out.length >= maxBulkExpandKeys) return
|
||
if (shouldSkipGroupValue(groupDef.key, value)) {
|
||
collectGroupKeys(out, groupRows, level + 1, parentKeys, levels, maxLevel)
|
||
return
|
||
}
|
||
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 sortProductGroupedTableRows (tableRows, sortBy, descending) {
|
||
const source = Array.isArray(tableRows) ? tableRows : []
|
||
if (!sortBy || !source.some(row => row?.__group)) {
|
||
return sortFlatTableRows(source, sortBy, descending)
|
||
}
|
||
|
||
const roots = []
|
||
const stack = []
|
||
|
||
for (const row of source) {
|
||
const node = { row, children: [] }
|
||
if (row?.__group) {
|
||
const level = Math.max(0, Number(row.level || 0))
|
||
stack.length = Math.min(stack.length, level)
|
||
const parent = level > 0 ? stack[level - 1] : null
|
||
if (parent) parent.children.push(node)
|
||
else roots.push(node)
|
||
stack[level] = node
|
||
stack.length = level + 1
|
||
} else {
|
||
const parent = stack[stack.length - 1]
|
||
if (parent) parent.children.push(node)
|
||
else roots.push(node)
|
||
}
|
||
}
|
||
|
||
const sorted = []
|
||
const appendNodes = (nodes) => {
|
||
const ordered = sortNodeSiblings(nodes, sortBy, descending)
|
||
for (const node of ordered) {
|
||
sorted.push(node.row)
|
||
if (node.children.length) appendNodes(node.children)
|
||
}
|
||
}
|
||
appendNodes(roots)
|
||
return sorted
|
||
}
|
||
|
||
function sortNodeSiblings (nodes, sortBy, descending) {
|
||
const source = mergeDuplicateGroupSiblingNodes(nodes)
|
||
if (!sortBy) return source
|
||
return source.sort((a, b) => {
|
||
const cmp = compareColumnSortValues(sortValueForColumnName(a.row, sortBy), sortValueForColumnName(b.row, sortBy))
|
||
return descending ? -cmp : cmp
|
||
})
|
||
}
|
||
|
||
function mergeDuplicateGroupSiblingNodes (nodes) {
|
||
const out = []
|
||
const seen = new Map()
|
||
for (const node of nodes) {
|
||
if (!node?.row?.__group) {
|
||
out.push(node)
|
||
continue
|
||
}
|
||
const key = [
|
||
Number(node.row.level || 0),
|
||
String(node.row.group_field || ''),
|
||
String(node.row.group_value || node.row.label || '')
|
||
].join('|')
|
||
const existing = seen.get(key)
|
||
if (existing) {
|
||
existing.children.push(...node.children)
|
||
continue
|
||
}
|
||
seen.set(key, node)
|
||
out.push(node)
|
||
}
|
||
return out
|
||
}
|
||
|
||
function sortFlatTableRows (tableRows, sortBy, descending) {
|
||
if (!sortBy) return [...tableRows]
|
||
return [...tableRows].sort((a, b) => {
|
||
const cmp = compareColumnSortValues(sortValueForColumnName(a, sortBy), sortValueForColumnName(b, sortBy))
|
||
return descending ? -cmp : cmp
|
||
})
|
||
}
|
||
|
||
function sortValueForColumnName (row, columnName) {
|
||
const columns = [
|
||
...(productColumns.value || []),
|
||
...idleColumns,
|
||
...orderProductCustomerColumns,
|
||
...orderMarketDetailColumns,
|
||
...(visibleSalesBreakdownColumns.value || [])
|
||
]
|
||
const col = columns.find(item => item.name === columnName || item.field === columnName) || { name: columnName, field: columnName }
|
||
return sortValueForColumn(row, col)
|
||
}
|
||
|
||
function makeGroupRow (key, level, groupDef, value, groupRows) {
|
||
const imageSource = groupRows.find(row => row?.product_code) || {}
|
||
const aggregate = aggregateGroupFields(groupRows, groupDef.key)
|
||
const colorSummary = groupDef.key === 'color_code' ? value : ''
|
||
const yakaSummary = groupDef.key === 'yaka_kodu' ? value : ''
|
||
const colorDescriptionSummary = groupDef.key === 'color_code' ? distinctSummary(groupRows, 'color_description') : ''
|
||
return {
|
||
__group: true,
|
||
row_key: `group|${key}`,
|
||
key,
|
||
level,
|
||
group_field: groupDef.key,
|
||
group_value: value,
|
||
label: colorYakaGroupLabel(groupDef.key, value, colorSummary, yakaSummary),
|
||
count: groupRows.length,
|
||
image_product_code: imageSource.product_code || '',
|
||
image_color_code: imageSource.color_code || '',
|
||
image_color_description: imageSource.color_description || '',
|
||
image_yaka_kodu: imageSource.yaka_kodu || '',
|
||
product_code: groupDef.key === 'product_code' ? value : '',
|
||
color_code: colorSummary,
|
||
color_description: colorDescriptionSummary,
|
||
yaka_kodu: yakaSummary,
|
||
item_description: groupDef.key === 'item_description' ? value : '',
|
||
kategori: groupDef.key === 'kategori' ? value : '',
|
||
urun_ilk_grubu: groupDef.key === 'urun_ilk_grubu' ? value : '',
|
||
askili_yan: groupDef.key === 'askili_yan' ? value : '',
|
||
urun_ana_grubu: groupDef.key === 'urun_ana_grubu' ? value : '',
|
||
urun_alt_grubu: groupDef.key === 'urun_alt_grubu' ? value : '',
|
||
market_key: groupDef.key === 'market_key' ? value : '',
|
||
country: groupDef.key === 'country' ? value : '',
|
||
customer_segment: groupDef.key === 'customer_segment' ? value : '',
|
||
customer_code: groupDef.key === 'customer_code' ? value : '',
|
||
customer_name: groupDef.key === 'customer_name' ? value : '',
|
||
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: weightedAverageProductCost(groupRows, 'base_price_usd'),
|
||
cost_price_usd: weightedAverageProductCost(groupRows, 'cost_price_usd'),
|
||
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_bucket: dominantValue(groupRows, 'performance_bucket'),
|
||
...aggregate,
|
||
performance_score: groupPerformanceScore(aggregate, groupDef.key),
|
||
recommendation: dominantValue(groupRows, 'recommendation') || `${formatNumber(groupRows.length, 0)} satır`
|
||
}
|
||
}
|
||
|
||
function aggregateGroupFields (sourceRows, groupField = '') {
|
||
const out = {}
|
||
const fields = new Set()
|
||
for (const row of sourceRows) {
|
||
Object.keys(row || {}).forEach(key => fields.add(key))
|
||
}
|
||
|
||
for (const field of fields) {
|
||
if (field === 'row_key' || field === 'key') continue
|
||
if (field === 'stock_qty') {
|
||
out[field] = distinctVariantStockQty(sourceRows)
|
||
continue
|
||
}
|
||
if (field === 'idle_cost_usd') {
|
||
out[field] = distinctVariantStockCost(sourceRows)
|
||
continue
|
||
}
|
||
if (field === 'base_price_usd' || field === 'cost_price_usd') {
|
||
out[field] = weightedAverageProductCost(sourceRows, field)
|
||
continue
|
||
}
|
||
if (shouldSumField(field)) {
|
||
out[field] = sumRows(sourceRows, field)
|
||
} else if (shouldAverageField(field)) {
|
||
out[field] = weightedAverageOrAverage(sourceRows, field, weightFieldForMetric(field))
|
||
}
|
||
}
|
||
|
||
applyDerivedGroupMetrics(out, sourceRows, groupField)
|
||
return out
|
||
}
|
||
|
||
function shouldSumField (field) {
|
||
return /(_qty|_usd|_count|_value_usd|line_count|invoice_count|order_count|product_count|market_count|customer_count|overdue_qty|stock_qty|net_stock_after_order|idle_cost_usd)$/i.test(field)
|
||
}
|
||
|
||
function shouldAverageField (field) {
|
||
return /^(avg_|unit_|base_price|cost_price|gross_margin|expected_margin|sales_index|performance_score|stock_days|stock_turnover)/i.test(field) ||
|
||
/(_price_usd|_margin|_index|_days)$/i.test(field)
|
||
}
|
||
|
||
function weightFieldForMetric (field) {
|
||
if (field.includes('_180d')) return 'sales_qty_180d'
|
||
if (field.includes('_365d')) return 'sales_qty_365d'
|
||
if (field.includes('_total')) return 'sales_qty_total'
|
||
if (field.includes('order') || field.includes('expected_')) return 'order_qty'
|
||
if (field.includes('stock')) return 'stock_qty'
|
||
return 'sales_qty_90d'
|
||
}
|
||
|
||
function applyDerivedGroupMetrics (out, sourceRows, groupField = '') {
|
||
for (const suffix of ['90d', '180d', '365d', 'total']) {
|
||
const sales = Number(out[`sales_usd_${suffix}`] || 0)
|
||
const qty = Number(out[`sales_qty_${suffix}`] || 0)
|
||
if (qty > 0) out[`avg_price_usd_${suffix}`] = sales / qty
|
||
if (sales > 0) {
|
||
if (out[`gross_profit_base_usd_${suffix}`] !== undefined) {
|
||
out[`gross_margin_base_${suffix}`] = Number(out[`gross_profit_base_usd_${suffix}`] || 0) / sales
|
||
}
|
||
if (out[`gross_profit_cost_usd_${suffix}`] !== undefined) {
|
||
out[`gross_margin_cost_${suffix}`] = Number(out[`gross_profit_cost_usd_${suffix}`] || 0) / sales
|
||
}
|
||
if (out[`gross_profit_usd_${suffix}`] !== undefined) {
|
||
out[`gross_margin_${suffix}`] = Number(out[`gross_profit_usd_${suffix}`] || 0) / sales
|
||
}
|
||
}
|
||
}
|
||
|
||
const orderUsd = Number(out.order_usd || 0)
|
||
if (orderUsd > 0) {
|
||
if (out.expected_profit_base_usd !== undefined) out.expected_margin_base = Number(out.expected_profit_base_usd || 0) / orderUsd
|
||
if (out.expected_profit_cost_usd !== undefined) out.expected_margin_cost = Number(out.expected_profit_cost_usd || 0) / orderUsd
|
||
}
|
||
|
||
const orderQty = Number(out.order_qty || 0)
|
||
if (orderQty > 0) out.avg_order_price_usd = Number(out.order_usd || 0) / orderQty
|
||
if (out.net_stock_after_order !== undefined || orderQty > 0) {
|
||
out.net_stock_after_order = Number(out.stock_qty || 0) - orderQty
|
||
}
|
||
|
||
if (sourceRows.some(row => row.performance_bucket)) {
|
||
out.performance_bucket = dominantValue(sourceRows, 'performance_bucket')
|
||
}
|
||
out.customer_score_90d = customerSalesPeriodScore(periodMetricSource(out, '90d'))
|
||
out.customer_score_180d = customerSalesPeriodScore(periodMetricSource(out, '180d'))
|
||
out.customer_score_365d = customerSalesPeriodScore(periodMetricSource(out, '365d'))
|
||
out.customer_score_total = customerSalesPeriodScore(periodMetricSource(out, 'total'))
|
||
out.performance_score_90d = productSalesPeriodScore(productPeriodMetricSource(out, '90d'))
|
||
out.performance_score_180d = productSalesPeriodScore(productPeriodMetricSource(out, '180d'))
|
||
out.performance_score_365d = productSalesPeriodScore(productPeriodMetricSource(out, '365d'))
|
||
out.performance_score_total = productSalesPeriodScore(productPeriodMetricSource(out, 'total'))
|
||
out.performance_score = groupPerformanceScore(out, groupField)
|
||
}
|
||
|
||
function groupPerformanceScore (row, groupField = '') {
|
||
if (Number(row?.order_qty || 0) > 0 || Number(row?.order_usd || 0) > 0) return orderGroupPerformanceScore(row)
|
||
if (['customer_code', 'customer_name'].includes(groupField)) return customerSalesGroupPerformanceScore(row)
|
||
return salesGroupPerformanceScore(row)
|
||
}
|
||
|
||
function salesGroupPerformanceScore (row) {
|
||
return productSalesPeriodScore(productPeriodMetricSource(row, '90d'))
|
||
}
|
||
|
||
function productSalesPeriodScore (row) {
|
||
const salesIndex = Number(row?.sales_index_90d || 0)
|
||
const margin = Number(row?.gross_margin_cost_90d ?? 0)
|
||
const invoices = Number(row?.invoice_count_90d || 0)
|
||
const qty = Number(row?.sales_qty_90d || 0)
|
||
let stockTurnover = Number(row?.stock_turnover_90d || 0)
|
||
const stockQty = Number(row?.stock_qty || 0)
|
||
if (!stockTurnover && stockQty > 0) stockTurnover = qty / stockQty
|
||
return Math.min(35, Math.max(0, salesIndex) * 18) +
|
||
Math.min(30, Math.max(0, margin) * 60) +
|
||
Math.min(15, invoices * 1.5) +
|
||
Math.min(10, qty / 10) +
|
||
Math.min(10, Math.max(0, stockTurnover) * 5)
|
||
}
|
||
|
||
function productPeriodMetricSource (row, suffix) {
|
||
const salesIndex = Number(row?.[`sales_index_${suffix}`] || 0)
|
||
const qty = Number(row?.[`sales_qty_${suffix}`] || 0)
|
||
const stockQty = Number(row?.stock_qty || 0)
|
||
const turnoverKey = `stock_turnover_${suffix}`
|
||
const hasTurnover = Object.prototype.hasOwnProperty.call(row || {}, turnoverKey)
|
||
return {
|
||
sales_index_90d: salesIndex,
|
||
gross_margin_cost_90d: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
|
||
gross_margin_90d: Number(row?.[`gross_margin_${suffix}`] ?? 0),
|
||
invoice_count_90d: Number(row?.[`invoice_count_${suffix}`] || 0),
|
||
sales_qty_90d: qty,
|
||
stock_turnover_90d: hasTurnover ? Number(row?.[turnoverKey] || 0) : (stockQty > 0 ? qty / stockQty : 0),
|
||
stock_qty: stockQty
|
||
}
|
||
}
|
||
|
||
function customerSalesGroupPerformanceScore (row) {
|
||
const periodToScore = {
|
||
90: Number(row?.customer_score_90d || 0),
|
||
180: Number(row?.customer_score_180d || 0),
|
||
360: Number(row?.customer_score_365d || 0),
|
||
total: Number(row?.customer_score_total || 0)
|
||
}
|
||
const selected = selectedPeriods.value.length ? selectedPeriods.value : ['90', '180', '360', 'total']
|
||
const scores = selected.map(period => periodToScore[period]).filter(score => Number.isFinite(score) && score > 0)
|
||
if (!scores.length) return 0
|
||
return scores.reduce((sum, score) => sum + score, 0) / scores.length
|
||
}
|
||
|
||
function customerSalesPeriodScore (row) {
|
||
const salesUSD = Number(row?.sales_usd || 0)
|
||
const margin = Number(row?.gross_margin_cost ?? 0)
|
||
const invoices = Number(row?.invoice_count || 0)
|
||
const qty = Number(row?.sales_qty || 0)
|
||
const productCount = Number(row?.product_count || 0)
|
||
return Math.min(35, salesUSD / 1000) +
|
||
Math.min(25, Math.max(0, margin) * 55) +
|
||
Math.min(20, invoices * 2) +
|
||
Math.min(10, qty / 10) +
|
||
Math.min(10, productCount)
|
||
}
|
||
|
||
function periodMetricSource (row, suffix) {
|
||
return {
|
||
sales_usd: Number(row?.[`sales_usd_${suffix}`] || 0),
|
||
gross_margin_cost: Number(row?.[`gross_margin_cost_${suffix}`] ?? 0),
|
||
gross_margin: Number(row?.[`gross_margin_${suffix}`] ?? 0),
|
||
invoice_count: Number(row?.[`invoice_count_${suffix}`] || 0),
|
||
sales_qty: Number(row?.[`sales_qty_${suffix}`] || 0),
|
||
product_count: Number(row?.product_count || 0)
|
||
}
|
||
}
|
||
|
||
function orderGroupPerformanceScore (row) {
|
||
const orderUSD = Number(row?.order_usd || 0)
|
||
const margin = Number(row?.expected_margin_cost || 0)
|
||
const orders = Number(row?.order_count || 0)
|
||
const qty = Number(row?.order_qty || 0)
|
||
const stockBonus = Number(row?.net_stock_after_order || 0) >= 0 ? 10 : 0
|
||
return Math.min(35, orderUSD / 1000) +
|
||
Math.min(25, Math.max(0, margin) * 60) +
|
||
Math.min(15, orders * 2) +
|
||
Math.min(15, qty / 10) +
|
||
stockBonus
|
||
}
|
||
|
||
function sumRows (sourceRows, field) {
|
||
return sourceRows.reduce((sum, row) => sum + Number(row[field] || 0), 0)
|
||
}
|
||
|
||
function productVariantStockKey (row) {
|
||
const productCode = String(row?.product_code || '').trim()
|
||
const colorCode = String(row?.color_code || '').trim()
|
||
const yakaKodu = String(row?.yaka_kodu || '').trim()
|
||
if (!productCode && !colorCode && !yakaKodu) return ''
|
||
return `${productCode}|${colorCode}|${yakaKodu}`
|
||
}
|
||
|
||
function distinctVariantStockQty (sourceRows) {
|
||
const seen = new Set()
|
||
let total = 0
|
||
let keyed = false
|
||
for (const row of sourceRows) {
|
||
const key = productVariantStockKey(row)
|
||
const stockQty = Number(row?.stock_qty || 0)
|
||
if (!key) continue
|
||
keyed = true
|
||
if (seen.has(key)) continue
|
||
seen.add(key)
|
||
total += stockQty
|
||
}
|
||
return keyed ? total : sumRows(sourceRows, 'stock_qty')
|
||
}
|
||
|
||
function distinctVariantStockCost (sourceRows) {
|
||
const seen = new Set()
|
||
let total = 0
|
||
let keyed = false
|
||
for (const row of sourceRows) {
|
||
const key = productVariantStockKey(row)
|
||
if (!key) continue
|
||
keyed = true
|
||
if (seen.has(key)) continue
|
||
seen.add(key)
|
||
const stockQty = Number(row?.stock_qty || 0)
|
||
const costPrice = Number(row?.cost_price_usd || 0)
|
||
total += stockQty * costPrice
|
||
}
|
||
return keyed ? total : sumRows(sourceRows, 'idle_cost_usd')
|
||
}
|
||
|
||
function distinctCount (sourceRows, field) {
|
||
const values = new Set()
|
||
for (const row of sourceRows) {
|
||
const value = String(row[field] || '').trim()
|
||
if (value && value !== '-') values.add(value)
|
||
}
|
||
return values.size
|
||
}
|
||
|
||
function distinctSummary (sourceRows, field, fallbackField) {
|
||
const values = new Set()
|
||
for (const row of sourceRows) {
|
||
const value = String(row[field] || (fallbackField ? row[fallbackField] : '') || '').trim()
|
||
if (value && value !== '-') values.add(value)
|
||
if (values.size > 1) return `${values.size} farklı`
|
||
}
|
||
return Array.from(values)[0] || ''
|
||
}
|
||
|
||
function distinctTextSummary (current, next) {
|
||
const left = String(current || '').trim()
|
||
const right = String(next || '').trim()
|
||
if (!right || right === 'STOK') return left
|
||
if (!left) return right
|
||
if (left === right || left === 'Çoklu') return left
|
||
return 'Çoklu'
|
||
}
|
||
|
||
function dominantValue (sourceRows, field) {
|
||
const counts = new Map()
|
||
for (const row of sourceRows) {
|
||
const value = String(row[field] || '').trim()
|
||
if (!value) continue
|
||
counts.set(value, (counts.get(value) || 0) + 1)
|
||
}
|
||
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] || ''
|
||
}
|
||
|
||
function ratio (amount, base) {
|
||
const divisor = Number(base || 0)
|
||
if (!divisor) return 0
|
||
return Number(amount || 0) / divisor
|
||
}
|
||
|
||
function averageRows (sourceRows, field) {
|
||
const values = sourceRows.map(row => Number(row[field] || 0)).filter(value => Number.isFinite(value) && value !== 0)
|
||
if (!values.length) return 0
|
||
return values.reduce((sum, value) => sum + value, 0) / values.length
|
||
}
|
||
|
||
function weightedAverage (sourceRows, amountField, qtyField) {
|
||
const qty = sumRows(sourceRows, qtyField)
|
||
if (!qty) return 0
|
||
return sumRows(sourceRows, amountField) / qty
|
||
}
|
||
|
||
function weightedAverageOrAverage (sourceRows, valueField, qtyField) {
|
||
const weightedRows = sourceRows
|
||
.map(row => ({
|
||
value: Number(row[valueField] || 0),
|
||
qty: Number(row[qtyField] || 0)
|
||
}))
|
||
.filter(row => Number.isFinite(row.value) && Number.isFinite(row.qty) && row.qty > 0)
|
||
const qty = weightedRows.reduce((sum, row) => sum + row.qty, 0)
|
||
if (qty > 0) {
|
||
return weightedRows.reduce((sum, row) => sum + row.value * row.qty, 0) / qty
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function weightedAverageProductCost (sourceRows, valueField) {
|
||
const salesRows = sourceRows
|
||
.map(row => ({
|
||
value: Number(row[valueField] || 0),
|
||
qty: Number(row.sales_qty_total || 0)
|
||
}))
|
||
.filter(row => Number.isFinite(row.value) && Number.isFinite(row.qty) && row.value > 0 && row.qty > 0)
|
||
const salesQty = salesRows.reduce((sum, row) => sum + row.qty, 0)
|
||
if (salesQty > 0) {
|
||
return salesRows.reduce((sum, row) => sum + row.value * row.qty, 0) / salesQty
|
||
}
|
||
|
||
const variants = new Map()
|
||
for (const row of sourceRows) {
|
||
const key = productVariantStockKey(row)
|
||
if (!key || variants.has(key)) continue
|
||
variants.set(key, row)
|
||
}
|
||
const stockSource = variants.size > 0 ? Array.from(variants.values()) : sourceRows
|
||
const stockRows = stockSource
|
||
.map(row => ({
|
||
value: Number(row[valueField] || 0),
|
||
qty: Number(row.stock_qty || 0)
|
||
}))
|
||
.filter(row => Number.isFinite(row.value) && Number.isFinite(row.qty) && row.value > 0 && row.qty > 0)
|
||
const stockQty = stockRows.reduce((sum, row) => sum + row.qty, 0)
|
||
if (stockQty > 0) {
|
||
return stockRows.reduce((sum, row) => sum + row.value * row.qty, 0) / stockQty
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function marginFromSalesCost (salesUSD, qty, unitCost) {
|
||
const sales = Number(salesUSD || 0)
|
||
if (!sales) return 0
|
||
return (sales - (Number(qty || 0) * Number(unitCost || 0))) / sales
|
||
}
|
||
|
||
function marginFromRows (sourceRows, salesField, qtyField, unitCostField) {
|
||
const sales = sumRows(sourceRows, salesField)
|
||
if (!sales) return 0
|
||
const costAmount = sourceRows.reduce((sum, row) => {
|
||
return sum + Number(row[qtyField] || 0) * Number(row[unitCostField] || 0)
|
||
}, 0)
|
||
return (sales - costAmount) / sales
|
||
}
|
||
|
||
function normalizeGroupValue (value) {
|
||
return String(value || '').trim()
|
||
}
|
||
|
||
function shouldSkipGroupValue (field, value) {
|
||
const text = String(value || '').trim()
|
||
if (field === 'urun_ilk_grubu') return !text || text === '-'
|
||
if (field === 'askili_yan') return !text || text === '-'
|
||
return false
|
||
}
|
||
|
||
function isGroupExpanded (key) {
|
||
return expandedGroups.value[key] === true
|
||
}
|
||
|
||
function toggleGroup (key) {
|
||
lastManualExpandedGroupKey.value = key
|
||
expandedGroups.value = {
|
||
...expandedGroups.value,
|
||
[key]: !isGroupExpanded(key)
|
||
}
|
||
scheduleLoadBackendGroupedRows()
|
||
}
|
||
|
||
async function collapseAllProductGroups () {
|
||
if (bulkExpandLoading.value) return
|
||
detailLevelMenuOpen.value = false
|
||
const tabPrefix = `tab:${activeTab.value}|`
|
||
expandedGroups.value = Object.fromEntries(
|
||
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith(tabPrefix))
|
||
)
|
||
lastManualExpandedGroupKey.value = ''
|
||
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
|
||
bulkExpandLoading.value = true
|
||
try {
|
||
await loadBackendGroupedRows({ force: true })
|
||
} finally {
|
||
bulkExpandLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function expandSelectedProductGroups () {
|
||
if (bulkExpandLoading.value) return
|
||
detailLevelMenuOpen.value = false
|
||
const keys = productAutoExpandKeys.value
|
||
const maxKeys = Math.min(maxBulkExpandKeys, maxBackendExpandedKeysForTab(activeTab.value))
|
||
const expandableKeys = keys.slice(0, maxKeys)
|
||
const tabPrefix = `tab:${activeTab.value}|`
|
||
const next = Object.fromEntries(
|
||
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith(tabPrefix))
|
||
)
|
||
for (const key of expandableKeys) next[key] = true
|
||
expandedGroups.value = next
|
||
lastManualExpandedGroupKey.value = ''
|
||
if (keys.length > expandableKeys.length) {
|
||
Notify.create({
|
||
type: 'warning',
|
||
message: `${formatNumber(expandableKeys.length, 0)} grup açıldı. Daha derin detaylar için satırları kademeli açın.`
|
||
})
|
||
}
|
||
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
|
||
bulkExpandLoading.value = true
|
||
try {
|
||
await loadBackendGroupedRows({ force: true })
|
||
} finally {
|
||
bulkExpandLoading.value = false
|
||
}
|
||
}
|
||
|
||
function groupLabelColumnName (row) {
|
||
const field = String(row?.group_field || '')
|
||
if (field === 'color_code' || field === 'yaka_kodu' || field === 'color_yaka') return 'color_yaka'
|
||
return field || 'product_code'
|
||
}
|
||
|
||
function groupDisplayLabel (row) {
|
||
const field = String(row?.group_field || '')
|
||
if (field === 'color_code' || field === 'yaka_kodu' || field === 'color_yaka') {
|
||
return colorYakaValue(row) || row?.label || row?.group_value || ''
|
||
}
|
||
return row?.label || row?.group_value || ''
|
||
}
|
||
|
||
function groupIndentPx (row) {
|
||
return '0px'
|
||
}
|
||
|
||
function hasProductVariantImageKey (row) {
|
||
const product = String(row?.image_product_code || row?.product_code || '').trim()
|
||
const color = String(row?.image_color_code || row?.color_code || '').trim()
|
||
return Boolean(product && color)
|
||
}
|
||
|
||
function canShowProductImage (row) {
|
||
return isProductKpiTab.value && hasProductVariantImageKey(row)
|
||
}
|
||
|
||
function groupShowsImage (row) {
|
||
return isProductKpiTab.value &&
|
||
row?.__group &&
|
||
['product_code', 'color_code', 'yaka_kodu', 'color_yaka'].includes(String(row.group_field || '')) &&
|
||
hasProductVariantImageKey(row)
|
||
}
|
||
|
||
const groupDimensionFields = new Set(dimensionColumnNames)
|
||
|
||
function groupRowClasses (row) {
|
||
const level = Math.min(Number(row?.level || 0), 5)
|
||
const field = String(row?.group_field || 'unknown').replace(/[^a-z0-9_-]/gi, '-').toLowerCase()
|
||
return ['group-row', `group-row-level-${level}`, `group-row-field-${field}`]
|
||
}
|
||
|
||
function groupCellClass (colOrName) {
|
||
const name = colOrName?.name || colOrName
|
||
if (name === 'image') return 'product-image-cell'
|
||
if (colOrName?.align === 'right') return 'text-right'
|
||
return /(^|_)(score|qty|count|usd|days|turnover|index|margin)$/.test(name) || ['stock_qty', 'net_stock_after_order'].includes(name)
|
||
? 'text-right'
|
||
: ''
|
||
}
|
||
|
||
function formatGroupCell (row, colOrName) {
|
||
const name = colOrName?.name || colOrName
|
||
if (name === 'image') return ''
|
||
if (name === 'recommendation') return row.recommendation || ''
|
||
if (name === 'color_yaka' && ['color_code', 'yaka_kodu', 'color_yaka'].includes(String(row?.group_field || ''))) return colorYakaValue(row)
|
||
if (groupDimensionFields.has(name) && name !== row?.group_field) return ''
|
||
return formatTableCell(row, colOrName)
|
||
}
|
||
|
||
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,
|
||
stock_turnover_90d: existingOrStockTurnover(row?.stock_turnover_90d, row?.sales_qty_90d, row?.stock_qty),
|
||
stock_turnover_180d: existingOrStockTurnover(row?.stock_turnover_180d, row?.sales_qty_180d, row?.stock_qty),
|
||
stock_turnover_365d: existingOrStockTurnover(row?.stock_turnover_365d, row?.sales_qty_365d, row?.stock_qty),
|
||
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),
|
||
gross_margin_cost_180d: marginFromSalesCost(row?.sales_usd_180d, row?.sales_qty_180d, row?.cost_price_usd)
|
||
}
|
||
}
|
||
|
||
function withGeneralMargins (row) {
|
||
return {
|
||
...row,
|
||
stock_turnover_total: existingOrStockTurnover(row?.stock_turnover_total, row?.sales_qty_total, row?.stock_qty),
|
||
gross_margin_base_total: marginFromSalesCost(row?.sales_usd_total, row?.sales_qty_total, row?.base_price_usd),
|
||
gross_margin_cost_total: marginFromSalesCost(row?.sales_usd_total, row?.sales_qty_total, row?.cost_price_usd)
|
||
}
|
||
}
|
||
|
||
function stockTurnover (salesQty, stockQty) {
|
||
const stock = Number(stockQty || 0)
|
||
if (stock <= 0) return 0
|
||
return Number(salesQty || 0) / stock
|
||
}
|
||
|
||
function existingOrStockTurnover (value, salesQty, stockQty) {
|
||
const current = Number(value)
|
||
return value !== undefined && value !== null && Number.isFinite(current) ? current : stockTurnover(salesQty, stockQty)
|
||
}
|
||
|
||
function filterKey (tableKey, name) {
|
||
return `${tableKey}:${name}`
|
||
}
|
||
|
||
function isColumnFilterable (name) {
|
||
return name !== 'image'
|
||
}
|
||
|
||
function columnFilterOptions (tableKey, name) {
|
||
const options = columnFilterOptionMap.value[tableKey]?.[name] || []
|
||
const search = String(columnFilterSearch[filterKey(tableKey, name)] || '').trim().toLocaleLowerCase('tr')
|
||
if (!search) return options
|
||
return options.filter(option => String(option.label || '').toLocaleLowerCase('tr').includes(search))
|
||
}
|
||
|
||
function hasColumnFilter (tableKey, name) {
|
||
return selectedColumnFilters(tableKey, name).length > 0
|
||
}
|
||
|
||
function getColumnFilterBadgeValue (tableKey, name) {
|
||
return selectedColumnFilters(tableKey, name).length
|
||
}
|
||
|
||
function selectedColumnFilters (tableKey, name) {
|
||
const selected = columnFilters[filterKey(tableKey, name)]
|
||
return Array.isArray(selected) ? selected : []
|
||
}
|
||
|
||
function hasActiveTableFilters (tableKey) {
|
||
const prefix = `${tableKey}:`
|
||
return Object.entries(columnFilters).some(([key, value]) => key.startsWith(prefix) && Array.isArray(value) && value.length > 0)
|
||
}
|
||
|
||
function isColumnFilterValueSelected (tableKey, name, value) {
|
||
return selectedColumnFilters(tableKey, name).includes(value)
|
||
}
|
||
|
||
function toggleColumnFilterValue (tableKey, name, value) {
|
||
runWithFilterBusy(() => {
|
||
const key = filterKey(tableKey, name)
|
||
const selected = selectedColumnFilters(tableKey, name)
|
||
columnFilters[key] = selected.includes(value)
|
||
? selected.filter(item => item !== value)
|
||
: [...selected, value]
|
||
})
|
||
}
|
||
|
||
function clearColumnFilter (tableKey, name) {
|
||
runWithFilterBusy(() => {
|
||
columnFilters[filterKey(tableKey, name)] = []
|
||
})
|
||
}
|
||
|
||
function selectAllColumnFilterOptions (tableKey, name) {
|
||
runWithFilterBusy(() => {
|
||
columnFilters[filterKey(tableKey, name)] = columnFilterOptions(tableKey, name).map(option => option.value)
|
||
})
|
||
}
|
||
|
||
function runWithFilterBusy (apply) {
|
||
filterBusy.value = true
|
||
const done = () => {
|
||
filterBusy.value = false
|
||
}
|
||
const execute = () => {
|
||
apply()
|
||
if (typeof window === 'undefined') {
|
||
setTimeout(done, 0)
|
||
return
|
||
}
|
||
window.requestAnimationFrame(() => {
|
||
window.requestAnimationFrame(done)
|
||
})
|
||
}
|
||
if (typeof window === 'undefined') {
|
||
execute()
|
||
return
|
||
}
|
||
window.requestAnimationFrame(execute)
|
||
}
|
||
|
||
function filterRowsForTable (tableKey, sourceRows, sourceColumns) {
|
||
return sourceRows.filter(row => {
|
||
return sourceColumns.every(col => {
|
||
if (!isColumnFilterable(col.name)) return true
|
||
const selected = selectedColumnFilters(tableKey, col.name)
|
||
if (!selected.length) return true
|
||
return selected.includes(columnFilterValue(row, col))
|
||
})
|
||
})
|
||
}
|
||
|
||
function columnFilterValue (row, col) {
|
||
return String(rawColumnCellValue(row, col) ?? '').trim()
|
||
}
|
||
|
||
function columnFilterLabel (row, col) {
|
||
return formattedColumnCellValue(row, col) || columnFilterValue(row, col)
|
||
}
|
||
|
||
function colorYakaValue (row) {
|
||
const groupField = String(row?.group_field || '')
|
||
const groupValue = String(row?.group_value || row?.label || '').trim()
|
||
const color = String(row?.image_color_code || row?.color_code || '').trim()
|
||
const colorDescription = String(row?.image_color_description || row?.color_description || '').trim()
|
||
const yaka = String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
|
||
const colorLabel = colorDisplayLabel(groupField === 'color_code' ? groupValue || color : color, colorDescription)
|
||
if (row?.__group && groupField === 'color_yaka') {
|
||
const parts = groupValue.split('/').map(part => part.trim()).filter(Boolean)
|
||
const groupColor = parts[0] || ''
|
||
const groupYaka = parts.slice(1).join('/')
|
||
return [colorDisplayLabel(groupColor, colorDescription), groupYaka].filter(value => value && value !== '-').join('/')
|
||
}
|
||
if (row?.__group && groupField === 'color_code') return colorLabel
|
||
if (row?.__group && groupField === 'yaka_kodu') {
|
||
const parentColor = colorLabel && colorLabel !== '-' ? colorLabel : ''
|
||
const groupYaka = groupValue && groupValue !== '-' ? groupValue : yaka
|
||
return [parentColor, groupYaka].filter(Boolean).join('/')
|
||
}
|
||
return [colorLabel, yaka].filter(value => value && value !== '-').join('/')
|
||
}
|
||
|
||
function colorDisplayLabel (code, description) {
|
||
const colorCode = String(code || '').trim()
|
||
const desc = String(description || '').trim()
|
||
if (!colorCode) return ''
|
||
if (!desc) return colorCode
|
||
return `${colorCode}-${desc.toLocaleUpperCase('tr-TR')}`
|
||
}
|
||
|
||
function colorYakaGroupLabel (groupField, groupValue, colorSummary, yakaSummary) {
|
||
const field = String(groupField || '')
|
||
const value = String(groupValue || '').trim()
|
||
if (field === 'color_code') return value
|
||
if (field === 'yaka_kodu') {
|
||
const color = String(colorSummary || '').trim()
|
||
const yaka = value || String(yakaSummary || '').trim()
|
||
return [color, yaka].filter(part => part && part !== '-').join('/')
|
||
}
|
||
return value
|
||
}
|
||
|
||
function productCategoryValue (row) {
|
||
return row?.kategori || ''
|
||
}
|
||
|
||
function productFirstGroupValue (row) {
|
||
return cleanProductFirstGroupValue(row?.urun_ilk_grubu)
|
||
}
|
||
|
||
function productMainGroupValue (row) {
|
||
return row?.urun_ana_grubu || ''
|
||
}
|
||
|
||
function cleanOptionalGroupValue (value) {
|
||
const text = String(value || '').trim()
|
||
return text === '-' ? '' : text
|
||
}
|
||
|
||
function cleanProductFirstGroupValue (value) {
|
||
const text = String(value || '').trim()
|
||
if (text === '-') return ''
|
||
const normalized = text
|
||
.toLocaleUpperCase('tr-TR')
|
||
.replaceAll('İ', 'I')
|
||
.replaceAll('Ş', 'S')
|
||
.replaceAll('Ğ', 'G')
|
||
.replaceAll('Ü', 'U')
|
||
.replaceAll('Ö', 'O')
|
||
.replaceAll('Ç', 'C')
|
||
return ['YETISKIN', 'YETISKIN/GARSON', 'GARSON'].includes(normalized) ? '' : text
|
||
}
|
||
|
||
function rawProductCellValue (row, name) {
|
||
switch (name) {
|
||
case 'color_yaka': return colorYakaValue(row)
|
||
case 'kategori': return productCategoryValue(row)
|
||
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
||
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
||
case 'urun_ana_grubu': return productMainGroupValue(row)
|
||
case 'market_key': return displayMarketName(row.market_key)
|
||
case 'performance_bucket': return bucketLabel(row.performance_bucket)
|
||
default: return row[name]
|
||
}
|
||
}
|
||
|
||
function rawColumnCellValue (row, col) {
|
||
const name = col?.name || col
|
||
switch (name) {
|
||
case 'color_yaka': return colorYakaValue(row)
|
||
case 'kategori': return productCategoryValue(row)
|
||
case 'urun_ilk_grubu': return productFirstGroupValue(row)
|
||
case 'askili_yan': return cleanOptionalGroupValue(row?.askili_yan)
|
||
case 'urun_ana_grubu': return productMainGroupValue(row)
|
||
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:
|
||
if (row[name] !== undefined && row[name] !== null) return row[name]
|
||
if (typeof col?.field === 'function') return col.field(row)
|
||
if (typeof col?.field === 'string') return row[col.field]
|
||
return ''
|
||
}
|
||
}
|
||
|
||
function attachColumnSorts (targetColumns) {
|
||
targetColumns.forEach(col => {
|
||
if (!col || col.name === 'image') return
|
||
col.sortable = col.sortable !== false
|
||
col.sort = (_a, _b, rowA, rowB) => compareColumnSortValues(sortValueForColumn(rowA, col), sortValueForColumn(rowB, col))
|
||
})
|
||
}
|
||
|
||
function sortValueForColumn (row, col) {
|
||
const name = col?.name || col
|
||
const raw = rawColumnCellValue(row, col)
|
||
const direct = row?.[name]
|
||
if (typeof direct === 'number' && Number.isFinite(direct)) return direct
|
||
if (typeof raw === 'number' && Number.isFinite(raw)) return raw
|
||
if (direct instanceof Date) return direct.getTime()
|
||
if (raw instanceof Date) return raw.getTime()
|
||
if (typeof direct === 'boolean') return direct ? 1 : 0
|
||
if (typeof raw === 'boolean') return raw ? 1 : 0
|
||
|
||
if (isNumericSortColumn(name, col)) {
|
||
const numeric = parseSortNumber(direct ?? raw)
|
||
return Number.isFinite(numeric) ? numeric : 0
|
||
}
|
||
|
||
return String(raw ?? '').trim().toLocaleLowerCase('tr-TR')
|
||
}
|
||
|
||
function compareColumnSortValues (left, right) {
|
||
const leftNumber = typeof left === 'number' && Number.isFinite(left)
|
||
const rightNumber = typeof right === 'number' && Number.isFinite(right)
|
||
if (leftNumber && rightNumber) return left - right
|
||
return String(left ?? '').localeCompare(String(right ?? ''), 'tr', { numeric: true, sensitivity: 'base' })
|
||
}
|
||
|
||
function isNumericSortColumn (name, col) {
|
||
return col?.align === 'right' || isMoneyField(name) || isPercentField(name) || isIntegerField(name) || isDecimalField(name)
|
||
}
|
||
|
||
function parseSortNumber (value) {
|
||
if (typeof value === 'number') return value
|
||
if (value === null || value === undefined || value === '') return NaN
|
||
const text = String(value).trim()
|
||
if (!text) return NaN
|
||
const normalized = text
|
||
.replace(/\s/g, '')
|
||
.replace(/[%$₺€£]/g, '')
|
||
.replace(/\.(?=\d{3}(\D|$))/g, '')
|
||
.replace(/,/g, '.')
|
||
.replace(/[^\d.-]/g, '')
|
||
return Number(normalized)
|
||
}
|
||
|
||
function formattedColumnCellValue (row, col) {
|
||
if (typeof col?.field === 'function') return col.field(row)
|
||
return formatProductCell(row, col?.name || col)
|
||
}
|
||
|
||
function formatTableCell (row, colOrName) {
|
||
const col = typeof colOrName === 'object' ? colOrName : null
|
||
const name = col?.name || colOrName
|
||
if (name === 'image') return ''
|
||
if (name === 'color_yaka') return colorYakaValue(row)
|
||
if (['kategori', 'urun_ilk_grubu', 'urun_ana_grubu'].includes(name)) return rawProductCellValue(row, name) || ''
|
||
if (name === 'market_key') return displayMarketName(row.market_key)
|
||
if (name === 'performance_bucket') return bucketLabel(row.performance_bucket)
|
||
if (name === 'is_overdue') return row.is_overdue ? 'Evet' : 'Hayır'
|
||
if (isMoneyField(name)) return formatMoney(row[name], 'USD')
|
||
if (isPercentField(name)) return formatPercent(row[name])
|
||
if (isIntegerField(name)) return formatNumber(row[name], 0)
|
||
if (isDecimalField(name)) return formatNumber(row[name], 2)
|
||
if (typeof col?.field === 'function') return col.field(row)
|
||
return formatProductCell(row, name)
|
||
}
|
||
|
||
function isMoneyField (name) {
|
||
return /(^|_)(usd|cost_value_usd|value_usd)$/.test(name) ||
|
||
/(price_usd|cost_usd|profit.*usd|sales_usd|order_usd|base_price_usd|cost_price_usd|idle_cost_usd|stock_cost_value_usd|risk_stock_cost_value_usd)/.test(name)
|
||
}
|
||
|
||
function isPercentField (name) {
|
||
return /(margin|gross_margin|expected_margin)/.test(name)
|
||
}
|
||
|
||
function isIntegerField (name) {
|
||
return /(qty|count|stock_qty|line_count|invoice_count|order_count|product_count|market_count|customer_count|overdue_qty|net_stock_after_order)$/.test(name)
|
||
}
|
||
|
||
function isDecimalField (name) {
|
||
return /(days|turnover|index|score|avg_daily)/.test(name)
|
||
}
|
||
|
||
function formatProductCell (row, name) {
|
||
switch (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 'stock_turnover_90d':
|
||
case 'stock_turnover_180d':
|
||
case 'stock_turnover_365d':
|
||
case 'stock_turnover_total':
|
||
case 'sales_index_90d':
|
||
case 'sales_index_total':
|
||
case 'performance_score':
|
||
return formatNumber(row[name], 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':
|
||
case 'gross_margin_base_180d':
|
||
case 'gross_margin_cost_180d':
|
||
case 'gross_margin_base_total':
|
||
case 'gross_margin_cost_total':
|
||
case 'gross_margin_90d':
|
||
case 'gross_margin_180d':
|
||
return formatPercent(row[name])
|
||
case 'kategori':
|
||
return productCategoryValue(row)
|
||
case 'urun_ilk_grubu':
|
||
return productFirstGroupValue(row)
|
||
case 'urun_ana_grubu':
|
||
return productMainGroupValue(row)
|
||
case 'market_key':
|
||
return displayMarketName(row.market_key)
|
||
default:
|
||
return rawProductCellValue(row, name) || ''
|
||
}
|
||
}
|
||
|
||
function normalizeRow (row) {
|
||
const product = String(row?.product_code || '').trim()
|
||
const color = String(row?.color_code || '').trim()
|
||
const yaka = String(row?.yaka_kodu || '').trim()
|
||
const market = String(row?.market_key || '').trim()
|
||
return withPeriodScores(withProductMargins({
|
||
...row,
|
||
row_key: `${product}|${color}|${yaka}|${market}`
|
||
}))
|
||
}
|
||
|
||
function withPeriodScores (row) {
|
||
const next = { ...row }
|
||
next.performance_score_90d = Number(next.performance_score_90d || productSalesPeriodScore(productPeriodMetricSource(next, '90d')))
|
||
next.performance_score_180d = Number(next.performance_score_180d || productSalesPeriodScore(productPeriodMetricSource(next, '180d')))
|
||
next.performance_score_365d = Number(next.performance_score_365d || productSalesPeriodScore(productPeriodMetricSource(next, '365d')))
|
||
next.performance_score_total = Number(next.performance_score_total || productSalesPeriodScore(productPeriodMetricSource(next, 'total')))
|
||
if (!Number(next.performance_score || 0)) next.performance_score = next.performance_score_90d
|
||
return next
|
||
}
|
||
|
||
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()
|
||
const yaka = String(row?.yaka_kodu || '').trim()
|
||
const market = String(row?.market_key || '').trim()
|
||
return withPeriodScores(withGeneralMargins({
|
||
...row,
|
||
row_key: `general|${product}|${color}|${yaka}|${market}`
|
||
}))
|
||
}
|
||
|
||
function normalizeOrderAnalysisRow (row) {
|
||
const product = String(row?.product_code || '').trim()
|
||
const color = String(row?.color_code || '').trim()
|
||
const yaka = String(row?.yaka_kodu || '').trim()
|
||
const market = String(row?.market_key || '').trim()
|
||
return withOrderPeriodScores({
|
||
...row,
|
||
row_key: `orders|${product}|${color}|${yaka}|${market}`
|
||
})
|
||
}
|
||
|
||
function normalizeOrderGroupRow (row) {
|
||
const key = String(row?.group_key || '').trim()
|
||
const mode = String(row?.breakdown || '').trim()
|
||
return withOrderPeriodScores({
|
||
...row,
|
||
row_key: `order-group|${mode}|${key}|${row?.market_key || ''}|${row?.customer_code || ''}`
|
||
})
|
||
}
|
||
|
||
function normalizeOrderProductCustomerRow (row) {
|
||
const product = String(row?.product_code || '').trim()
|
||
const color = String(row?.color_code || '').trim()
|
||
const yaka = String(row?.yaka_kodu || '').trim()
|
||
const market = String(row?.market_key || '').trim()
|
||
const customer = String(row?.customer_code || '').trim()
|
||
return withOrderPeriodScores({
|
||
...row,
|
||
row_key: `order-product-customer|${product}|${color}|${yaka}|${market}|${customer}`
|
||
})
|
||
}
|
||
|
||
function normalizeOrderMarketDetailRow (row) {
|
||
return withOrderPeriodScores({
|
||
...row,
|
||
row_key: `order-market-detail|${row?.market_key || ''}|${row?.customer_code || ''}|${row?.order_number || ''}|${row?.product_code || ''}|${row?.color_code || ''}|${row?.yaka_kodu || ''}`
|
||
})
|
||
}
|
||
|
||
function withOrderPeriodScores (row) {
|
||
const next = { ...row }
|
||
const score = Number(next.performance_score || orderGroupPerformanceScore(next))
|
||
next.performance_score_90d = Number(next.performance_score_90d || score)
|
||
next.performance_score_180d = Number(next.performance_score_180d || score)
|
||
next.performance_score_365d = Number(next.performance_score_365d || score)
|
||
next.performance_score_total = Number(next.performance_score_total || score)
|
||
next.performance_score = score
|
||
return next
|
||
}
|
||
|
||
function normalizeSalesBreakdownRow (row) {
|
||
const next = withPeriodScores({ ...row })
|
||
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
||
next.customer_score_total = Number(next.customer_score_total || customerSalesPeriodScore(periodMetricSource(next, 'total')))
|
||
return {
|
||
...next,
|
||
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(' - ') || '-'
|
||
}
|
||
return row?.group_key || row?.market_key || '-'
|
||
}
|
||
|
||
function productImageKey (row) {
|
||
if (!hasProductVariantImageKey(row)) return ''
|
||
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 ''
|
||
const normalized = raw.replace(/\\/g, '/')
|
||
const idx = normalized.toLowerCase().indexOf('/uploads/')
|
||
if (idx >= 0) return normalized.slice(idx)
|
||
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
|
||
return ''
|
||
}
|
||
|
||
function resolveProductImageUrl (item) {
|
||
if (!item || typeof item !== 'object') return ''
|
||
|
||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||
if (contentURL.startsWith('/api/')) return contentURL
|
||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
||
|
||
const imageId = Number(item.id || item.ID || 0)
|
||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
||
|
||
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
||
if (thumbURL) return thumbURL
|
||
|
||
const fullURL = String(item.full_url || item.fullUrl || '').trim()
|
||
if (fullURL) return fullURL
|
||
|
||
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
|
||
if (uploadsPath) return uploadsPath
|
||
|
||
const fileName = String(item.file_name || item.FileName || '').trim()
|
||
return fileName ? `/uploads/image/${fileName}` : ''
|
||
}
|
||
|
||
async function fetchProductImagesForRow (row) {
|
||
if (!hasProductVariantImageKey(row)) return []
|
||
const key = productImageKey(row)
|
||
if (imageFailedKeys.value.has(key)) return []
|
||
if (Object.prototype.hasOwnProperty.call(imageListByKey.value, key)) {
|
||
return imageListByKey.value[key]
|
||
}
|
||
if (performanceStore.hasImageKey(key)) {
|
||
const urls = performanceStore.imageList(key)
|
||
imageListByKey.value = { ...imageListByKey.value, [key]: urls }
|
||
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: urls[0] || '' }
|
||
return urls
|
||
}
|
||
|
||
const code = String(row?.image_product_code || row?.product_code || '').trim()
|
||
if (!code) return []
|
||
|
||
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', {
|
||
params: {
|
||
code,
|
||
dim1: color || undefined,
|
||
dim3: yaka || undefined
|
||
},
|
||
timeout: 60000
|
||
})
|
||
const list = Array.isArray(resp?.data) ? resp.data : []
|
||
const urls = list.map(resolveProductImageUrl).filter(Boolean)
|
||
performanceStore.setImageCache(key, urls)
|
||
if (urls.length && imageFailedKeys.value.has(key)) {
|
||
const nextFailed = new Set(imageFailedKeys.value)
|
||
nextFailed.delete(key)
|
||
imageFailedKeys.value = nextFailed
|
||
}
|
||
imageListByKey.value = { ...imageListByKey.value, [key]: urls }
|
||
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: urls[0] || '' }
|
||
return urls
|
||
} catch {
|
||
performanceStore.setImageCache(key, [])
|
||
imageListByKey.value = { ...imageListByKey.value, [key]: [] }
|
||
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: '' }
|
||
return []
|
||
}
|
||
}
|
||
|
||
function getCachedProductImageUrl (row) {
|
||
if (!canShowProductImage(row)) return ''
|
||
const key = productImageKey(row)
|
||
if (imageFailedKeys.value.has(key)) return ''
|
||
return imageUrlByKey.value[key] || performanceStore.imageUrl(key) || ''
|
||
}
|
||
|
||
function markProductImageFailed (row) {
|
||
const key = productImageKey(row)
|
||
if (!key) return
|
||
imageFailedKeys.value = new Set([...imageFailedKeys.value, key])
|
||
performanceStore.setImageCache(key, [])
|
||
imageListByKey.value = { ...imageListByKey.value, [key]: [] }
|
||
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: '' }
|
||
}
|
||
|
||
async function primeProductImages (sourceRows) {
|
||
if (!isProductKpiTab.value) return
|
||
imagePrimeActiveRequests += 1
|
||
imagePrimeLoading.value = true
|
||
const items = []
|
||
const seen = new Set()
|
||
try {
|
||
for (const row of sourceRows || []) {
|
||
if (!canShowProductImage(row)) continue
|
||
const key = productImageKey(row)
|
||
const code = String(row?.image_product_code || row?.product_code || '').trim()
|
||
if (!key || !code || imageFailedKeys.value.has(key) || seen.has(key) || Object.prototype.hasOwnProperty.call(imageListByKey.value, key) || performanceStore.hasImageKey(key)) continue
|
||
seen.add(key)
|
||
items.push({
|
||
key,
|
||
code,
|
||
dim1: String(row?.image_color_code || row?.color_code || '').trim(),
|
||
dim3: String(row?.image_yaka_kodu || row?.yaka_kodu || '').trim()
|
||
})
|
||
if (items.length >= 160) break
|
||
}
|
||
if (!items.length) return
|
||
|
||
const batch = await performanceStore.fetchImageBatch(items)
|
||
const nextLists = { ...imageListByKey.value }
|
||
const nextUrls = { ...imageUrlByKey.value }
|
||
for (const item of items) {
|
||
const urls = batch?.lists?.[item.key] || performanceStore.imageList(item.key) || []
|
||
if (!urls.length) imageFailedKeys.value = new Set([...imageFailedKeys.value, item.key])
|
||
nextLists[item.key] = urls
|
||
nextUrls[item.key] = imageFailedKeys.value.has(item.key) ? '' : (batch?.urls?.[item.key] || urls[0] || '')
|
||
}
|
||
imageListByKey.value = nextLists
|
||
imageUrlByKey.value = nextUrls
|
||
} catch {
|
||
await Promise.all(items.slice(0, 24).map(item => fetchProductImagesForRow({
|
||
product_code: item.code,
|
||
color_code: item.dim1,
|
||
yaka_kodu: item.dim3
|
||
})))
|
||
} finally {
|
||
imagePrimeActiveRequests = Math.max(0, imagePrimeActiveRequests - 1)
|
||
imagePrimeLoading.value = imagePrimeActiveRequests > 0
|
||
}
|
||
}
|
||
|
||
async function openProductImageDialog (row) {
|
||
imageDialogRow.value = row
|
||
const urls = await fetchProductImagesForRow(row)
|
||
imageDialogUrls.value = urls
|
||
imageSlide.value = 0
|
||
imageDialogTitle.value = String(row?.image_product_code || row?.product_code || '-')
|
||
imageDialogSubtitle.value = [
|
||
row?.item_description,
|
||
row?.image_color_code || row?.color_code,
|
||
row?.image_yaka_kodu || row?.yaka_kodu
|
||
].filter(Boolean).join(' | ')
|
||
imageDialog.value = true
|
||
await loadProductDetailRows(row)
|
||
}
|
||
|
||
async function openPerformanceDialog (row) {
|
||
performanceDialogRow.value = row
|
||
performanceDialogTitle.value = String(row?.product_code || '-')
|
||
performanceDialogSubtitle.value = [
|
||
row?.item_description,
|
||
row?.color_code,
|
||
row?.yaka_kodu,
|
||
row?.market_key
|
||
].filter(Boolean).join(' | ')
|
||
performanceDialog.value = true
|
||
await loadProductDetailRows(row)
|
||
}
|
||
|
||
async function loadProductDetailRows (row) {
|
||
detailLoading.value = true
|
||
detailSalesRows.value = []
|
||
detailStockSizes.value = []
|
||
try {
|
||
const params = {
|
||
product_code: row?.image_product_code || row?.product_code || '',
|
||
color_code: row?.image_color_code || row?.color_code || '',
|
||
yaka_kodu: row?.image_yaka_kodu || row?.yaka_kodu || ''
|
||
}
|
||
if (!params.product_code) return
|
||
const [salesResp, stockResp] = await Promise.all([
|
||
api.get('/pricing/product-performance/sales-details', { params: { ...params, limit: 500 }, timeout: 60000 }),
|
||
api.get('/pricing/product-performance/stock-sizes', { params, timeout: 60000 })
|
||
])
|
||
detailSalesRows.value = (Array.isArray(salesResp?.data) ? salesResp.data : []).map((item, idx) => ({
|
||
...item,
|
||
detail_key: `${item.sales_date}|${item.ref_number}|${item.customer_code}|${idx}`
|
||
})).sort((a, b) => Number(b.sales_usd || 0) - Number(a.sales_usd || 0))
|
||
detailStockSizes.value = Array.isArray(stockResp?.data) ? stockResp.data : []
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün detay analizi alınamadı' })
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function backendGroupedSupportedTab (tabKey = activeTab.value) {
|
||
return tabKey === 'products' ||
|
||
tabKey === 'product_detail' ||
|
||
tabKey === 'idle' ||
|
||
tabKey === 'order_product_customers' ||
|
||
tabKey === 'order_market_details' ||
|
||
salesBreakdownTabKeys.includes(tabKey)
|
||
}
|
||
|
||
function activeExpandedGroupKeys () {
|
||
const tabPrefix = `tab:${activeTab.value}|`
|
||
const validFields = new Set(activeGroupLevels.value.map(level => level.key))
|
||
const maxKeys = maxBackendExpandedKeysForTab(activeTab.value)
|
||
const keys = Object.entries(expandedGroups.value)
|
||
.filter(([, value]) => value === true)
|
||
.map(([key]) => key)
|
||
.filter(key => key.startsWith(tabPrefix))
|
||
.filter(key => key.split('|').slice(1).every(part => validFields.has(String(part).split(':')[0])))
|
||
.sort((a, b) => {
|
||
const depthDiff = a.split('|').length - b.split('|').length
|
||
if (depthDiff !== 0) return depthDiff
|
||
return a.localeCompare(b, 'tr', { numeric: true })
|
||
})
|
||
const manualKey = lastManualExpandedGroupKey.value
|
||
const out = []
|
||
if (manualKey && keys.includes(manualKey)) out.push(manualKey)
|
||
for (const key of keys) {
|
||
if (out.length >= maxKeys) break
|
||
if (key !== manualKey) out.push(key)
|
||
}
|
||
return out
|
||
}
|
||
|
||
function pruneActiveExpandedGroups () {
|
||
const tabPrefix = `tab:${activeTab.value}|`
|
||
const allowed = new Set(activeExpandedGroupKeys())
|
||
const next = {}
|
||
for (const [key, value] of Object.entries(expandedGroups.value)) {
|
||
if (!value) continue
|
||
if (key.startsWith(tabPrefix) && !allowed.has(key)) continue
|
||
next[key] = value
|
||
}
|
||
expandedGroups.value = next
|
||
}
|
||
|
||
function scheduleLoadBackendGroupedRows () {
|
||
if (backendGroupedSupportedTab(activeTab.value)) backendGroupedLoading.value = true
|
||
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
|
||
backendGroupedTimer = window.setTimeout(() => {
|
||
void loadBackendGroupedRows()
|
||
}, 260)
|
||
}
|
||
|
||
function switchPerformanceTab (tabName) {
|
||
if (!tabName || tabName === activeTab.value || loading.value) return
|
||
backendGroupedLoading.value = true
|
||
activeTab.value = tabName
|
||
}
|
||
|
||
async function loadBackendGroupedRows (options = {}) {
|
||
const tabKey = activeTab.value
|
||
if (!backendGroupedSupportedTab(tabKey)) return
|
||
if (tabKey === 'product_detail' && !ensureDetailMainGroupSelection()) {
|
||
backendGroupedRows.value = {
|
||
...backendGroupedRows.value,
|
||
product_detail: []
|
||
}
|
||
backendGroupedLoading.value = false
|
||
return
|
||
}
|
||
pruneActiveExpandedGroups()
|
||
backendGroupedLoading.value = true
|
||
const requestKey = `${tabKey}|${activeExpandedGroupKeys().join('~')}|${selectedDetailMainGroup.value || ''}`
|
||
const params = {
|
||
mode: tabKey,
|
||
groupLevels: activeGroupLevels.value.map(level => level.key),
|
||
expandedKeys: activeExpandedGroupKeys(),
|
||
limit: tabKey === 'products' || tabKey === 'product_detail' || tabKey === 'idle' ? productKpiFetchLimit : 50000
|
||
}
|
||
if (tabKey === 'product_detail') {
|
||
params.urunAnaGrubu = selectedDetailMainGroup.value
|
||
}
|
||
try {
|
||
const startedAt = performance.now()
|
||
const rows = await performanceStore.fetchGroupedRows(params, { force: options.force === true })
|
||
const currentKey = `${activeTab.value}|${activeExpandedGroupKeys().join('~')}|${selectedDetailMainGroup.value || ''}`
|
||
if (requestKey !== currentKey) return
|
||
const normalizedRows = Array.isArray(rows) ? rows.map(row => normalizeGroupedDisplayRow(tabKey, row)) : []
|
||
backendGroupedRows.value = {
|
||
...backendGroupedRows.value,
|
||
[tabKey]: normalizedRows
|
||
}
|
||
console.info('[ProductPerformance][ui] grouped applied', {
|
||
tab: tabKey,
|
||
rows: normalizedRows.length,
|
||
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2))
|
||
})
|
||
} catch (err) {
|
||
console.warn('product performance grouped rows failed', err)
|
||
} finally {
|
||
backendGroupedLoading.value = false
|
||
}
|
||
}
|
||
|
||
function normalizeGroupedDisplayRow (tabKey, row) {
|
||
const nextRow = { ...row }
|
||
normalizeGroupedProductHierarchyValues(nextRow)
|
||
if (tabKey === 'order_product_customers' || tabKey === 'order_market_details') {
|
||
return withOrderPeriodScores(nextRow)
|
||
}
|
||
const next = withPeriodScores(nextRow)
|
||
if (tabKey !== 'products' && tabKey !== 'product_detail' && tabKey !== 'idle') {
|
||
next.customer_score_90d = Number(next.customer_score_90d || customerSalesPeriodScore(periodMetricSource(next, '90d')))
|
||
next.customer_score_180d = Number(next.customer_score_180d || customerSalesPeriodScore(periodMetricSource(next, '180d')))
|
||
next.customer_score_365d = Number(next.customer_score_365d || customerSalesPeriodScore(periodMetricSource(next, '365d')))
|
||
next.customer_score_total = Number(next.customer_score_total || customerSalesPeriodScore(periodMetricSource(next, 'total')))
|
||
}
|
||
return next
|
||
}
|
||
|
||
function normalizeGroupedProductHierarchyValues (row) {
|
||
row.urun_ilk_grubu = cleanProductFirstGroupValue(row.urun_ilk_grubu)
|
||
if (row.__group && row.group_field === 'urun_ilk_grubu') {
|
||
const cleanValue = cleanProductFirstGroupValue(row.group_value || row.label || row.urun_ilk_grubu)
|
||
row.group_value = cleanValue
|
||
row.label = cleanValue
|
||
row.urun_ilk_grubu = cleanValue
|
||
}
|
||
return row
|
||
}
|
||
|
||
function responseRowCount (data) {
|
||
if (Array.isArray(data)) return data.length
|
||
if (Array.isArray(data?.rows)) return data.rows.length
|
||
if (data && typeof data === 'object') return Object.keys(data).length
|
||
return 0
|
||
}
|
||
|
||
async function timedProductPerformanceGet (label, url, config = {}) {
|
||
const startedAt = performance.now()
|
||
console.info('[ProductPerformance][api] start', {
|
||
label,
|
||
url,
|
||
params: config?.params || {}
|
||
})
|
||
try {
|
||
const resp = await api.get(url, config)
|
||
console.info('[ProductPerformance][api] done', {
|
||
label,
|
||
rows: responseRowCount(resp?.data),
|
||
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2))
|
||
})
|
||
return resp
|
||
} catch (err) {
|
||
console.warn('[ProductPerformance][api] failed', {
|
||
label,
|
||
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2)),
|
||
message: err?.response?.data || err?.message || err
|
||
})
|
||
throw err
|
||
}
|
||
}
|
||
|
||
async function reload () {
|
||
loading.value = true
|
||
try {
|
||
generalRowsLoaded.value = false
|
||
generalRows.value = []
|
||
marketRowsLoaded.value = false
|
||
countryRowsLoaded.value = false
|
||
customerRowsLoaded.value = false
|
||
const [summaryResp, listResp] = await Promise.all([
|
||
timedProductPerformanceGet('summary', '/pricing/product-performance/summary', { timeout: 60000 }),
|
||
timedProductPerformanceGet('product-kpi', '/pricing/product-performance', {
|
||
params: {
|
||
page: 1,
|
||
limit: productKpiFetchLimit
|
||
},
|
||
timeout: 60000
|
||
})
|
||
])
|
||
summary.value = summaryResp?.data || {}
|
||
rows.value = (Array.isArray(listResp?.data?.rows) ? listResp.data.rows : []).map(normalizeRow)
|
||
await ensureActiveTabData(false)
|
||
await loadBackendGroupedRows({ force: true })
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün performans verisi alınamadı' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadGeneralRows (showLoading = true) {
|
||
if (generalRowsLoaded.value) return
|
||
if (showLoading) loading.value = true
|
||
try {
|
||
const resp = await timedProductPerformanceGet('general', '/pricing/product-performance/general', { params: { limit: reportFetchLimit }, timeout: 90000 })
|
||
const normalizedGeneralRows = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeGeneralRow)
|
||
generalRows.value = normalizedGeneralRows
|
||
generalRowsLoaded.value = true
|
||
|
||
const generalByVariant = new Map(normalizedGeneralRows.map(row => [performanceVariantKey(row), generalMetricFields(row)]))
|
||
rows.value = rows.value.map(row => {
|
||
const generalMetrics = generalByVariant.get(performanceVariantKey(row)) || {}
|
||
return Object.keys(generalMetrics).length ? normalizeRow({ ...row, ...generalMetrics }) : row
|
||
})
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Genel performans verisi alınamadı' })
|
||
} finally {
|
||
if (showLoading) loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadOrderProductCustomers (showLoading = true) {
|
||
if (showLoading) loading.value = true
|
||
try {
|
||
const resp = await timedProductPerformanceGet('orders:product-customers', '/pricing/product-performance/orders/product-customers', { params: { limit: reportFetchLimit }, timeout: 90000 })
|
||
orderProductCustomerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderProductCustomerRow)
|
||
orderProductCustomersLoaded.value = true
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ürün müşteri sipariş analizi alınamadı' })
|
||
} finally {
|
||
if (showLoading) loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadOrderMarketDetails (showLoading = true) {
|
||
if (showLoading) loading.value = true
|
||
try {
|
||
const resp = await timedProductPerformanceGet('orders:market-details', '/pricing/product-performance/orders/market-details', { params: { limit: reportFetchLimit }, timeout: 90000 })
|
||
orderMarketDetailRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(normalizeOrderMarketDetailRow)
|
||
orderMarketDetailsLoaded.value = true
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Piyasa sipariş detay analizi alınamadı' })
|
||
} finally {
|
||
if (showLoading) loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadMarketRows (showLoading = true) {
|
||
if (marketRowsLoaded.value) return
|
||
if (showLoading) loading.value = true
|
||
try {
|
||
const resp = await timedProductPerformanceGet('markets', '/pricing/product-performance/markets', { params: { limit: reportFetchLimit }, timeout: 60000 })
|
||
marketRows.value = Array.isArray(resp?.data) ? resp.data : []
|
||
marketRowsLoaded.value = true
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Piyasa performans verisi alınamadı' })
|
||
} finally {
|
||
if (showLoading) loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadCountryRows (showLoading = true) {
|
||
if (countryRowsLoaded.value) return
|
||
if (showLoading) loading.value = true
|
||
try {
|
||
const resp = await timedProductPerformanceGet('countries', '/pricing/product-performance/countries', { params: { limit: reportFetchLimit }, timeout: 60000 })
|
||
countryRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(row => ({
|
||
...row,
|
||
country_key: `${row.country || '-'}|${row.customer_segment || '-'}|${row.market_key || '-'}`
|
||
}))
|
||
countryRowsLoaded.value = true
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Ülke performans verisi alınamadı' })
|
||
} finally {
|
||
if (showLoading) loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadCustomerRows (showLoading = true) {
|
||
if (customerRowsLoaded.value) return
|
||
if (showLoading) loading.value = true
|
||
try {
|
||
const resp = await timedProductPerformanceGet('customers', '/pricing/product-performance/customers', {
|
||
params: { limit: reportFetchLimit, breakdown: customerBreakdown.value },
|
||
timeout: 60000
|
||
})
|
||
customerRows.value = (Array.isArray(resp?.data) ? resp.data : []).map(row => ({
|
||
...row,
|
||
customer_key: `${row.breakdown || '-'}|${row.market_key || '-'}|${row.country || '-'}|${row.customer_segment || '-'}|${row.customer_code || '-'}`
|
||
}))
|
||
customerRowsLoaded.value = true
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Müşteri performans verisi alınamadı' })
|
||
} finally {
|
||
if (showLoading) loading.value = false
|
||
}
|
||
}
|
||
|
||
async function ensureActiveTabData (showLoading = true) {
|
||
const tab = activeTab.value
|
||
if (tab === 'general') return loadGeneralRows(showLoading)
|
||
if (tab === 'markets') return loadMarketRows(showLoading)
|
||
if (tab === 'countries') return loadCountryRows(showLoading)
|
||
if (tab === 'customers') return loadCustomerRows(showLoading)
|
||
}
|
||
|
||
async function loadOrderAnalysis (showLoading = true) {
|
||
if (showLoading) loading.value = true
|
||
try {
|
||
const [productResp, marketResp, customerResp] = await Promise.all([
|
||
timedProductPerformanceGet('orders:products', '/pricing/product-performance/orders', { params: { limit: reportFetchLimit }, timeout: 90000 }),
|
||
timedProductPerformanceGet('orders:groups:market', '/pricing/product-performance/orders/groups', { params: { limit: reportFetchLimit, breakdown: 'market' }, timeout: 90000 }),
|
||
timedProductPerformanceGet('orders:groups:customer', '/pricing/product-performance/orders/groups', { params: { limit: reportFetchLimit, breakdown: 'customer' }, timeout: 90000 })
|
||
])
|
||
orderAnalysisRows.value = (Array.isArray(productResp?.data) ? productResp.data : []).map(normalizeOrderAnalysisRow)
|
||
orderMarketRows.value = (Array.isArray(marketResp?.data) ? marketResp.data : []).map(normalizeOrderGroupRow)
|
||
orderCustomerRows.value = (Array.isArray(customerResp?.data) ? customerResp.data : []).map(normalizeOrderGroupRow)
|
||
orderAnalysisLoaded.value = true
|
||
} catch (err) {
|
||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Sipariş analiz verisi alınamadı' })
|
||
} finally {
|
||
if (showLoading) loading.value = false
|
||
}
|
||
}
|
||
|
||
function bucketLabel (value) {
|
||
return bucketOptions.find(x => x.value === value)?.label || value || '-'
|
||
}
|
||
|
||
function bucketColor (value) {
|
||
switch (value) {
|
||
case 'YILDIZ_URUN': return 'green-7'
|
||
case 'STOKSUZ_TALEP': return 'blue-7'
|
||
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'
|
||
}
|
||
}
|
||
|
||
watch(activeTab, () => {
|
||
void ensureActiveTabData(false)
|
||
scheduleLoadBackendGroupedRows()
|
||
})
|
||
|
||
watch(detailMainGroupOptions, () => {
|
||
ensureDetailMainGroupSelection()
|
||
}, { immediate: true })
|
||
|
||
watch(selectedDetailMainGroup, () => {
|
||
if (activeTab.value === 'product_detail') {
|
||
expandedGroups.value = Object.fromEntries(
|
||
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith('tab:product_detail|'))
|
||
)
|
||
backendGroupedRows.value = {
|
||
...backendGroupedRows.value,
|
||
product_detail: []
|
||
}
|
||
scheduleLoadBackendGroupedRows()
|
||
}
|
||
})
|
||
|
||
watch(productTableRows, tableRows => {
|
||
if (!isProductKpiTab.value) return
|
||
const sourceRows = activeTab.value === 'product_detail' ? displayDetailProductTableRows.value : tableRows
|
||
const imageRows = sourceRows
|
||
.filter(row => !row.__group || groupShowsImage(row))
|
||
.slice(0, 240)
|
||
void primeProductImages(imageRows)
|
||
}, { immediate: true })
|
||
|
||
onMounted(reload)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.product-performance-page {
|
||
position: relative;
|
||
background: #f6f7f9;
|
||
}
|
||
|
||
.page-busy-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 9000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
background: rgba(246, 247, 249, 0.82);
|
||
backdrop-filter: blur(2px);
|
||
pointer-events: all;
|
||
}
|
||
|
||
.page-busy-label {
|
||
color: #1f2937;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.performance-top-bar {
|
||
min-height: 34px;
|
||
}
|
||
|
||
.performance-page-title {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
line-height: 1.12;
|
||
}
|
||
|
||
.performance-page-subtitle {
|
||
font-size: 10px;
|
||
line-height: 1.15;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.performance-top-actions {
|
||
min-height: 30px;
|
||
}
|
||
|
||
.performance-top-actions :deep(.q-btn) {
|
||
min-height: 26px;
|
||
padding: 2px 8px;
|
||
font-size: 11px;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.performance-top-actions :deep(.q-btn .q-icon) {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.period-selector {
|
||
min-height: 26px;
|
||
border: 1px solid #d5dce5;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.period-selector :deep(.q-option-group) {
|
||
gap: 4px;
|
||
}
|
||
|
||
.period-selector :deep(.q-checkbox) {
|
||
min-height: 22px;
|
||
}
|
||
|
||
.period-selector :deep(.q-checkbox__inner) {
|
||
font-size: 26px;
|
||
width: 26px;
|
||
height: 26px;
|
||
}
|
||
|
||
.period-selector :deep(.q-checkbox__label) {
|
||
font-size: 11px;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.performance-tab-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
|
||
gap: 4px;
|
||
}
|
||
|
||
.performance-tab-button {
|
||
min-height: 24px;
|
||
padding: 3px 6px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
border: 1px solid #d5dce5;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
color: #4b5563;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
line-height: 1.15;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.performance-tab-button span {
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.performance-tab-button.active {
|
||
border-color: var(--q-primary);
|
||
color: var(--q-primary);
|
||
background: #eef6ff;
|
||
}
|
||
|
||
.performance-tab-button:disabled {
|
||
cursor: wait;
|
||
opacity: 0.62;
|
||
}
|
||
|
||
.main-group-bar {
|
||
display: flex;
|
||
gap: 6px;
|
||
overflow-x: auto;
|
||
padding: 6px;
|
||
border: 1px solid #d7dde5;
|
||
border-radius: 6px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.main-group-button {
|
||
flex: 0 0 auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-height: 30px;
|
||
padding: 4px 10px;
|
||
border: 1px solid #c9d2dd;
|
||
border-radius: 6px;
|
||
background: #f7f9fb;
|
||
color: #26384c;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.main-group-button small {
|
||
color: #64748b;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.main-group-button.active {
|
||
border-color: #1d4ed8;
|
||
background: #dbeafe;
|
||
color: #173a75;
|
||
}
|
||
|
||
.main-group-button:disabled {
|
||
cursor: wait;
|
||
opacity: 0.62;
|
||
}
|
||
|
||
.metric-card {
|
||
min-height: 38px;
|
||
}
|
||
|
||
.metric-card :deep(.q-card__section) {
|
||
padding: 5px 7px;
|
||
}
|
||
|
||
.metric-card :deep(.text-caption) {
|
||
font-size: 10px;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
line-height: 1.08;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.detail-level-menu {
|
||
width: 230px;
|
||
padding: 7px;
|
||
}
|
||
|
||
.performance-table {
|
||
max-height: calc(100vh - 160px);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.performance-table :deep(.q-table__middle) {
|
||
max-height: calc(100vh - 220px);
|
||
overflow: auto;
|
||
}
|
||
|
||
.performance-table :deep(.q-table__bottom) {
|
||
display: none;
|
||
}
|
||
|
||
.performance-table :deep(.q-table) {
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
}
|
||
|
||
.performance-table :deep(.q-table th),
|
||
.performance-table :deep(.q-table td) {
|
||
border-right: 1px solid #d5dce5;
|
||
border-bottom: 1px solid #d5dce5;
|
||
vertical-align: middle;
|
||
white-space: normal;
|
||
padding: 4px 5px;
|
||
}
|
||
|
||
.performance-table :deep(.q-table th) {
|
||
height: 42px;
|
||
min-height: 42px;
|
||
padding: 3px 4px;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr),
|
||
.performance-table :deep(.q-table tbody td) {
|
||
height: auto;
|
||
min-height: 38px;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody td) {
|
||
max-width: 320px;
|
||
padding-top: 4px;
|
||
padding-bottom: 4px;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.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.text-right),
|
||
.performance-table :deep(.q-table td.text-right) {
|
||
min-width: 98px;
|
||
width: 98px;
|
||
max-width: none;
|
||
white-space: nowrap;
|
||
overflow-wrap: normal;
|
||
word-break: normal;
|
||
}
|
||
|
||
.performance-table :deep(.q-table td.text-right span:not(.q-badge):not(.q-icon)) {
|
||
display: inline-block;
|
||
-webkit-line-clamp: unset;
|
||
-webkit-box-orient: initial;
|
||
max-width: none;
|
||
white-space: nowrap;
|
||
overflow: visible;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.performance-table :deep(.q-table th.text-right .header-label) {
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.performance-table :deep(.q-table th:not(.text-right)),
|
||
.performance-table :deep(.q-table td:not(.text-right)) {
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody td .q-field),
|
||
.performance-table :deep(.q-table tbody td input),
|
||
.performance-table :deep(.q-table tbody td textarea) {
|
||
min-width: 72px;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody td .q-field__control),
|
||
.performance-table :deep(.q-table tbody td .q-field__native),
|
||
.performance-table :deep(.q-table tbody td .q-field__input) {
|
||
min-height: 24px;
|
||
font-size: 10px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.performance-table :deep(.q-table th.color-yaka-cell),
|
||
.performance-table :deep(.q-table td.color-yaka-cell) {
|
||
width: 170px;
|
||
min-width: 170px;
|
||
max-width: 190px;
|
||
font-size: 10.5px;
|
||
}
|
||
|
||
.performance-table :deep(.q-table th:first-child),
|
||
.performance-table :deep(.q-table td:first-child) {
|
||
border-left: 1px solid #d5dce5;
|
||
}
|
||
|
||
.performance-table :deep(.q-table thead),
|
||
.performance-table :deep(.q-table thead tr),
|
||
.performance-table :deep(.q-table th) {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
background: #f8fbff;
|
||
}
|
||
|
||
.performance-table :deep(th) {
|
||
border-top: 1px solid #d5dce5;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table) {
|
||
min-width: 3400px;
|
||
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: 30;
|
||
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: 420px;
|
||
min-width: 420px;
|
||
max-width: 420px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(2)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(2)) {
|
||
left: 420px;
|
||
width: 140px;
|
||
min-width: 140px;
|
||
max-width: 140px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(3)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(3)) {
|
||
left: 560px;
|
||
width: 110px;
|
||
min-width: 110px;
|
||
max-width: 110px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(4)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(4)) {
|
||
left: 670px;
|
||
width: 150px;
|
||
min-width: 150px;
|
||
max-width: 150px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(5)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(5)) {
|
||
left: 820px;
|
||
width: 170px;
|
||
min-width: 170px;
|
||
max-width: 170px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(6)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(6)) {
|
||
left: 990px;
|
||
width: 150px;
|
||
min-width: 150px;
|
||
max-width: 150px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(7)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(7)) {
|
||
left: 1140px;
|
||
width: 130px;
|
||
min-width: 130px;
|
||
max-width: 130px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(8)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(8)) {
|
||
left: 1270px;
|
||
width: 90px;
|
||
min-width: 90px;
|
||
max-width: 90px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(9)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(9)) {
|
||
left: 1360px;
|
||
width: 80px;
|
||
min-width: 80px;
|
||
max-width: 80px;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(10)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(10)) {
|
||
left: 1440px;
|
||
width: 130px;
|
||
min-width: 130px;
|
||
max-width: 130px;
|
||
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table) {
|
||
min-width: 2850px;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(n+8)) {
|
||
position: static;
|
||
left: auto;
|
||
z-index: auto;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(n+8)) {
|
||
left: auto;
|
||
z-index: 20;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(-n+7)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(-n+7)) {
|
||
position: sticky;
|
||
z-index: 2;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(-n+7)) {
|
||
z-index: 30;
|
||
background: #f8fbff;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table tbody tr:not(.group-row) td:nth-child(-n+7)) {
|
||
background: #fff;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(1)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(1)) {
|
||
left: 0;
|
||
width: 420px;
|
||
min-width: 420px;
|
||
max-width: 420px;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(2)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(2)) {
|
||
left: 420px;
|
||
width: 150px;
|
||
min-width: 150px;
|
||
max-width: 150px;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(3)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(3)) {
|
||
left: 570px;
|
||
width: 150px;
|
||
min-width: 150px;
|
||
max-width: 150px;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(4)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(4)) {
|
||
left: 720px;
|
||
width: 130px;
|
||
min-width: 130px;
|
||
max-width: 130px;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(5)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(5)) {
|
||
left: 850px;
|
||
width: 170px;
|
||
min-width: 170px;
|
||
max-width: 170px;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(6)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(6)) {
|
||
left: 1020px;
|
||
width: 170px;
|
||
min-width: 170px;
|
||
max-width: 190px;
|
||
}
|
||
|
||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(7)),
|
||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(7)) {
|
||
left: 1190px;
|
||
width: 120px;
|
||
min-width: 120px;
|
||
max-width: 120px;
|
||
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table) {
|
||
min-width: 1700px;
|
||
table-layout: fixed;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th),
|
||
.sticky-dim-table :deep(.q-table td) {
|
||
min-width: 118px;
|
||
width: 118px;
|
||
max-width: 180px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th.text-right),
|
||
.sticky-dim-table :deep(.q-table td.text-right),
|
||
.product-breakdown-table :deep(.q-table th.text-right),
|
||
.product-breakdown-table :deep(.q-table td.text-right) {
|
||
min-width: 124px;
|
||
width: 124px;
|
||
max-width: none;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table td.text-right) {
|
||
font-size: 10.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.product-breakdown-table :deep(.q-table th:nth-child(n+11):not(.text-right)),
|
||
.product-breakdown-table :deep(.q-table td:nth-child(n+11):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(n+4):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(n+4):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(n+6):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(n+6):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(n+7):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-6 :deep(.q-table td:nth-child(n+7):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(n+8):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-7 :deep(.q-table td:nth-child(n+8):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-8 :deep(.q-table th:nth-child(n+9):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-8 :deep(.q-table td:nth-child(n+9):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-9 :deep(.q-table th:nth-child(n+10):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-9 :deep(.q-table td:nth-child(n+10):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(n+11):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-10 :deep(.q-table td:nth-child(n+11):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-11 :deep(.q-table th:nth-child(n+12):not(.text-right)),
|
||
.sticky-dim-table.sticky-dim-11 :deep(.q-table td:nth-child(n+12):not(.text-right)) {
|
||
min-width: 140px;
|
||
width: 140px;
|
||
max-width: 210px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(1)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(1)) {
|
||
left: 0;
|
||
position: sticky;
|
||
z-index: 2;
|
||
background: #fff;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(2)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(2)) {
|
||
left: 118px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(3)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(3)) {
|
||
left: 236px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(4)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(4)) {
|
||
left: 354px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(5)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(5)) {
|
||
left: 472px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(6)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(6)) {
|
||
left: 590px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(7)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(7)) {
|
||
left: 708px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(8)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(8)) {
|
||
left: 826px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(9)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(9)) {
|
||
left: 944px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(10)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(10)) {
|
||
left: 1062px;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table th:nth-child(11)),
|
||
.sticky-dim-table :deep(.q-table td:nth-child(11)) {
|
||
left: 1180px;
|
||
}
|
||
|
||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
|
||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(-n+3)),
|
||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
|
||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(-n+5)),
|
||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
|
||
.sticky-dim-table.sticky-dim-6 :deep(.q-table td:nth-child(-n+6)),
|
||
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(-n+7)),
|
||
.sticky-dim-table.sticky-dim-7 :deep(.q-table td:nth-child(-n+7)),
|
||
.sticky-dim-table.sticky-dim-8 :deep(.q-table th:nth-child(-n+8)),
|
||
.sticky-dim-table.sticky-dim-8 :deep(.q-table td:nth-child(-n+8)),
|
||
.sticky-dim-table.sticky-dim-9 :deep(.q-table th:nth-child(-n+9)),
|
||
.sticky-dim-table.sticky-dim-9 :deep(.q-table td:nth-child(-n+9)),
|
||
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(-n+10)),
|
||
.sticky-dim-table.sticky-dim-10 :deep(.q-table td:nth-child(-n+10)),
|
||
.sticky-dim-table.sticky-dim-11 :deep(.q-table th:nth-child(-n+11)),
|
||
.sticky-dim-table.sticky-dim-11 :deep(.q-table td:nth-child(-n+11)) {
|
||
position: sticky;
|
||
z-index: 2;
|
||
}
|
||
|
||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(-n+3)),
|
||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(-n+5)),
|
||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(-n+6)),
|
||
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(-n+7)),
|
||
.sticky-dim-table.sticky-dim-8 :deep(.q-table th:nth-child(-n+8)),
|
||
.sticky-dim-table.sticky-dim-9 :deep(.q-table th:nth-child(-n+9)),
|
||
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(-n+10)),
|
||
.sticky-dim-table.sticky-dim-11 :deep(.q-table th:nth-child(-n+11)) {
|
||
z-index: 30;
|
||
background: #f8fbff;
|
||
}
|
||
|
||
.sticky-dim-table :deep(.q-table tbody td:nth-child(-n+11)) {
|
||
background: #fff;
|
||
}
|
||
|
||
.sticky-dim-table.sticky-dim-3 :deep(.q-table th:nth-child(3)),
|
||
.sticky-dim-table.sticky-dim-3 :deep(.q-table td:nth-child(3)),
|
||
.sticky-dim-table.sticky-dim-5 :deep(.q-table th:nth-child(5)),
|
||
.sticky-dim-table.sticky-dim-5 :deep(.q-table td:nth-child(5)),
|
||
.sticky-dim-table.sticky-dim-6 :deep(.q-table th:nth-child(6)),
|
||
.sticky-dim-table.sticky-dim-6 :deep(.q-table td:nth-child(6)),
|
||
.sticky-dim-table.sticky-dim-7 :deep(.q-table th:nth-child(7)),
|
||
.sticky-dim-table.sticky-dim-7 :deep(.q-table td:nth-child(7)),
|
||
.sticky-dim-table.sticky-dim-8 :deep(.q-table th:nth-child(8)),
|
||
.sticky-dim-table.sticky-dim-8 :deep(.q-table td:nth-child(8)),
|
||
.sticky-dim-table.sticky-dim-9 :deep(.q-table th:nth-child(9)),
|
||
.sticky-dim-table.sticky-dim-9 :deep(.q-table td:nth-child(9)),
|
||
.sticky-dim-table.sticky-dim-10 :deep(.q-table th:nth-child(10)),
|
||
.sticky-dim-table.sticky-dim-10 :deep(.q-table td:nth-child(10)),
|
||
.sticky-dim-table.sticky-dim-11 :deep(.q-table th:nth-child(11)),
|
||
.sticky-dim-table.sticky-dim-11 :deep(.q-table td:nth-child(11)) {
|
||
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(1)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(1)) {
|
||
left: 0;
|
||
width: 170px;
|
||
min-width: 170px;
|
||
max-width: 190px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(2)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(2)) {
|
||
left: 170px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(3)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(3)) {
|
||
left: 288px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(4)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(4)) {
|
||
left: 406px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(5)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(5)) {
|
||
left: 524px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(6)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(6)) {
|
||
left: 642px;
|
||
width: 130px;
|
||
min-width: 130px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(7)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(7)) {
|
||
left: 772px;
|
||
width: 170px;
|
||
min-width: 170px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(8)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(8)) {
|
||
left: 942px;
|
||
width: 116px;
|
||
min-width: 116px;
|
||
}
|
||
|
||
.sales-color-yaka-table :deep(.q-table th:nth-child(9)),
|
||
.sales-color-yaka-table :deep(.q-table td:nth-child(9)) {
|
||
left: 1058px;
|
||
width: 190px;
|
||
min-width: 190px;
|
||
box-shadow: 8px 0 10px -10px rgba(17, 24, 39, 0.45);
|
||
}
|
||
|
||
.filterable-header-cell {
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.header-with-filter {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 18px;
|
||
align-items: center;
|
||
gap: 2px;
|
||
min-width: 82px;
|
||
width: 100%;
|
||
}
|
||
|
||
.header-with-filter > span {
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 3;
|
||
-webkit-box-orient: vertical;
|
||
line-height: 1.12;
|
||
white-space: normal;
|
||
overflow: hidden;
|
||
overflow-wrap: anywhere;
|
||
text-overflow: clip;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.header-filter-btn {
|
||
width: 18px;
|
||
height: 18px;
|
||
min-height: 18px;
|
||
padding: 0;
|
||
}
|
||
|
||
.header-filter-ghost {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.excel-filter-menu {
|
||
width: 260px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.excel-filter-select :deep(.q-field__control) {
|
||
min-height: 32px;
|
||
}
|
||
|
||
.excel-filter-actions {
|
||
gap: 8px;
|
||
}
|
||
|
||
.excel-filter-options {
|
||
max-height: 260px;
|
||
margin-top: 4px;
|
||
border: 1px solid #e0e3e7;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.excel-filter-option {
|
||
min-height: 32px;
|
||
}
|
||
|
||
.excel-filter-empty {
|
||
padding: 12px;
|
||
color: #7a8390;
|
||
text-align: center;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row td) {
|
||
background: #edf3fb;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
border-color: #c4d2e2;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-0 td) {
|
||
background: #123b63;
|
||
color: #fff;
|
||
border-color: #0b2845;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-1 td) {
|
||
background: #1f6f54;
|
||
color: #fff;
|
||
border-color: #18563f;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-2 td) {
|
||
background: #ffe2a8;
|
||
color: #1f2937;
|
||
border-color: #d7a84d;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-3 td) {
|
||
background: #d8f0dc;
|
||
color: #1f2937;
|
||
border-color: #9bc7a3;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-4 td) {
|
||
background: #e7ecff;
|
||
color: #1f2937;
|
||
border-color: #b8c2ef;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-level-5 td) {
|
||
background: #f2f3f5;
|
||
color: #1f2937;
|
||
border-color: #c4c9d1;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-field-color_yaka td) {
|
||
background: #f7e3bf;
|
||
color: #1f2937;
|
||
border-top: 2px solid #c99032;
|
||
border-bottom: 2px solid #c99032;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-field-color_yaka td:first-child) {
|
||
border-left: 6px solid #b76a00;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-field-market_key td) {
|
||
background: #d9e8ff;
|
||
color: #132238;
|
||
border-top: 2px solid #7da9e6;
|
||
border-bottom: 2px solid #7da9e6;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row.group-row-field-market_key td:first-child) {
|
||
border-left: 6px solid #2563b8;
|
||
}
|
||
|
||
.performance-table :deep(.q-table tbody tr.group-row td span),
|
||
.performance-table :deep(.q-table tbody tr.group-row .group-title),
|
||
.performance-table :deep(.q-table tbody tr.group-row .q-btn),
|
||
.performance-table :deep(.q-table tbody tr.group-row .q-icon) {
|
||
color: inherit;
|
||
opacity: 1;
|
||
}
|
||
|
||
.group-cell-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.group-cell-label :deep(.q-btn) {
|
||
flex: 0 0 20px;
|
||
width: 20px;
|
||
height: 20px;
|
||
min-height: 20px;
|
||
}
|
||
|
||
.group-title {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
margin-left: 0;
|
||
white-space: normal;
|
||
overflow: visible;
|
||
overflow-wrap: anywhere;
|
||
line-height: 1.18;
|
||
}
|
||
|
||
.product-image-cell {
|
||
width: 420px;
|
||
min-width: 420px;
|
||
max-width: 420px;
|
||
padding: 10px !important;
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.product-thumb-button {
|
||
width: 400px;
|
||
height: 400px;
|
||
border: 1px solid #d9dde3;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.product-thumb,
|
||
.product-thumb-placeholder {
|
||
width: 384px;
|
||
height: 384px;
|
||
}
|
||
|
||
.product-thumb {
|
||
display: block;
|
||
object-fit: contain;
|
||
object-position: center;
|
||
}
|
||
|
||
.product-thumb-placeholder {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #9aa3af;
|
||
background: #f3f5f7;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.product-image-dialog {
|
||
background: #f6f7f9;
|
||
}
|
||
|
||
.product-image-dialog-body {
|
||
height: calc(100vh - 74px);
|
||
padding: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.performance-image-layout {
|
||
display: grid;
|
||
grid-template-columns: minmax(280px, 330px) minmax(360px, 1fr) minmax(440px, 1.25fr);
|
||
gap: 12px;
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.image-product-fields,
|
||
.image-sales-panel,
|
||
.image-media-panel {
|
||
min-height: 0;
|
||
}
|
||
|
||
.image-product-fields,
|
||
.image-sales-panel {
|
||
overflow: auto;
|
||
background: #fff;
|
||
border: 1px solid #d9dde3;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.image-section-title {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: var(--q-primary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.image-field-row {
|
||
display: grid;
|
||
grid-template-columns: 92px minmax(0, 1fr);
|
||
gap: 8px;
|
||
align-items: start;
|
||
min-height: 30px;
|
||
padding: 6px 0;
|
||
border-bottom: 1px solid #edf0f3;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.image-field-row .k {
|
||
color: #6b7280;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.image-field-row .v {
|
||
color: #1f2937;
|
||
font-weight: 700;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.image-product-section {
|
||
position: relative;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.image-size-stock-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(58px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.image-size-stock-cell {
|
||
min-height: 54px;
|
||
padding: 8px 6px;
|
||
border: 1px solid #dfe4ea;
|
||
border-radius: 6px;
|
||
background: #f8fafc;
|
||
text-align: center;
|
||
}
|
||
|
||
.image-size-label {
|
||
display: block;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.image-size-qty {
|
||
display: block;
|
||
margin-top: 3px;
|
||
font-size: 16px;
|
||
font-weight: 800;
|
||
color: #111827;
|
||
}
|
||
|
||
.image-empty-text {
|
||
color: #7a8390;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.product-image-carousel {
|
||
height: 100%;
|
||
background: #fff;
|
||
border: 1px solid #d9dde3;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.product-image-large {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.product-image-empty {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #7a8390;
|
||
background: #fff;
|
||
border: 1px solid #d9dde3;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.image-metric-card .q-card__section {
|
||
padding: 8px;
|
||
}
|
||
|
||
.image-metric-card .metric-value {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.product-performance-dialog {
|
||
background: #f6f7f9;
|
||
}
|
||
|
||
.product-performance-dialog :deep(.q-table__container),
|
||
.product-image-dialog :deep(.q-table__container) {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.product-performance-dialog :deep(.q-table__middle),
|
||
.product-image-dialog :deep(.q-table__middle) {
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.dialog-detail-table,
|
||
.dialog-stock-table {
|
||
width: 100%;
|
||
min-width: 0;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table),
|
||
.dialog-stock-table :deep(.q-table) {
|
||
width: 100%;
|
||
table-layout: fixed;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th),
|
||
.dialog-detail-table :deep(.q-table td),
|
||
.dialog-stock-table :deep(.q-table th),
|
||
.dialog-stock-table :deep(.q-table td) {
|
||
padding: 4px 5px;
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
line-height: 1.18;
|
||
vertical-align: top;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th) {
|
||
font-size: 10px;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th:nth-child(1)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(1)) {
|
||
width: 9%;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th:nth-child(2)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(2)) {
|
||
width: 12%;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th:nth-child(3)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(3)) {
|
||
width: 11%;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th:nth-child(4)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(4)) {
|
||
width: 9%;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th:nth-child(5)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(5)) {
|
||
width: 12%;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th:nth-child(6)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(6)) {
|
||
width: 23%;
|
||
}
|
||
|
||
.dialog-detail-table :deep(.q-table th:nth-child(7)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(7)),
|
||
.dialog-detail-table :deep(.q-table th:nth-child(8)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(8)),
|
||
.dialog-detail-table :deep(.q-table th:nth-child(9)),
|
||
.dialog-detail-table :deep(.q-table td:nth-child(9)) {
|
||
width: 8%;
|
||
white-space: nowrap;
|
||
overflow-wrap: normal;
|
||
word-break: normal;
|
||
}
|
||
|
||
.dialog-stock-table :deep(.q-table th:nth-child(1)),
|
||
.dialog-stock-table :deep(.q-table td:nth-child(1)) {
|
||
width: 58%;
|
||
}
|
||
|
||
.dialog-stock-table :deep(.q-table th:nth-child(2)),
|
||
.dialog-stock-table :deep(.q-table td:nth-child(2)) {
|
||
width: 42%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@media (max-width: 1260px) {
|
||
.product-image-dialog-body {
|
||
overflow: auto;
|
||
}
|
||
|
||
.performance-image-layout {
|
||
grid-template-columns: 1fr;
|
||
height: auto;
|
||
}
|
||
|
||
.image-media-panel {
|
||
min-height: 520px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
|