Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-04-14 16:17:43 +03:00
parent b1a3bbd3c5
commit 214677da1e
21 changed files with 3265 additions and 339 deletions

View File

@@ -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', {