2351 lines
76 KiB
Vue
2351 lines
76 KiB
Vue
<template>
|
|
<q-page class="q-pa-xs pricing-page order-price-list-page">
|
|
<teleport to="body">
|
|
<div
|
|
v-if="pageBusy"
|
|
class="page-busy-overlay"
|
|
@click.stop
|
|
@mousedown.stop
|
|
@mouseup.stop
|
|
@touchstart.stop
|
|
@wheel.stop
|
|
>
|
|
<q-spinner-gears size="56px" color="primary" />
|
|
<div class="page-busy-label">Yukleniyor...</div>
|
|
</div>
|
|
</teleport>
|
|
|
|
<div class="top-bar row items-center justify-between q-mb-xs">
|
|
<div class="text-subtitle1 text-weight-bold">Fiyat Listesi</div>
|
|
<div class="top-actions">
|
|
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--filters">
|
|
<q-select
|
|
v-model="topUrunIlkGrubu"
|
|
dense
|
|
outlined
|
|
clearable
|
|
emit-value
|
|
map-options
|
|
use-input
|
|
:options="topUrunIlkGrubuOptions"
|
|
:loading="Boolean(serverFilterLoading.urunIlkGrubu)"
|
|
:disable="pageBusy"
|
|
label="Urun Ilk Grubu"
|
|
style="min-width: 220px"
|
|
@filter="onTopFilterSearchUrunIlkGrubu"
|
|
@update:model-value="onTopUrunIlkGrubuChange"
|
|
/>
|
|
<q-select
|
|
v-model="topUrunAnaGrubu"
|
|
dense
|
|
outlined
|
|
clearable
|
|
emit-value
|
|
map-options
|
|
use-input
|
|
:options="topUrunAnaGrubuOptions"
|
|
:loading="Boolean(serverFilterLoading.urunAnaGrubu)"
|
|
:disable="pageBusy"
|
|
label="Urun Ana Grubu"
|
|
style="min-width: 240px"
|
|
@filter="onTopFilterSearchUrunAnaGrubu"
|
|
@update:model-value="onTopUrunAnaGrubuChange"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
icon="filter_alt"
|
|
label="Listeyi Getir"
|
|
:disable="pageBusy || !canFetch"
|
|
:loading="loading"
|
|
@click="reloadData({ page: 1 })"
|
|
/>
|
|
<q-btn
|
|
flat
|
|
color="grey-7"
|
|
icon="restart_alt"
|
|
label="Secimleri Sifirla"
|
|
:disable="pageBusy"
|
|
@click="resetSelections"
|
|
/>
|
|
</div>
|
|
|
|
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions">
|
|
<div class="toolbar-group">
|
|
<q-btn-dropdown dense color="secondary" outline icon="view_module" label="Fiyat Secimi" :auto-close="false" :disable="pageBusy">
|
|
<q-list dense class="currency-menu-list">
|
|
<q-item clickable @click="selectAllPrices">
|
|
<q-item-section>Tumunu Sec</q-item-section>
|
|
</q-item>
|
|
<q-item clickable @click="clearAllPrices">
|
|
<q-item-section>Tumunu Temizle</q-item-section>
|
|
</q-item>
|
|
<q-separator />
|
|
<q-item v-for="option in priceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
|
|
<q-item-section avatar>
|
|
<q-checkbox
|
|
dense
|
|
:model-value="selectedPriceSet.has(option.value)"
|
|
:disable="pageBusy"
|
|
@click.stop
|
|
@update:model-value="() => togglePriceOption(option.value)"
|
|
/>
|
|
</q-item-section>
|
|
<q-item-section>{{ option.label }}</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-btn-dropdown>
|
|
|
|
<q-btn
|
|
dense
|
|
flat
|
|
color="grey-8"
|
|
icon="view_sidebar"
|
|
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
|
|
:disable="pageBusy"
|
|
@click="leftDetailsExpanded = !leftDetailsExpanded"
|
|
/>
|
|
<q-toggle
|
|
v-model="showInStockOnly"
|
|
dense
|
|
color="primary"
|
|
label="Sadece stogu olanlar"
|
|
:disable="pageBusy"
|
|
/>
|
|
</div>
|
|
|
|
<div class="toolbar-group">
|
|
<q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :disable="pageBusy || filteredRows.length === 0">
|
|
<q-list dense style="min-width: 220px;">
|
|
<q-item clickable @click="exportVisibleExcel">
|
|
<q-item-section avatar><q-icon name="grid_on" /></q-item-section>
|
|
<q-item-section>Excel'e Aktar</q-item-section>
|
|
</q-item>
|
|
<q-item clickable @click="printVisibleRows">
|
|
<q-item-section avatar><q-icon name="picture_as_pdf" /></q-item-section>
|
|
<q-item-section>PDF / Yazdir</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-btn-dropdown>
|
|
</div>
|
|
|
|
<q-space />
|
|
|
|
<div class="toolbar-group toolbar-group--paging">
|
|
<q-pagination
|
|
v-model="currentPage"
|
|
color="primary"
|
|
:max="Math.max(1, totalPages || 1)"
|
|
:max-pages="8"
|
|
boundary-links
|
|
direction-links
|
|
:disable="pageBusy"
|
|
@update:model-value="onPageChange"
|
|
/>
|
|
<div class="text-caption text-grey-8">
|
|
Sayfa {{ currentPage }} / {{ Math.max(1, totalPages || 1) }} - {{ filteredRows.length }} satir
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
|
<div v-if="showGuidanceOverlay" class="empty-overlay">
|
|
<div class="empty-overlay-inner">
|
|
<div class="text-subtitle1 text-weight-bold">Liste Icin Filtre Secin</div>
|
|
<div class="text-body2 q-mt-xs">
|
|
Urun Ilk Grubu, Urun Ana Grubu veya Urun Kodu secip <b>LISTEYI GETIR</b>'e basin.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
ref="topScrollRef"
|
|
class="top-x-scroll"
|
|
@scroll.passive="onTopScroll"
|
|
>
|
|
<div
|
|
class="top-x-scroll-inner"
|
|
:style="{ width: `${tableMinWidth}px` }"
|
|
/>
|
|
</div>
|
|
<q-table
|
|
ref="mainTableRef"
|
|
class="pane-table pricing-table order-price-list-table"
|
|
flat
|
|
dense
|
|
row-key="rowKey"
|
|
:rows="filteredRows"
|
|
:columns="visibleColumns"
|
|
:loading="loading"
|
|
:rows-per-page-options="[0]"
|
|
:pagination="{ rowsPerPage: 0 }"
|
|
hide-bottom
|
|
:table-style="tableStyle"
|
|
>
|
|
<template #header="props">
|
|
<q-tr :props="props" class="header-row-fixed">
|
|
<q-th
|
|
v-for="col in props.cols"
|
|
:key="col.name"
|
|
:props="props"
|
|
:class="[col.headerClasses, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
|
:style="getHeaderCellStyle(col)"
|
|
>
|
|
<div class="header-with-filter">
|
|
<span>{{ col.label }}</span>
|
|
<q-btn
|
|
v-if="col.name === 'productCode'"
|
|
flat
|
|
dense
|
|
round
|
|
size="8px"
|
|
icon="filter_alt"
|
|
:color="selectedProductCodes.length > 0 ? 'primary' : 'grey-7'"
|
|
:disable="pageBusy"
|
|
class="header-filter-btn"
|
|
@click.stop
|
|
>
|
|
<q-badge v-if="selectedProductCodes.length > 0" color="primary" floating rounded>
|
|
{{ selectedProductCodes.length }}
|
|
</q-badge>
|
|
<q-menu
|
|
class="product-code-filter-menu"
|
|
anchor="bottom left"
|
|
self="top left"
|
|
@before-show="onProductCodeMenuShow"
|
|
>
|
|
<div class="q-pa-sm filter-menu-panel">
|
|
<q-input
|
|
v-model="filterSearch.productCode"
|
|
dense
|
|
outlined
|
|
debounce="250"
|
|
placeholder="Urun kodu ara"
|
|
:disable="pageBusy"
|
|
@update:model-value="onProductCodeSearchText"
|
|
>
|
|
<template #prepend>
|
|
<q-icon name="search" />
|
|
</template>
|
|
</q-input>
|
|
<div class="row items-center justify-between q-mt-xs">
|
|
<q-btn flat dense size="sm" label="Tumunu Sec" :disable="pageBusy || productCodeOptions.length === 0" @click="selectAllProductCodeOptions" />
|
|
<q-btn flat dense size="sm" label="Temizle" :disable="pageBusy || selectedProductCodes.length === 0" @click="clearProductCodeOptions" />
|
|
</div>
|
|
<q-separator class="q-my-xs" />
|
|
<q-scroll-area style="height: 260px; width: 320px;">
|
|
<q-list dense>
|
|
<q-item
|
|
v-for="option in productCodeOptions"
|
|
:key="option.value"
|
|
clickable
|
|
:disable="pageBusy"
|
|
@click="toggleProductCodeValue(option.value)"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-checkbox
|
|
dense
|
|
:model-value="selectedProductCodeSet.has(option.value)"
|
|
:disable="pageBusy"
|
|
@click.stop
|
|
@update:model-value="() => toggleProductCodeValue(option.value)"
|
|
/>
|
|
</q-item-section>
|
|
<q-item-section>{{ option.label }}</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-scroll-area>
|
|
<q-separator class="q-my-xs" />
|
|
<div class="row items-center justify-end q-gutter-xs">
|
|
<q-btn v-close-popup dense flat label="Kapat" />
|
|
<q-btn v-close-popup dense color="primary" label="Uygula" :disable="pageBusy" @click="reloadData({ page: 1 })" />
|
|
</div>
|
|
</div>
|
|
</q-menu>
|
|
</q-btn>
|
|
<q-btn
|
|
v-else-if="col.name === 'campaignLabel'"
|
|
flat
|
|
dense
|
|
round
|
|
size="8px"
|
|
icon="filter_alt"
|
|
:color="selectedCampaignLabels.length > 0 ? 'primary' : 'grey-7'"
|
|
:disable="pageBusy"
|
|
class="header-filter-btn"
|
|
@click.stop
|
|
>
|
|
<q-badge v-if="selectedCampaignLabels.length > 0" color="primary" floating rounded>
|
|
{{ selectedCampaignLabels.length }}
|
|
</q-badge>
|
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
|
<div class="excel-filter-menu">
|
|
<q-input
|
|
v-model="campaignFilterSearch"
|
|
dense
|
|
outlined
|
|
clearable
|
|
class="excel-filter-select"
|
|
placeholder="Kampanya ara"
|
|
/>
|
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
|
<q-btn flat dense size="sm" label="Tumunu Sec" :disable="pageBusy || filteredCampaignOptions.length === 0" @click="selectAllCampaignOptions" />
|
|
<q-btn flat dense size="sm" label="Temizle" :disable="pageBusy || selectedCampaignLabels.length === 0" @click="clearCampaignOptions" />
|
|
</div>
|
|
<q-virtual-scroll
|
|
v-if="filteredCampaignOptions.length > 0"
|
|
class="excel-filter-options"
|
|
:items="filteredCampaignOptions"
|
|
:virtual-scroll-item-size="32"
|
|
separator
|
|
>
|
|
<template #default="{ item: option }">
|
|
<q-item
|
|
:key="`campaign-${option.value}`"
|
|
dense
|
|
clickable
|
|
:disable="pageBusy"
|
|
class="excel-filter-option"
|
|
@click="toggleCampaignValue(option.value)"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-checkbox
|
|
dense
|
|
size="sm"
|
|
:model-value="selectedCampaignLabelSet.has(option.value)"
|
|
:disable="pageBusy"
|
|
@update:model-value="() => toggleCampaignValue(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">Sonuc yok</div>
|
|
</div>
|
|
</q-menu>
|
|
</q-btn>
|
|
<q-btn
|
|
v-else-if="col.name === 'variantCodes'"
|
|
flat
|
|
dense
|
|
round
|
|
size="8px"
|
|
icon="filter_alt"
|
|
:color="selectedVariantCodes.length > 0 ? 'primary' : 'grey-7'"
|
|
:disable="pageBusy"
|
|
class="header-filter-btn"
|
|
@click.stop
|
|
>
|
|
<q-badge v-if="selectedVariantCodes.length > 0" color="primary" floating rounded>
|
|
{{ selectedVariantCodes.length }}
|
|
</q-badge>
|
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
|
<div class="excel-filter-menu">
|
|
<q-input
|
|
v-model="variantFilterSearch"
|
|
dense
|
|
outlined
|
|
clearable
|
|
class="excel-filter-select"
|
|
placeholder="Varyant ara"
|
|
/>
|
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
|
<q-btn flat dense size="sm" label="Tumunu Sec" :disable="pageBusy || filteredVariantOptions.length === 0" @click="selectAllVariantOptions" />
|
|
<q-btn flat dense size="sm" label="Temizle" :disable="pageBusy || selectedVariantCodes.length === 0" @click="clearVariantOptions" />
|
|
</div>
|
|
<q-virtual-scroll
|
|
v-if="filteredVariantOptions.length > 0"
|
|
class="excel-filter-options"
|
|
:items="filteredVariantOptions"
|
|
:virtual-scroll-item-size="32"
|
|
separator
|
|
>
|
|
<template #default="{ item: option }">
|
|
<q-item
|
|
:key="`variant-${option.value}`"
|
|
dense
|
|
clickable
|
|
:disable="pageBusy"
|
|
class="excel-filter-option"
|
|
@click="toggleVariantValue(option.value)"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-checkbox
|
|
dense
|
|
size="sm"
|
|
:model-value="selectedVariantCodeSet.has(option.value)"
|
|
:disable="pageBusy"
|
|
@update:model-value="() => toggleVariantValue(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">Sonuc yok</div>
|
|
</div>
|
|
</q-menu>
|
|
</q-btn>
|
|
<q-btn
|
|
v-else-if="isLocalFilterableColumn(col.name)"
|
|
flat
|
|
dense
|
|
round
|
|
size="8px"
|
|
icon="filter_alt"
|
|
:color="getColumnFilterValues(col.name).length > 0 ? 'primary' : 'grey-7'"
|
|
:disable="pageBusy"
|
|
class="header-filter-btn"
|
|
@click.stop
|
|
>
|
|
<q-badge v-if="getColumnFilterValues(col.name).length > 0" color="primary" floating rounded>
|
|
{{ getColumnFilterValues(col.name).length }}
|
|
</q-badge>
|
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
|
<div class="excel-filter-menu">
|
|
<q-input
|
|
:model-value="columnFilterSearch[col.name] || ''"
|
|
dense
|
|
outlined
|
|
clearable
|
|
class="excel-filter-select"
|
|
placeholder="Filtre ara"
|
|
@update:model-value="(val) => setColumnFilterSearch(col.name, val)"
|
|
/>
|
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
|
<q-btn flat dense size="sm" label="Tumunu Sec" :disable="pageBusy || getFilteredColumnOptions(col).length === 0" @click="selectAllColumnOptions(col)" />
|
|
<q-btn flat dense size="sm" label="Temizle" :disable="pageBusy || getColumnFilterValues(col.name).length === 0" @click="clearColumnOptions(col.name)" />
|
|
</div>
|
|
<q-virtual-scroll
|
|
v-if="getFilteredColumnOptions(col).length > 0"
|
|
class="excel-filter-options"
|
|
:items="getFilteredColumnOptions(col)"
|
|
:virtual-scroll-item-size="32"
|
|
separator
|
|
>
|
|
<template #default="{ item: option }">
|
|
<q-item
|
|
:key="`${col.name}-${option.value}`"
|
|
dense
|
|
clickable
|
|
:disable="pageBusy"
|
|
class="excel-filter-option"
|
|
@click="toggleColumnFilterValue(col.name, option.value)"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-checkbox
|
|
dense
|
|
size="sm"
|
|
:model-value="getColumnFilterSet(col.name).has(option.value)"
|
|
:disable="pageBusy"
|
|
@update:model-value="() => toggleColumnFilterValue(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">Sonuc yok</div>
|
|
</div>
|
|
</q-menu>
|
|
</q-btn>
|
|
<span v-else class="header-filter-ghost"></span>
|
|
</div>
|
|
</q-th>
|
|
</q-tr>
|
|
</template>
|
|
|
|
<template #body-cell="props">
|
|
<q-td
|
|
:props="props"
|
|
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
|
:style="getBodyCellStyle(props.col)"
|
|
>
|
|
{{ props.value }}
|
|
</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-image="props">
|
|
<q-td
|
|
:props="props"
|
|
:class="['image-cell', props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
|
:style="getBodyCellStyle(props.col)"
|
|
>
|
|
<q-img
|
|
v-if="props.row.imageUrl"
|
|
:src="props.row.imageUrl"
|
|
class="product-thumb cursor-pointer"
|
|
fit="cover"
|
|
no-spinner
|
|
@click.stop="openProductCard(props.row)"
|
|
/>
|
|
<div v-else class="product-thumb-placeholder cursor-pointer" @click.stop="openProductCard(props.row)">
|
|
<q-icon name="image_not_supported" size="24px" color="grey-6" />
|
|
</div>
|
|
</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-campaignLabel="props">
|
|
<q-td
|
|
:props="props"
|
|
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
|
:style="getBodyCellStyle(props.col)"
|
|
>
|
|
<div class="campaign-cell-content">
|
|
<span v-if="props.row.campaignLabel" class="campaign-text" :title="props.row.campaignLabel">
|
|
{{ props.row.campaignLabel }}
|
|
</span>
|
|
</div>
|
|
</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-campaignRate="props">
|
|
<q-td
|
|
:props="props"
|
|
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
|
:style="getBodyCellStyle(props.col)"
|
|
>
|
|
<span class="cell-text campaign-rate-text" :title="String(props.row.campaignRate ?? '')">
|
|
{{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }}
|
|
</span>
|
|
</q-td>
|
|
</template>
|
|
|
|
<template v-for="name in priceColumnNames" #[`body-cell-${name}`]="props" :key="name">
|
|
<q-td
|
|
:props="props"
|
|
:class="['text-right', props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
|
:style="getBodyCellStyle(props.col)"
|
|
>
|
|
<span :class="['cell-text', 'price-cell-text', { 'campaign-price-text': name.endsWith('Campaign') }]" :title="formatPrice(props.row[name])">
|
|
{{ formatPrice(props.row[name]) }}
|
|
</span>
|
|
</q-td>
|
|
</template>
|
|
</q-table>
|
|
</div>
|
|
|
|
<q-dialog v-model="productCardDialog" maximized @hide="onProductCardDialogHide">
|
|
<q-card class="product-card-dialog">
|
|
<q-card-section class="row items-center q-pb-sm">
|
|
<div class="text-h6">Urun Karti</div>
|
|
<q-space />
|
|
<q-btn icon="close" flat round dense v-close-popup />
|
|
</q-card-section>
|
|
|
|
<q-separator />
|
|
|
|
<q-card-section class="q-pt-md">
|
|
<div class="product-card-content">
|
|
<div class="product-card-images">
|
|
<q-carousel
|
|
v-if="productCardImages.length"
|
|
v-model="productCardSlide"
|
|
animated
|
|
swipeable
|
|
navigation
|
|
arrows
|
|
height="100%"
|
|
class="product-card-carousel rounded-borders"
|
|
>
|
|
<q-carousel-slide
|
|
v-for="(img, idx) in productCardImages"
|
|
:key="'img-' + idx"
|
|
:name="idx"
|
|
class="column no-wrap flex-center"
|
|
>
|
|
<div class="dialog-image-stage cursor-pointer" @click="openProductImageFullscreen(img)">
|
|
<q-img :src="img" fit="contain" class="dialog-image" />
|
|
</div>
|
|
</q-carousel-slide>
|
|
</q-carousel>
|
|
<div v-else class="dialog-image-empty">
|
|
<q-icon name="image_not_supported" size="36px" color="grey-6" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="product-card-fields">
|
|
<div class="field-row field-row-head"><span class="k">Urun</span><span class="v">{{ productCardData.productCode || '-' }} / {{ productCardData.variantCodes || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Urun Kodu</span><span class="v">{{ productCardData.productCode || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Varyant</span><span class="v">{{ productCardData.variantCodes || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Marka</span><span class="v">{{ productCardData.marka || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Marka Grubu</span><span class="v">{{ productCardData.brandGroupSelection || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Kategori</span><span class="v">{{ productCardData.kategori || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Urun Ilk Grubu</span><span class="v">{{ productCardData.urunIlkGrubu || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Urun Ana Grubu</span><span class="v">{{ productCardData.urunAnaGrubu || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Urun Alt Grubu</span><span class="v">{{ productCardData.urunAltGrubu || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Icerik</span><span class="v">{{ productCardData.icerik || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Kampanya</span><span class="v">{{ productCardData.campaignLabel || '-' }}</span></div>
|
|
<div class="field-row"><span class="k">Stok</span><span class="v">{{ formatStock(productCardData.stockQty || 0) }}</span></div>
|
|
|
|
<div class="product-card-section">
|
|
<div class="product-card-section-title">Fiyat Bilgileri</div>
|
|
<div v-if="productCardPriceRows.length" class="price-info-grid">
|
|
<div v-for="item in productCardPriceRows" :key="item.key" class="price-info-row">
|
|
<span class="price-label">{{ item.label }}</span>
|
|
<span class="price-value">{{ item.price || '-' }}</span>
|
|
<span class="price-campaign">{{ item.campaignPrice || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-else class="product-card-empty-text">Secili fiyat kolonu yok.</div>
|
|
</div>
|
|
|
|
<div class="product-card-section">
|
|
<div class="product-card-section-title">Beden Stoklari</div>
|
|
<q-inner-loading :showing="productCardStockLoading">
|
|
<q-spinner size="24px" color="primary" />
|
|
</q-inner-loading>
|
|
<div v-if="productCardSizeRows.length" class="size-stock-grid">
|
|
<div v-for="item in productCardSizeRows" :key="item.size" class="size-stock-cell">
|
|
<span class="size-label">{{ item.size }}</span>
|
|
<span class="size-qty">{{ formatStock(item.qty) }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="!productCardStockLoading" class="product-card-empty-text">Beden stogu bulunamadi.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="productImageFullscreenDialog" maximized>
|
|
<q-card class="image-fullscreen-dialog">
|
|
<q-card-section class="row items-center q-pb-sm">
|
|
<div class="text-h6">Urun Fotografi</div>
|
|
<q-space />
|
|
<q-btn icon="close" flat round dense v-close-popup />
|
|
</q-card-section>
|
|
<q-separator />
|
|
<q-card-section class="image-fullscreen-body">
|
|
<q-carousel
|
|
v-if="fullscreenImages.length"
|
|
v-model="productImageFullscreenSlide"
|
|
animated
|
|
swipeable
|
|
navigation
|
|
arrows
|
|
height="calc(100vh - 120px)"
|
|
class="image-fullscreen-carousel"
|
|
>
|
|
<q-carousel-slide
|
|
v-for="(img, idx) in fullscreenImages"
|
|
:key="'full-img-' + idx"
|
|
:name="idx"
|
|
class="column no-wrap flex-center"
|
|
>
|
|
<div class="image-fullscreen-stage">
|
|
<q-img :src="img" fit="contain" class="image-fullscreen-img" />
|
|
</div>
|
|
</q-carousel-slide>
|
|
</q-carousel>
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-dialog>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
import { Notify } from 'quasar'
|
|
import api from 'src/services/api'
|
|
|
|
const PAGE_LIMIT = 250
|
|
const GUIDANCE_MSG = 'Liste icin filtre secin.'
|
|
|
|
const priceOptions = ['USD', 'EUR', 'TRY'].flatMap((cur) => [1, 2, 3, 4, 5, 6].map((lv) => ({
|
|
label: `${cur} ${lv}`,
|
|
value: `${cur.toLowerCase()}${lv}`
|
|
})))
|
|
const campaignPairs = priceOptions.map((x) => ({ base: x.value, derived: `${x.value}Campaign` }))
|
|
const priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived])
|
|
|
|
const topUrunIlkGrubu = ref(null)
|
|
const topUrunAnaGrubu = ref(null)
|
|
const selectedProductCodes = ref([])
|
|
const selectedCampaignLabels = ref([])
|
|
const selectedVariantCodes = ref([])
|
|
const campaignFilterSearch = ref('')
|
|
const variantFilterSearch = ref('')
|
|
const columnFilters = ref({})
|
|
const columnFilterSearch = ref({})
|
|
const selectedPriceOptions = ref(['usd5', 'try5'])
|
|
const leftDetailsExpanded = ref(true)
|
|
const showInStockOnly = ref(false)
|
|
|
|
const rows = ref([])
|
|
const loading = ref(false)
|
|
const renderPending = ref(false)
|
|
const error = ref(GUIDANCE_MSG)
|
|
const currentPage = ref(1)
|
|
const totalPages = ref(1)
|
|
const totalCount = ref(0)
|
|
|
|
const serverFilterOptionMap = ref({})
|
|
const serverFilterLoading = ref({})
|
|
const serverFilterLastQuery = ref({})
|
|
const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' })
|
|
const imageCache = new Map()
|
|
const imageListCache = new Map()
|
|
const variantCodeCollator = new Intl.Collator('tr', { numeric: true, sensitivity: 'base' })
|
|
const mainTableRef = ref(null)
|
|
const topScrollRef = ref(null)
|
|
let syncingScroll = false
|
|
|
|
const productCardDialog = ref(false)
|
|
const productCardData = ref({})
|
|
const productCardImages = ref([])
|
|
const productCardSlide = ref(0)
|
|
const productCardStockLoading = ref(false)
|
|
const productCardSizeRows = ref([])
|
|
const productImageFullscreenDialog = ref(false)
|
|
const productImageFullscreenSlide = ref(0)
|
|
const fullscreenImages = computed(() => productCardImages.value || [])
|
|
|
|
const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || []))
|
|
const productCardPriceRows = computed(() => {
|
|
const row = productCardData.value || {}
|
|
return priceOptions
|
|
.filter((option) => selectedPriceSet.value.has(option.value))
|
|
.map((option) => ({
|
|
key: option.value,
|
|
label: option.label,
|
|
price: formatPrice(row?.[option.value]),
|
|
campaignPrice: formatPrice(row?.[`${option.value}Campaign`])
|
|
}))
|
|
})
|
|
const selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || []))
|
|
const selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || []))
|
|
const selectedVariantCodeSet = computed(() => new Set(selectedVariantCodes.value || []))
|
|
const pageBusy = computed(() => loading.value || renderPending.value)
|
|
const canFetch = computed(() => Boolean(topUrunIlkGrubu.value || topUrunAnaGrubu.value || selectedProductCodes.value.length > 0))
|
|
const showGuidanceOverlay = computed(() => !loading.value && rows.value.length === 0 && error.value === GUIDANCE_MSG)
|
|
|
|
const topUrunIlkGrubuOptions = computed(() => serverFilterOptionMap.value.urunIlkGrubu || [])
|
|
const topUrunAnaGrubuOptions = computed(() => serverFilterOptionMap.value.urunAnaGrubu || [])
|
|
const productCodeOptions = computed(() => serverFilterOptionMap.value.productCode || [])
|
|
const campaignOptions = computed(() => {
|
|
const uniq = new Set()
|
|
for (const row of rows.value || []) {
|
|
const val = toText(row?.campaignLabel)
|
|
if (val) uniq.add(val)
|
|
}
|
|
return Array.from(uniq)
|
|
.sort((a, b) => a.localeCompare(b, 'tr'))
|
|
.map((value) => ({ label: value, value }))
|
|
})
|
|
const filteredCampaignOptions = computed(() => {
|
|
const q = toText(campaignFilterSearch.value).toLocaleLowerCase('tr')
|
|
const list = campaignOptions.value
|
|
return q ? list.filter((x) => x.label.toLocaleLowerCase('tr').includes(q)) : list
|
|
})
|
|
const variantOptions = computed(() => {
|
|
const uniq = new Set()
|
|
for (const row of rows.value || []) {
|
|
const val = toText(row?.variantCodes)
|
|
if (val) uniq.add(val)
|
|
}
|
|
return Array.from(uniq)
|
|
.sort((a, b) => variantCodeCollator.compare(a, b))
|
|
.map((value) => ({ label: value, value }))
|
|
})
|
|
const filteredVariantOptions = computed(() => {
|
|
const q = toText(variantFilterSearch.value).toLocaleLowerCase('tr')
|
|
const list = variantOptions.value
|
|
return q ? list.filter((x) => x.label.toLocaleLowerCase('tr').includes(q)) : list
|
|
})
|
|
|
|
function toText (value) {
|
|
return String(value ?? '').trim()
|
|
}
|
|
|
|
function toNumber (value) {
|
|
const n = Number(String(value ?? '0').replace(/\./g, '').replace(',', '.'))
|
|
return Number.isFinite(n) ? n : 0
|
|
}
|
|
|
|
function round2 (value) {
|
|
const n = Number(value)
|
|
return Number.isFinite(n) ? Number(n.toFixed(2)) : null
|
|
}
|
|
|
|
function formatPrice (value) {
|
|
const n = Number(value)
|
|
if (!Number.isFinite(n) || n === 0) return ''
|
|
return n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
}
|
|
|
|
function formatStock (value) {
|
|
const n = Number(value)
|
|
if (!Number.isFinite(n)) return ''
|
|
return n.toLocaleString('tr-TR', { maximumFractionDigits: 2 })
|
|
}
|
|
|
|
function parseStockNumber (value) {
|
|
if (typeof value === 'number') return Number.isFinite(value) ? value : 0
|
|
const text = String(value ?? '').trim()
|
|
if (!text) return 0
|
|
const normalized = text.replace(/\./g, '').replace(',', '.')
|
|
const n = Number.parseFloat(normalized)
|
|
return Number.isFinite(n) ? n : 0
|
|
}
|
|
|
|
function normalizeCardToken (value) {
|
|
return String(value ?? '').trim().toUpperCase()
|
|
}
|
|
|
|
function parseVariantTokens (variantCode) {
|
|
const parts = String(variantCode || '').split('-').map((x) => normalizeCardToken(x)).filter(Boolean)
|
|
return {
|
|
color: parts[0] || '',
|
|
dim2: parts.length > 1 ? parts.slice(1).join('-') : ''
|
|
}
|
|
}
|
|
|
|
function stockRowText (row, ...keys) {
|
|
for (const key of keys) {
|
|
const value = String(row?.[key] ?? '').trim()
|
|
if (value) return value
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function matchesProductCardVariant (stockRow, cardRow) {
|
|
const tokens = parseVariantTokens(cardRow?.variantCodes)
|
|
if (!tokens.color && !tokens.dim2) return true
|
|
const color = normalizeCardToken(stockRowText(stockRow, 'Renk_Kodu', 'ColorCode', 'colorCode'))
|
|
const dim2 = normalizeCardToken(stockRowText(stockRow, 'Yaka', 'ItemDim2Code', 'itemDim2Code', 'Renk2'))
|
|
if (tokens.color && color !== tokens.color) return false
|
|
if (tokens.dim2 && dim2 !== tokens.dim2) return false
|
|
return true
|
|
}
|
|
|
|
function buildSizeStockRows (stockRows, cardRow) {
|
|
const totals = new Map()
|
|
for (const item of stockRows || []) {
|
|
if (!matchesProductCardVariant(item, cardRow)) continue
|
|
const size = stockRowText(item, 'Beden', 'Size', 'ItemDim1Code', 'itemDim1Code')
|
|
if (!size) continue
|
|
const qty = parseStockNumber(item?.Kullanilabilir_Envanter ?? item?.StockQty ?? item?.qty)
|
|
totals.set(size, (totals.get(size) || 0) + qty)
|
|
}
|
|
return Array.from(totals.entries())
|
|
.map(([size, qty]) => ({ size, qty }))
|
|
.sort((a, b) => variantCodeCollator.compare(a.size, b.size))
|
|
}
|
|
|
|
function mapProductRow (raw, index) {
|
|
const row = {
|
|
id: index + 1,
|
|
productCode: toText(raw?.ProductCode),
|
|
stockQty: toNumber(raw?.StockQty),
|
|
stockEntryDate: toText(raw?.StockEntryDate),
|
|
lastPricingDate: toText(raw?.LastPricingDate),
|
|
askiliYan: toText(raw?.AskiliYan),
|
|
kategori: toText(raw?.Kategori),
|
|
urunIlkGrubu: toText(raw?.UrunIlkGrubu),
|
|
urunAnaGrubu: toText(raw?.UrunAnaGrubu),
|
|
urunAltGrubu: toText(raw?.UrunAltGrubu),
|
|
icerik: toText(raw?.Icerik),
|
|
karisim: toText(raw?.Karisim),
|
|
marka: toText(raw?.Marka),
|
|
brandGroupSelection: toText(raw?.BrandGroupSec)
|
|
}
|
|
for (const p of priceOptions) row[p.value] = toNumber(raw?.[p.value.toUpperCase()])
|
|
return row
|
|
}
|
|
|
|
function applyCampaignPrices (row) {
|
|
const rate = Number(row?.campaignRate || 0)
|
|
for (const p of campaignPairs) {
|
|
const base = Number(row?.[p.base] || 0)
|
|
row[p.derived] = rate > 0 && base > 0 ? round2(base * (1 - rate / 100)) : null
|
|
}
|
|
}
|
|
|
|
function buildCampaignLabel (variant) {
|
|
const code = toText(variant?.campaign_code)
|
|
const title = toText(variant?.campaign_title)
|
|
if (code && title) return `${code} - ${title}`
|
|
return code || title
|
|
}
|
|
|
|
function buildRows (products, variants) {
|
|
const byCode = new Map()
|
|
for (const v of variants || []) {
|
|
const code = toText(v?.product_code)
|
|
if (!code) continue
|
|
if (!byCode.has(code)) byCode.set(code, [])
|
|
byCode.get(code).push(v)
|
|
}
|
|
|
|
const out = []
|
|
for (const p of products) {
|
|
const list = byCode.get(p.productCode) || []
|
|
if (list.length === 0) {
|
|
const row = {
|
|
...p,
|
|
rowKey: `${p.productCode}|0|0`,
|
|
imageUrl: '',
|
|
variantCodes: '',
|
|
variantStocks: formatStock(p.stockQty),
|
|
campaignLabel: '',
|
|
campaignRate: null,
|
|
lastCampaignDate: ''
|
|
}
|
|
applyCampaignPrices(row)
|
|
out.push(row)
|
|
continue
|
|
}
|
|
list.sort((a, b) => variantCodeCollator.compare(toText(a?.variant_code), toText(b?.variant_code)))
|
|
for (const v of list) {
|
|
const d1 = Number(v?.dim1 || 0)
|
|
const d3 = v?.dim3 == null ? 0 : Number(v?.dim3 || 0)
|
|
const row = {
|
|
...p,
|
|
rowKey: `${p.productCode}|${d1}|${d3}`,
|
|
imageUrl: '',
|
|
dim1: d1,
|
|
dim3: d3,
|
|
variantCodes: toText(v?.variant_code),
|
|
variantStocks: formatStock(v?.stock_qty),
|
|
stockQty: Number(v?.stock_qty ?? 0),
|
|
campaignLabel: buildCampaignLabel(v),
|
|
campaignRate: Number(v?.discount_rate || 0) || null,
|
|
lastCampaignDate: toText(v?.campaign_last_dttm)
|
|
}
|
|
applyCampaignPrices(row)
|
|
out.push(row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
async function fetchServerFilterOptions (field, q = '') {
|
|
const query = toText(q)
|
|
const last = toText(serverFilterLastQuery.value[field])
|
|
const cached = Array.isArray(serverFilterOptionMap.value[field]) && serverFilterOptionMap.value[field].length > 0
|
|
if (cached && last === query) return
|
|
serverFilterLoading.value = { ...serverFilterLoading.value, [field]: true }
|
|
serverFilterLastQuery.value = { ...serverFilterLastQuery.value, [field]: query }
|
|
try {
|
|
const params = { field, q: query, limit: field === 'productCode' ? 200 : 160 }
|
|
if (field === 'urunAnaGrubu' && topUrunIlkGrubu.value) params.urun_ilk_grubu = topUrunIlkGrubu.value
|
|
const res = await api.get('/order/price-list/options', { params })
|
|
const items = Array.isArray(res?.data?.items) ? res.data.items : []
|
|
serverFilterOptionMap.value = {
|
|
...serverFilterOptionMap.value,
|
|
[field]: items.map((x) => ({
|
|
label: toText(x?.label ?? x?.value),
|
|
value: toText(x?.value ?? x?.label)
|
|
})).filter((x) => x.value)
|
|
}
|
|
} finally {
|
|
serverFilterLoading.value = { ...serverFilterLoading.value, [field]: false }
|
|
}
|
|
}
|
|
|
|
function onTopFilterSearchUrunIlkGrubu (val, update) {
|
|
update(() => {
|
|
filterSearch.value.urunIlkGrubu = toText(val)
|
|
void fetchServerFilterOptions('urunIlkGrubu', val)
|
|
})
|
|
}
|
|
|
|
function onTopFilterSearchUrunAnaGrubu (val, update) {
|
|
update(() => {
|
|
filterSearch.value.urunAnaGrubu = toText(val)
|
|
void fetchServerFilterOptions('urunAnaGrubu', val)
|
|
})
|
|
}
|
|
|
|
function onProductCodeMenuShow () {
|
|
void fetchServerFilterOptions('productCode', filterSearch.value.productCode)
|
|
}
|
|
|
|
function onProductCodeSearchText (val) {
|
|
void fetchServerFilterOptions('productCode', val)
|
|
}
|
|
|
|
function toggleProductCodeValue (value) {
|
|
const v = toText(value)
|
|
if (!v) return
|
|
const set = new Set(selectedProductCodes.value || [])
|
|
if (set.has(v)) set.delete(v)
|
|
else set.add(v)
|
|
selectedProductCodes.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
|
|
}
|
|
|
|
function selectAllProductCodeOptions () {
|
|
const set = new Set(selectedProductCodes.value || [])
|
|
for (const option of productCodeOptions.value) {
|
|
const v = toText(option.value)
|
|
if (v) set.add(v)
|
|
}
|
|
selectedProductCodes.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
|
|
}
|
|
|
|
function clearProductCodeOptions () {
|
|
selectedProductCodes.value = []
|
|
}
|
|
|
|
function toggleCampaignValue (value) {
|
|
const v = toText(value)
|
|
if (!v) return
|
|
const set = new Set(selectedCampaignLabels.value || [])
|
|
if (set.has(v)) set.delete(v)
|
|
else set.add(v)
|
|
selectedCampaignLabels.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
|
|
}
|
|
|
|
function selectAllCampaignOptions () {
|
|
const set = new Set(selectedCampaignLabels.value || [])
|
|
for (const option of filteredCampaignOptions.value) {
|
|
const v = toText(option.value)
|
|
if (v) set.add(v)
|
|
}
|
|
selectedCampaignLabels.value = Array.from(set).sort((a, b) => a.localeCompare(b, 'tr'))
|
|
}
|
|
|
|
function clearCampaignOptions () {
|
|
selectedCampaignLabels.value = []
|
|
}
|
|
|
|
function toggleVariantValue (value) {
|
|
const v = toText(value)
|
|
if (!v) return
|
|
const set = new Set(selectedVariantCodes.value || [])
|
|
if (set.has(v)) set.delete(v)
|
|
else set.add(v)
|
|
selectedVariantCodes.value = Array.from(set).sort((a, b) => variantCodeCollator.compare(a, b))
|
|
}
|
|
|
|
function selectAllVariantOptions () {
|
|
const set = new Set(selectedVariantCodes.value || [])
|
|
for (const option of filteredVariantOptions.value) {
|
|
const v = toText(option.value)
|
|
if (v) set.add(v)
|
|
}
|
|
selectedVariantCodes.value = Array.from(set).sort((a, b) => variantCodeCollator.compare(a, b))
|
|
}
|
|
|
|
function clearVariantOptions () {
|
|
selectedVariantCodes.value = []
|
|
}
|
|
|
|
function isLocalFilterableColumn (name) {
|
|
if (!name || ['image', 'productCode', 'variantCodes', 'campaignLabel'].includes(name)) return false
|
|
return true
|
|
}
|
|
|
|
function getColumnFilterValues (name) {
|
|
const list = columnFilters.value?.[name]
|
|
return Array.isArray(list) ? list : []
|
|
}
|
|
|
|
function getColumnFilterSet (name) {
|
|
return new Set(getColumnFilterValues(name))
|
|
}
|
|
|
|
function setColumnFilterSearch (name, value) {
|
|
columnFilterSearch.value = { ...columnFilterSearch.value, [name]: toText(value) }
|
|
}
|
|
|
|
function getColumnOptions (col) {
|
|
const uniq = new Set()
|
|
for (const row of rows.value || []) {
|
|
uniq.add(exportCell(row, col))
|
|
}
|
|
return Array.from(uniq)
|
|
.sort((a, b) => String(a).localeCompare(String(b), 'tr', { numeric: true, sensitivity: 'base' }))
|
|
.map((value) => ({ label: value || '-', value }))
|
|
}
|
|
|
|
function getFilteredColumnOptions (col) {
|
|
const q = toText(columnFilterSearch.value?.[col.name]).toLocaleLowerCase('tr')
|
|
const list = getColumnOptions(col)
|
|
return q ? list.filter((x) => String(x.label || '').toLocaleLowerCase('tr').includes(q)) : list
|
|
}
|
|
|
|
function toggleColumnFilterValue (name, value) {
|
|
const set = new Set(getColumnFilterValues(name))
|
|
if (set.has(value)) set.delete(value)
|
|
else set.add(value)
|
|
columnFilters.value = { ...columnFilters.value, [name]: Array.from(set) }
|
|
}
|
|
|
|
function selectAllColumnOptions (col) {
|
|
const set = new Set(getColumnFilterValues(col.name))
|
|
for (const option of getFilteredColumnOptions(col)) set.add(option.value)
|
|
columnFilters.value = { ...columnFilters.value, [col.name]: Array.from(set) }
|
|
}
|
|
|
|
function clearColumnOptions (name) {
|
|
columnFilters.value = { ...columnFilters.value, [name]: [] }
|
|
}
|
|
|
|
function onTopUrunIlkGrubuChange () {
|
|
topUrunAnaGrubu.value = null
|
|
void fetchServerFilterOptions('urunAnaGrubu', '')
|
|
}
|
|
|
|
function onTopUrunAnaGrubuChange () {}
|
|
|
|
function requestParams (page) {
|
|
const params = {
|
|
page,
|
|
limit: Math.min(500, Math.max(PAGE_LIMIT, selectedProductCodes.value.length || 0)),
|
|
include_total: 1
|
|
}
|
|
if (topUrunIlkGrubu.value) params.urun_ilk_grubu = topUrunIlkGrubu.value
|
|
if (topUrunAnaGrubu.value) params.urun_ana_grubu = topUrunAnaGrubu.value
|
|
if (selectedProductCodes.value.length > 0) params.product_code = selectedProductCodes.value.join(',')
|
|
return params
|
|
}
|
|
|
|
async function reloadData ({ page = 1 } = {}) {
|
|
if (!canFetch.value) {
|
|
rows.value = []
|
|
error.value = GUIDANCE_MSG
|
|
return
|
|
}
|
|
loading.value = true
|
|
renderPending.value = true
|
|
try {
|
|
const productRes = await api.request({
|
|
method: 'GET',
|
|
url: '/order/price-list/products',
|
|
params: requestParams(page),
|
|
timeout: 180000
|
|
})
|
|
const products = (Array.isArray(productRes?.data) ? productRes.data : []).map(mapProductRow)
|
|
totalCount.value = Number(productRes?.headers?.['x-total-count'] || products.length || 0)
|
|
totalPages.value = Math.max(1, Number(productRes?.headers?.['x-total-pages'] || 1))
|
|
currentPage.value = Math.max(1, Number(productRes?.headers?.['x-page'] || page))
|
|
|
|
const codes = products.map((x) => x.productCode).filter(Boolean)
|
|
let variants = []
|
|
if (codes.length > 0) {
|
|
const variantRes = await api.request({
|
|
method: 'GET',
|
|
url: '/order/price-list/variant-rows',
|
|
params: { product_code: codes.join(',') },
|
|
timeout: 180000
|
|
})
|
|
variants = Array.isArray(variantRes?.data) ? variantRes.data : []
|
|
}
|
|
rows.value = buildRows(products, variants)
|
|
error.value = ''
|
|
void loadImagesForRows(rows.value)
|
|
await nextTick()
|
|
} catch (err) {
|
|
rows.value = []
|
|
error.value = err?.response?.data || err?.message || 'Fiyat listesi alinamadi'
|
|
Notify.create({ type: 'negative', message: error.value })
|
|
} finally {
|
|
loading.value = false
|
|
setTimeout(() => { renderPending.value = false }, 120)
|
|
}
|
|
}
|
|
|
|
async function loadImagesForRows (list) {
|
|
const targets = []
|
|
const seen = new Set()
|
|
for (const row of list) {
|
|
const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}`
|
|
if (!row.productCode || seen.has(key)) continue
|
|
seen.add(key)
|
|
targets.push({ row, key })
|
|
}
|
|
const concurrency = 12
|
|
let cursor = 0
|
|
let loaded = 0
|
|
const workers = Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
|
|
for (;;) {
|
|
const target = targets[cursor]
|
|
cursor += 1
|
|
if (!target) return
|
|
const { row, key } = target
|
|
if (imageCache.has(key)) {
|
|
row.imageUrl = imageCache.get(key)
|
|
continue
|
|
}
|
|
try {
|
|
const res = await api.get('/product-images', {
|
|
params: {
|
|
code: row.productCode,
|
|
dim1_id: row.dim1 || '',
|
|
dim3_id: row.dim3 || ''
|
|
},
|
|
timeout: 15000
|
|
})
|
|
const first = Array.isArray(res?.data) ? res.data[0] : null
|
|
const url = resolveProductImageUrl(first)
|
|
imageCache.set(key, url)
|
|
row.imageUrl = url
|
|
imageListCache.set(key, Array.isArray(res?.data) ? res.data : [])
|
|
} catch {
|
|
imageCache.set(key, '')
|
|
}
|
|
loaded += 1
|
|
if (loaded % 12 === 0) rows.value = [...rows.value]
|
|
}
|
|
})
|
|
await Promise.all(workers)
|
|
rows.value = [...rows.value]
|
|
}
|
|
|
|
function normalizeUploadsPath (storagePath) {
|
|
const raw = toText(storagePath)
|
|
if (!raw) return ''
|
|
const normalized = raw.replace(/\\/g, '/')
|
|
const idx = normalized.toLowerCase().indexOf('/uploads/')
|
|
if (idx >= 0) return normalized.slice(idx)
|
|
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
|
|
return ''
|
|
}
|
|
|
|
function resolveProductImageUrl (item) {
|
|
if (!item || typeof item !== 'object') return ''
|
|
const imageId = Number(item.id || item.ID || 0)
|
|
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
|
const thumbUrl = toText(item.thumb_url || item.thumbUrl)
|
|
if (thumbUrl) return thumbUrl
|
|
const fullUrl = toText(item.full_url || item.fullUrl)
|
|
if (fullUrl) return fullUrl
|
|
const contentUrl = toText(item.content_url || item.ContentURL)
|
|
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
|
|
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage)
|
|
if (uploadsPath) return uploadsPath
|
|
const fileName = toText(item.file_name || item.FileName)
|
|
return fileName ? `/uploads/image/${fileName}` : ''
|
|
}
|
|
|
|
async function fetchImageListForRow (row) {
|
|
const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}`
|
|
if (imageListCache.has(key)) return imageListCache.get(key) || []
|
|
const res = await api.get('/product-images', {
|
|
params: {
|
|
code: row.productCode,
|
|
dim1_id: row.dim1 || '',
|
|
dim3_id: row.dim3 || ''
|
|
},
|
|
timeout: 15000
|
|
})
|
|
const list = Array.isArray(res?.data) ? res.data : []
|
|
imageListCache.set(key, list)
|
|
return list
|
|
}
|
|
|
|
async function openProductCard (row) {
|
|
if (!row) return
|
|
productCardData.value = { ...row }
|
|
productCardSizeRows.value = []
|
|
productCardDialog.value = true
|
|
productCardSlide.value = 0
|
|
productCardStockLoading.value = true
|
|
try {
|
|
const [list, stockRes] = await Promise.all([
|
|
fetchImageListForRow(row),
|
|
api.get('/product-stock-query', { params: { code: row.productCode }, timeout: 30000 })
|
|
])
|
|
const images = list.map(resolveProductImageUrl).filter(Boolean)
|
|
if (row.imageUrl && !images.includes(row.imageUrl)) images.unshift(row.imageUrl)
|
|
productCardImages.value = Array.from(new Set(images))
|
|
productCardSizeRows.value = buildSizeStockRows(Array.isArray(stockRes?.data) ? stockRes.data : [], row)
|
|
} catch {
|
|
productCardImages.value = row.imageUrl ? [row.imageUrl] : []
|
|
productCardSizeRows.value = []
|
|
} finally {
|
|
productCardStockLoading.value = false
|
|
}
|
|
}
|
|
|
|
function openProductImageFullscreen (src) {
|
|
const value = toText(src)
|
|
if (!value) return
|
|
const idx = Math.max(0, fullscreenImages.value.findIndex((x) => toText(x) === value))
|
|
productImageFullscreenSlide.value = idx
|
|
productImageFullscreenDialog.value = true
|
|
}
|
|
|
|
function onProductCardDialogHide () {
|
|
productImageFullscreenDialog.value = false
|
|
productCardStockLoading.value = false
|
|
productCardSizeRows.value = []
|
|
}
|
|
|
|
function resetSelections () {
|
|
topUrunIlkGrubu.value = null
|
|
topUrunAnaGrubu.value = null
|
|
selectedProductCodes.value = []
|
|
selectedCampaignLabels.value = []
|
|
selectedVariantCodes.value = []
|
|
columnFilters.value = {}
|
|
columnFilterSearch.value = {}
|
|
rows.value = []
|
|
error.value = GUIDANCE_MSG
|
|
currentPage.value = 1
|
|
totalPages.value = 1
|
|
totalCount.value = 0
|
|
}
|
|
|
|
function onPageChange (page) {
|
|
void reloadData({ page })
|
|
}
|
|
|
|
function togglePriceOption (value) {
|
|
const set = new Set(selectedPriceOptions.value || [])
|
|
if (set.has(value)) set.delete(value)
|
|
else set.add(value)
|
|
selectedPriceOptions.value = priceOptions.map((x) => x.value).filter((x) => set.has(x))
|
|
}
|
|
|
|
function selectAllPrices () {
|
|
selectedPriceOptions.value = priceOptions.map((x) => x.value)
|
|
}
|
|
|
|
function clearAllPrices () {
|
|
selectedPriceOptions.value = []
|
|
}
|
|
|
|
function col (name, label, field, width, extra = {}) {
|
|
return {
|
|
name,
|
|
label,
|
|
field,
|
|
align: extra.align || 'left',
|
|
sortable: !!extra.sortable,
|
|
style: `width:${width}px;min-width:${width}px;max-width:${width}px;`,
|
|
headerStyle: `width:${width}px;min-width:${width}px;max-width:${width}px;`,
|
|
classes: extra.classes || '',
|
|
headerClasses: extra.headerClasses || extra.classes || ''
|
|
}
|
|
}
|
|
|
|
const allColumns = [
|
|
col('image', '', 'imageUrl', 108, { align: 'center', classes: 'image-col sticky-col' }),
|
|
col('brandGroupSelection', 'MARKA GRUBU', 'brandGroupSelection', 86, { classes: 'ps-col sticky-col' }),
|
|
col('marka', 'MARKA', 'marka', 72, { sortable: true, classes: 'ps-col sticky-col' }),
|
|
col('productCode', 'URUN KODU', 'productCode', 112, { sortable: true, classes: 'ps-col product-code-col sticky-col' }),
|
|
col('variantCodes', 'VARYANT', 'variantCodes', 82, { align: 'center', classes: 'ps-col variant-col sticky-col center-col' }),
|
|
col('variantStocks', 'STOK', 'stockQty', 64, { align: 'center', sortable: true, classes: 'ps-col variant-stock-col sticky-col center-col' }),
|
|
col('campaignLabel', 'KAMPANYA', 'campaignLabel', 118, { align: 'center', classes: 'ps-col campaign-col sticky-col center-col' }),
|
|
col('campaignRate', 'IND %', 'campaignRate', 56, { align: 'center', classes: 'ps-col campaign-rate-col sticky-col center-col' }),
|
|
col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col center-col' }),
|
|
col('kategori', 'KATEGORI', 'kategori', 72, { sortable: true, classes: 'ps-col center-col' }),
|
|
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 72, { sortable: true, classes: 'ps-col center-col' }),
|
|
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 84, { sortable: true, classes: 'ps-col center-col' }),
|
|
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 84, { sortable: true, classes: 'ps-col center-col' }),
|
|
col('icerik', 'ICERIK', 'icerik', 92, { sortable: true, classes: 'ps-col' }),
|
|
col('karisim', 'KARISIM', 'karisim', 88, { sortable: true, classes: 'ps-col karisim-wrap-col' }),
|
|
...campaignPairs.flatMap((p) => [
|
|
col(p.base, p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2'), p.base, 78, { align: 'right', classes: `${p.base.slice(0, 3)}-col` }),
|
|
col(p.derived, `${p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2')} KMP`, p.derived, 88, { align: 'right', classes: `${p.base.slice(0, 3)}-col campaign-price-col` })
|
|
])
|
|
]
|
|
|
|
const compactHiddenColumnNames = new Set([
|
|
'variantStocks',
|
|
'campaignLabel',
|
|
'campaignRate',
|
|
'askiliYan',
|
|
'kategori',
|
|
'urunIlkGrubu',
|
|
'urunAnaGrubu',
|
|
'urunAltGrubu',
|
|
'icerik',
|
|
'karisim'
|
|
])
|
|
|
|
const visibleColumns = computed(() => allColumns.filter((c) => {
|
|
if (/^(usd|eur|try)[1-6]$/.test(c.name)) return selectedPriceSet.value.has(c.name)
|
|
if (/^(usd|eur|try)[1-6]Campaign$/.test(c.name)) return selectedPriceSet.value.has(c.name.replace(/Campaign$/, ''))
|
|
if (!leftDetailsExpanded.value && compactHiddenColumnNames.has(c.name)) return false
|
|
return true
|
|
}))
|
|
|
|
const filteredRows = computed(() => {
|
|
const campaignSet = selectedCampaignLabelSet.value
|
|
const variantSet = selectedVariantCodeSet.value
|
|
const localFilters = columnFilters.value || {}
|
|
let list = rows.value || []
|
|
if (showInStockOnly.value) {
|
|
list = list.filter((row) => Number(row?.stockQty ?? 0) > 0)
|
|
}
|
|
if (campaignSet.size > 0) {
|
|
list = list.filter((row) => campaignSet.has(toText(row?.campaignLabel)))
|
|
}
|
|
if (variantSet.size > 0) {
|
|
list = list.filter((row) => variantSet.has(toText(row?.variantCodes)))
|
|
}
|
|
const active = Object.entries(localFilters).filter(([, values]) => Array.isArray(values) && values.length > 0)
|
|
if (active.length > 0) {
|
|
const byName = new Map(allColumns.map((c) => [c.name, c]))
|
|
list = list.filter((row) => active.every(([name, values]) => {
|
|
const col = byName.get(name)
|
|
if (!col) return true
|
|
return new Set(values).has(exportCell(row, col))
|
|
}))
|
|
}
|
|
return list
|
|
})
|
|
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
|
const tableStyle = computed(() => ({
|
|
width: `${tableMinWidth.value}px`,
|
|
minWidth: `${tableMinWidth.value}px`,
|
|
tableLayout: 'fixed'
|
|
}))
|
|
const stickyColumnNames = computed(() => {
|
|
const visible = new Set(visibleColumns.value.map((x) => x.name))
|
|
const expanded = [
|
|
'image',
|
|
'brandGroupSelection',
|
|
'marka',
|
|
'productCode',
|
|
'variantCodes',
|
|
'variantStocks',
|
|
'campaignLabel',
|
|
'campaignRate',
|
|
'askiliYan',
|
|
'kategori',
|
|
'urunIlkGrubu',
|
|
'urunAnaGrubu',
|
|
'urunAltGrubu',
|
|
'icerik',
|
|
'karisim'
|
|
]
|
|
const compact = ['image', 'brandGroupSelection', 'marka', 'productCode', 'variantCodes']
|
|
return (leftDetailsExpanded.value ? expanded : compact).filter((x) => visible.has(x))
|
|
})
|
|
const stickyBoundaryColumnName = computed(() => {
|
|
const list = stickyColumnNames.value
|
|
return list.length ? list[list.length - 1] : ''
|
|
})
|
|
const stickyColumnNameSet = computed(() => new Set(stickyColumnNames.value))
|
|
const stickyLeftMap = computed(() => {
|
|
const map = {}
|
|
let left = 0
|
|
for (const name of stickyColumnNames.value) {
|
|
const colDef = allColumns.find((x) => x.name === name)
|
|
if (!colDef) continue
|
|
map[name] = left
|
|
left += extractWidth(colDef.style)
|
|
}
|
|
return map
|
|
})
|
|
const stickyScrollComp = computed(() => {
|
|
const boundaryName = stickyBoundaryColumnName.value
|
|
const boundaryCol = allColumns.find((x) => x.name === boundaryName)
|
|
return ((stickyLeftMap.value[boundaryName] || 0) + extractWidth(boundaryCol?.style)) * 1.2
|
|
})
|
|
|
|
function isStickyCol (name) {
|
|
return stickyColumnNameSet.value.has(name)
|
|
}
|
|
|
|
function isStickyBoundary (name) {
|
|
return name === stickyBoundaryColumnName.value
|
|
}
|
|
|
|
function getHeaderCellStyle (col) {
|
|
const base = col.headerStyle || col.style || ''
|
|
if (!isStickyCol(col.name)) return base
|
|
return `${base};left:${stickyLeftMap.value[col.name] || 0}px;`
|
|
}
|
|
|
|
function getBodyCellStyle (col) {
|
|
const base = col.style || ''
|
|
if (!isStickyCol(col.name)) return base
|
|
return `${base};left:${stickyLeftMap.value[col.name] || 0}px;`
|
|
}
|
|
|
|
function extractWidth (style) {
|
|
const m = String(style || '').match(/width:(\d+)px/)
|
|
return m ? Number(m[1]) : 80
|
|
}
|
|
|
|
function exportCell (row, col) {
|
|
if (col.name === 'image') return row.imageUrl || ''
|
|
if (priceColumnNames.includes(col.name)) return formatPrice(row[col.field])
|
|
if (col.name === 'variantStocks') return formatStock(row.stockQty)
|
|
if (col.name === 'campaignRate') return row.campaignRate ? formatPrice(row.campaignRate) : ''
|
|
return toText(row[col.field])
|
|
}
|
|
|
|
function isExcelNumericColumn (col) {
|
|
return priceColumnNames.includes(col.name) || col.name === 'campaignRate'
|
|
}
|
|
|
|
function excelNumericCell (value) {
|
|
const n = Number(value)
|
|
if (!Number.isFinite(n) || n === 0) return '<td></td>'
|
|
const display = n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
const raw = n.toFixed(2)
|
|
return `<td style="mso-number-format:'0.00';text-align:right;" x:num="${raw}">${escapeHtml(display)}</td>`
|
|
}
|
|
|
|
function exportExcelCellHtml (row, col) {
|
|
if (priceColumnNames.includes(col.name)) return excelNumericCell(row[col.field])
|
|
if (col.name === 'campaignRate') return excelNumericCell(row.campaignRate)
|
|
return `<td>${escapeHtml(exportCell(row, col))}</td>`
|
|
}
|
|
|
|
function exportVisibleExcel () {
|
|
const cols = visibleColumns.value.filter((c) => c.name !== 'image')
|
|
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
|
|
return exportExcelCellHtml(row, c)
|
|
}).join('')}</tr>`).join('')
|
|
const html = `<!doctype html><html xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8"></head><body><table border="1"><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label)}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></body></html>`
|
|
const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `fiyat_listesi_${new Date().toISOString().slice(0, 10)}.xls`
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
a.remove()
|
|
URL.revokeObjectURL(url)
|
|
void notifyExportTaken('excel')
|
|
}
|
|
|
|
function printVisibleRows () {
|
|
const cols = visibleColumns.value
|
|
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
|
|
if (c.name === 'image' && row.imageUrl) return `<td><img src="${row.imageUrl}" class="thumb"></td>`
|
|
return `<td class="${isExcelNumericColumn(c) ? 'num' : ''}">${escapeHtml(exportCell(row, c))}</td>`
|
|
}).join('')}</tr>`).join('')
|
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Fiyat Listesi</title><style>
|
|
@page { size: A3 landscape; margin: 8mm; }
|
|
body { font-family: Arial, sans-serif; font-size: 8px; }
|
|
h1 { font-size: 16px; margin: 0 0 8px; }
|
|
table { border-collapse: collapse; width: 100%; }
|
|
th { background: #957116; color: #fff; }
|
|
th, td { border: 1px solid #ccc; padding: 3px; vertical-align: middle; }
|
|
.num { text-align: right; }
|
|
.thumb { width: 100px; height: 100px; object-fit: cover; }
|
|
</style></head><body><h1>Fiyat Listesi</h1><table><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table><script>window.onload=function(){window.print()}<\/script></body></html>`
|
|
const win = window.open('', '_blank')
|
|
if (!win) return
|
|
win.document.open()
|
|
win.document.write(html)
|
|
win.document.close()
|
|
void notifyExportTaken('pdf')
|
|
}
|
|
|
|
async function notifyExportTaken (format) {
|
|
try {
|
|
await api.post('/order/price-list/export-notify', {
|
|
format,
|
|
row_count: filteredRows.value.length,
|
|
price_fields: [...selectedPriceOptions.value],
|
|
product_codes: [...selectedProductCodes.value],
|
|
campaign_labels: [...selectedCampaignLabels.value],
|
|
first_groups: Array.from(new Set(filteredRows.value.map((row) => toText(row.urunIlkGrubu)).filter(Boolean))).sort((a, b) => a.localeCompare(b, 'tr')),
|
|
urun_ilk_grubu: topUrunIlkGrubu.value || '',
|
|
urun_ana_grubu: topUrunAnaGrubu.value || ''
|
|
}, { timeout: 30000 })
|
|
} catch (err) {
|
|
console.warn('[order-price-list][ui] export notify failed', err?.response?.data || err?.message || err)
|
|
}
|
|
}
|
|
|
|
function getTableMiddleEl () {
|
|
return mainTableRef.value?.$el?.querySelector?.('.q-table__middle') || null
|
|
}
|
|
|
|
function onTopScroll () {
|
|
if (syncingScroll) return
|
|
const middle = getTableMiddleEl()
|
|
const top = topScrollRef.value
|
|
if (!middle || !top) return
|
|
syncingScroll = true
|
|
middle.scrollLeft = top.scrollLeft
|
|
requestAnimationFrame(() => { syncingScroll = false })
|
|
}
|
|
|
|
function bindTableScrollSync () {
|
|
const middle = getTableMiddleEl()
|
|
if (!middle || middle.__orderPriceListScrollBound) return
|
|
middle.__orderPriceListScrollBound = true
|
|
middle.addEventListener('scroll', () => {
|
|
if (syncingScroll) return
|
|
const top = topScrollRef.value
|
|
if (!top) return
|
|
syncingScroll = true
|
|
top.scrollLeft = middle.scrollLeft
|
|
requestAnimationFrame(() => { syncingScroll = false })
|
|
}, { passive: true })
|
|
}
|
|
|
|
function escapeHtml (value) {
|
|
return String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
}
|
|
|
|
watch(selectedProductCodes, (list) => {
|
|
const clean = Array.isArray(list) ? [...new Set(list.map(toText).filter(Boolean))] : []
|
|
if (clean.join('\u0001') !== (Array.isArray(list) ? list.join('\u0001') : '')) {
|
|
selectedProductCodes.value = clean
|
|
}
|
|
})
|
|
|
|
watch([tableMinWidth, rows], async () => {
|
|
await nextTick()
|
|
bindTableScrollSync()
|
|
})
|
|
|
|
onMounted(() => {
|
|
void fetchServerFilterOptions('urunIlkGrubu', '')
|
|
void fetchServerFilterOptions('urunAnaGrubu', '')
|
|
void fetchServerFilterOptions('productCode', '')
|
|
void nextTick(bindTableScrollSync)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.order-price-list-page {
|
|
position: relative;
|
|
height: calc(100vh - 58px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
--pricing-row-height: 108px;
|
|
--pricing-header-height: 88px;
|
|
--pricing-table-height: calc(100vh - 156px);
|
|
}
|
|
|
|
.top-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
align-items: flex-start;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.top-actions-row {
|
|
width: 100%;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.top-bar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 60;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
background: #fff;
|
|
padding-top: 2px;
|
|
padding-bottom: 4px;
|
|
}
|
|
|
|
.top-actions-row--filters,
|
|
.top-actions-row--actions {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.toolbar-group {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 6px;
|
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
border-radius: 4px;
|
|
background: #fff;
|
|
}
|
|
|
|
.toolbar-group--paging {
|
|
gap: 10px;
|
|
}
|
|
|
|
.toolbar-group :deep(.q-btn) {
|
|
min-height: 32px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.toolbar-group :deep(.q-btn__content) {
|
|
gap: 6px;
|
|
padding: 0;
|
|
}
|
|
|
|
.toolbar-group :deep(.q-btn__wrapper) {
|
|
padding: 4px 10px;
|
|
}
|
|
|
|
.table-wrap {
|
|
position: relative;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
border-radius: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
--top-scroll-height: 22px;
|
|
}
|
|
|
|
.product-thumb {
|
|
width: 100px;
|
|
height: 100px;
|
|
border-radius: 4px;
|
|
background: #f4f4f4;
|
|
}
|
|
|
|
.product-thumb-placeholder {
|
|
width: 100px;
|
|
height: 100px;
|
|
border-radius: 4px;
|
|
background: #f4f4f4;
|
|
border: 1px dashed rgba(0, 0, 0, 0.16);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.image-cell {
|
|
padding: 4px !important;
|
|
}
|
|
|
|
.top-x-scroll {
|
|
flex: 0 0 var(--top-scroll-height);
|
|
height: var(--top-scroll-height);
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
background: #f8fafc;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.14);
|
|
scrollbar-gutter: stable;
|
|
z-index: 20;
|
|
}
|
|
|
|
.top-x-scroll-inner {
|
|
height: 18px;
|
|
}
|
|
|
|
.top-x-scroll::-webkit-scrollbar {
|
|
height: 14px;
|
|
}
|
|
|
|
.top-x-scroll::-webkit-scrollbar-track {
|
|
background: #edf1f5;
|
|
}
|
|
|
|
.top-x-scroll::-webkit-scrollbar-thumb {
|
|
background: rgba(25, 118, 210, 0.42);
|
|
border: 3px solid #edf1f5;
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.pane-table {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.pricing-table :deep(.q-table__middle) {
|
|
height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
|
min-height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
|
max-height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
|
overflow: auto !important;
|
|
scrollbar-gutter: stable both-edges;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.pricing-table :deep(.q-table) {
|
|
width: max-content;
|
|
min-width: 100%;
|
|
table-layout: fixed;
|
|
font-size: 11px;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
margin-right: var(--sticky-scroll-comp, 0px);
|
|
}
|
|
|
|
.pricing-table :deep(.q-table__container) {
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
background: transparent !important;
|
|
height: 100% !important;
|
|
}
|
|
|
|
.pricing-table :deep(td),
|
|
.pricing-table :deep(.q-table tbody tr) {
|
|
height: var(--pricing-row-height) !important;
|
|
min-height: var(--pricing-row-height) !important;
|
|
max-height: var(--pricing-row-height) !important;
|
|
line-height: 1.25;
|
|
padding: 0 !important;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
}
|
|
|
|
.pricing-table :deep(td > div),
|
|
.pricing-table :deep(td > .q-td) {
|
|
height: 100% !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
}
|
|
|
|
.pricing-table :deep(td) {
|
|
overflow: hidden !important;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
vertical-align: middle !important;
|
|
}
|
|
|
|
.pricing-table :deep(th),
|
|
.pricing-table :deep(.q-table thead tr),
|
|
.pricing-table :deep(.q-table thead tr.header-row-fixed),
|
|
.pricing-table :deep(.q-table thead th),
|
|
.pricing-table :deep(.q-table thead tr.header-row-fixed > th) {
|
|
height: var(--pricing-header-height) !important;
|
|
min-height: var(--pricing-header-height) !important;
|
|
max-height: var(--pricing-header-height) !important;
|
|
}
|
|
|
|
.pricing-table :deep(th) {
|
|
padding-top: 0;
|
|
padding-bottom: 0;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
overflow-wrap: anywhere;
|
|
overflow: hidden;
|
|
text-align: center;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.pricing-table :deep(.q-table thead th) {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 30;
|
|
background: #fff;
|
|
vertical-align: middle !important;
|
|
}
|
|
|
|
.pricing-table :deep(.sticky-col) {
|
|
position: sticky !important;
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
.pricing-table :deep(thead .sticky-col) {
|
|
z-index: 35 !important;
|
|
}
|
|
|
|
.pricing-table :deep(tbody .sticky-col) {
|
|
z-index: 12 !important;
|
|
background: #fff !important;
|
|
}
|
|
|
|
.pricing-table :deep(.sticky-boundary) {
|
|
border-right: 2px solid rgba(25, 118, 210, 0.18) !important;
|
|
box-shadow: 8px 0 12px -10px rgba(15, 23, 42, 0.55);
|
|
}
|
|
|
|
.pricing-table :deep(tbody td:not(.sticky-col)) {
|
|
position: relative;
|
|
z-index: 1 !important;
|
|
}
|
|
|
|
.pricing-table :deep(tbody td.sticky-col)::after,
|
|
.pricing-table :deep(thead th.sticky-col)::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: inherit;
|
|
z-index: -1;
|
|
}
|
|
|
|
.pricing-table :deep(th.ps-col),
|
|
.pricing-table :deep(td.ps-col) {
|
|
background: #fff;
|
|
color: var(--q-primary);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.pricing-table :deep(td.ps-col),
|
|
.pricing-table :deep(td.usd-col),
|
|
.pricing-table :deep(td.eur-col),
|
|
.pricing-table :deep(td.try-col) {
|
|
vertical-align: middle !important;
|
|
}
|
|
|
|
.pricing-table :deep(th.usd-col),
|
|
.pricing-table :deep(td.usd-col) {
|
|
background: #ecf9f0;
|
|
color: #178a3e;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.pricing-table :deep(th.eur-col),
|
|
.pricing-table :deep(td.eur-col) {
|
|
background: #fdeeee;
|
|
color: #c62828;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.pricing-table :deep(th.try-col),
|
|
.pricing-table :deep(td.try-col) {
|
|
background: #edf4ff;
|
|
color: #1e63c6;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.pricing-table :deep(th.usd-col),
|
|
.pricing-table :deep(th.eur-col),
|
|
.pricing-table :deep(th.try-col),
|
|
.pricing-table :deep(td.usd-col),
|
|
.pricing-table :deep(td.eur-col),
|
|
.pricing-table :deep(td.try-col) {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.pricing-table :deep(td.campaign-price-col),
|
|
.pricing-table :deep(th.campaign-price-col) {
|
|
background: #fff3f1;
|
|
color: #c62828;
|
|
font-weight: 800;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.pricing-table :deep(td.center-col) {
|
|
text-align: center !important;
|
|
}
|
|
|
|
.pricing-table :deep(td.karisim-wrap-col) {
|
|
white-space: normal !important;
|
|
text-overflow: clip !important;
|
|
word-break: break-word;
|
|
overflow-wrap: anywhere;
|
|
vertical-align: top !important;
|
|
text-align: left !important;
|
|
font-size: 9px;
|
|
line-height: 1.15;
|
|
padding: 6px 4px !important;
|
|
align-content: flex-start;
|
|
}
|
|
|
|
.pricing-table :deep(td.karisim-wrap-col .q-td__content),
|
|
.pricing-table :deep(td.karisim-wrap-col > div) {
|
|
align-items: flex-start !important;
|
|
justify-content: flex-start !important;
|
|
white-space: normal !important;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header-with-filter {
|
|
display: grid;
|
|
grid-template-columns: 1fr 20px;
|
|
align-items: center;
|
|
column-gap: 4px;
|
|
height: 100%;
|
|
line-height: 1.25;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header-with-filter > span {
|
|
min-width: 0;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
text-align: center;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
overflow-wrap: anywhere;
|
|
font-weight: 800;
|
|
line-height: 1.12;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 4;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.cell-text {
|
|
display: block;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
line-height: 1.1;
|
|
padding-top: 0;
|
|
}
|
|
|
|
.price-cell-text {
|
|
width: 100%;
|
|
text-align: right;
|
|
font-weight: 800;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.campaign-price-text {
|
|
color: #c62828;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.campaign-cell-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 22px;
|
|
}
|
|
|
|
.campaign-text {
|
|
display: block;
|
|
max-width: 100%;
|
|
color: #c62828;
|
|
font-weight: 900;
|
|
line-height: 1.12;
|
|
text-align: center;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.campaign-rate-text {
|
|
width: 100%;
|
|
color: #c62828;
|
|
font-weight: 900;
|
|
text-align: center;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.header-filter-btn {
|
|
width: 20px;
|
|
height: 20px;
|
|
min-width: 20px;
|
|
justify-self: end;
|
|
}
|
|
|
|
.header-filter-ghost {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.excel-filter-menu,
|
|
.filter-menu-panel {
|
|
min-width: 230px;
|
|
padding: 8px;
|
|
}
|
|
|
|
.excel-filter-select :deep(.q-field__control) {
|
|
min-height: 30px;
|
|
}
|
|
|
|
.excel-filter-options {
|
|
max-height: 220px;
|
|
margin-top: 8px;
|
|
overflow: auto;
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.excel-filter-option {
|
|
min-height: 32px;
|
|
}
|
|
|
|
.excel-filter-empty {
|
|
padding: 10px 8px;
|
|
color: #607d8b;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.page-busy-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 30000;
|
|
background: rgba(255, 255, 255, 0.68);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.page-busy-label {
|
|
font-weight: 600;
|
|
color: #4c4c4c;
|
|
}
|
|
|
|
.empty-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255, 255, 255, 0.72);
|
|
}
|
|
|
|
.empty-overlay-inner {
|
|
border: 1px solid #ddd;
|
|
background: #fff;
|
|
border-radius: 6px;
|
|
padding: 16px 20px;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.product-card-dialog {
|
|
--pc-media-h: calc(100vh - 180px);
|
|
--pc-media-w: min(74vw, 1220px);
|
|
background: #f9f8f5;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
:deep(.product-card-dialog > .q-card__section:last-child) {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.product-card-content {
|
|
display: grid;
|
|
grid-template-columns: minmax(360px, 420px) minmax(760px, 1fr);
|
|
gap: 14px;
|
|
align-items: stretch;
|
|
justify-content: start;
|
|
height: 100%;
|
|
}
|
|
|
|
.product-card-images {
|
|
grid-column: 2;
|
|
grid-row: 1;
|
|
height: var(--pc-media-h);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.product-card-carousel {
|
|
width: var(--pc-media-w);
|
|
height: 100%;
|
|
max-width: 100%;
|
|
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
|
|
border: 1px solid #e4dac7;
|
|
}
|
|
|
|
.dialog-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.dialog-image-stage {
|
|
width: var(--pc-media-w);
|
|
max-width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
border-radius: 10px;
|
|
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.dialog-image-empty {
|
|
width: var(--pc-media-w);
|
|
max-width: 100%;
|
|
height: var(--pc-media-h);
|
|
border: 1px dashed #c5b28d;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #faf6ee;
|
|
}
|
|
|
|
.product-card-fields {
|
|
grid-column: 1;
|
|
grid-row: 1;
|
|
border: 1px solid #e4dac7;
|
|
border-radius: 12px;
|
|
background: linear-gradient(180deg, #ffffff 0%, #fdfaf4 100%);
|
|
padding: 12px;
|
|
height: var(--pc-media-h);
|
|
overflow: auto;
|
|
}
|
|
|
|
.field-row {
|
|
display: grid;
|
|
grid-template-columns: 150px 1fr;
|
|
gap: 8px;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid #efe5d5;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.field-row.field-row-head {
|
|
background: #f8f3e9;
|
|
border: 1px solid #e6dccb;
|
|
border-radius: 8px;
|
|
padding: 8px 10px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.field-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.field-row .k {
|
|
color: #6b5a33;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.field-row .v {
|
|
color: #1f1f1f;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.product-card-section {
|
|
position: relative;
|
|
margin-top: 12px;
|
|
border: 1px solid #e6dccb;
|
|
border-radius: 8px;
|
|
background: #fffdf8;
|
|
padding: 10px;
|
|
}
|
|
|
|
.product-card-section-title {
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
color: #5e4a22;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.price-info-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 4px;
|
|
}
|
|
|
|
.price-info-row {
|
|
display: grid;
|
|
grid-template-columns: 70px 1fr 1fr;
|
|
gap: 6px;
|
|
align-items: center;
|
|
min-height: 26px;
|
|
padding: 4px 6px;
|
|
border: 1px solid #f0e5d2;
|
|
border-radius: 6px;
|
|
background: #fff;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.price-label {
|
|
font-weight: 800;
|
|
color: #6b5a33;
|
|
}
|
|
|
|
.price-value,
|
|
.price-campaign {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.price-campaign {
|
|
color: #b13a2b;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.size-stock-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
|
|
gap: 6px;
|
|
}
|
|
|
|
.size-stock-cell {
|
|
min-height: 42px;
|
|
border: 1px solid #eadfca;
|
|
border-radius: 6px;
|
|
background: #fff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 4px;
|
|
}
|
|
|
|
.size-label {
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
color: #6b5a33;
|
|
}
|
|
|
|
.size-qty {
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
color: #1f1f1f;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.product-card-empty-text {
|
|
color: #7a6d55;
|
|
font-size: 12px;
|
|
padding: 6px 0;
|
|
}
|
|
|
|
.image-fullscreen-dialog {
|
|
background: #f4f0e2;
|
|
}
|
|
|
|
.image-fullscreen-body {
|
|
height: calc(100vh - 72px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.image-fullscreen-carousel {
|
|
width: min(98vw, 1500px);
|
|
}
|
|
|
|
.image-fullscreen-stage {
|
|
width: min(96vw, 1400px);
|
|
height: calc(100vh - 120px);
|
|
border-radius: 10px;
|
|
background: linear-gradient(180deg, #f1e7d3 0%, #e9dcc4 100%);
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.image-fullscreen-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.q-btn,
|
|
.q-icon,
|
|
.cursor-pointer {
|
|
cursor: pointer !important;
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.product-card-content {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.product-card-images,
|
|
.product-card-fields {
|
|
grid-column: 1;
|
|
grid-row: auto;
|
|
}
|
|
}
|
|
</style>
|