ui: update ProductPerformanceProfitability page to enhance table views and tabs management
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<q-page class="product-performance-page q-pa-sm">
|
||||
<div class="row items-center justify-between q-col-gutter-sm q-mb-sm">
|
||||
<div class="col-12 col-md">
|
||||
@@ -9,15 +9,6 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-auto row items-center q-gutter-sm">
|
||||
<q-btn outline color="secondary" icon="refresh" label="Yenile" :loading="loading" @click="reload" />
|
||||
<q-btn
|
||||
v-if="canUpdate"
|
||||
color="primary"
|
||||
unelevated
|
||||
icon="sync"
|
||||
label="Delta Çalıştır"
|
||||
:loading="refreshing"
|
||||
@click="runDelta"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,11 +47,81 @@
|
||||
bordered
|
||||
row-key="row_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="generalRows"
|
||||
:rows="filteredGeneralRows"
|
||||
:columns="generalColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'performance_score', descending: true }"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
<q-th :props="props" class="filterable-header-cell">
|
||||
<div class="header-with-filter">
|
||||
<span class="header-label" :title="props.col.label">{{ props.col.label }}</span>
|
||||
<q-btn
|
||||
v-if="isColumnFilterable(props.col.name)"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="8px"
|
||||
icon="filter_alt"
|
||||
:color="hasColumnFilter('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)">
|
||||
@@ -93,11 +154,81 @@
|
||||
bordered
|
||||
row-key="row_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="orderProductCustomerRows"
|
||||
:rows="filteredOrderProductCustomerRows"
|
||||
:columns="orderProductCustomerColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_usd', descending: true }"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
<q-th :props="props" class="filterable-header-cell">
|
||||
<div class="header-with-filter">
|
||||
<span class="header-label" :title="props.col.label">{{ props.col.label }}</span>
|
||||
<q-btn
|
||||
v-if="isColumnFilterable(props.col.name)"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="8px"
|
||||
icon="filter_alt"
|
||||
:color="hasColumnFilter('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)">
|
||||
@@ -140,11 +271,81 @@
|
||||
bordered
|
||||
row-key="row_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="orderMarketDetailRows"
|
||||
:rows="filteredOrderMarketDetailRows"
|
||||
:columns="orderMarketDetailColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100, sortBy: 'order_date', descending: true }"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
<q-th :props="props" class="filterable-header-cell">
|
||||
<div class="header-with-filter">
|
||||
<span class="header-label" :title="props.col.label">{{ props.col.label }}</span>
|
||||
<q-btn
|
||||
v-if="isColumnFilterable(props.col.name)"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="8px"
|
||||
icon="filter_alt"
|
||||
:color="hasColumnFilter('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)">
|
||||
@@ -176,33 +377,75 @@
|
||||
:pagination="pagination"
|
||||
@request="onRequest"
|
||||
>
|
||||
<template #header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
<q-tr class="table-filter-row">
|
||||
<q-th v-for="col in props.cols" :key="`filter-${col.name}`">
|
||||
<q-select
|
||||
v-if="isColumnFilterable(col.name)"
|
||||
v-model="columnFilters[col.name]"
|
||||
:options="columnFilterOptions(col.name)"
|
||||
<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
|
||||
borderless
|
||||
clearable
|
||||
multiple
|
||||
use-chips
|
||||
emit-value
|
||||
map-options
|
||||
options-dense
|
||||
class="header-filter"
|
||||
popup-content-class="header-filter-popup"
|
||||
placeholder="Seç"
|
||||
flat
|
||||
round
|
||||
size="8px"
|
||||
icon="filter_alt"
|
||||
:color="hasColumnFilter('products', props.col.name) ? 'primary' : 'grey-7'"
|
||||
class="header-filter-btn"
|
||||
@click.stop
|
||||
/>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
>
|
||||
<q-badge v-if="hasColumnFilter('products', props.col.name)" color="primary" floating rounded>
|
||||
{{ getColumnFilterBadgeValue('products', 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('products', 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('products', props.col.name)" />
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter('products', props.col.name)" />
|
||||
</div>
|
||||
<q-virtual-scroll
|
||||
v-if="columnFilterOptions('products', props.col.name).length > 0"
|
||||
class="excel-filter-options"
|
||||
:items="columnFilterOptions('products', 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('products', props.col.name, option.value)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
dense
|
||||
size="sm"
|
||||
:model-value="isColumnFilterValueSelected('products', props.col.name, option.value)"
|
||||
@update:model-value="() => toggleColumnFilterValue('products', 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">
|
||||
@@ -266,11 +509,81 @@
|
||||
bordered
|
||||
row-key="row_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="idleRows"
|
||||
:rows="filteredIdleRows"
|
||||
:columns="idleColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
>
|
||||
<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)">
|
||||
@@ -309,11 +622,81 @@
|
||||
bordered
|
||||
row-key="market_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="marketRows"
|
||||
:rows="filteredMarketRows"
|
||||
:columns="marketColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
>
|
||||
<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') }}
|
||||
@@ -353,11 +736,81 @@
|
||||
bordered
|
||||
row-key="customer_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="customerRows"
|
||||
:rows="filteredCustomerRows"
|
||||
:columns="customerColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
>
|
||||
<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') }}
|
||||
@@ -382,11 +835,81 @@
|
||||
bordered
|
||||
row-key="country_key"
|
||||
class="performance-table bg-white"
|
||||
:rows="countryRows"
|
||||
:rows="filteredCountryRows"
|
||||
:columns="countryColumns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 100 }"
|
||||
>
|
||||
<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') }}
|
||||
@@ -499,13 +1022,8 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { formatMoney, formatNumber, formatPercent } from 'src/utils/formatters'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
|
||||
const perm = usePermissionStore()
|
||||
const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))
|
||||
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const rows = ref([])
|
||||
const generalRows = ref([])
|
||||
const orderAnalysisRows = ref([])
|
||||
@@ -539,11 +1057,12 @@ const detailSalesRows = ref([])
|
||||
const detailStockSizes = ref([])
|
||||
|
||||
const columnFilters = reactive({})
|
||||
const columnFilterSearch = reactive({})
|
||||
const expandedGroups = ref({})
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 100,
|
||||
rowsPerPage: 500,
|
||||
rowsNumber: 0,
|
||||
sortBy: 'performance_score',
|
||||
descending: true
|
||||
@@ -891,43 +1410,6 @@ const performanceCards = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const filterableColumnNames = new Set([
|
||||
'product_code',
|
||||
'color_code',
|
||||
'yaka_kodu',
|
||||
'item_description',
|
||||
'kategori',
|
||||
'urun_ilk_grubu',
|
||||
'askili_yan',
|
||||
'urun_ana_grubu',
|
||||
'urun_alt_grubu',
|
||||
'market_key',
|
||||
'stock_qty',
|
||||
'sales_qty_90d',
|
||||
'sales_qty_180d',
|
||||
'sales_usd_90d',
|
||||
'sales_usd_180d',
|
||||
'stock_days_90d',
|
||||
'stock_days_180d',
|
||||
'avg_price_usd_90d',
|
||||
'avg_price_usd_180d',
|
||||
'base_price_usd',
|
||||
'cost_price_usd',
|
||||
'unit_profit_base_90d',
|
||||
'unit_profit_cost_90d',
|
||||
'unit_profit_base_180d',
|
||||
'unit_profit_cost_180d',
|
||||
'gross_profit_usd_90d',
|
||||
'gross_profit_usd_180d',
|
||||
'gross_margin_90d',
|
||||
'gross_margin_180d',
|
||||
'market_count_90d',
|
||||
'customer_count_90d',
|
||||
'sales_index_90d',
|
||||
'performance_score',
|
||||
'performance_bucket'
|
||||
])
|
||||
|
||||
const groupLevels = [
|
||||
{ key: 'kategori', label: 'Kategori' },
|
||||
{ key: 'urun_ilk_grubu', label: 'Ürün İlk Grubu', fallback: 'yas_grubu' },
|
||||
@@ -938,32 +1420,46 @@ const groupLevels = [
|
||||
{ key: 'color_code', label: 'Renk' }
|
||||
]
|
||||
|
||||
const filteredProductRows = computed(() => rows.value.filter(row => {
|
||||
return columns.every(col => {
|
||||
if (!isColumnFilterable(col.name)) return true
|
||||
const selected = Array.isArray(columnFilters[col.name]) ? columnFilters[col.name] : []
|
||||
if (!selected.length) return true
|
||||
return selected.includes(columnFilterValue(row, col.name))
|
||||
})
|
||||
const tableFilterSourceMap = computed(() => ({
|
||||
products: { rows: rows.value, columns },
|
||||
general: { rows: generalRows.value, columns: generalColumns },
|
||||
order_product_customers: { rows: orderProductCustomerRows.value, columns: orderProductCustomerColumns },
|
||||
order_market_details: { rows: orderMarketDetailRows.value, columns: orderMarketDetailColumns },
|
||||
idle: { rows: idleRows.value, columns: idleColumns },
|
||||
markets: { rows: marketRows.value, columns: marketColumns },
|
||||
countries: { rows: countryRows.value, columns: countryColumns },
|
||||
customers: { rows: customerRows.value, columns: customerColumns }
|
||||
}))
|
||||
|
||||
const filteredProductRows = computed(() => filterRowsForTable('products', rows.value, columns))
|
||||
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 columnFilterOptionMap = computed(() => {
|
||||
const out = {}
|
||||
for (const col of columns) {
|
||||
if (!isColumnFilterable(col.name)) continue
|
||||
const seen = new Map()
|
||||
for (const row of rows.value) {
|
||||
const value = columnFilterValue(row, col.name)
|
||||
if (!value) continue
|
||||
if (!seen.has(value)) {
|
||||
seen.set(value, {
|
||||
label: columnFilterLabel(row, col.name),
|
||||
value
|
||||
})
|
||||
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 }))
|
||||
}
|
||||
out[col.name] = Array.from(seen.values())
|
||||
.sort((a, b) => String(a.label).localeCompare(String(b.label), 'tr', { numeric: true }))
|
||||
}
|
||||
return out
|
||||
})
|
||||
@@ -1072,20 +1568,71 @@ function toggleGroup (key) {
|
||||
}
|
||||
}
|
||||
|
||||
function filterKey (tableKey, name) {
|
||||
return `${tableKey}:${name}`
|
||||
}
|
||||
|
||||
function isColumnFilterable (name) {
|
||||
return filterableColumnNames.has(name)
|
||||
return name !== 'image'
|
||||
}
|
||||
|
||||
function columnFilterOptions (name) {
|
||||
return columnFilterOptionMap.value[name] || []
|
||||
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 columnFilterValue (row, name) {
|
||||
return String(rawProductCellValue(row, name) ?? '').trim()
|
||||
function hasColumnFilter (tableKey, name) {
|
||||
return selectedColumnFilters(tableKey, name).length > 0
|
||||
}
|
||||
|
||||
function columnFilterLabel (row, name) {
|
||||
return formatProductCell(row, name) || columnFilterValue(row, name)
|
||||
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 isColumnFilterValueSelected (tableKey, name, value) {
|
||||
return selectedColumnFilters(tableKey, name).includes(value)
|
||||
}
|
||||
|
||||
function toggleColumnFilterValue (tableKey, name, value) {
|
||||
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) {
|
||||
columnFilters[filterKey(tableKey, name)] = []
|
||||
}
|
||||
|
||||
function selectAllColumnFilterOptions (tableKey, name) {
|
||||
columnFilters[filterKey(tableKey, name)] = columnFilterOptions(tableKey, name).map(option => option.value)
|
||||
}
|
||||
|
||||
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 rawProductCellValue (row, name) {
|
||||
@@ -1097,6 +1644,26 @@ function rawProductCellValue (row, name) {
|
||||
}
|
||||
}
|
||||
|
||||
function rawColumnCellValue (row, col) {
|
||||
const name = col?.name || col
|
||||
switch (name) {
|
||||
case 'urun_ilk_grubu': return row.urun_ilk_grubu || row.yas_grubu || ''
|
||||
case 'urun_ana_grubu': return row.urun_ana_grubu || row.seri || ''
|
||||
case '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 formattedColumnCellValue (row, col) {
|
||||
if (typeof col?.field === 'function') return col.field(row)
|
||||
return formatProductCell(row, col?.name || col)
|
||||
}
|
||||
|
||||
function formatProductCell (row, name) {
|
||||
switch (name) {
|
||||
case 'stock_qty':
|
||||
@@ -1356,10 +1923,10 @@ async function reload () {
|
||||
timeout: 60000
|
||||
}),
|
||||
api.get('/pricing/product-performance/general', { params: { limit: 500 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/markets', { params: { limit: 200 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/countries', { params: { limit: 200 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/markets', { params: { limit: 500 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/countries', { params: { limit: 500 }, timeout: 60000 }),
|
||||
api.get('/pricing/product-performance/customers', {
|
||||
params: { limit: 200, breakdown: customerBreakdown.value },
|
||||
params: { limit: 500, breakdown: customerBreakdown.value },
|
||||
timeout: 60000
|
||||
})
|
||||
])
|
||||
@@ -1437,22 +2004,6 @@ async function loadOrderAnalysis (showLoading = true) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runDelta () {
|
||||
refreshing.value = true
|
||||
try {
|
||||
const resp = await api.post('/pricing/product-performance/refresh', { mode: 'delta' }, { timeout: 4 * 60 * 60 * 1000 })
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: `Delta tamamlandı: satış ${resp?.data?.sales_rows || 0}, stok ${resp?.data?.stock_rows || 0}, KPI ${resp?.data?.kpi_rows || 0}`
|
||||
})
|
||||
await reload()
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Delta çalıştırılamadı' })
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onRequest (props) {
|
||||
pagination.value = { ...pagination.value, ...props.pagination }
|
||||
reload()
|
||||
@@ -1516,18 +2067,64 @@ onMounted(reload)
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.performance-table :deep(thead tr:nth-child(2) th) {
|
||||
top: 40px;
|
||||
z-index: 2;
|
||||
.filterable-header-cell {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-filter-row :deep(.q-field__control) {
|
||||
min-height: 28px;
|
||||
height: 28px;
|
||||
.header-with-filter {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 20px;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
.header-filter {
|
||||
min-width: 96px;
|
||||
.header-with-filter > span {
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.header-filter-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.group-row td {
|
||||
@@ -1608,3 +2205,5 @@ onMounted(reload)
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user