@@ -76,7 +76,7 @@
flat
bordered
row-key="row_key"
class="performance-table bg-white"
class="performance-table sticky-dim-table sticky-dim-7 bg-white"
:rows="filteredGeneralRows"
:columns="generalColumns"
:loading="loading"
@@ -155,12 +155,13 @@
<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)">
<q- img
<img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
fit="contain"
class="product-thumb"
no-spinner
loading="lazy"
decoding="async"
alt=""
/>
<div v-else class="product-thumb-placeholder">
<q-icon name="image" size="22px" />
@@ -183,10 +184,10 @@
flat
bordered
row-key="row_key"
class="performance-table bg-white"
:rows="filtered OrderProductCustomerRows"
class="performance-table sticky-dim-table sticky-dim-10 bg-white"
:rows="display OrderProductCustomerTable Rows"
:columns="orderProductCustomerColumns"
:loading="loading"
:loading="loading || backendGroupedLoading "
:pagination="{ rowsPerPage: 100, sortBy: ' order _usd ', descending: true }"
>
<template #header-cell="props">
@@ -262,12 +263,13 @@
<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)">
<q- img
<img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
fit="contain"
class="product-thumb"
no-spinner
loading="lazy"
decoding="async"
alt=""
/>
<div v-else class="product-thumb-placeholder">
<q-icon name="image" size="22px" />
@@ -293,6 +295,72 @@
{{ formatMoney(props.row.expected_profit_cost_usd, ' USD ') }}
</q-td>
</template>
<template #body="props">
<q-tr
v-if="props.row.__group"
:props="props"
:class="[' group - row ', `group-row-level-${Math.min(Number(props.row.level || 0), 5)}`]"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
<template v-if="col.name === ' image ' && groupShowsImage(props.row)">
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
<img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
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: `${props.row.level * 18}px` }"
>
<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">{{ props.row.label }}</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"
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
@@ -300,10 +368,10 @@
flat
bordered
row-key="row_key"
class="performance-table bg-white"
:rows="filtered OrderMarketDetailRows"
class="performance-table sticky-dim-table sticky-dim-10 bg-white"
:rows="display OrderMarketDetailTable Rows"
:columns="orderMarketDetailColumns"
:loading="loading"
:loading="loading || backendGroupedLoading "
:pagination="{ rowsPerPage: 100, sortBy: ' order _date ', descending: true }"
>
<template #header-cell="props">
@@ -393,6 +461,40 @@
{{ formatMoney(props.row.expected_profit_cost_usd, ' USD ') }}
</q-td>
</template>
<template #body="props">
<q-tr
v-if="props.row.__group"
:props="props"
:class="[' group - row ', `group-row-level-${Math.min(Number(props.row.level || 0), 5)}`]"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
<div
v-if="col.name === groupLabelColumnName(props.row)"
class="group-cell-label"
:style="{ paddingLeft: `${props.row.level * 18}px` }"
>
<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">{{ props.row.label }}</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
@@ -401,9 +503,9 @@
bordered
row-key="row_key"
class="performance-table product-breakdown-table bg-white"
:rows="p roductTableRows"
:rows="displayP roductTableRows"
:columns="productColumns"
:loading="loading"
:loading="loading || backendGroupedLoading "
:pagination="pagination"
@request="onRequest"
>
@@ -487,12 +589,13 @@
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
<template v-if="col.name === ' image ' && groupShowsImage(props.row)">
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
<q- img
<img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
fit="contain"
class="product-thumb"
no-spinner
loading="lazy"
decoding="async"
alt=""
/>
<div v-else class="product-thumb-placeholder">
<q-icon name="image" size="22px" />
@@ -522,12 +625,13 @@
<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)">
<q- img
<img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
fit="contain"
class="product-thumb"
no-spinner
loading="lazy"
decoding="async"
alt=""
/>
<div v-else class="product-thumb-placeholder">
<q-icon name="image" size="22px" />
@@ -549,10 +653,10 @@
flat
bordered
row-key="row_key"
class="performance-table bg-white"
:rows="filteredId leRows"
class="performance-table sticky-dim-table sticky-dim-6 bg-white"
:rows="displayIdleTab leRows"
:columns="idleColumns"
:loading="loading"
:loading="loading || backendGroupedLoading "
:pagination="{ rowsPerPage: 100 }"
>
<template #header-cell="props">
@@ -628,12 +732,13 @@
<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)">
<q- img
<img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
fit="contain"
class="product-thumb"
no-spinner
loading="lazy"
decoding="async"
alt=""
/>
<div v-else class="product-thumb-placeholder">
<q-icon name="image" size="22px" />
@@ -655,6 +760,72 @@
</q-badge>
</q-td>
</template>
<template #body="props">
<q-tr
v-if="props.row.__group"
:props="props"
:class="[' group - row ', `group-row-level-${Math.min(Number(props.row.level || 0), 5)}`]"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
<template v-if="col.name === ' image ' && groupShowsImage(props.row)">
<button type="button" class="product-thumb-button" @click.stop="openProductImageDialog(props.row)">
<img
v-if="getCachedProductImageUrl(props.row)"
:src="getCachedProductImageUrl(props.row)"
class="product-thumb"
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: `${props.row.level * 18}px` }"
>
<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">{{ props.row.label }}</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"
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
@@ -662,7 +833,7 @@
flat
bordered
row-key="market_key"
class="performance-table bg-white"
class="performance-table sticky-dim-table sticky-dim-9 bg-white"
:rows="filteredMarketRows"
:columns="marketColumns"
:loading="loading"
@@ -760,10 +931,10 @@
flat
bordered
row-key="row_key"
class="performance-table bg-white"
:rows="filtered ActiveSalesBreakdownRows"
class="performance-table sticky-dim-table sticky-dim-9 bg-white"
:rows="display ActiveSalesBreakdownTable Rows"
:columns="visibleSalesBreakdownColumns"
:loading="loading"
:loading="loading || backendGroupedLoading "
:pagination="{ rowsPerPage: 100, sortBy: ' performance _score ', descending: true }"
>
<template #header-cell="props">
@@ -858,6 +1029,40 @@
</q-badge>
</q-td>
</template>
<template #body="props">
<q-tr
v-if="props.row.__group"
:props="props"
:class="[' group - row ', `group-row-level-${Math.min(Number(props.row.level || 0), 5)}`]"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props" :class="groupCellClass(col.name)">
<div
v-if="col.name === groupLabelColumnName(props.row)"
class="group-cell-label"
:style="{ paddingLeft: `${props.row.level * 18}px` }"
>
<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">{{ props.row.label }}</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 '">
@@ -881,7 +1086,7 @@
flat
bordered
row-key="customer_key"
class="performance-table bg-white"
class="performance-table sticky-dim-table sticky-dim-5 bg-white"
:rows="filteredCustomerRows"
:columns="customerColumns"
:loading="loading"
@@ -980,7 +1185,7 @@
flat
bordered
row-key="country_key"
class="performance-table bg-white"
class="performance-table sticky-dim-table sticky-dim-3 bg-white"
:rows="filteredCountryRows"
:columns="countryColumns"
:loading="loading"
@@ -1080,28 +1285,78 @@
</q-card-section>
<q-separator />
<q-card-section class="product-image-dialog-body">
<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 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
title="Satış / Müşteri Özeti"
row-key="detail_key"
:rows="detailSalesRows"
:columns="detailSalesColumns"
:loading="detailLoading"
:pagination="{ rowsPerPage: 50 }"
/>
</section>
</div>
</q-card-section>
</q-card>
@@ -1201,6 +1456,7 @@ 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(' ')
@@ -1208,6 +1464,9 @@ const performanceDialogSubtitle = ref('')
const detailLoading = ref(false)
const detailSalesRows = ref([])
const detailStockSizes = ref([])
const backendGroupedRows = ref({})
const backendGroupedLoading = ref(false)
let backendGroupedTimer = null
const columnFilters = reactive({})
const columnFilterSearch = reactive({})
@@ -1305,6 +1564,10 @@ const columns = [
{ name: ' stock _days _90d ', label: ' Stok Gün ', field: row => formatNumber(row.stock_days_90d, 1), align: ' right ', sortable: true },
{ name: ' stock _days _180d ', label: ' 180 G Stok Gün ', field: row => formatNumber(row.stock_days_180d, 1), align: ' right ', sortable: true },
{ name: ' stock _days _total ', label: ' Genel Stok Gün ', field: row => formatNumber(row.stock_days_total, 1), align: ' right ', sortable: true },
{ name: ' stock _turnover _90d ', label: ' 90 G Stok Devir ', field: row => formatNumber(row.stock_turnover_90d, 2), align: ' right ', sortable: true },
{ name: ' stock _turnover _180d ', label: ' 180 G Stok Devir ', field: row => formatNumber(row.stock_turnover_180d, 2), align: ' right ', sortable: true },
{ name: ' stock _turnover _365d ', label: ' 360 G 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: ' 180 G Ort . USD ', field: row => formatMoney(row.avg_price_usd_180d, ' USD '), align: ' right ' },
{ name: ' avg _price _usd _365d ', label: ' 360 G Ort . USD ', field: row => formatMoney(row.avg_price_usd_365d, ' USD '), align: ' right ' },
@@ -1523,6 +1786,7 @@ const idleColumns = [
{ name: ' idle _cost _usd ', label: ' Stok Maliyeti USD ', field: ' idle _cost _usd ', align: ' right ', sortable: true },
{ name: ' sales _qty _90d ', label: ' 90 G Satış ', field: row => formatNumber(row.sales_qty_90d, 0), align: ' right ', sortable: true },
{ name: ' stock _days _90d ', label: ' Stok Gün ', field: row => formatNumber(row.stock_days_90d, 1), align: ' right ', sortable: true },
{ name: ' stock _turnover _90d ', label: ' 90 G 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 ' }
]
@@ -1707,23 +1971,48 @@ const visibleOrderAnalysisColumns = computed(() => {
return orderAnalysisColumns
})
const performanceCards = computed(() => {
const row = performanceDialogRow.value || {}
function performanceCardsForRow (row) {
const source = row || {}
return [
{ key: ' score ', label: ' Skor ', value: formatNumber(row .performance_score, 1) },
{ key: ' stock ', label: ' Stok ', value: formatNumber(row .stock_qty, 0) },
{ key: ' sales90 ', label: ' 90 G Satış ', value: formatNumber(row .sales_qty_90d, 0) },
{ key: ' sales180 ', label: ' 180 G Satış ', value: formatNumber(row .sales_qty_180d, 0) },
{ key: ' markets ', label: ' 90 G Piyasa ', value: formatNumber(row .market_count_90d, 0) },
{ key: ' customers ', label: ' 90 G Müşteri ', value: formatNumber(row .customer_count_90d, 0) },
{ key: ' avg ', label: ' Ort . USD Satış ', value: formatMoney(row .avg_price_usd_90d, ' USD ') },
{ key: ' score ', label: ' Skor ', value: formatNumber(source .performance_score, 1) },
{ key: ' stock ', label: ' Stok ', value: formatNumber(source .stock_qty, 0) },
{ key: ' sales90 ', label: ' 90 G Satış ', value: formatNumber(source .sales_qty_90d, 0) },
{ key: ' sales180 ', label: ' 180 G Satış ', value: formatNumber(source .sales_qty_180d, 0) },
{ key: ' markets ', label: ' 90 G Piyasa ', value: formatNumber(source .market_count_90d, 0) },
{ key: ' customers ', label: ' 90 G 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: ' 90 G Taban Marj ', value: formatPercent(source.gross_margin_base_90d) },
{ key: ' marginCost ', label: ' 90 G Çı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: ' desc ', label: ' Açıklama ', value: row.item_description || ' - ' },
{ key: ' category ', label: ' Kategori ', value: row.kategori || ' - ' },
{ key: ' hanger ', label: ' Askı lı / Yan ', value: row.askili_yan || ' - ' },
{ key: ' first ', label: ' İlk Grup ', value: row.urun_ilk_grubu || row.yas_grubu || ' - ' },
{ key: ' main ', label: ' Ana Grup ', value: row.urun_ana_grubu || row.seri || ' - ' },
{ 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 ') },
{ key: ' profitBase ', label: ' P . Başı Taban K / Z ', value: formatMoney(row.unit_profit_base_90d, ' USD ') },
{ key: ' profitCost ', label: ' P . Başı Çıplak K / Z ', value: formatMoney(row.unit_profit_cost_90d, ' USD ') },
{ key: ' marginBase ', label: ' 90 G Taban Marj ', value: formatPercent(row.gross_margin_base_90d) },
{ key: ' marginCost ', label: ' 90 G Çıplak Marj ', value: formatPercent(row.gross_margin_cost_90d) },
{ key: ' bucket ', label: ' Durum ', value: bucketLabel(row.performance_bucket) }
{ key: ' cost ', label: ' Çıplak Maliyet ', value: formatMoney(row.cost_price_usd, ' USD ') }
]
})
@@ -1841,6 +2130,14 @@ const filteredMarketRows = computed(() => filterRowsForTable('markets', marketRo
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 === ' 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 = {}
@@ -1868,19 +2165,29 @@ const columnFilterOptionMap = computed(() => {
const productTableRows = computed(() => {
const out = []
appendGroupRows(out, filteredProductRows.value, 0, [], groupLevels)
appendGroupRows(out, filteredProductRows.value, 0, [' tab : products ' ], groupLevels)
return out
})
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 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 productGroupKeys = computed(() => {
const keys = []
collectGroupKeys(keys, filteredProductRows.value, 0, [], groupLevels)
collectGroupKeys(keys, filteredProductRows.value, 0, [' tab : products ' ], groupLevels)
return keys
})
const productAutoExpandKeys = computed(() => {
const keys = []
collectGroupKeys(keys, filteredProductRows.value, 0, [ ], activeGroupLevels.value, autoExpandThroughLevel.value)
collectGroupKeys(keys, activeGroupSourceRows.value, 0, [`tab:${activeTab.value}` ], activeGroupLevels.value, autoExpandThroughLevel.value)
return keys
})
@@ -1914,6 +2221,18 @@ function appendGroupRows (out, sourceRows, level, parentKeys, levels = groupLeve
})
}
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]
return Array.isArray(rows) && rows.length ? rows : fallbackRows
}
function collectGroupKeys (out, sourceRows, level, parentKeys, levels = groupLevels, maxLevel = levels.length - 1) {
if (level >= levels.length) return
@@ -2013,10 +2332,81 @@ function makeGroupRow (key, level, groupDef, value, groupRows) {
sales_index_total: averageRows(groupRows, ' sales _index _total '),
performance_score: weightedAverageOrAverage(groupRows, ' performance _score ', ' sales _qty _90d '),
performance_bucket: dominantValue(groupRows, ' performance _bucket '),
...aggregateGroupFields(groupRows),
recommendation: `${formatNumber(groupRows.length, 0)} satı r`
}
}
function aggregateGroupFields (sourceRows) {
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 (shouldSumField(field)) {
out[field] = sumRows(sourceRows, field)
} else if (shouldAverageField(field)) {
out[field] = weightedAverageOrAverage(sourceRows, field, weightFieldForMetric(field))
}
}
applyDerivedGroupMetrics(out, sourceRows)
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) {
for (const suffix of [' 90 d ', ' 180 d ', ' 365 d ', ' 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 (sourceRows.some(row => row.performance_bucket)) {
out.performance_bucket = dominantValue(sourceRows, ' performance _bucket ')
}
}
function sumRows (sourceRows, field) {
return sourceRows.reduce((sum, row) => sum + Number(row[field] || 0), 0)
}
@@ -2120,12 +2510,14 @@ function toggleGroup (key) {
...expandedGroups.value,
[key]: !isGroupExpanded(key)
}
scheduleLoadBackendGroupedRows()
}
function toggleAllProductGroups () {
const keys = productAutoExpandKeys.value
if (allProductGroupsExpanded.value) {
expandedGroups.value = {}
scheduleLoadBackendGroupedRows()
return
}
expandSelectedProductGroups()
@@ -2136,6 +2528,7 @@ function expandSelectedProductGroups () {
const next = { ...expandedGroups.value }
for (const key of keys) next[key] = true
expandedGroups.value = next
scheduleLoadBackendGroupedRows()
}
function groupLabelColumnName (row) {
@@ -2146,23 +2539,29 @@ function groupShowsImage (row) {
return Number(row?.level ?? 0) < autoExpandThroughLevel.value
}
function groupCellClass (n ame) {
function groupCellClass (colOrN ame) {
const name = colOrName?.name || colOrName
if (name === ' image ') return ' product - image - cell '
return [' stock _qty ', ' sales _qty _90d ', ' sales _qty _180d ', ' sales _qty _365d ', ' sales _qty _total ', ' sales _usd _90d ', ' sales _usd _180d ', ' sales _usd _365d ', ' sales _usd _total ', ' stock _days _90d ', ' stock _days _180d ', ' stock _days _total ', ' avg _price _usd _90d ', ' avg _price _usd _180d ', ' avg _price _usd _365d ', ' avg _price _usd _total ', ' base _price _usd ', ' cost _price _usd ', ' unit _profit _base _90d ', ' unit _profit _cost _90d ', ' unit _profit _base _180d ', ' unit _profit _cost _180d ', ' unit _profit _base _total ', ' unit _profit _cost _total ', ' gross _profit _usd _90d ', ' gross _profit _usd _180d ', ' gross _profit _usd _total ', ' gross _margin _base _90d ', ' gross _margin _cost _90d ', ' gross _margin _base _180d ', ' gross _margin _cost _180d ', ' gross _margin _base _total ', ' gross _margin _cost _total ', ' gross _margin _90d ', ' gross _margin _180d ', ' market _count _90d ', ' customer _count _90d ', ' market _count _total ', ' customer _count _total ', ' sales _index _90d ', ' sales _index _total ', ' performance _score '].includes(name)
if (colOrName?.align === ' right ') return ' text - right '
return [' stock _qty ', ' sales _qty _90d ', ' sales _qty _180d ', ' sales _qty _365d ', ' sales _qty _total ', ' sales _usd _90d ', ' sales _usd _180d ', ' sales _usd _365d ', ' sales _usd _total ', ' stock _days _90d ', ' stock _days _180d ', ' stock _days _total ', ' stock _turnover _90d ', ' stock _turnover _180d ', ' stock _turnover _365d ', ' stock _turnover _total ', ' avg _price _usd _90d ', ' avg _price _usd _180d ', ' avg _price _usd _365d ', ' avg _price _usd _total ', ' base _price _usd ', ' cost _price _usd ', ' unit _profit _base _90d ', ' unit _profit _cost _90d ', ' unit _profit _base _180d ', ' unit _profit _cost _180d ', ' unit _profit _base _total ', ' unit _profit _cost _total ', ' gross _profit _usd _90d ', ' gross _profit _usd _180d ', ' gross _profit _usd _total ', ' gross _margin _base _90d ', ' gross _margin _cost _90d ', ' gross _margin _base _180d ', ' gross _margin _cost _180d ', ' gross _margin _base _total ', ' gross _margin _cost _total ', ' gross _margin _90d ', ' gross _margin _180d ', ' market _count _90d ', ' customer _count _90d ', ' market _count _total ', ' customer _count _total ', ' sales _index _90d ', ' sales _index _total ', ' performance _score '].includes(name)
? ' text - right '
: ' '
}
function formatGroupCell (row, n ame) {
function formatGroupCell (row, colOrN ame) {
const name = colOrName?.name || colOrName
if (name === ' image ') return ' '
if (name === ' recommendation ') return row.recommendation || ' '
return formatProduct Cell(row, n ame)
return formatTable Cell(row, colOrN ame)
}
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: stockTurnover(row?.sales_qty_90d, row?.stock_qty),
stock_turnover_180d: stockTurnover(row?.sales_qty_180d, row?.stock_qty),
stock_turnover_365d: stockTurnover(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),
@@ -2173,11 +2572,18 @@ function withProductMargins (row) {
function withGeneralMargins (row) {
return {
...row,
stock_turnover_total: stockTurnover(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 filterKey (tableKey, name) {
return `${tableKey}:${name}`
}
@@ -2276,6 +2682,38 @@ function formattedColumnCellValue (row, col) {
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 === ' 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], name === ' performance _score ' ? 1 : 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 ':
@@ -2291,6 +2729,10 @@ function formatProductCell (row, name) {
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 ':
@@ -2522,19 +2964,54 @@ function getCachedProductImageUrl (row) {
}
async function primeProductImages (sourceRows) {
const uniqueRow s = []
const item s = []
const seen = new Set()
for (const row of sourceRows || []) {
const key = productImageKey(row)
if (!key || seen.has(key)) continue
if (!key || seen.has(key) || Object.prototype.hasOwnProperty.call(imageListByKey.value, key) ) continue
seen.add(key)
uniqueRow s.push(row)
if (uniqueRows.length >= 80) break
item s.push({
key,
code: String(row?.image_product_code || row?.product_code || ' ').trim(),
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
try {
const resp = await api.post(' / product - images / batch ', { items }, { timeout: 60000 })
const nextLists = { ...imageListByKey.value }
const nextUrls = { ...imageUrlByKey.value }
const returned = new Set()
for (const item of Array.isArray(resp?.data) ? resp.data : []) {
const key = String(item?.key || ' ').trim()
if (!key) continue
returned.add(key)
const urls = (Array.isArray(item.images) ? item.images : []).map(resolveProductImageUrl).filter(Boolean)
nextLists[key] = urls
nextUrls[key] = urls[0] || ' '
}
for (const item of items) {
if (!returned.has(item.key)) {
nextLists[item.key] = []
nextUrls[item.key] = ' '
}
}
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
})))
}
await Promise.all(uniqueRows.map(row => fetchProductImagesForRow(row)))
}
async function openProductImageDialog (row) {
imageDialogRow.value = row
const urls = await fetchProductImagesForRow(row)
imageDialogUrls.value = urls
imageSlide.value = 0
@@ -2545,6 +3022,7 @@ async function openProductImageDialog (row) {
row?.image_yaka_kodu || row?.yaka_kodu
].filter(Boolean).join(' | ')
imageDialog.value = true
await loadProductDetailRows(row)
}
async function openPerformanceDialog (row) {
@@ -2557,15 +3035,20 @@ async function openPerformanceDialog (row) {
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?.product_code || ' ',
color_code: row?.color_code || ' ',
yaka_kodu: row?.yaka_kodu || ' '
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: 200 }, timeout: 60000 }),
api.get(' / pricing / product - performance / stock - sizes ', { params, timeout: 60000 })
@@ -2582,6 +3065,52 @@ async function openPerformanceDialog (row) {
}
}
function backendGroupedSupportedTab (tabKey = activeTab.value) {
return tabKey === ' products ' ||
tabKey === ' idle ' ||
tabKey === ' order _product _customers ' ||
tabKey === ' order _market _details ' ||
salesBreakdownTabKeys.includes(tabKey)
}
function activeExpandedGroupKeys () {
return Object.entries(expandedGroups.value)
.filter(([, value]) => value === true)
.map(([key]) => key)
}
function scheduleLoadBackendGroupedRows () {
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
backendGroupedTimer = window.setTimeout(() => {
void loadBackendGroupedRows()
}, 120)
}
async function loadBackendGroupedRows () {
const tabKey = activeTab.value
if (!backendGroupedSupportedTab(tabKey)) return
backendGroupedLoading.value = true
try {
const resp = await api.get(' / pricing / product - performance / grouped ', {
params: {
mode: tabKey,
group_levels: activeGroupLevels.value.map(level => level.key).join(' , '),
expanded_keys: activeExpandedGroupKeys().join(' , '),
limit: tabKey === ' products ' || tabKey === ' idle ' ? productKpiFetchLimit : 50000
},
timeout: 90000
})
backendGroupedRows.value = {
...backendGroupedRows.value,
[tabKey]: Array.isArray(resp?.data) ? resp.data : []
}
} catch (err) {
console.warn(' product performance grouped rows failed ', err)
} finally {
backendGroupedLoading.value = false
}
}
async function reload () {
loading.value = true
try {
@@ -2638,6 +3167,7 @@ async function reload () {
if (activeTab.value === ' order _market _details ') {
await loadOrderMarketDetails(false)
}
await loadBackendGroupedRows()
void primeProductImages(rows.value)
} catch (err) {
Notify.create({ type: ' negative ', message: err?.response?.data || err?.message || ' Ürün performans verisi alı namadı ' })
@@ -2721,6 +3251,11 @@ watch(activeTab, tab => {
if (tab === ' order _market _details ' && !orderMarketDetailsLoaded.value) {
void loadOrderMarketDetails(true)
}
scheduleLoadBackendGroupedRows()
})
watch(activeSelectedExpandLevelKeys, () => {
scheduleLoadBackendGroupedRows()
})
watch(productTableRows, tableRows => {
@@ -2777,7 +3312,7 @@ onMounted(reload)
}
. performance - table : deep ( . q - table tbody td ) {
max - width : 220 px ;
max - width : 320 px ;
}
. performance - table : deep ( . q - table tbody td span : not ( . q - badge ) : not ( . q - icon ) ) {
@@ -2802,110 +3337,222 @@ onMounted(reload)
}
. product - breakdown - table : deep ( . q - table ) {
min - width : 2050 px ;
min - width : 3200 px ;
table - layout : fixed ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( - n + 10 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( - n + 10 ) ) {
. product - breakdown - table : deep ( . q - table th : nth - child ( - n + 11 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( - n + 11 ) ) {
position : sticky ;
z - index : 2 ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( - n + 10 ) ) {
. product - breakdown - table : deep ( . q - table th : nth - child ( - n + 11 ) ) {
z - index : 4 ;
background : # f8fbff ;
}
. product - breakdown - table : deep ( . q - table tbody tr : not ( . group - row ) td : nth - child ( - n + 10 ) ) {
. product - breakdown - table : deep ( . q - table tbody tr : not ( . group - row ) td : nth - child ( - n + 11 ) ) {
background : # fff ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 1 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 1 ) ) {
left : 0 ;
width : 80 px ;
min - width : 80 px ;
max - width : 80 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 2 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 2 ) ) {
left : 80 px ;
width : 96 px ;
min - width : 96 px ;
max - width : 96 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 3 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 3 ) ) {
left : 176 px ;
width : 48 px ;
min - width : 48 px ;
max - width : 48 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 4 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 4 ) ) {
left : 224 px ;
width : 48 px ;
min - width : 48 px ;
max - width : 48 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 5 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 5 ) ) {
left : 272 px ;
width : 150 px ;
min - width : 150 px ;
max - width : 150 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 6 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 6 ) ) {
left : 422 px ;
width : 72 px ;
min - width : 72 px ;
max - width : 72 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 7 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 7 ) ) {
left : 494 px ;
width : 72 px ;
min - width : 72 px ;
max - width : 72 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 8 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 8 ) ) {
left : 566 px ;
width : 92 px ;
min - width : 92 px ;
max - width : 92 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 2 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 2 ) ) {
left : 92 px ;
width : 120 px ;
min - width : 120 px ;
max - width : 120 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 3 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 3 ) ) {
left : 212 px ;
width : 64 px ;
min - width : 64 px ;
max - width : 64 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 4 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 4 ) ) {
left : 276 px ;
width : 64 px ;
min - width : 64 px ;
max - width : 64 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 5 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 5 ) ) {
left : 340 px ;
width : 200 px ;
min - width : 200 px ;
max - width : 200 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 6 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 6 ) ) {
left : 540 px ;
width : 110 px ;
min - width : 110 px ;
max - width : 110 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 7 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 7 ) ) {
left : 650 px ;
width : 100 px ;
min - width : 100 px ;
max - width : 100 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 8 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 8 ) ) {
left : 750 px ;
width : 130 px ;
min - width : 130 px ;
max - width : 130 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 9 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 9 ) ) {
left : 658 px ;
width : 96 px ;
min - width : 96 px ;
max - width : 96 px ;
left : 880 px ;
width : 130 px ;
min - width : 130 px ;
max - width : 130 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 10 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 10 ) ) {
left : 754 px ;
width : 96 px ;
min - width : 96 px ;
max - width : 96 px ;
box - shadow : 8 px 0 10 px - 10 px rgba ( 17 , 24 , 39 , 0.45 ) ;
left : 1010 px ;
width : 130 px ;
min - width : 130 px ;
max - width : 130 px ;
}
. product - breakdown - table : deep ( . q - table th : nth - child ( 11 ) ) ,
. product - breakdown - table : deep ( . q - table td : nth - child ( 11 ) ) {
width : 180 px ;
min - width : 140 px ;
left : 1140 px ;
width : 190 px ;
min - width : 190 px ;
max - width : 190 px ;
box - shadow : 8 px 0 10 px - 10 px rgba ( 17 , 24 , 39 , 0.45 ) ;
}
. sticky - dim - table : deep ( . q - table ) {
min - width : 2200 px ;
table - layout : fixed ;
}
. sticky - dim - table : deep ( . q - table th ) ,
. sticky - dim - table : deep ( . q - table td ) {
min - width : 130 px ;
width : 130 px ;
max - width : 180 px ;
}
. 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 : 130 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 3 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 3 ) ) {
left : 260 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 4 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 4 ) ) {
left : 390 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 5 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 5 ) ) {
left : 520 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 6 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 6 ) ) {
left : 650 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 7 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 7 ) ) {
left : 780 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 8 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 8 ) ) {
left : 910 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 9 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 9 ) ) {
left : 1040 px ;
}
. sticky - dim - table : deep ( . q - table th : nth - child ( 10 ) ) ,
. sticky - dim - table : deep ( . q - table td : nth - child ( 10 ) ) {
left : 1170 px ;
}
. 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 - 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 ) ) {
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 - 9 : deep ( . q - table th : nth - child ( - n + 9 ) ) ,
. sticky - dim - table . sticky - dim - 10 : deep ( . q - table th : nth - child ( - n + 10 ) ) {
z - index : 4 ;
background : # f8fbff ;
}
. sticky - dim - table : deep ( . q - table tbody td : nth - child ( - n + 10 ) ) {
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 - 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 ) ) {
box - shadow : 8 px 0 10 px - 10 px rgba ( 17 , 24 , 39 , 0.45 ) ;
}
. filterable - header - cell {
@@ -2914,10 +3561,10 @@ onMounted(reload)
. header - with - filter {
display : grid ;
grid - template - columns : minmax ( 0 , 1 fr ) 20 px ;
grid - template - columns : minmax ( 0 , 1 fr ) 24 px ;
align - items : center ;
gap : 2 px ;
min - width : 42 px ;
gap : 4 px ;
min - width : 80 px ;
}
. header - with - filter > span {
@@ -2928,9 +3575,9 @@ onMounted(reload)
}
. header - filter - btn {
width : 20 px ;
height : 20 px ;
min - height : 20 px ;
width : 24 px ;
height : 24 px ;
min - height : 24 px ;
}
. header - filter - ghost {
@@ -3047,6 +3694,11 @@ onMounted(reload)
height : 82 px ;
}
. product - thumb {
display : block ;
object - fit : contain ;
}
. product - thumb - placeholder {
display : flex ;
align - items : center ;
@@ -3063,6 +3715,101 @@ onMounted(reload)
. product - image - dialog - body {
height : calc ( 100 vh - 74 px ) ;
padding : 12 px ;
overflow : hidden ;
}
. performance - image - layout {
display : grid ;
grid - template - columns : minmax ( 280 px , 330 px ) minmax ( 360 px , 1 fr ) minmax ( 440 px , 1.25 fr ) ;
gap : 12 px ;
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 : 1 px solid # d9dde3 ;
border - radius : 8 px ;
padding : 12 px ;
}
. image - section - title {
font - size : 12 px ;
font - weight : 700 ;
color : var ( -- q - primary ) ;
text - transform : uppercase ;
letter - spacing : 0 ;
margin - bottom : 8 px ;
}
. image - field - row {
display : grid ;
grid - template - columns : 92 px minmax ( 0 , 1 fr ) ;
gap : 8 px ;
align - items : start ;
min - height : 30 px ;
padding : 6 px 0 ;
border - bottom : 1 px solid # edf0f3 ;
font - size : 12 px ;
}
. image - field - row . k {
color : # 6 b7280 ;
font - weight : 600 ;
}
. image - field - row . v {
color : # 1 f2937 ;
font - weight : 700 ;
overflow - wrap : anywhere ;
}
. image - product - section {
position : relative ;
margin - top : 14 px ;
}
. image - size - stock - grid {
display : grid ;
grid - template - columns : repeat ( auto - fill , minmax ( 58 px , 1 fr ) ) ;
gap : 8 px ;
}
. image - size - stock - cell {
min - height : 54 px ;
padding : 8 px 6 px ;
border : 1 px solid # dfe4ea ;
border - radius : 6 px ;
background : # f8fafc ;
text - align : center ;
}
. image - size - label {
display : block ;
font - size : 11 px ;
font - weight : 700 ;
color : # 6 b7280 ;
}
. image - size - qty {
display : block ;
margin - top : 3 px ;
font - size : 16 px ;
font - weight : 800 ;
color : # 111827 ;
}
. image - empty - text {
color : # 7 a8390 ;
font - size : 12 px ;
}
. product - image - carousel {
@@ -3074,7 +3821,7 @@ onMounted(reload)
. product - image - large {
width : 100 % ;
height : calc ( 100 vh - 130 px ) ;
height : 100 % ;
}
. product - image - empty {
@@ -3088,6 +3835,29 @@ onMounted(reload)
border : 1 px solid # d9dde3 ;
border - radius : 8 px ;
}
. image - metric - card . q - card _ _section {
padding : 8 px ;
}
. image - metric - card . metric - value {
font - size : 14 px ;
}
@ media ( max - width : 1260 px ) {
. product - image - dialog - body {
overflow : auto ;
}
. performance - image - layout {
grid - template - columns : 1 fr ;
height : auto ;
}
. image - media - panel {
min - height : 520 px ;
}
}
< / style >