Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -279,6 +279,19 @@ const menuItems = [
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Fiyatlandırma',
|
||||
icon: 'request_quote',
|
||||
|
||||
children: [
|
||||
{
|
||||
label: 'Ürün Fiyatlandırma',
|
||||
to: '/app/pricing/product-pricing',
|
||||
permission: 'order:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Sistem',
|
||||
icon: 'settings',
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md full-width order-prod-page">
|
||||
<div class="row items-center justify-between page-header">
|
||||
<div>
|
||||
<div class="text-h6 text-weight-bold">Uretime Verilen Urunleri Guncelle</div>
|
||||
<div class="row items-center justify-between page-header">
|
||||
<div>
|
||||
<div class="text-h6 text-weight-bold">Uretime Verilen Urunleri Guncelle</div>
|
||||
</div>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="store.loading"
|
||||
@click="refreshAll"
|
||||
/>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
color="secondary"
|
||||
icon="save"
|
||||
label="Secili Degisiklikleri Kaydet"
|
||||
:loading="store.saving"
|
||||
@click="onBulkSubmit"
|
||||
/>
|
||||
</div>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="store.loading"
|
||||
@click="refreshAll"
|
||||
/>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
color="secondary"
|
||||
icon="save"
|
||||
label="Secili Degisiklikleri Kaydet"
|
||||
:loading="store.saving"
|
||||
@click="onBulkSubmit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar row q-col-gutter-md">
|
||||
<div class="col-5">
|
||||
@@ -60,17 +60,17 @@
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<q-input
|
||||
:model-value="formatDate(header?.AverageDueDate)"
|
||||
v-model="headerAverageDueDate"
|
||||
label="Tahmini Termin Tarihi"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
type="date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<q-table
|
||||
<div class="table-wrap">
|
||||
<q-table
|
||||
class="q-mt-md prod-table"
|
||||
flat
|
||||
bordered
|
||||
@@ -85,160 +85,172 @@
|
||||
:table-style="{ tableLayout: 'fixed', width: '100%' }"
|
||||
hide-bottom
|
||||
>
|
||||
<template #header-cell-select="props">
|
||||
<q-th :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
:model-value="allSelectedVisible"
|
||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #body-cell-select="props">
|
||||
<q-td :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
:model-value="!!selectedMap[props.row.RowKey]"
|
||||
@update:model-value="(val) => toggleRowSelection(props.row.RowKey, val)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewItemCode="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewItemEntryMode"
|
||||
dense
|
||||
filled
|
||||
emit-value
|
||||
map-options
|
||||
:options="newItemEntryModeOptions"
|
||||
label="Kod Giris Tipi"
|
||||
@update:model-value="val => onNewItemEntryModeChange(props.row, val)"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="props.row.NewItemEntryMode === 'selected'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="productCodeSelectOptions"
|
||||
label="Eski Kod Sec"
|
||||
@filter="onFilterProductCode"
|
||||
@update:model-value="val => onSelectProduct(props.row, val)"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-else-if="props.row.NewItemEntryMode === 'typed'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
maxlength="13"
|
||||
placeholder="X999-XX99999"
|
||||
label="Yeni Kod Ekle"
|
||||
:class="newItemInputClass(props.row)"
|
||||
@update:model-value="val => onNewItemChange(props.row, val, 'typed')"
|
||||
/>
|
||||
|
||||
<div v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'" class="q-mt-xs row items-center no-wrap">
|
||||
<q-badge :color="newItemBadgeColor(props.row)" text-color="white">
|
||||
{{ newItemBadgeLabel(props.row) }}
|
||||
</q-badge>
|
||||
<span class="text-caption q-ml-sm text-grey-8">{{ newItemHintText(props.row) }}</span>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode === 'new'"
|
||||
class="q-ml-sm"
|
||||
dense
|
||||
flat
|
||||
<template #header-cell-select="props">
|
||||
<q-th :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="warning"
|
||||
label="Urun Boyutlandirma"
|
||||
@click="openCdItemDialog(props.row.NewItemCode)"
|
||||
:model-value="allSelectedVisible"
|
||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'"
|
||||
class="q-ml-xs"
|
||||
dense
|
||||
flat
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #body-cell-select="props">
|
||||
<q-td :props="props" class="text-center" style="width: 44px">
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="primary"
|
||||
label="Urun Ozellikleri"
|
||||
@click="openAttributeDialog(props.row.NewItemCode)"
|
||||
:model-value="!!selectedMap[props.row.RowKey]"
|
||||
@update:model-value="(val) => toggleRowSelection(props.row.RowKey, val)"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewColor="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewColor"
|
||||
:options="getColorOptions(props.row)"
|
||||
option-label="colorLabel"
|
||||
option-value="color_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewColorChange(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-NewItemCode="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewItemEntryMode"
|
||||
dense
|
||||
filled
|
||||
emit-value
|
||||
map-options
|
||||
:options="newItemEntryModeOptions"
|
||||
label="Kod Giris Tipi"
|
||||
@update:model-value="val => onNewItemEntryModeChange(props.row, val)"
|
||||
/>
|
||||
|
||||
<template #body-cell-NewDim2="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewDim2"
|
||||
:options="getSecondColorOptions(props.row)"
|
||||
option-label="item_dim2_label"
|
||||
option-value="item_dim2_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni 2. Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewDim2Change(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
<q-select
|
||||
v-if="props.row.NewItemEntryMode === 'selected'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="productCodeSelectOptions"
|
||||
label="Eski Kod Sec"
|
||||
@filter="onFilterProductCode"
|
||||
@update:model-value="val => onSelectProduct(props.row, val)"
|
||||
/>
|
||||
|
||||
<template #body-cell-NewDesc="props">
|
||||
<q-td :props="props" class="cell-new">
|
||||
<q-input
|
||||
v-model="props.row.NewDesc"
|
||||
dense
|
||||
filled
|
||||
type="textarea"
|
||||
autogrow
|
||||
label="Yeni Aciklama"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
<q-input
|
||||
v-else-if="props.row.NewItemEntryMode === 'typed'"
|
||||
class="q-mt-xs"
|
||||
v-model="props.row.NewItemCode"
|
||||
dense
|
||||
filled
|
||||
maxlength="13"
|
||||
placeholder="X999-XX99999"
|
||||
label="Yeni Kod Ekle"
|
||||
:class="newItemInputClass(props.row)"
|
||||
@update:model-value="val => onNewItemChange(props.row, val, 'typed')"
|
||||
/>
|
||||
|
||||
<div v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'" class="q-mt-xs row items-center no-wrap">
|
||||
<q-badge :color="newItemBadgeColor(props.row)" text-color="white">
|
||||
{{ newItemBadgeLabel(props.row) }}
|
||||
</q-badge>
|
||||
<span class="text-caption q-ml-sm text-grey-8">{{ newItemHintText(props.row) }}</span>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode === 'new'"
|
||||
class="q-ml-sm"
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
color="warning"
|
||||
label="Urun Boyutlandirma"
|
||||
@click="openCdItemDialog(props.row.NewItemCode)"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="props.row.NewItemMode && props.row.NewItemMode !== 'empty'"
|
||||
class="q-ml-xs"
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
color="primary"
|
||||
label="Urun Ozellikleri"
|
||||
@click="openAttributeDialog(props.row.NewItemCode)"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewColor="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewColor"
|
||||
:options="getColorOptions(props.row)"
|
||||
option-label="colorLabel"
|
||||
option-value="color_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewColorChange(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewDim2="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.NewDim2"
|
||||
:options="getSecondColorOptions(props.row)"
|
||||
option-label="item_dim2_label"
|
||||
option-value="item_dim2_code"
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Yeni 2. Renk"
|
||||
:disable="isColorSelectionLocked(props.row)"
|
||||
@update:model-value="() => onNewDim2Change(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewDueDate="props">
|
||||
<q-td :props="props">
|
||||
<q-input
|
||||
v-model="props.row.NewDueDate"
|
||||
dense
|
||||
filled
|
||||
type="date"
|
||||
label="Yeni Termin"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-NewDesc="props">
|
||||
<q-td :props="props" class="cell-new">
|
||||
<q-input
|
||||
v-model="props.row.NewDesc"
|
||||
dense
|
||||
filled
|
||||
type="textarea"
|
||||
autogrow
|
||||
label="Yeni Aciklama"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
|
||||
Hata: {{ store.error }}
|
||||
@@ -255,6 +267,38 @@
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<div class="row q-col-gutter-sm items-center q-mb-md bg-grey-2 q-pa-sm rounded-borders">
|
||||
<div class="col-12 col-md-8">
|
||||
<q-select
|
||||
v-model="copySourceCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
label="Benzer Eski Urun Kodundan Getir"
|
||||
placeholder="Kopyalanacak urun kodunu yazin"
|
||||
:options="productCodeSelectOptions"
|
||||
@filter="onFilterProductCode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="content_copy"
|
||||
label="Ozellikleri Kopyala"
|
||||
class="full-width"
|
||||
:disable="!copySourceCode"
|
||||
@click="copyFromOldProduct('cdItem')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select v-model="cdItemDraftForm.ItemDimTypeCode" dense filled use-input fill-input hide-selected input-debounce="0" emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemDimTypeCodes')" label="Boyut Secenekleri" />
|
||||
@@ -280,6 +324,40 @@
|
||||
<q-badge color="primary">{{ attributeTargetCode || '-' }}</q-badge>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<div class="row q-col-gutter-sm items-center q-mb-md bg-grey-2 q-pa-sm rounded-borders">
|
||||
<div class="col-12 col-md-8">
|
||||
<q-select
|
||||
v-model="copySourceCode"
|
||||
dense
|
||||
filled
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="0"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
label="Benzer Eski Urun Kodundan Getir"
|
||||
placeholder="Kopyalanacak urun kodunu yazin"
|
||||
:options="productCodeSelectOptions"
|
||||
@filter="onFilterProductCode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="content_copy"
|
||||
label="Ozellikleri Kopyala"
|
||||
class="full-width"
|
||||
:disable="!copySourceCode"
|
||||
@click="copyFromOldProduct('attributes')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section style="max-height: 68vh; overflow: auto;">
|
||||
<div
|
||||
v-for="(row, idx) in attributeRows"
|
||||
@@ -353,8 +431,10 @@ const rows = ref([])
|
||||
const descFilter = ref('')
|
||||
const productOptions = ref([])
|
||||
const selectedMap = ref({})
|
||||
const headerAverageDueDate = ref('')
|
||||
const cdItemDialogOpen = ref(false)
|
||||
const cdItemTargetCode = ref('')
|
||||
const copySourceCode = ref(null)
|
||||
const cdItemDraftForm = ref(createEmptyCdItemDraft(''))
|
||||
const attributeDialogOpen = ref(false)
|
||||
const attributeTargetCode = ref('')
|
||||
@@ -363,13 +443,16 @@ const attributeRows = ref([])
|
||||
const columns = [
|
||||
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false, style: 'width:44px;', headerStyle: 'width:44px;' },
|
||||
{ name: 'OldItemCode', label: 'Eski Urun Kodu', field: 'OldItemCode', align: 'left', sortable: true, style: 'min-width:90px;white-space:normal', headerStyle: 'min-width:90px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColor', align: 'left', sortable: true, style: 'min-width:80px;white-space:normal', headerStyle: 'min-width:80px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColorLabel', align: 'left', sortable: true, style: 'min-width:120px;white-space:normal', headerStyle: 'min-width:120px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldDim2', label: 'Eski 2. Renk', field: 'OldDim2', align: 'left', sortable: true, style: 'min-width:80px;white-space:normal', headerStyle: 'min-width:80px;white-space:normal', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldDesc', label: 'Eski Aciklama', field: 'OldDesc', align: 'left', sortable: false, style: 'min-width:130px;', headerStyle: 'min-width:130px;', headerClasses: 'col-old col-desc', classes: 'col-old col-desc' },
|
||||
{ name: 'OldSizes', label: 'Bedenler', field: 'OldSizesLabel', align: 'left', sortable: false, style: 'min-width:90px;', headerStyle: 'min-width:90px;', headerClasses: 'col-old col-wrap', classes: 'col-old col-wrap' },
|
||||
{ name: 'OldTotalQty', label: 'Siparis Adedi', field: 'OldTotalQtyLabel', align: 'right', sortable: false, style: 'min-width:90px;', headerStyle: 'min-width:90px;', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'OldDueDate', label: 'Eski Termin', field: 'OldDueDate', align: 'left', sortable: true, style: 'min-width:100px;', headerStyle: 'min-width:100px;', headerClasses: 'col-old', classes: 'col-old' },
|
||||
{ name: 'NewItemCode', label: 'Yeni Urun Kodu', field: 'NewItemCode', align: 'left', sortable: false, style: 'min-width:130px;', headerStyle: 'min-width:130px;', headerClasses: 'col-new col-new-first', classes: 'col-new col-new-first' },
|
||||
{ name: 'NewColor', label: 'Yeni Urun Rengi', field: 'NewColor', align: 'left', sortable: false, style: 'min-width:100px;', headerStyle: 'min-width:100px;', headerClasses: 'col-new', classes: 'col-new' },
|
||||
{ name: 'NewDim2', label: 'Yeni 2. Renk', field: 'NewDim2', align: 'left', sortable: false, style: 'min-width:100px;', headerStyle: 'min-width:100px;', headerClasses: 'col-new', classes: 'col-new' },
|
||||
{ name: 'NewDueDate', label: 'Yeni Termin', field: 'NewDueDate', align: 'left', sortable: false, style: 'min-width:120px;', headerStyle: 'min-width:120px;', headerClasses: 'col-new', classes: 'col-new' },
|
||||
{ name: 'NewDesc', label: 'Yeni Aciklama', field: 'NewDesc', align: 'left', sortable: false, style: 'min-width:140px;', headerStyle: 'min-width:140px;', headerClasses: 'col-new col-desc', classes: 'col-new col-desc' }
|
||||
]
|
||||
|
||||
@@ -403,6 +486,23 @@ function formatDate (val) {
|
||||
return text.length >= 10 ? text.slice(0, 10) : text
|
||||
}
|
||||
|
||||
function normalizeDateInput (val) {
|
||||
return formatDate(val || '')
|
||||
}
|
||||
|
||||
const hasHeaderAverageDueDateChange = computed(() => (
|
||||
normalizeDateInput(headerAverageDueDate.value) !==
|
||||
normalizeDateInput(header.value?.AverageDueDate)
|
||||
))
|
||||
|
||||
watch(
|
||||
() => header.value?.AverageDueDate,
|
||||
(value) => {
|
||||
headerAverageDueDate.value = normalizeDateInput(value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const needle = normalizeSearchText(descFilter.value)
|
||||
if (!needle) return rows.value
|
||||
@@ -457,6 +557,19 @@ function applyNewItemVisualState (row, source = 'typed') {
|
||||
row.NewItemSource = info.mode === 'empty' ? '' : source
|
||||
}
|
||||
|
||||
function syncRowsForKnownExistingCode (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
for (const row of (rows.value || [])) {
|
||||
if (String(row?.NewItemCode || '').trim().toUpperCase() !== code) continue
|
||||
row.NewItemCode = code
|
||||
row.NewItemMode = 'existing'
|
||||
if (!row.NewItemEntryMode) {
|
||||
row.NewItemEntryMode = row.NewItemSource === 'selected' ? 'selected' : 'typed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function newItemInputClass (row) {
|
||||
return {
|
||||
'new-item-existing': row?.NewItemMode === 'existing',
|
||||
@@ -579,9 +692,7 @@ function isNewCodeSetupComplete (itemCode) {
|
||||
|
||||
function isColorSelectionLocked (row) {
|
||||
const code = String(row?.NewItemCode || '').trim().toUpperCase()
|
||||
if (!code) return true
|
||||
if (row?.NewItemMode !== 'new') return false
|
||||
return !isNewCodeSetupComplete(code)
|
||||
return !code
|
||||
}
|
||||
|
||||
function openNewCodeSetupFlow (itemCode) {
|
||||
@@ -746,25 +857,39 @@ function collectLinesFromRows (selectedRows) {
|
||||
NewItemCode: String(row.NewItemCode || '').trim().toUpperCase(),
|
||||
NewColor: normalizeShortCode(row.NewColor, 3),
|
||||
NewDim2: normalizeShortCode(row.NewDim2, 3),
|
||||
NewDesc: mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc)
|
||||
NewDesc: mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc),
|
||||
OldDueDate: row.OldDueDate || '',
|
||||
NewDueDate: row.NewDueDate || ''
|
||||
}
|
||||
|
||||
const oldItemCode = String(row.OldItemCode || '').trim().toUpperCase()
|
||||
const oldColor = normalizeShortCode(row.OldColor, 3)
|
||||
const oldDim2 = normalizeShortCode(row.OldDim2, 3)
|
||||
const oldDesc = String(row.OldDesc || '').trim()
|
||||
const oldDueDateValue = row.OldDueDate || ''
|
||||
const newDueDateValue = row.NewDueDate || ''
|
||||
|
||||
const hasChange = (
|
||||
baseLine.NewItemCode !== oldItemCode ||
|
||||
baseLine.NewColor !== oldColor ||
|
||||
baseLine.NewDim2 !== oldDim2 ||
|
||||
String(baseLine.NewDesc || '').trim() !== oldDesc
|
||||
String(baseLine.NewDesc || '').trim() !== oldDesc ||
|
||||
newDueDateValue !== oldDueDateValue
|
||||
)
|
||||
if (!hasChange) continue
|
||||
|
||||
for (const id of (row.OrderLineIDs || [])) {
|
||||
const orderLines = Array.isArray(row.OrderLines) && row.OrderLines.length
|
||||
? row.OrderLines
|
||||
: (row.OrderLineIDs || []).map(id => ({
|
||||
OrderLineID: id,
|
||||
ItemDim1Code: ''
|
||||
}))
|
||||
|
||||
for (const line of orderLines) {
|
||||
lines.push({
|
||||
OrderLineID: id,
|
||||
...baseLine
|
||||
...baseLine,
|
||||
OrderLineID: line?.OrderLineID,
|
||||
ItemDim1Code: store.toPayloadDim1Code(row, line?.ItemDim1Code || '')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -830,9 +955,67 @@ function isDummyLookupOption (key, codeRaw, descRaw) {
|
||||
return false
|
||||
}
|
||||
|
||||
async function copyFromOldProduct (targetType = 'cdItem') {
|
||||
const sourceCode = String(copySourceCode.value || '').trim().toUpperCase()
|
||||
if (!sourceCode) return
|
||||
|
||||
$q.loading.show({ message: 'Ozellikler kopyalaniyor...' })
|
||||
try {
|
||||
if (targetType === 'cdItem') {
|
||||
const data = await store.fetchCdItemByCode(sourceCode)
|
||||
if (data) {
|
||||
const targetCode = cdItemTargetCode.value
|
||||
const draft = createEmptyCdItemDraft(targetCode)
|
||||
for (const k of Object.keys(draft)) {
|
||||
if (data[k] !== undefined && data[k] !== null) {
|
||||
draft[k] = String(data[k])
|
||||
}
|
||||
}
|
||||
cdItemDraftForm.value = draft
|
||||
persistCdItemDraft()
|
||||
$q.notify({ type: 'positive', message: 'Boyutlandirma bilgileri kopyalandi.' })
|
||||
} else {
|
||||
$q.notify({ type: 'warning', message: 'Kaynak urun bilgisi bulunamadi.' })
|
||||
}
|
||||
} else if (targetType === 'attributes') {
|
||||
const data = await store.fetchProductItemAttributes(sourceCode, 1, true)
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
// Mevcut attributeRows uzerindeki degerleri guncelle
|
||||
for (const row of attributeRows.value) {
|
||||
const sourceAttr = data.find(d => Number(d.attribute_type_code || d.AttributeTypeCode) === Number(row.AttributeTypeCodeNumber))
|
||||
if (sourceAttr) {
|
||||
const attrCode = String(sourceAttr.attribute_code || sourceAttr.AttributeCode || '').trim()
|
||||
if (attrCode) {
|
||||
// Seceneklerde var mi kontrol et, yoksa ekle (UI'da gorunmesi icin)
|
||||
if (!row.AllOptions.some(opt => String(opt.value).trim() === attrCode)) {
|
||||
row.AllOptions.unshift({ value: attrCode, label: attrCode })
|
||||
row.Options = [...row.AllOptions]
|
||||
}
|
||||
row.AttributeCode = attrCode
|
||||
}
|
||||
}
|
||||
}
|
||||
const targetCode = String(attributeTargetCode.value || '').trim().toUpperCase()
|
||||
if (targetCode) {
|
||||
store.setProductAttributeDraft(targetCode, JSON.parse(JSON.stringify(attributeRows.value || [])))
|
||||
}
|
||||
$q.notify({ type: 'positive', message: 'Urun ozellikleri kopyalandi.' })
|
||||
} else {
|
||||
$q.notify({ type: 'warning', message: 'Kaynak urun ozellikleri bulunamadi.' })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[OrderProductionUpdate] copyFromOldProduct failed', err)
|
||||
$q.notify({ type: 'negative', message: 'Kopyalama sirasinda hata olustu.' })
|
||||
} finally {
|
||||
$q.loading.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function openCdItemDialog (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
copySourceCode.value = null
|
||||
await store.fetchCdItemLookups()
|
||||
|
||||
cdItemTargetCode.value = code
|
||||
@@ -848,6 +1031,13 @@ async function openCdItemDialog (itemCode) {
|
||||
cdItemDialogOpen.value = true
|
||||
}
|
||||
|
||||
function persistCdItemDraft () {
|
||||
const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value)
|
||||
if (!payload.ItemCode) return null
|
||||
store.setCdItemDraft(payload.ItemCode, payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
function normalizeCdItemDraftForPayload (draftRaw) {
|
||||
const d = draftRaw || {}
|
||||
const toIntOrNil = (v) => {
|
||||
@@ -882,12 +1072,16 @@ function normalizeCdItemDraftForPayload (draftRaw) {
|
||||
}
|
||||
|
||||
async function saveCdItemDraft () {
|
||||
const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value)
|
||||
if (!payload.ItemCode) {
|
||||
const payload = persistCdItemDraft()
|
||||
if (!payload?.ItemCode) {
|
||||
$q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' })
|
||||
return
|
||||
}
|
||||
store.setCdItemDraft(payload.ItemCode, payload)
|
||||
console.info('[OrderProductionUpdate] saveCdItemDraft', {
|
||||
code: payload.ItemCode,
|
||||
itemDimTypeCode: payload.ItemDimTypeCode,
|
||||
productHierarchyID: payload.ProductHierarchyID
|
||||
})
|
||||
cdItemDialogOpen.value = false
|
||||
await openAttributeDialog(payload.ItemCode)
|
||||
}
|
||||
@@ -981,6 +1175,7 @@ function mergeAttributeDraftWithLookupOptions (draftRows, lookupRows) {
|
||||
async function openAttributeDialog (itemCode) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
copySourceCode.value = null
|
||||
attributeTargetCode.value = code
|
||||
const existingDraft = store.getProductAttributeDraft(code)
|
||||
const modeInfo = store.classifyItemCode(code)
|
||||
@@ -1001,6 +1196,10 @@ async function openAttributeDialog (itemCode) {
|
||||
code,
|
||||
dbCurrentCount: Array.isArray(dbCurrent) ? dbCurrent.length : 0
|
||||
})
|
||||
if (Array.isArray(dbCurrent) && dbCurrent.length) {
|
||||
store.markItemCodeKnownExisting(code, true)
|
||||
syncRowsForKnownExistingCode(code)
|
||||
}
|
||||
|
||||
const dbMap = new Map(
|
||||
(dbCurrent || []).map(x => [
|
||||
@@ -1026,7 +1225,7 @@ async function openAttributeDialog (itemCode) {
|
||||
}
|
||||
})
|
||||
|
||||
const useDraft = modeInfo.mode !== 'existing' && Array.isArray(existingDraft) && existingDraft.length
|
||||
const useDraft = Array.isArray(existingDraft) && existingDraft.length
|
||||
attributeRows.value = useDraft
|
||||
? JSON.parse(JSON.stringify(mergeAttributeDraftWithLookupOptions(existingDraft, baseRows)))
|
||||
: JSON.parse(JSON.stringify(baseRows))
|
||||
@@ -1050,9 +1249,7 @@ async function openAttributeDialog (itemCode) {
|
||||
row.Options = [...row.AllOptions]
|
||||
}
|
||||
}
|
||||
if (modeInfo.mode === 'existing') {
|
||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows)))
|
||||
} else if ((!existingDraft || !existingDraft.length) && baseRows.length) {
|
||||
if ((!existingDraft || !existingDraft.length) && baseRows.length) {
|
||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows)))
|
||||
}
|
||||
attributeDialogOpen.value = true
|
||||
@@ -1081,6 +1278,26 @@ function saveAttributeDraft () {
|
||||
$q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' })
|
||||
}
|
||||
|
||||
watch(
|
||||
cdItemDraftForm,
|
||||
() => {
|
||||
if (!cdItemDialogOpen.value) return
|
||||
persistCdItemDraft()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
attributeRows,
|
||||
(rows) => {
|
||||
if (!attributeDialogOpen.value) return
|
||||
const code = String(attributeTargetCode.value || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(rows || [])))
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
async function collectProductAttributesFromSelectedRows (selectedRows) {
|
||||
const codeSet = [...new Set(
|
||||
(selectedRows || [])
|
||||
@@ -1205,7 +1422,7 @@ async function collectProductAttributesFromSelectedRows (selectedRows) {
|
||||
return { errMsg: '', productAttributes: out }
|
||||
}
|
||||
|
||||
function collectCdItemsFromSelectedRows (selectedRows) {
|
||||
async function collectCdItemsFromSelectedRows (selectedRows) {
|
||||
const codes = [...new Set(
|
||||
(selectedRows || [])
|
||||
.filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim())
|
||||
@@ -1215,7 +1432,16 @@ function collectCdItemsFromSelectedRows (selectedRows) {
|
||||
|
||||
const out = []
|
||||
for (const code of codes) {
|
||||
const draft = store.getCdItemDraft(code)
|
||||
let draft = store.getCdItemDraft(code)
|
||||
if (!draft) {
|
||||
const existingCdItem = await store.fetchCdItemByCode(code)
|
||||
if (existingCdItem) {
|
||||
store.markItemCodeKnownExisting(code, true)
|
||||
syncRowsForKnownExistingCode(code)
|
||||
draft = normalizeCdItemDraftForPayload(existingCdItem)
|
||||
store.setCdItemDraft(code, draft)
|
||||
}
|
||||
}
|
||||
if (!draft) {
|
||||
return { errMsg: `${code} icin cdItem bilgisi eksik`, cdItems: [] }
|
||||
}
|
||||
@@ -1235,11 +1461,49 @@ function buildMailLineLabelFromRow (row) {
|
||||
return [item, colorPart, desc].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function buildUpdateMailLineLabelFromRow (row) {
|
||||
const newItem = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase()
|
||||
const newColor = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase()
|
||||
const newDim2 = String(row?.NewDim2 || row?.OldDim2 || '').trim().toUpperCase()
|
||||
const desc = mergeDescWithAutoNote(row, row?.NewDesc || row?.OldDesc || '')
|
||||
|
||||
if (!newItem) return ''
|
||||
const colorPart = newDim2 ? `${newColor}-${newDim2}` : newColor
|
||||
return [newItem, colorPart, desc].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function buildDueDateChangeRowsFromSelectedRows (selectedRows) {
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
|
||||
for (const row of (selectedRows || [])) {
|
||||
const itemCode = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase()
|
||||
const colorCode = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase()
|
||||
const itemDim2Code = String(row?.NewDim2 || row?.OldDim2 || '').trim().toUpperCase()
|
||||
const oldDueDate = formatDate(row?.OldDueDate)
|
||||
const newDueDate = formatDate(row?.NewDueDate)
|
||||
if (!itemCode || !newDueDate || oldDueDate === newDueDate) continue
|
||||
|
||||
const key = [itemCode, colorCode, itemDim2Code, oldDueDate, newDueDate].join('||')
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
out.push({
|
||||
itemCode,
|
||||
colorCode,
|
||||
itemDim2Code,
|
||||
oldDueDate,
|
||||
newDueDate
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
function buildProductionUpdateMailPayload (selectedRows) {
|
||||
const updatedItems = [
|
||||
...new Set(
|
||||
(selectedRows || [])
|
||||
.map(buildMailLineLabelFromRow)
|
||||
.map(buildUpdateMailLineLabelFromRow)
|
||||
.filter(Boolean)
|
||||
)
|
||||
]
|
||||
@@ -1248,10 +1512,29 @@ function buildProductionUpdateMailPayload (selectedRows) {
|
||||
operation: 'update',
|
||||
deletedItems: [],
|
||||
updatedItems,
|
||||
addedItems: []
|
||||
addedItems: [],
|
||||
dueDateChanges: buildDueDateChangeRowsFromSelectedRows(selectedRows)
|
||||
}
|
||||
}
|
||||
|
||||
function formatBarcodeValidationMessages (validations) {
|
||||
return (Array.isArray(validations) ? validations : [])
|
||||
.map(v => String(v?.message || '').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function showBarcodeValidationDialog (validations) {
|
||||
const messages = formatBarcodeValidationMessages(validations)
|
||||
if (!messages.length) return false
|
||||
$q.dialog({
|
||||
title: 'Barkod Validasyonlari',
|
||||
message: messages.join('<br>'),
|
||||
html: true,
|
||||
ok: { label: 'Tamam', color: 'negative' }
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
async function sendUpdateMailAfterApply (selectedRows) {
|
||||
const orderId = String(orderHeaderID.value || '').trim()
|
||||
if (!orderId) return
|
||||
@@ -1275,6 +1558,7 @@ async function sendUpdateMailAfterApply (selectedRows) {
|
||||
deletedItems: payload.deletedItems,
|
||||
updatedItems: payload.updatedItems,
|
||||
addedItems: payload.addedItems,
|
||||
dueDateChanges: payload.dueDateChanges,
|
||||
extraRecipients: ['urun@baggi.com.tr']
|
||||
})
|
||||
|
||||
@@ -1318,11 +1602,34 @@ function buildGroupKey (item) {
|
||||
function formatSizes (sizeMap) {
|
||||
const entries = Object.entries(sizeMap || {})
|
||||
if (!entries.length) return { list: [], label: '-' }
|
||||
entries.sort((a, b) => String(a[0]).localeCompare(String(b[0])))
|
||||
entries.sort((a, b) => {
|
||||
const left = String(a[0] || '').trim()
|
||||
const right = String(b[0] || '').trim()
|
||||
if (/^\d+$/.test(left) && /^\d+$/.test(right)) {
|
||||
return Number(left) - Number(right)
|
||||
}
|
||||
return left.localeCompare(right)
|
||||
})
|
||||
const label = entries.map(([k, v]) => (v > 1 ? `${k}(${v})` : k)).join(', ')
|
||||
return { list: entries.map(([k]) => k), label }
|
||||
}
|
||||
|
||||
function formatCodeDescriptionLabel (code, description) {
|
||||
const codeText = String(code || '').trim().toUpperCase()
|
||||
const descText = String(description || '').trim()
|
||||
if (!codeText) return descText
|
||||
if (!descText) return codeText
|
||||
return `${codeText} - ${descText}`
|
||||
}
|
||||
|
||||
function formatQtyLabel (value) {
|
||||
const qty = Number(value || 0)
|
||||
if (!Number.isFinite(qty)) return '0'
|
||||
return Number.isInteger(qty)
|
||||
? String(qty)
|
||||
: qty.toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
function groupItems (items, prevRows = []) {
|
||||
const prevMap = new Map()
|
||||
for (const r of prevRows || []) {
|
||||
@@ -1334,7 +1641,8 @@ function groupItems (items, prevRows = []) {
|
||||
NewDim2: String(r.NewDim2 || '').trim().toUpperCase(),
|
||||
NewItemMode: String(r.NewItemMode || '').trim(),
|
||||
NewItemSource: String(r.NewItemSource || '').trim(),
|
||||
NewItemEntryMode: String(r.NewItemEntryMode || '').trim()
|
||||
NewItemEntryMode: String(r.NewItemEntryMode || '').trim(),
|
||||
NewDueDate: String(r.NewDueDate || '').trim()
|
||||
})
|
||||
}
|
||||
const map = new Map()
|
||||
@@ -1350,12 +1658,19 @@ function groupItems (items, prevRows = []) {
|
||||
OrderHeaderID: it.OrderHeaderID,
|
||||
OldItemCode: it.OldItemCode,
|
||||
OldColor: it.OldColor,
|
||||
OldColorDescription: it.OldColorDescription,
|
||||
OldColorLabel: formatCodeDescriptionLabel(it.OldColor, it.OldColorDescription),
|
||||
OldDim2: it.OldDim2,
|
||||
OldDim3: it.OldDim3,
|
||||
OldDesc: it.OldDesc,
|
||||
OldDueDate: it.OldDueDate || '',
|
||||
NewDueDate: (prev.NewDueDate || it.OldDueDate || ''),
|
||||
OrderLineIDs: [],
|
||||
OrderLines: [],
|
||||
OldSizes: [],
|
||||
OldSizesLabel: '',
|
||||
OldTotalQty: 0,
|
||||
OldTotalQtyLabel: '0',
|
||||
NewItemCode: prev.NewItemCode || '',
|
||||
NewColor: prev.NewColor || '',
|
||||
NewDim2: prev.NewDim2 || '',
|
||||
@@ -1363,18 +1678,34 @@ function groupItems (items, prevRows = []) {
|
||||
NewItemMode: prev.NewItemMode || 'empty',
|
||||
NewItemSource: prev.NewItemSource || '',
|
||||
NewItemEntryMode: prev.NewItemEntryMode || '',
|
||||
IsVariantMissing: !!it.IsVariantMissing
|
||||
IsVariantMissing: !!it.IsVariantMissing,
|
||||
yasPayloadMap: {}
|
||||
})
|
||||
}
|
||||
|
||||
const g = map.get(key)
|
||||
if (it?.OrderLineID) g.OrderLineIDs.push(it.OrderLineID)
|
||||
|
||||
const size = String(it?.OldDim1 || '').trim()
|
||||
const rawSize = String(it?.OldDim1 || '').trim()
|
||||
const size = store.normalizeDim1ForUi(rawSize)
|
||||
const rawSizeUpper = rawSize.toUpperCase()
|
||||
if (/^(\d+)\s*(Y|YAS|YAŞ)$/.test(rawSizeUpper) && size) {
|
||||
g.yasPayloadMap[size] = store.pickPreferredYasPayloadLabel(
|
||||
g.yasPayloadMap[size],
|
||||
rawSizeUpper
|
||||
)
|
||||
}
|
||||
if (it?.OrderLineID) {
|
||||
g.OrderLines.push({
|
||||
OrderLineID: it.OrderLineID,
|
||||
ItemDim1Code: size
|
||||
})
|
||||
}
|
||||
if (size !== '') {
|
||||
g.__sizeMap = g.__sizeMap || {}
|
||||
g.__sizeMap[size] = (g.__sizeMap[size] || 0) + 1
|
||||
}
|
||||
g.__oldQtyTotal = Number(g.__oldQtyTotal || 0) + Number(it?.OldQty || 0)
|
||||
if (it?.IsVariantMissing) g.IsVariantMissing = true
|
||||
}
|
||||
|
||||
@@ -1383,6 +1714,8 @@ function groupItems (items, prevRows = []) {
|
||||
const sizes = formatSizes(g.__sizeMap || {})
|
||||
g.OldSizes = sizes.list
|
||||
g.OldSizesLabel = sizes.label
|
||||
g.OldTotalQty = Number(g.__oldQtyTotal || 0)
|
||||
g.OldTotalQtyLabel = formatQtyLabel(g.OldTotalQty)
|
||||
const info = store.classifyItemCode(g.NewItemCode)
|
||||
g.NewItemCode = info.normalized
|
||||
g.NewItemMode = info.mode
|
||||
@@ -1391,6 +1724,7 @@ function groupItems (items, prevRows = []) {
|
||||
g.NewItemEntryMode = g.NewItemSource === 'selected' ? 'selected' : 'typed'
|
||||
}
|
||||
delete g.__sizeMap
|
||||
delete g.__oldQtyTotal
|
||||
out.push(g)
|
||||
}
|
||||
|
||||
@@ -1406,8 +1740,10 @@ async function refreshAll () {
|
||||
async function onBulkSubmit () {
|
||||
const flowStart = nowMs()
|
||||
const selectedRows = rows.value.filter(r => !!selectedMap.value[r.RowKey])
|
||||
if (!selectedRows.length) {
|
||||
$q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz.' })
|
||||
const headerAverageDueDateValue = normalizeDateInput(headerAverageDueDate.value)
|
||||
const headerDateChanged = hasHeaderAverageDueDateChange.value
|
||||
if (!selectedRows.length && !headerDateChanged) {
|
||||
$q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz veya ustteki termin tarihini degistiriniz.' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1417,23 +1753,31 @@ async function onBulkSubmit () {
|
||||
$q.notify({ type: 'negative', message: errMsg })
|
||||
return
|
||||
}
|
||||
if (!lines.length) {
|
||||
if (!lines.length && !headerDateChanged) {
|
||||
$q.notify({ type: 'warning', message: 'Secili satirlarda degisiklik yok.' })
|
||||
return
|
||||
}
|
||||
const { errMsg: cdErrMsg, cdItems } = collectCdItemsFromSelectedRows(selectedRows)
|
||||
if (cdErrMsg) {
|
||||
$q.notify({ type: 'negative', message: cdErrMsg })
|
||||
const firstCode = String(cdErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openCdItemDialog(firstCode)
|
||||
return
|
||||
}
|
||||
const { errMsg: attrErrMsg, productAttributes } = await collectProductAttributesFromSelectedRows(selectedRows)
|
||||
if (attrErrMsg) {
|
||||
$q.notify({ type: 'negative', message: attrErrMsg })
|
||||
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openAttributeDialog(firstCode)
|
||||
return
|
||||
|
||||
let cdItems = []
|
||||
let productAttributes = []
|
||||
if (lines.length > 0) {
|
||||
const { errMsg: cdErrMsg, cdItems: nextCdItems } = await collectCdItemsFromSelectedRows(selectedRows)
|
||||
if (cdErrMsg) {
|
||||
$q.notify({ type: 'negative', message: cdErrMsg })
|
||||
const firstCode = String(cdErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openCdItemDialog(firstCode)
|
||||
return
|
||||
}
|
||||
cdItems = nextCdItems
|
||||
|
||||
const { errMsg: attrErrMsg, productAttributes: nextProductAttributes } = await collectProductAttributesFromSelectedRows(selectedRows)
|
||||
if (attrErrMsg) {
|
||||
$q.notify({ type: 'negative', message: attrErrMsg })
|
||||
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
|
||||
if (firstCode) openAttributeDialog(firstCode)
|
||||
return
|
||||
}
|
||||
productAttributes = nextProductAttributes
|
||||
}
|
||||
|
||||
console.info('[OrderProductionUpdate] onBulkSubmit prepared', {
|
||||
@@ -1442,62 +1786,88 @@ async function onBulkSubmit () {
|
||||
lineCount: lines.length,
|
||||
cdItemCount: cdItems.length,
|
||||
attributeCount: productAttributes.length,
|
||||
headerAverageDueDate: headerAverageDueDateValue,
|
||||
headerDateChanged,
|
||||
prepDurationMs: Math.round(nowMs() - prepStart)
|
||||
})
|
||||
|
||||
try {
|
||||
const validateStart = nowMs()
|
||||
const validate = await store.validateUpdates(orderHeaderID.value, lines)
|
||||
console.info('[OrderProductionUpdate] validate finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
lineCount: lines.length,
|
||||
missingCount: Number(validate?.missingCount || 0),
|
||||
durationMs: Math.round(nowMs() - validateStart)
|
||||
})
|
||||
const missingCount = validate?.missingCount || 0
|
||||
if (missingCount > 0) {
|
||||
const missingList = (validate?.missing || []).map(v => (
|
||||
`${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}`
|
||||
))
|
||||
$q.dialog({
|
||||
title: 'Eksik Varyantlar',
|
||||
message: `Eksik varyant bulundu: ${missingCount}<br><br>${missingList.join('<br>')}`,
|
||||
html: true,
|
||||
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
||||
cancel: { label: 'Vazgec', flat: true }
|
||||
}).onOk(async () => {
|
||||
const applyStart = nowMs()
|
||||
await store.applyUpdates(orderHeaderID.value, lines, true, cdItems, productAttributes)
|
||||
console.info('[OrderProductionUpdate] apply finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
insertMissing: true,
|
||||
durationMs: Math.round(nowMs() - applyStart)
|
||||
})
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
selectedMap.value = {}
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
const applyChanges = async (insertMissing) => {
|
||||
const applyStart = nowMs()
|
||||
const applyResult = await store.applyUpdates(
|
||||
orderHeaderID.value,
|
||||
lines,
|
||||
insertMissing,
|
||||
cdItems,
|
||||
productAttributes,
|
||||
headerDateChanged ? headerAverageDueDateValue : null
|
||||
)
|
||||
console.info('[OrderProductionUpdate] apply finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
insertMissing: !!insertMissing,
|
||||
lineCount: lines.length,
|
||||
barcodeInserted: Number(applyResult?.barcodeInserted || 0),
|
||||
headerAverageDueDate: headerAverageDueDateValue,
|
||||
headerDateChanged,
|
||||
durationMs: Math.round(nowMs() - applyStart)
|
||||
})
|
||||
return
|
||||
await store.fetchHeader(orderHeaderID.value)
|
||||
if (lines.length > 0) {
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
}
|
||||
selectedMap.value = {}
|
||||
if (lines.length > 0) {
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
} else {
|
||||
$q.notify({ type: 'positive', message: 'Tahmini termin tarihi guncellendi.' })
|
||||
}
|
||||
}
|
||||
|
||||
const applyStart = nowMs()
|
||||
await store.applyUpdates(orderHeaderID.value, lines, false, cdItems, productAttributes)
|
||||
console.info('[OrderProductionUpdate] apply finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
insertMissing: false,
|
||||
durationMs: Math.round(nowMs() - applyStart)
|
||||
})
|
||||
await store.fetchItems(orderHeaderID.value)
|
||||
selectedMap.value = {}
|
||||
await sendUpdateMailAfterApply(selectedRows)
|
||||
if (lines.length > 0) {
|
||||
const validateStart = nowMs()
|
||||
const validate = await store.validateUpdates(orderHeaderID.value, lines)
|
||||
console.info('[OrderProductionUpdate] validate finished', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
lineCount: lines.length,
|
||||
missingCount: Number(validate?.missingCount || 0),
|
||||
barcodeValidationCount: Number(validate?.barcodeValidationCount || 0),
|
||||
durationMs: Math.round(nowMs() - validateStart)
|
||||
})
|
||||
if (showBarcodeValidationDialog(validate?.barcodeValidations)) {
|
||||
return
|
||||
}
|
||||
const missingCount = validate?.missingCount || 0
|
||||
if (missingCount > 0) {
|
||||
const missingList = (validate?.missing || []).map(v => (
|
||||
`${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}`
|
||||
))
|
||||
$q.dialog({
|
||||
title: 'Eksik Varyantlar',
|
||||
message: `Eksik varyant bulundu: ${missingCount}<br><br>${missingList.join('<br>')}`,
|
||||
html: true,
|
||||
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
|
||||
cancel: { label: 'Vazgec', flat: true }
|
||||
}).onOk(async () => {
|
||||
await applyChanges(true)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await applyChanges(false)
|
||||
} catch (err) {
|
||||
console.error('[OrderProductionUpdate] onBulkSubmit failed', {
|
||||
orderHeaderID: orderHeaderID.value,
|
||||
selectedRowCount: selectedRows.length,
|
||||
lineCount: lines.length,
|
||||
headerAverageDueDate: headerAverageDueDateValue,
|
||||
headerDateChanged,
|
||||
apiError: err?.response?.data,
|
||||
message: err?.message
|
||||
})
|
||||
if (showBarcodeValidationDialog(err?.response?.data?.barcodeValidations)) {
|
||||
return
|
||||
}
|
||||
$q.notify({ type: 'negative', message: store.error || 'Toplu kayit islemi basarisiz.' })
|
||||
}
|
||||
console.info('[OrderProductionUpdate] onBulkSubmit total', {
|
||||
|
||||
1188
ui/src/pages/ProductPricing.vue
Normal file
1188
ui/src/pages/ProductPricing.vue
Normal file
@@ -0,0 +1,1188 @@
|
||||
<template>
|
||||
<q-page class="q-pa-xs pricing-page">
|
||||
<div class="top-bar row items-center justify-between q-mb-xs">
|
||||
<div class="text-subtitle1 text-weight-bold">Urun Fiyatlandirma</div>
|
||||
<div class="row items-center q-gutter-xs">
|
||||
<q-btn-dropdown color="secondary" outline icon="view_module" label="Doviz Gorunumu" :auto-close="false">
|
||||
<q-list dense class="currency-menu-list">
|
||||
<q-item clickable @click="selectAllCurrencies">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearAllCurrencies">
|
||||
<q-item-section>Tumunu Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-for="option in currencyOptions" :key="option.value" clickable @click="toggleCurrencyRow(option.value)">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="isCurrencySelected(option.value)"
|
||||
dense
|
||||
@update:model-value="(val) => toggleCurrency(option.value, val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>{{ option.label }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<q-btn
|
||||
flat
|
||||
:color="showSelectedOnly ? 'primary' : 'grey-7'"
|
||||
:icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'"
|
||||
:label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'"
|
||||
:disable="!showSelectedOnly && selectedRowCount === 0"
|
||||
@click="toggleShowSelectedOnly"
|
||||
/>
|
||||
<q-btn flat color="grey-7" icon="restart_alt" label="Filtreleri Sifirla" @click="resetAll" />
|
||||
<q-btn color="primary" icon="refresh" label="Veriyi Yenile" :loading="store.loading" @click="reloadData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
||||
<q-table
|
||||
ref="mainTableRef"
|
||||
class="pane-table pricing-table"
|
||||
flat
|
||||
dense
|
||||
row-key="id"
|
||||
:rows="filteredRows"
|
||||
:columns="visibleColumns"
|
||||
:loading="store.loading"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="rowHeight"
|
||||
:virtual-scroll-sticky-size-start="headerHeight"
|
||||
:virtual-scroll-slice-size="36"
|
||||
: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)"
|
||||
>
|
||||
<q-checkbox
|
||||
v-if="col.name === 'select'"
|
||||
size="sm"
|
||||
color="primary"
|
||||
:model-value="allSelectedVisible"
|
||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
<div v-else class="header-with-filter">
|
||||
<span>{{ col.label }}</span>
|
||||
<q-btn
|
||||
v-if="isHeaderFilterField(col.field)"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="8px"
|
||||
icon="filter_alt"
|
||||
:color="hasFilter(col.field) ? 'primary' : 'grey-7'"
|
||||
class="header-filter-btn"
|
||||
>
|
||||
<q-badge v-if="hasFilter(col.field)" color="primary" floating rounded>
|
||||
{{ getFilterBadgeValue(col.field) }}
|
||||
</q-badge>
|
||||
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||
<div v-if="isMultiSelectFilterField(col.field)" class="excel-filter-menu">
|
||||
<q-input
|
||||
v-model="columnFilterSearch[col.field]"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
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="Tumunu Sec" @click="selectAllColumnFilterOptions(col.field)" />
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearColumnFilter(col.field)" />
|
||||
</div>
|
||||
<q-virtual-scroll
|
||||
v-if="getFilterOptionsForField(col.field).length > 0"
|
||||
class="excel-filter-options"
|
||||
:items="getFilterOptionsForField(col.field)"
|
||||
:virtual-scroll-item-size="32"
|
||||
separator
|
||||
>
|
||||
<template #default="{ item: option }">
|
||||
<q-item
|
||||
:key="`${col.field}-${option.value}`"
|
||||
dense
|
||||
clickable
|
||||
class="excel-filter-option"
|
||||
@click="toggleColumnFilterValue(col.field, option.value)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
dense
|
||||
size="sm"
|
||||
:model-value="isColumnFilterValueSelected(col.field, option.value)"
|
||||
@update:model-value="() => toggleColumnFilterValue(col.field, 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>
|
||||
<div v-else-if="isNumberRangeFilterField(col.field)" class="excel-filter-menu">
|
||||
<div class="range-filter-grid">
|
||||
<q-input
|
||||
v-model="numberRangeFilters[col.field].min"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Min"
|
||||
inputmode="decimal"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
<q-input
|
||||
v-model="numberRangeFilters[col.field].max"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Max"
|
||||
inputmode="decimal"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="row justify-end q-pt-xs">
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearRangeFilter(col.field)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isDateRangeFilterField(col.field)" class="excel-filter-menu">
|
||||
<div class="range-filter-grid">
|
||||
<q-input
|
||||
v-model="dateRangeFilters[col.field].from"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
type="date"
|
||||
label="Baslangic"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
<q-input
|
||||
v-model="dateRangeFilters[col.field].to"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
type="date"
|
||||
label="Bitis"
|
||||
class="range-filter-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="row justify-end q-pt-xs">
|
||||
<q-btn flat dense size="sm" label="Temizle" @click="clearRangeFilter(col.field)" />
|
||||
</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>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #body-cell-select="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
class="text-center selection-col"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="primary"
|
||||
:model-value="!!selectedMap[props.row.id]"
|
||||
@update:model-value="(val) => toggleRowSelection(props.row.id, val)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-productCode="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="product-code-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-stockQty="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="stock-qty-text">{{ formatStock(props.value) }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-stockEntryDate="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="date-cell-text">{{ formatDateDisplay(props.value) }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-lastPricingDate="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span :class="['date-cell-text', { 'date-warning': needsRepricing(props.row) }]">
|
||||
{{ formatDateDisplay(props.value) }}
|
||||
</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-brandGroupSelection="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<select
|
||||
class="native-cell-select"
|
||||
:value="props.row.brandGroupSelection"
|
||||
@change="(e) => onBrandGroupSelectionChange(props.row, e.target.value)"
|
||||
>
|
||||
<option value="">Seciniz</option>
|
||||
<option v-for="opt in brandGroupOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<input
|
||||
v-if="editableColumnSet.has(props.col.name)"
|
||||
class="native-cell-input text-right"
|
||||
:value="formatPrice(props.row[props.col.field])"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
@change="(e) => onEditableCellChange(props.row, props.col.field, e.target.value)"
|
||||
/>
|
||||
<span v-else class="cell-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-xs">
|
||||
Hata: {{ store.error }}
|
||||
</q-banner>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||
|
||||
const store = useProductPricingStore()
|
||||
|
||||
const usdToTry = 38.25
|
||||
const eurToTry = 41.6
|
||||
const multipliers = [1, 1.03, 1.06, 1.09, 1.12, 1.15]
|
||||
const rowHeight = 31
|
||||
const headerHeight = 72
|
||||
|
||||
const brandGroupOptions = [
|
||||
{ label: 'MARKA GRUBU A', value: 'MARKA GRUBU A' },
|
||||
{ label: 'MARKA GRUBU B', value: 'MARKA GRUBU B' },
|
||||
{ label: 'MARKA GRUBU C', value: 'MARKA GRUBU C' }
|
||||
]
|
||||
|
||||
const currencyOptions = [
|
||||
{ label: 'USD', value: 'USD' },
|
||||
{ label: 'EUR', value: 'EUR' },
|
||||
{ label: 'TRY', value: 'TRY' }
|
||||
]
|
||||
|
||||
const multiFilterColumns = [
|
||||
{ field: 'productCode', label: 'Urun Kodu' },
|
||||
{ field: 'askiliYan', label: 'Askili Yan' },
|
||||
{ field: 'kategori', label: 'Kategori' },
|
||||
{ field: 'urunIlkGrubu', label: 'Urun Ilk Grubu' },
|
||||
{ field: 'urunAnaGrubu', label: 'Urun Ana Grubu' },
|
||||
{ field: 'urunAltGrubu', label: 'Urun Alt Grubu' },
|
||||
{ field: 'icerik', label: 'Icerik' },
|
||||
{ field: 'karisim', label: 'Karisim' }
|
||||
]
|
||||
const numberRangeFilterFields = ['stockQty']
|
||||
const dateRangeFilterFields = ['stockEntryDate', 'lastPricingDate']
|
||||
const columnFilters = ref({
|
||||
productCode: [],
|
||||
askiliYan: [],
|
||||
kategori: [],
|
||||
urunIlkGrubu: [],
|
||||
urunAnaGrubu: [],
|
||||
urunAltGrubu: [],
|
||||
icerik: [],
|
||||
karisim: []
|
||||
})
|
||||
const columnFilterSearch = ref({
|
||||
productCode: '',
|
||||
askiliYan: '',
|
||||
kategori: '',
|
||||
urunIlkGrubu: '',
|
||||
urunAnaGrubu: '',
|
||||
urunAltGrubu: '',
|
||||
icerik: '',
|
||||
karisim: ''
|
||||
})
|
||||
const numberRangeFilters = ref({
|
||||
stockQty: { min: '', max: '' }
|
||||
})
|
||||
const dateRangeFilters = ref({
|
||||
stockEntryDate: { from: '', to: '' },
|
||||
lastPricingDate: { from: '', to: '' }
|
||||
})
|
||||
const multiSelectFilterFieldSet = new Set(multiFilterColumns.map((x) => x.field))
|
||||
const numberRangeFilterFieldSet = new Set(numberRangeFilterFields)
|
||||
const dateRangeFilterFieldSet = new Set(dateRangeFilterFields)
|
||||
const headerFilterFieldSet = new Set([
|
||||
...multiFilterColumns.map((x) => x.field),
|
||||
...numberRangeFilterFields,
|
||||
...dateRangeFilterFields
|
||||
])
|
||||
|
||||
const mainTableRef = ref(null)
|
||||
const selectedMap = ref({})
|
||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||
const showSelectedOnly = ref(false)
|
||||
|
||||
const editableColumns = [
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry',
|
||||
'usd1',
|
||||
'usd2',
|
||||
'usd3',
|
||||
'usd4',
|
||||
'usd5',
|
||||
'usd6',
|
||||
'eur1',
|
||||
'eur2',
|
||||
'eur3',
|
||||
'eur4',
|
||||
'eur5',
|
||||
'eur6',
|
||||
'try1',
|
||||
'try2',
|
||||
'try3',
|
||||
'try4',
|
||||
'try5',
|
||||
'try6'
|
||||
]
|
||||
const editableColumnSet = new Set(editableColumns)
|
||||
|
||||
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('select', '', 'select', 40, { align: 'center', classes: 'text-center selection-col' }),
|
||||
col('productCode', 'URUN KODU', 'productCode', 108, { sortable: true, classes: 'ps-col product-code-col' }),
|
||||
col('stockQty', 'STOK ADET', 'stockQty', 72, { align: 'right', sortable: true, classes: 'ps-col stock-col' }),
|
||||
col('stockEntryDate', 'STOK GIRIS TARIHI', 'stockEntryDate', 92, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('lastPricingDate', 'SON FIYATLANDIRMA TARIHI', 'lastPricingDate', 108, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('askiliYan', 'ASKILI YAN', 'askiliYan', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('karisim', 'KARISIM', 'karisim', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('marka', 'MARKA', 'marka', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('brandGroupSelection', 'MARKA GRUBU SECIMI', 'brandGroupSelection', 76),
|
||||
col('costPrice', 'MALIYET FIYATI', 'costPrice', 74, { align: 'right', sortable: true, classes: 'usd-col' }),
|
||||
col('expenseForBasePrice', 'TABAN FIYAT MASRAF', 'expenseForBasePrice', 86, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 74, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceTry', 'TABAN TRY', 'basePriceTry', 74, { align: 'right', classes: 'try-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 62, { align: 'right', classes: 'try-col' })
|
||||
]
|
||||
|
||||
const stickyColumnNames = [
|
||||
'select',
|
||||
'productCode',
|
||||
'stockQty',
|
||||
'stockEntryDate',
|
||||
'lastPricingDate',
|
||||
'askiliYan',
|
||||
'kategori',
|
||||
'urunIlkGrubu',
|
||||
'urunAnaGrubu',
|
||||
'urunAltGrubu',
|
||||
'icerik',
|
||||
'karisim',
|
||||
'marka',
|
||||
'brandGroupSelection',
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry'
|
||||
]
|
||||
const stickyBoundaryColumnName = 'basePriceTry'
|
||||
const stickyColumnNameSet = new Set(stickyColumnNames)
|
||||
|
||||
const visibleColumns = computed(() => {
|
||||
const selected = new Set(selectedCurrencies.value)
|
||||
return allColumns.filter((c) => {
|
||||
if (c.name.startsWith('usd')) return selected.has('USD')
|
||||
if (c.name.startsWith('eur')) return selected.has('EUR')
|
||||
if (c.name.startsWith('try')) return selected.has('TRY')
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const stickyLeftMap = computed(() => {
|
||||
const map = {}
|
||||
let left = 0
|
||||
for (const colName of stickyColumnNames) {
|
||||
const c = allColumns.find((x) => x.name === colName)
|
||||
if (!c) continue
|
||||
map[colName] = left
|
||||
left += extractWidth(c.style)
|
||||
}
|
||||
return map
|
||||
})
|
||||
const stickyScrollComp = computed(() => {
|
||||
const boundaryCol = allColumns.find((x) => x.name === stickyBoundaryColumnName)
|
||||
return (stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)
|
||||
})
|
||||
|
||||
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 rows = computed(() => store.rows || [])
|
||||
const multiFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
multiFilterColumns.forEach(({ field }) => {
|
||||
const uniq = new Set()
|
||||
rows.value.forEach((row) => {
|
||||
const val = String(row?.[field] ?? '').trim()
|
||||
if (val) uniq.add(val)
|
||||
})
|
||||
map[field] = Array.from(uniq)
|
||||
.sort((a, b) => a.localeCompare(b, 'tr'))
|
||||
.map((v) => ({ label: v, value: v }))
|
||||
})
|
||||
return map
|
||||
})
|
||||
const filteredFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
multiFilterColumns.forEach(({ field }) => {
|
||||
const search = String(columnFilterSearch.value[field] || '').trim().toLocaleLowerCase('tr')
|
||||
const options = multiFilterOptionMap.value[field] || []
|
||||
map[field] = search
|
||||
? options.filter((option) => option.label.toLocaleLowerCase('tr').includes(search))
|
||||
: options
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter((row) => {
|
||||
if (showSelectedOnly.value && !selectedMap.value[row.id]) return false
|
||||
for (const mf of multiFilterColumns) {
|
||||
const selected = columnFilters.value[mf.field] || []
|
||||
if (selected.length > 0 && !selected.includes(String(row?.[mf.field] ?? '').trim())) return false
|
||||
}
|
||||
const stockQtyMin = parseNullableNumber(numberRangeFilters.value.stockQty?.min)
|
||||
const stockQtyMax = parseNullableNumber(numberRangeFilters.value.stockQty?.max)
|
||||
const stockQty = Number(row?.stockQty ?? 0)
|
||||
if (stockQtyMin !== null && stockQty < stockQtyMin) return false
|
||||
if (stockQtyMax !== null && stockQty > stockQtyMax) return false
|
||||
if (!matchesDateRange(String(row?.stockEntryDate || '').trim(), dateRangeFilters.value.stockEntryDate)) return false
|
||||
if (!matchesDateRange(String(row?.lastPricingDate || '').trim(), dateRangeFilters.value.lastPricingDate)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const visibleRowIds = computed(() => filteredRows.value.map((row) => row.id))
|
||||
const selectedRowCount = computed(() => Object.values(selectedMap.value).filter(Boolean).length)
|
||||
const selectedVisibleCount = computed(() => visibleRowIds.value.filter((id) => !!selectedMap.value[id]).length)
|
||||
const allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||
|
||||
function isHeaderFilterField (field) {
|
||||
return headerFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function isMultiSelectFilterField (field) {
|
||||
return multiSelectFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function isNumberRangeFilterField (field) {
|
||||
return numberRangeFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function isDateRangeFilterField (field) {
|
||||
return dateRangeFilterFieldSet.has(field)
|
||||
}
|
||||
|
||||
function hasFilter (field) {
|
||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length > 0
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
const filter = numberRangeFilters.value[field]
|
||||
return !!String(filter?.min || '').trim() || !!String(filter?.max || '').trim()
|
||||
}
|
||||
if (isDateRangeFilterField(field)) {
|
||||
const filter = dateRangeFilters.value[field]
|
||||
return !!String(filter?.from || '').trim() || !!String(filter?.to || '').trim()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getFilterBadgeValue (field) {
|
||||
if (isMultiSelectFilterField(field)) return (columnFilters.value[field] || []).length
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
const filter = numberRangeFilters.value[field]
|
||||
return [filter?.min, filter?.max].filter((x) => String(x || '').trim()).length
|
||||
}
|
||||
if (isDateRangeFilterField(field)) {
|
||||
const filter = dateRangeFilters.value[field]
|
||||
return [filter?.from, filter?.to].filter((x) => String(x || '').trim()).length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function clearColumnFilter (field) {
|
||||
if (!isMultiSelectFilterField(field)) return
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: []
|
||||
}
|
||||
}
|
||||
|
||||
function clearRangeFilter (field) {
|
||||
if (isNumberRangeFilterField(field)) {
|
||||
numberRangeFilters.value = {
|
||||
...numberRangeFilters.value,
|
||||
[field]: { min: '', max: '' }
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isDateRangeFilterField(field)) {
|
||||
dateRangeFilters.value = {
|
||||
...dateRangeFilters.value,
|
||||
[field]: { from: '', to: '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterOptionsForField (field) {
|
||||
return filteredFilterOptionMap.value[field] || []
|
||||
}
|
||||
|
||||
function isColumnFilterValueSelected (field, value) {
|
||||
return (columnFilters.value[field] || []).includes(value)
|
||||
}
|
||||
|
||||
function toggleColumnFilterValue (field, value) {
|
||||
const current = new Set(columnFilters.value[field] || [])
|
||||
if (current.has(value)) current.delete(value)
|
||||
else current.add(value)
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: Array.from(current)
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllColumnFilterOptions (field) {
|
||||
const options = getFilterOptionsForField(field)
|
||||
columnFilters.value = {
|
||||
...columnFilters.value,
|
||||
[field]: options.map((option) => option.value)
|
||||
}
|
||||
}
|
||||
|
||||
function extractWidth (style) {
|
||||
const m = String(style || '').match(/width:(\d+)px/)
|
||||
return m ? Number(m[1]) : 0
|
||||
}
|
||||
|
||||
function isStickyCol (colName) {
|
||||
return stickyColumnNameSet.has(colName)
|
||||
}
|
||||
|
||||
function isStickyBoundary (colName) {
|
||||
return colName === stickyBoundaryColumnName
|
||||
}
|
||||
|
||||
function getHeaderCellStyle (col) {
|
||||
if (!isStickyCol(col.name)) return undefined
|
||||
return { left: `${stickyLeftMap.value[col.name] || 0}px`, zIndex: 22 }
|
||||
}
|
||||
|
||||
function getBodyCellStyle (col) {
|
||||
if (!isStickyCol(col.name)) return undefined
|
||||
return { left: `${stickyLeftMap.value[col.name] || 0}px`, zIndex: 12 }
|
||||
}
|
||||
|
||||
function round2 (value) {
|
||||
return Number(Number(value || 0).toFixed(2))
|
||||
}
|
||||
|
||||
function parseNumber (val) {
|
||||
const normalized = String(val ?? '')
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '')
|
||||
.replace(',', '.')
|
||||
const n = Number(normalized)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
function parseNullableNumber (val) {
|
||||
const text = String(val ?? '').trim()
|
||||
if (!text) return null
|
||||
const normalized = text
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '')
|
||||
.replace(',', '.')
|
||||
const n = Number(normalized)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
function matchesDateRange (value, filter) {
|
||||
const from = String(filter?.from || '').trim()
|
||||
const to = String(filter?.to || '').trim()
|
||||
if (!from && !to) return true
|
||||
if (!value) return false
|
||||
if (from && value < from) return false
|
||||
if (to && value > to) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function formatPrice (val) {
|
||||
const n = parseNumber(val)
|
||||
return n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
function formatStock (val) {
|
||||
const n = Number(val || 0)
|
||||
if (!Number.isFinite(n)) return '0'
|
||||
const hasFraction = Math.abs(n % 1) > 0.0001
|
||||
return n.toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: hasFraction ? 2 : 0,
|
||||
maximumFractionDigits: hasFraction ? 2 : 0
|
||||
})
|
||||
}
|
||||
|
||||
function formatDateDisplay (val) {
|
||||
const text = String(val || '').trim()
|
||||
if (!text) return '-'
|
||||
const [year, month, day] = text.split('-')
|
||||
if (!year || !month || !day) return text
|
||||
return `${day}.${month}.${year}`
|
||||
}
|
||||
|
||||
function needsRepricing (row) {
|
||||
const stockEntryDate = String(row?.stockEntryDate || '').trim()
|
||||
const lastPricingDate = String(row?.lastPricingDate || '').trim()
|
||||
if (!stockEntryDate) return false
|
||||
if (!lastPricingDate) return true
|
||||
return lastPricingDate < stockEntryDate
|
||||
}
|
||||
|
||||
function recalcByBasePrice (row) {
|
||||
row.basePriceTry = round2((row.basePriceUsd * usdToTry) + row.expenseForBasePrice)
|
||||
multipliers.forEach((multiplier, index) => {
|
||||
row[`usd${index + 1}`] = round2(row.basePriceUsd * multiplier)
|
||||
row[`eur${index + 1}`] = round2((row.basePriceUsd * usdToTry * multiplier) / eurToTry)
|
||||
row[`try${index + 1}`] = round2(row.basePriceTry * multiplier)
|
||||
})
|
||||
}
|
||||
|
||||
function onEditableCellChange (row, field, val) {
|
||||
const parsed = parseNumber(val)
|
||||
store.updateCell(row, field, parsed)
|
||||
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') recalcByBasePrice(row)
|
||||
}
|
||||
|
||||
function onBrandGroupSelectionChange (row, val) {
|
||||
store.updateBrandGroupSelection(row, val)
|
||||
}
|
||||
|
||||
function toggleRowSelection (rowId, val) {
|
||||
selectedMap.value = { ...selectedMap.value, [rowId]: !!val }
|
||||
}
|
||||
|
||||
function toggleSelectAllVisible (val) {
|
||||
const next = { ...selectedMap.value }
|
||||
visibleRowIds.value.forEach((id) => { next[id] = !!val })
|
||||
selectedMap.value = next
|
||||
}
|
||||
|
||||
function resetAll () {
|
||||
columnFilters.value = {
|
||||
productCode: [],
|
||||
askiliYan: [],
|
||||
kategori: [],
|
||||
urunIlkGrubu: [],
|
||||
urunAnaGrubu: [],
|
||||
urunAltGrubu: [],
|
||||
icerik: [],
|
||||
karisim: []
|
||||
}
|
||||
columnFilterSearch.value = {
|
||||
productCode: '',
|
||||
askiliYan: '',
|
||||
kategori: '',
|
||||
urunIlkGrubu: '',
|
||||
urunAnaGrubu: '',
|
||||
urunAltGrubu: '',
|
||||
icerik: '',
|
||||
karisim: ''
|
||||
}
|
||||
numberRangeFilters.value = {
|
||||
stockQty: { min: '', max: '' }
|
||||
}
|
||||
dateRangeFilters.value = {
|
||||
stockEntryDate: { from: '', to: '' },
|
||||
lastPricingDate: { from: '', to: '' }
|
||||
}
|
||||
showSelectedOnly.value = false
|
||||
selectedMap.value = {}
|
||||
}
|
||||
|
||||
function toggleShowSelectedOnly () {
|
||||
if (!showSelectedOnly.value && selectedRowCount.value === 0) return
|
||||
showSelectedOnly.value = !showSelectedOnly.value
|
||||
}
|
||||
|
||||
function isCurrencySelected (code) {
|
||||
return selectedCurrencies.value.includes(code)
|
||||
}
|
||||
|
||||
function toggleCurrency (code, checked) {
|
||||
const set = new Set(selectedCurrencies.value)
|
||||
if (checked) set.add(code)
|
||||
else set.delete(code)
|
||||
selectedCurrencies.value = currencyOptions.map((x) => x.value).filter((x) => set.has(x))
|
||||
}
|
||||
|
||||
function toggleCurrencyRow (code) {
|
||||
toggleCurrency(code, !isCurrencySelected(code))
|
||||
}
|
||||
|
||||
function selectAllCurrencies () {
|
||||
selectedCurrencies.value = currencyOptions.map((x) => x.value)
|
||||
}
|
||||
|
||||
function clearAllCurrencies () {
|
||||
selectedCurrencies.value = []
|
||||
}
|
||||
|
||||
async function reloadData () {
|
||||
await store.fetchRows()
|
||||
selectedMap.value = {}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await reloadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pricing-page {
|
||||
--pricing-row-height: 31px;
|
||||
--pricing-header-height: 72px;
|
||||
--pricing-table-height: calc(100vh - 210px);
|
||||
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.currency-menu-list {
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pane-table {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.q-table__middle) {
|
||||
height: var(--pricing-table-height);
|
||||
min-height: var(--pricing-table-height);
|
||||
max-height: var(--pricing-table-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(th),
|
||||
.pricing-table :deep(td) {
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.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: var(--pricing-row-height);
|
||||
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;
|
||||
padding: 0 4px !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: nowrap;
|
||||
word-break: normal;
|
||||
text-overflow: ellipsis;
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.header-filter-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.header-filter-ghost {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.excel-filter-menu {
|
||||
min-width: 230px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.range-filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.range-filter-field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.excel-filter-select :deep(.q-field__control) {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.excel-filter-select :deep(.q-field__native),
|
||||
.excel-filter-select :deep(.q-field__input) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.excel-filter-actions {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 .cell-text),
|
||||
.pricing-table :deep(td.ps-col .product-code-text),
|
||||
.pricing-table :deep(td.ps-col .stock-qty-text) {
|
||||
font-size: 11px;
|
||||
line-height: 1.1;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.stock-qty-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.date-cell-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.date-warning {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.selection-col),
|
||||
.pricing-table :deep(td.selection-col) {
|
||||
background: #fff;
|
||||
color: var(--q-primary);
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.selection-col) {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.selection-col .q-checkbox__inner) {
|
||||
color: var(--q-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th.selection-col .q-checkbox),
|
||||
.pricing-table :deep(td.selection-col .q-checkbox) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.selection-col .q-checkbox__bg) {
|
||||
background: #fff;
|
||||
border-color: var(--q-primary);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.cell-text {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.1;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.product-code-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.native-cell-input,
|
||||
.native-cell-select {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 3px;
|
||||
border: 1px solid #cfd8dc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.native-cell-input:focus,
|
||||
.native-cell-select:focus {
|
||||
outline: none;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
</style>
|
||||
@@ -311,6 +311,14 @@ const routes = [
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
|
||||
/* ================= PRICING ================= */
|
||||
{
|
||||
path: 'pricing/product-pricing',
|
||||
name: 'product-pricing',
|
||||
component: () => import('pages/ProductPricing.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= PASSWORD ================= */
|
||||
|
||||
|
||||
@@ -6,12 +6,16 @@ function extractApiErrorMessage (err, fallback) {
|
||||
const data = err?.response?.data
|
||||
if (typeof data === 'string' && data.trim()) return data
|
||||
if (data && typeof data === 'object') {
|
||||
const validationMessages = Array.isArray(data.barcodeValidations)
|
||||
? data.barcodeValidations.map(v => String(v?.message || '').trim()).filter(Boolean)
|
||||
: []
|
||||
const msg = String(data.message || '').trim()
|
||||
const step = String(data.step || '').trim()
|
||||
const detail = String(data.detail || '').trim()
|
||||
const parts = [msg]
|
||||
if (step) parts.push(`step=${step}`)
|
||||
if (detail) parts.push(detail)
|
||||
if (validationMessages.length) parts.push(validationMessages.join(' | '))
|
||||
const merged = parts.filter(Boolean).join(' | ')
|
||||
if (merged) return merged
|
||||
}
|
||||
@@ -36,6 +40,51 @@ function nowMs () {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
const YAS_NUMERIC_SIZES = new Set(['2', '4', '6', '8', '10', '12', '14'])
|
||||
|
||||
function safeStr (value) {
|
||||
return value == null ? '' : String(value).trim()
|
||||
}
|
||||
|
||||
function normalizeProductionDim1Label (value) {
|
||||
let text = safeStr(value)
|
||||
if (!text) return ''
|
||||
|
||||
text = text.toUpperCase()
|
||||
const yasMatch = text.match(/^(\d+)\s*(Y|YAS|YAŞ)$/)
|
||||
if (yasMatch?.[1] && YAS_NUMERIC_SIZES.has(yasMatch[1])) {
|
||||
return yasMatch[1]
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function pickPreferredProductionYasPayloadLabel (currentRaw, nextRaw) {
|
||||
const current = safeStr(currentRaw).toUpperCase()
|
||||
const next = safeStr(nextRaw).toUpperCase()
|
||||
if (!next) return current
|
||||
if (!current) return next
|
||||
|
||||
const currentHasYas = /YAS$|YAŞ$/.test(current)
|
||||
const nextHasYas = /YAS$|YAŞ$/.test(next)
|
||||
if (!currentHasYas && nextHasYas) return next
|
||||
return current
|
||||
}
|
||||
|
||||
function toProductionPayloadDim1 (row, value) {
|
||||
const base = normalizeProductionDim1Label(value)
|
||||
if (!base) return ''
|
||||
if (!YAS_NUMERIC_SIZES.has(base)) return base
|
||||
|
||||
const map =
|
||||
row?.yasPayloadMap && typeof row.yasPayloadMap === 'object'
|
||||
? row.yasPayloadMap
|
||||
: {}
|
||||
const mapped = safeStr(map[base]).toUpperCase()
|
||||
if (mapped) return mapped
|
||||
return `${base}Y`
|
||||
}
|
||||
|
||||
export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
@@ -54,6 +103,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
cdItemLookups: null,
|
||||
cdItemDraftsByCode: {},
|
||||
productAttributeDraftsByCode: {},
|
||||
knownExistingItemCodes: {},
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null
|
||||
@@ -71,18 +121,35 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
},
|
||||
|
||||
actions: {
|
||||
normalizeDim1ForUi (value) {
|
||||
return normalizeProductionDim1Label(value)
|
||||
},
|
||||
pickPreferredYasPayloadLabel (currentRaw, nextRaw) {
|
||||
return pickPreferredProductionYasPayloadLabel(currentRaw, nextRaw)
|
||||
},
|
||||
toPayloadDim1Code (row, value) {
|
||||
return toProductionPayloadDim1(row, value)
|
||||
},
|
||||
classifyItemCode (value) {
|
||||
const normalized = String(value || '').trim().toUpperCase()
|
||||
if (!normalized) {
|
||||
return { normalized: '', mode: 'empty', exists: false }
|
||||
}
|
||||
const exists = this.productCodeSet.has(normalized)
|
||||
const exists = this.productCodeSet.has(normalized) || !!this.knownExistingItemCodes[normalized]
|
||||
return {
|
||||
normalized,
|
||||
mode: exists ? 'existing' : 'new',
|
||||
exists
|
||||
}
|
||||
},
|
||||
markItemCodeKnownExisting (itemCode, exists = true) {
|
||||
const code = String(itemCode || '').trim().toUpperCase()
|
||||
if (!code) return
|
||||
this.knownExistingItemCodes = {
|
||||
...this.knownExistingItemCodes,
|
||||
[code]: !!exists
|
||||
}
|
||||
},
|
||||
|
||||
async fetchHeader (orderHeaderID) {
|
||||
if (!orderHeaderID) {
|
||||
@@ -134,6 +201,20 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
this.error = err?.response?.data || err?.message || 'Urun listesi alinamadi'
|
||||
}
|
||||
},
|
||||
async fetchCdItemByCode (code) {
|
||||
if (!code) return null
|
||||
try {
|
||||
const res = await api.get('/product-cditem', { params: { code } })
|
||||
const data = res?.data || null
|
||||
if (data) {
|
||||
this.markItemCodeKnownExisting(code, true)
|
||||
}
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('[OrderProductionItemStore] fetchCdItemByCode failed', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
async fetchColors (productCode) {
|
||||
const code = String(productCode || '').trim()
|
||||
if (!code) return []
|
||||
@@ -152,6 +233,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
const res = await api.get('/product-colors', { params: { code } })
|
||||
const data = res?.data
|
||||
const list = Array.isArray(data) ? data : []
|
||||
if (list.length) this.markItemCodeKnownExisting(code, true)
|
||||
this.colorOptionsByCode[code] = list
|
||||
console.info('[OrderProductionItemStore] fetchColors done', { code, count: list.length, durationMs: Math.round(nowMs() - t0) })
|
||||
return list
|
||||
@@ -284,6 +366,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
try {
|
||||
const res = await api.get('/product-item-attributes', { params: { itemTypeCode: itc, itemCode: code } })
|
||||
const list = Array.isArray(res?.data) ? res.data : []
|
||||
if (list.length) this.markItemCodeKnownExisting(code, true)
|
||||
this.productItemAttributesByKey[key] = list
|
||||
return list
|
||||
} catch (err) {
|
||||
@@ -359,6 +442,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
orderHeaderID,
|
||||
lineCount: lines?.length || 0,
|
||||
missingCount: Number(data?.missingCount || 0),
|
||||
barcodeValidationCount: Number(data?.barcodeValidationCount || 0),
|
||||
requestId: rid,
|
||||
durationMs: Math.round(nowMs() - t0)
|
||||
})
|
||||
@@ -371,7 +455,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = []) {
|
||||
async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = [], headerAverageDueDate = null) {
|
||||
if (!orderHeaderID) return { updated: 0, inserted: 0 }
|
||||
|
||||
this.saving = true
|
||||
@@ -384,11 +468,18 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
lineCount: lines?.length || 0,
|
||||
insertMissing: !!insertMissing,
|
||||
cdItemCount: cdItems?.length || 0,
|
||||
attributeCount: productAttributes?.length || 0
|
||||
attributeCount: productAttributes?.length || 0,
|
||||
headerAverageDueDate
|
||||
})
|
||||
const res = await api.post(
|
||||
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
|
||||
{ lines, insertMissing, cdItems, productAttributes }
|
||||
{
|
||||
lines,
|
||||
insertMissing,
|
||||
cdItems,
|
||||
productAttributes,
|
||||
HeaderAverageDueDate: headerAverageDueDate
|
||||
}
|
||||
)
|
||||
const data = res?.data || { updated: 0, inserted: 0 }
|
||||
const rid = res?.headers?.['x-debug-request-id'] || ''
|
||||
@@ -396,7 +487,9 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
|
||||
orderHeaderID,
|
||||
updated: Number(data?.updated || 0),
|
||||
inserted: Number(data?.inserted || 0),
|
||||
barcodeInserted: Number(data?.barcodeInserted || 0),
|
||||
attributeUpserted: Number(data?.attributeUpserted || 0),
|
||||
headerUpdated: !!data?.headerUpdated,
|
||||
requestId: rid,
|
||||
durationMs: Math.round(nowMs() - t0)
|
||||
})
|
||||
|
||||
88
ui/src/stores/ProductPricingStore.js
Normal file
88
ui/src/stores/ProductPricingStore.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
function toText (value) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
function toNumber (value) {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
|
||||
}
|
||||
|
||||
function mapRow (raw, index) {
|
||||
return {
|
||||
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),
|
||||
costPrice: toNumber(raw?.CostPrice),
|
||||
expenseForBasePrice: 0,
|
||||
basePriceUsd: 0,
|
||||
basePriceTry: 0,
|
||||
usd1: 0,
|
||||
usd2: 0,
|
||||
usd3: 0,
|
||||
usd4: 0,
|
||||
usd5: 0,
|
||||
usd6: 0,
|
||||
eur1: 0,
|
||||
eur2: 0,
|
||||
eur3: 0,
|
||||
eur4: 0,
|
||||
eur5: 0,
|
||||
eur6: 0,
|
||||
try1: 0,
|
||||
try2: 0,
|
||||
try3: 0,
|
||||
try4: 0,
|
||||
try5: 0,
|
||||
try6: 0
|
||||
}
|
||||
}
|
||||
|
||||
export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
state: () => ({
|
||||
rows: [],
|
||||
loading: false,
|
||||
error: ''
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchRows () {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
try {
|
||||
const res = await api.get('/pricing/products')
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
this.rows = data.map((x, i) => mapRow(x, i))
|
||||
} catch (err) {
|
||||
this.rows = []
|
||||
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||
this.error = toText(msg)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
updateCell (row, field, val) {
|
||||
if (!row || !field) return
|
||||
row[field] = toNumber(String(val ?? '').replace(',', '.'))
|
||||
},
|
||||
|
||||
updateBrandGroupSelection (row, val) {
|
||||
if (!row) return
|
||||
row.brandGroupSelection = toText(val)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -212,6 +212,8 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
orders: [],
|
||||
header: {},
|
||||
summaryRows: [],
|
||||
originalHeader: {},
|
||||
originalLines: [],
|
||||
|
||||
lastSavedAt: null,
|
||||
|
||||
@@ -534,6 +536,54 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
const normalized = Array.isArray(lines) ? lines : []
|
||||
const mapLabel = (ln) => this.buildMailLineLabel(ln)
|
||||
|
||||
const formatDate = (d) => {
|
||||
if (!d) return ''
|
||||
const s = String(d).split('T')[0]
|
||||
return s
|
||||
}
|
||||
|
||||
const oldDate = formatDate(this.originalHeader?.AverageDueDate)
|
||||
const newDate = formatDate(this.header?.AverageDueDate)
|
||||
const origMap = new Map()
|
||||
if (Array.isArray(this.originalLines)) {
|
||||
this.originalLines.forEach(ln => {
|
||||
if (ln.OrderLineID) origMap.set(String(ln.OrderLineID), ln)
|
||||
})
|
||||
}
|
||||
|
||||
const buildDueDateChanges = () => {
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
|
||||
normalized.forEach(ln => {
|
||||
if (ln?._deleteSignal || !ln?.OrderLineID || ln?._dirty !== true) return
|
||||
|
||||
const orig = origMap.get(String(ln.OrderLineID))
|
||||
if (!orig) return
|
||||
|
||||
const itemCode = String(ln?.ItemCode || '').trim().toUpperCase()
|
||||
const colorCode = String(ln?.ColorCode || '').trim().toUpperCase()
|
||||
const itemDim2Code = String(ln?.ItemDim2Code || '').trim().toUpperCase()
|
||||
const oldLnDate = formatDate(orig?.DueDate)
|
||||
const newLnDate = formatDate(ln?.DueDate)
|
||||
if (!itemCode || !newLnDate || oldLnDate === newLnDate) return
|
||||
|
||||
const key = [itemCode, colorCode, itemDim2Code, oldLnDate, newLnDate].join('||')
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
|
||||
out.push({
|
||||
itemCode,
|
||||
colorCode,
|
||||
itemDim2Code,
|
||||
oldDueDate: oldLnDate,
|
||||
newDueDate: newLnDate
|
||||
})
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
return {
|
||||
operation: 'create',
|
||||
@@ -543,7 +593,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
normalized
|
||||
.filter(ln => !ln?._deleteSignal)
|
||||
.map(mapLabel)
|
||||
)
|
||||
),
|
||||
oldDueDate: '',
|
||||
newDueDate: '',
|
||||
dueDateChanges: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,11 +606,22 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
.map(mapLabel)
|
||||
)
|
||||
|
||||
const updatedItems = uniq(
|
||||
normalized
|
||||
.filter(ln => !ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true)
|
||||
.map(mapLabel)
|
||||
)
|
||||
const updatedItems = []
|
||||
|
||||
normalized.forEach(ln => {
|
||||
if (!ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true) {
|
||||
let label = mapLabel(ln)
|
||||
const orig = origMap.get(String(ln.OrderLineID))
|
||||
if (orig) {
|
||||
const oldLnDate = formatDate(orig.DueDate)
|
||||
const newLnDate = formatDate(ln.DueDate)
|
||||
if (newLnDate && oldLnDate !== newLnDate) {
|
||||
label += ` (Termin: ${oldLnDate} -> ${newLnDate})`
|
||||
}
|
||||
}
|
||||
updatedItems.push(label)
|
||||
}
|
||||
})
|
||||
|
||||
const addedItems = uniq(
|
||||
normalized
|
||||
@@ -568,8 +632,11 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
return {
|
||||
operation: 'update',
|
||||
deletedItems,
|
||||
updatedItems,
|
||||
addedItems
|
||||
updatedItems: uniq(updatedItems),
|
||||
addedItems,
|
||||
oldDueDate: oldDate,
|
||||
newDueDate: newDate,
|
||||
dueDateChanges: buildDueDateChanges()
|
||||
}
|
||||
}
|
||||
,
|
||||
@@ -586,7 +653,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
operation: payload?.operation || 'create',
|
||||
deletedItems: Array.isArray(payload?.deletedItems) ? payload.deletedItems : [],
|
||||
updatedItems: Array.isArray(payload?.updatedItems) ? payload.updatedItems : [],
|
||||
addedItems: Array.isArray(payload?.addedItems) ? payload.addedItems : []
|
||||
addedItems: Array.isArray(payload?.addedItems) ? payload.addedItems : [],
|
||||
oldDueDate: payload?.oldDueDate || '',
|
||||
newDueDate: payload?.newDueDate || '',
|
||||
dueDateChanges: Array.isArray(payload?.dueDateChanges) ? payload.dueDateChanges : []
|
||||
})
|
||||
return res?.data || {}
|
||||
} catch (err) {
|
||||
@@ -1113,6 +1183,10 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
this.orders = Array.isArray(normalized) ? normalized : []
|
||||
this.summaryRows = [...this.orders]
|
||||
|
||||
// 💾 Snapshot for email comparison (v3.5)
|
||||
this.originalHeader = JSON.parse(JSON.stringify(this.header))
|
||||
this.originalLines = JSON.parse(JSON.stringify(this.summaryRows))
|
||||
|
||||
/* =======================================================
|
||||
🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN)
|
||||
- herhangi bir isClosed=true → view
|
||||
@@ -3202,6 +3276,7 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
||||
// 📧 Piyasa eşleşen alıcılara sipariş PDF gönderimi (kayıt başarılı olduktan sonra)
|
||||
try {
|
||||
const mailPayload = this.buildOrderMailPayload(lines, isNew)
|
||||
// UPDATE durumunda da mail gönderimi istendiği için isNew kontrolü kaldırıldı (v3.5)
|
||||
const mailRes = await this.sendOrderToMarketMails(serverOrderId, mailPayload)
|
||||
const sentCount = Number(mailRes?.sentCount || 0)
|
||||
$q.notify({
|
||||
|
||||
Reference in New Issue
Block a user