Files
bssapp/ui/src/pages/OrderProductionUpdate.vue
2026-04-15 15:54:44 +03:00

2100 lines
70 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>
<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"
:disable="store.loading || store.saving || isBulkSubmitting"
@click="onBulkSubmit"
/>
</div>
<div class="filter-bar row q-col-gutter-md">
<div class="col-5">
<q-input
:model-value="cariLabel"
label="Cari Secimi"
filled
dense
readonly
/>
</div>
<div class="col-3">
<q-input
v-model="descFilter"
label="Aciklama Ara"
filled
dense
clearable
/>
</div>
<div class="col-2">
<q-input
:model-value="header?.OrderNumber || ''"
label="Siparis No"
filled
dense
readonly
/>
</div>
<div class="col-2">
<q-input
:model-value="formatDate(header?.OrderDate)"
label="Olusturulma Tarihi"
filled
dense
readonly
/>
</div>
<div class="col-2">
<q-input
v-model="headerAverageDueDate"
label="Tahmini Termin Tarihi"
filled
dense
type="date"
/>
</div>
</div>
<div class="table-wrap">
<q-table
class="q-mt-md prod-table"
flat
bordered
dense
separator="cell"
row-key="RowKey"
:rows="filteredRows"
:columns="columns"
:loading="store.loading"
no-data-label="Uretime verilecek urun bulunamadi"
:rows-per-page-options="[0]"
: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]"
:disable="store.saving"
@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
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 }}
</q-banner>
<q-dialog v-model="cdItemDialogOpen" persistent>
<q-card style="min-width: 980px; max-width: 98vw;">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Urun Boyutlandirma</div>
<q-space />
<q-badge color="warning" text-color="black">
{{ cdItemTargetCode || '-' }}
</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('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" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ProductHierarchyID" dense filled use-input fill-input hide-selected input-debounce="0" emit-value map-options option-label="label" option-value="value" :options="lookupOptions('productHierarchyIDs')" label="Urun Hiyerarsi Grubu" />
</div>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Vazgec" color="grey-8" v-close-popup />
<q-btn color="primary" label="Taslagi Kaydet" @click="saveCdItemDraft" />
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="attributeDialogOpen" persistent>
<q-card style="min-width: 1100px; max-width: 98vw;">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Urun Ozellikleri (2. Pop-up)</div>
<q-space />
<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"
:key="`${row.AttributeTypeCodeNumber}-${idx}`"
class="row q-col-gutter-sm q-mb-xs items-center"
>
<div class="col-12 col-md-5">
<q-input :model-value="row.TypeLabel" dense filled readonly />
</div>
<div class="col-12 col-md-7">
<q-select
v-model="row.AttributeCode"
dense
filled
use-input
fill-input
hide-selected
input-debounce="0"
@filter="(val, update) => onFilterAttributeOption(row, val, update)"
emit-value
map-options
option-label="label"
option-value="value"
:options="row.Options"
/>
</div>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Vazgec" color="grey-8" v-close-popup />
<q-btn color="primary" label="Ozellikleri Taslaga Kaydet" @click="saveAttributeDraft" />
</q-card-actions>
</q-card>
</q-dialog>
<q-inner-loading :showing="store.saving">
<q-spinner-gears size="50px" color="primary" />
<div class="q-mt-md text-subtitle1">Degisiklikler kaydediliyor, lutfen bekleyiniz...</div>
</q-inner-loading>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useQuasar } from 'quasar'
import { useOrderProductionItemStore } from 'src/stores/OrderProductionItemStore'
import api from 'src/services/api'
import { normalizeSearchText } from 'src/utils/searchText'
const route = useRoute()
const $q = useQuasar()
const store = useOrderProductionItemStore()
const BAGGI_CODE_PATTERN = /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/
const BAGGI_CODE_ERROR = 'Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999'
function nowMs () {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now()
}
return Date.now()
}
const orderHeaderID = computed(() => String(route.params.orderHeaderID || '').trim())
const header = computed(() => store.header || {})
const cariLabel = computed(() => {
const code = header.value?.CurrAccCode || ''
const name = header.value?.CurrAccDescription || ''
if (!code && !name) return ''
if (!name) return code
return `${code} - ${name}`
})
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 suppressAutoSetupDialogs = ref(false)
const cdItemDraftForm = ref(createEmptyCdItemDraft(''))
const attributeDialogOpen = ref(false)
const attributeTargetCode = ref('')
const attributeRows = ref([])
const isBulkSubmitting = ref(false)
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: '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' }
]
onMounted(async () => {
await refreshAll()
})
watch(orderHeaderID, async (id) => {
await refreshAll()
})
watch(
() => store.items,
(items) => {
rows.value = groupItems(items || [], rows.value)
},
{ immediate: true }
)
watch(
() => store.products,
(products) => {
productOptions.value = products || []
},
{ immediate: true }
)
function formatDate (val) {
if (!val) return ''
const text = String(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
return rows.value.filter(r =>
normalizeSearchText(r?.OldDesc).includes(needle)
)
})
const visibleRowKeys = computed(() => filteredRows.value.map(r => r.RowKey))
const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(k => !!selectedMap.value[k]).length)
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
const newItemEntryModeOptions = [
{ label: 'Eski Kod Sec', value: 'selected' },
{ label: 'Yeni Kod Ekle', value: 'typed' }
]
const productCodeAllOptions = computed(() =>
(productOptions.value || []).map(p => {
const code = String(p?.ProductCode || '').trim().toUpperCase()
return { label: code, value: code }
}).filter(x => !!x.value && x.value.length === 13)
)
const productCodeSelectOptions = ref([])
watch(
productCodeAllOptions,
(list) => {
productCodeSelectOptions.value = Array.isArray(list) ? [...list] : []
},
{ immediate: true }
)
function onFilterProductCode (val, update) {
const needle = normalizeSearchText(val)
update(() => {
if (!needle) {
productCodeSelectOptions.value = [...productCodeAllOptions.value]
return
}
productCodeSelectOptions.value = (productCodeAllOptions.value || []).filter(opt => {
const label = normalizeSearchText(opt?.label || '')
const value = normalizeSearchText(opt?.value || '')
return label.includes(needle) || value.includes(needle)
})
})
}
function applyNewItemVisualState (row, source = 'typed') {
const info = store.classifyItemCode(row?.NewItemCode || '')
row.NewItemCode = info.normalized
row.NewItemMode = info.mode
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',
'new-item-new': row?.NewItemMode === 'new'
}
}
function newItemBadgeColor (row) {
return row?.NewItemMode === 'existing' ? 'positive' : 'warning'
}
function newItemBadgeLabel (row) {
return row?.NewItemMode === 'existing' ? 'MEVCUT KOD' : 'YENI KOD'
}
function newItemHintText (row) {
if (row?.NewItemMode === 'existing') {
return row?.NewItemSource === 'selected'
? 'Urun listesinden secildi'
: 'Elle girildi (sistemde bulundu)'
}
if (row?.NewItemMode === 'new') {
return store.getCdItemDraft(row?.NewItemCode) ? 'Yeni kod: cdItem taslagi hazir' : 'Yeni kod: cdItem taslagi gerekli'
}
return ''
}
function onSelectProduct (row, code) {
row.NewItemEntryMode = 'selected'
onNewItemChange(row, code, 'selected')
}
function onNewItemEntryModeChange (row, mode) {
row.NewItemEntryMode = String(mode || '').trim()
row.NewItemCode = ''
row.NewColor = ''
row.NewDim2 = ''
row.NewItemMode = 'empty'
row.NewItemSource = ''
}
function escapeRegExp (value) {
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function buildNextCodeFromPrefix (prefix) {
const normalizedPrefix = String(prefix || '').trim().toUpperCase()
if (!/^[A-Z][0-9]{3}-[A-Z]{3}$/.test(normalizedPrefix)) return ''
const codeRegex = new RegExp(`^${escapeRegExp(normalizedPrefix)}(\\d{5})$`)
let maxSuffix = 0
for (const p of (productOptions.value || [])) {
const code = String(p?.ProductCode || '').trim().toUpperCase()
const m = code.match(codeRegex)
if (!m) continue
const n = Number(m[1] || 0)
if (Number.isFinite(n) && n > maxSuffix) maxSuffix = n
}
const next = maxSuffix > 0 ? maxSuffix + 1 : 1
return `${normalizedPrefix}${String(next).padStart(5, '0')}`
}
function onNewItemChange (row, val, source = 'typed') {
const prevCode = String(row?.NewItemCode || '').trim().toUpperCase()
let next = String(val || '').trim().toUpperCase()
if (source === 'typed' && row?.NewItemEntryMode === 'typed' && /^[A-Z][0-9]{3}-[A-Z]{3}$/.test(next)) {
const autoCode = buildNextCodeFromPrefix(next)
if (autoCode) {
next = autoCode
$q.notify({ type: 'info', message: `Yeni kod otomatik tamamlandi: ${autoCode}` })
}
}
if (next.length > 13) {
$q.notify({ type: 'negative', message: 'Model kodu en fazla 13 karakter olabilir.' })
row.NewItemCode = next.slice(0, 13)
applyNewItemVisualState(row, source)
return
}
if (next.length === 13 && !isValidBaggiModelCode(next)) {
$q.notify({ type: 'negative', message: BAGGI_CODE_ERROR })
row.NewItemCode = prevCode
applyNewItemVisualState(row, source)
return
}
row.NewItemCode = next ? next.toUpperCase() : ''
applyNewItemVisualState(row, source)
row.NewColor = ''
row.NewDim2 = ''
row.NewDesc = mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc)
if (row.NewItemCode && isValidBaggiModelCode(row.NewItemCode)) {
if (row.NewItemMode === 'new') {
store.fetchNewColors(row.NewItemCode)
} else {
store.fetchColors(row.NewItemCode)
}
}
if (suppressAutoSetupDialogs.value) return
if (row.NewItemMode === 'new' && isValidBaggiModelCode(row.NewItemCode) && row.NewItemCode !== prevCode) {
openNewCodeSetupFlow(row.NewItemCode)
} else if (row.NewItemMode === 'existing' && isValidBaggiModelCode(row.NewItemCode) && row.NewItemCode !== prevCode) {
openAttributeDialog(row.NewItemCode)
}
}
function isAttributeDraftComplete (rows) {
if (!Array.isArray(rows) || !rows.length) return false
return rows.every(r => String(r?.AttributeCode || '').trim())
}
function isNewCodeSetupComplete (itemCode) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return false
const hasCdItem = !!store.getCdItemDraft(code)
const attrRows = store.getProductAttributeDraft(code)
return hasCdItem && isAttributeDraftComplete(attrRows)
}
function isColorSelectionLocked (row) {
const code = String(row?.NewItemCode || '').trim().toUpperCase()
return !code
}
function openNewCodeSetupFlow (itemCode) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return
if (!store.getCdItemDraft(code)) {
openCdItemDialog(code)
return
}
if (!isAttributeDraftComplete(store.getProductAttributeDraft(code))) {
openAttributeDialog(code)
}
}
function onNewColorChange (row) {
row.NewColor = normalizeShortCode(row.NewColor, 3)
row.NewDim2 = ''
row.NewDesc = mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc)
if (row.NewItemCode && row.NewColor) {
if (String(row?.NewItemMode || '').trim() === 'new') {
store.fetchNewSecondColors(row.NewItemCode, row.NewColor)
} else {
store.fetchSecondColors(row.NewItemCode, row.NewColor)
}
}
}
function onNewDim2Change (row) {
row.NewDim2 = normalizeShortCode(row.NewDim2, 3)
row.NewDesc = mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc)
}
function getColorOptions (row) {
const code = row?.NewItemCode || ''
const isNewMode = String(row?.NewItemMode || '').trim() === 'new'
const list = isNewMode
? (store.newColorOptionsByCode[code] || [])
: (store.colorOptionsByCode[code] || [])
return list.map(c => ({
...c,
colorLabel: `${c.color_code} - ${c.color_description || ''}`.trim()
}))
}
function getSecondColorOptions (row) {
const code = row?.NewItemCode || ''
const color = row?.NewColor || ''
const key = `${code}::${color}`
const isNewMode = String(row?.NewItemMode || '').trim() === 'new'
const list = isNewMode
? (store.newSecondColorOptionsByKey[key] || [])
: (store.secondColorOptionsByKey[key] || [])
return list.map(c => ({
...c,
item_dim2_label: `${c.item_dim2_code} - ${c.color_description || ''}`.trim()
}))
}
function toggleRowSelection (rowKey, checked) {
const next = { ...selectedMap.value }
if (checked) next[rowKey] = true
else delete next[rowKey]
selectedMap.value = next
}
function toggleSelectAllVisible (checked) {
const next = { ...selectedMap.value }
for (const key of visibleRowKeys.value) {
if (checked) next[key] = true
else delete next[key]
}
selectedMap.value = next
}
function normalizeShortCode (value, maxLen) {
return String(value || '').trim().toUpperCase().slice(0, maxLen)
}
function isValidBaggiModelCode (code) {
return BAGGI_CODE_PATTERN.test(code)
}
function formatCodeColorDim2 (itemCode, color, dim2) {
const item = String(itemCode || '').trim().toUpperCase()
const c1 = normalizeShortCode(color, 3)
const c2 = normalizeShortCode(dim2, 3)
const c1Safe = c1 || '-'
const c2Safe = c2 || '-'
return `${item}/${c1Safe}/${c2Safe}`
}
function buildAutoUpdateNote (row) {
const oldInfo = formatCodeColorDim2(row?.OldItemCode, row?.OldColor, row?.OldDim2)
const nextInfo = formatCodeColorDim2(row?.NewItemCode, row?.NewColor, row?.NewDim2)
return `Bu siparis satirinda kod ${oldInfo} bilgisinden ${nextInfo} bilgisine guncellenmistir.`
}
function isSelectionCompleteByOldShape (row) {
const hasModel = String(row?.NewItemCode || '').trim().length > 0
if (!hasModel) return false
const oldHasColor = String(row?.OldColor || '').trim().length > 0
const hasNewColor = normalizeShortCode(row?.NewColor, 3).length === 3
if (oldHasColor) return hasNewColor
return true
}
function stripAutoUpdateNote (text) {
const desc = String(text || '').trim()
if (!desc) return ''
const marker = ' Bu siparis satirinda kod '
const idx = desc.indexOf(marker)
if (idx > -1) return desc.slice(0, idx).trim()
if (desc.startsWith('Bu siparis satirinda kod ')) return ''
return desc
}
function mergeDescWithAutoNote (row, baseDesc) {
const desc = stripAutoUpdateNote(baseDesc)
if (!isSelectionCompleteByOldShape(row)) return desc
const note = buildAutoUpdateNote(row)
if (!note) return desc
if (desc.includes(note)) return desc
if (!desc) return note
return `${desc} ${note}`
}
function validateRowInput (row) {
const entryMode = String(row?.NewItemEntryMode || '').trim()
const newItemCode = String(row.NewItemCode || '').trim().toUpperCase()
const newColor = normalizeShortCode(row.NewColor, 3)
const newDim2 = normalizeShortCode(row.NewDim2, 3)
const oldColor = String(row.OldColor || '').trim()
if (!entryMode) return 'Lutfen once kod giris tipini seciniz (Eski Kod Sec / Yeni Kod Ekle).'
if (!newItemCode) return 'Yeni model kodu zorunludur.'
if (!isValidBaggiModelCode(newItemCode)) {
return BAGGI_CODE_ERROR
}
if (oldColor && !newColor) return 'Eski kayitta 1. renk oldugu icin yeni 1. renk zorunludur.'
if (newColor && newColor.length !== 3) return 'Yeni 1. renk kodu 3 karakter olmalidir.'
if (newDim2 && newDim2.length !== 3) return 'Yeni 2. renk kodu 3 karakter olmalidir.'
if (newDim2 && !newColor) return '2. renk girmek icin 1. renk zorunludur.'
row.NewItemCode = newItemCode
row.NewColor = newColor
row.NewDim2 = newDim2
row.NewDesc = mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc)
return ''
}
function collectLinesFromRows (selectedRows) {
const lines = []
for (const row of selectedRows) {
const errMsg = validateRowInput(row)
if (errMsg) {
return { errMsg, lines: [] }
}
const baseLine = {
NewItemCode: String(row.NewItemCode || '').trim().toUpperCase(),
NewColor: normalizeShortCode(row.NewColor, 3),
NewDim2: normalizeShortCode(row.NewDim2, 3),
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 ||
newDueDateValue !== oldDueDateValue
)
if (!hasChange) continue
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({
...baseLine,
OrderLineID: line?.OrderLineID,
ItemDim1Code: store.toPayloadDim1Code(row, line?.ItemDim1Code || '')
})
}
}
return { errMsg: '', lines }
}
function hasRowChange (row) {
const newItemCode = String(row?.NewItemCode || '').trim().toUpperCase()
const newColor = normalizeShortCode(row?.NewColor, 3)
const newDim2 = normalizeShortCode(row?.NewDim2, 3)
const newDesc = mergeDescWithAutoNote(row, row?.NewDesc || row?.OldDesc)
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 || ''
return (
newItemCode !== oldItemCode ||
newColor !== oldColor ||
newDim2 !== oldDim2 ||
String(newDesc || '').trim() !== oldDesc ||
newDueDateValue !== oldDueDateValue
)
}
function collectOptionalColorWarnings (rows) {
const warnings = []
for (const row of (rows || [])) {
const code = String(row?.NewItemCode || '').trim().toUpperCase()
if (!code) continue
const color = normalizeShortCode(row?.NewColor, 3)
const dim2 = normalizeShortCode(row?.NewDim2, 3)
if (!color) {
warnings.push(`${code} icin renk secmediniz.`)
continue
}
if (!dim2) {
warnings.push(`${code} icin 2. renk bos kalacak.`)
}
}
return [...new Set(warnings)]
}
function confirmOptionalColorWarnings (rows) {
const warnings = collectOptionalColorWarnings(rows)
if (!warnings.length) return Promise.resolve(true)
return new Promise((resolve) => {
$q.dialog({
title: 'Renk Uyarisi',
message: `${warnings.join('<br>')}<br><br>Devam etmek istiyor musunuz?`,
html: true,
ok: { label: 'Evet, Devam Et', color: 'warning' },
cancel: { label: 'Vazgec', flat: true }
}).onOk(() => resolve(true)).onCancel(() => resolve(false)).onDismiss(() => resolve(false))
})
}
function createEmptyCdItemDraft (itemCode) {
return {
ItemTypeCode: '1',
ItemCode: String(itemCode || '').trim().toUpperCase(),
ItemDimTypeCode: '1',
ProductTypeCode: '1',
ProductHierarchyID: '',
UnitOfMeasureCode1: 'AD',
ItemAccountGrCode: '',
ItemTaxGrCode: '%10',
ItemPaymentPlanGrCode: '',
ItemDiscountGrCode: '',
ItemVendorGrCode: '',
PromotionGroupCode: '',
ProductCollectionGrCode: '0',
StorePriceLevelCode: '0',
PerceptionOfFashionCode: '0',
CommercialRoleCode: '0',
StoreCapacityLevelCode: '',
CustomsTariffNumberCode: '',
CompanyCode: '1'
}
}
function lookupOptions (key) {
if (key === 'itemDimTypeCodes') {
return [
{ value: '1', label: '1 - RENK' },
{ value: '2', label: '2 - RENK-BEDEN' },
{ value: '3', label: '3 - RENK-BEDEN-YAKA' }
]
}
const list = store.cdItemLookups?.[key] || []
return list
.map(x => {
const code = String(x?.code || '').trim()
const desc = String(x?.description || '').trim()
return {
value: code,
label: desc ? `${code} - ${desc}` : code,
_desc: desc
}
})
.filter(opt => !isDummyLookupOption(key, opt.value, opt._desc))
.map(({ value, label }) => ({ value, label }))
}
function isDummyLookupOption (key, codeRaw, descRaw) {
const code = String(codeRaw || '').trim().toUpperCase()
const desc = String(descRaw || '').trim().toUpperCase()
if (!code) return true
if (code === '0' || code === '00' || code === '000' || code === '0000') return true
if (desc.includes('DUMMY')) return true
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 = String(cdItemTargetCode.value || '').trim().toUpperCase()
const draft = createEmptyCdItemDraft(targetCode)
for (const k of Object.keys(draft)) {
if (data[k] !== undefined && data[k] !== null) {
draft[k] = String(data[k])
}
}
// Source item kopyalansa da hedef popup kodu degismemeli.
draft.ItemCode = targetCode
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
const existing = store.getCdItemDraft(code)
const draft = createEmptyCdItemDraft(code)
if (existing) {
for (const [k, v] of Object.entries(existing)) {
if (v == null) continue
draft[k] = String(v)
}
}
cdItemDraftForm.value = draft
cdItemDialogOpen.value = true
}
function persistCdItemDraft () {
const targetCode = String(cdItemTargetCode.value || '').trim().toUpperCase()
const payload = normalizeCdItemDraftForPayload({
...(cdItemDraftForm.value || {}),
ItemCode: targetCode || String(cdItemDraftForm.value?.ItemCode || '').trim().toUpperCase()
})
if (!payload.ItemCode) return null
store.setCdItemDraft(payload.ItemCode, payload)
return payload
}
function normalizeCdItemDraftForPayload (draftRaw) {
const d = draftRaw || {}
const toIntOrNil = (v) => {
const n = Number(v)
return Number.isFinite(n) && n > 0 ? n : null
}
const toStrOrNil = (v) => {
const s = String(v || '').trim()
return s || null
}
return {
ItemTypeCode: toIntOrNil(d.ItemTypeCode) || 1,
ItemCode: String(d.ItemCode || '').trim().toUpperCase(),
ItemDimTypeCode: toIntOrNil(d.ItemDimTypeCode) || 1,
ProductTypeCode: null,
ProductHierarchyID: toIntOrNil(d.ProductHierarchyID),
UnitOfMeasureCode1: 'AD',
ItemAccountGrCode: null,
ItemTaxGrCode: '%10',
ItemPaymentPlanGrCode: null,
ItemDiscountGrCode: null,
ItemVendorGrCode: null,
PromotionGroupCode: null,
ProductCollectionGrCode: null,
StorePriceLevelCode: null,
PerceptionOfFashionCode: null,
CommercialRoleCode: null,
StoreCapacityLevelCode: null,
CustomsTariffNumberCode: null,
CompanyCode: '1'
}
}
async function saveCdItemDraft () {
const payload = persistCdItemDraft()
if (!payload?.ItemCode) {
$q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' })
return
}
console.info('[OrderProductionUpdate] saveCdItemDraft', {
code: payload.ItemCode,
itemDimTypeCode: payload.ItemDimTypeCode,
productHierarchyID: payload.ProductHierarchyID
})
cdItemDialogOpen.value = false
await openAttributeDialog(payload.ItemCode)
}
function buildAttributeRowsFromLookup (list) {
const grouped = new Map()
for (const it of (list || [])) {
const typeCode = Number(it?.attribute_type_code || it?.AttributeTypeCode || 0)
if (!typeCode) continue
if (!grouped.has(typeCode)) {
grouped.set(typeCode, {
typeCode,
typeDesc: String(it?.attribute_type_description || it?.AttributeTypeDescription || '').trim() || String(typeCode),
options: []
})
}
const g = grouped.get(typeCode)
const code = String(it?.attribute_code || it?.AttributeCode || '').trim()
const desc = String(it?.attribute_description || it?.AttributeDescription || '').trim()
if (!code) continue
g.options.push({
value: code,
label: `${code} - ${desc || code}`
})
}
const rows = [...grouped.values()]
.sort((a, b) => a.typeCode - b.typeCode)
.map(g => ({
AttributeTypeCodeNumber: g.typeCode,
TypeLabel: `${g.typeCode} - ${g.typeDesc}`,
AttributeCode: '',
AllOptions: [...g.options],
Options: [...g.options]
}))
return rows
}
function onFilterAttributeOption (row, val, update) {
const raw = String(val || '')
const needle = normalizeSearchText(raw)
const base = Array.isArray(row?.AllOptions) ? row.AllOptions : (Array.isArray(row?.Options) ? row.Options : [])
update(() => {
if (!needle) {
row.Options = [...base]
return
}
row.Options = base.filter(opt => {
const label = normalizeSearchText(opt?.label || '')
const value = normalizeSearchText(opt?.value || '')
return label.includes(needle) || value.includes(needle)
})
})
}
function mergeAttributeDraftWithLookupOptions (draftRows, lookupRows) {
const byType = new Map(
(lookupRows || []).map(r => [Number(r?.AttributeTypeCodeNumber || 0), r]).filter(x => x[0] > 0)
)
return (draftRows || []).map(d => {
const typeCode = Number(d?.AttributeTypeCodeNumber || 0)
const base = byType.get(typeCode)
const selectedCode = String(d?.AttributeCode || '').trim()
const baseAllOptions = Array.isArray(base?.AllOptions)
? [...base.AllOptions]
: (Array.isArray(base?.Options) ? [...base.Options] : [])
const draftAllOptions = Array.isArray(d?.AllOptions)
? [...d.AllOptions]
: (Array.isArray(d?.Options) ? [...d.Options] : [])
const allOptions = baseAllOptions.length ? baseAllOptions : draftAllOptions
if (selectedCode && !allOptions.some(opt => String(opt?.value || '').trim() === selectedCode)) {
allOptions.unshift({ value: selectedCode, label: selectedCode })
}
return {
...(base || d),
...d,
AttributeTypeCodeNumber: typeCode,
AttributeCode: selectedCode,
AllOptions: allOptions,
Options: [...allOptions]
}
})
}
async function openAttributeDialog (itemCode) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return
copySourceCode.value = null
attributeTargetCode.value = code
const existingDraft = JSON.parse(JSON.stringify(store.getProductAttributeDraft(code) || []))
const modeInfo = store.classifyItemCode(code)
const fetched = await store.fetchProductAttributes(1)
const fromLookup = buildAttributeRowsFromLookup(fetched)
console.info('[OrderProductionUpdate] openAttributeDialog lookup', {
code,
mode: modeInfo.mode,
fetchedCount: Array.isArray(fetched) ? fetched.length : 0,
rowCount: fromLookup.length
})
if (!fromLookup.length) {
$q.notify({ type: 'negative', message: 'Urun ozellikleri listesi alinamadi. Lutfen daha sonra tekrar deneyin.' })
return
}
// Draft varsa popup her zaman draft'tan acilir (yeniden acinca secimler kaybolmasin).
if (Array.isArray(existingDraft) && existingDraft.length) {
attributeRows.value = JSON.parse(JSON.stringify(
mergeAttributeDraftWithLookupOptions(existingDraft, fromLookup)
))
console.info('[OrderProductionUpdate] openAttributeDialog rowsPrepared', {
code,
mode: modeInfo.mode,
useDraft: true,
rowCount: Array.isArray(attributeRows.value) ? attributeRows.value.length : 0,
optionCounts: (attributeRows.value || []).map(r => ({
type: Number(r?.AttributeTypeCodeNumber || 0),
options: Array.isArray(r?.Options) ? r.Options.length : 0,
allOptions: Array.isArray(r?.AllOptions) ? r.AllOptions.length : 0,
selected: String(r?.AttributeCode || '').trim()
}))
})
for (const row of (attributeRows.value || [])) {
if (!Array.isArray(row.AllOptions)) row.AllOptions = Array.isArray(row.Options) ? [...row.Options] : []
if (!Array.isArray(row.Options)) row.Options = [...row.AllOptions]
}
attributeDialogOpen.value = true
return
}
const dbCurrent = await store.fetchProductItemAttributes(code, 1, true)
console.info('[OrderProductionUpdate] openAttributeDialog dbCurrent', {
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 => [
Number(x?.attribute_type_code || x?.AttributeTypeCode || 0),
String(x?.attribute_code || x?.AttributeCode || '').trim()
]).filter(x => x[0] > 0)
)
const baseRows = fromLookup.map(row => {
const currentCode = dbMap.get(Number(row.AttributeTypeCodeNumber || 0)) || ''
const currentOptions = Array.isArray(row.AllOptions)
? [...row.AllOptions]
: (Array.isArray(row.Options) ? [...row.Options] : [])
if (currentCode && !currentOptions.some(opt => String(opt?.value || '').trim() === currentCode)) {
currentOptions.unshift({ value: currentCode, label: currentCode })
}
return {
...row,
AttributeCode: currentCode,
OriginalAttributeCode: currentCode,
AllOptions: currentOptions,
Options: [...currentOptions]
}
})
const useDraft = Array.isArray(existingDraft) && existingDraft.length
attributeRows.value = JSON.parse(JSON.stringify(baseRows))
console.info('[OrderProductionUpdate] openAttributeDialog rowsPrepared', {
code,
mode: modeInfo.mode,
useDraft: false,
rowCount: Array.isArray(attributeRows.value) ? attributeRows.value.length : 0,
optionCounts: (attributeRows.value || []).map(r => ({
type: Number(r?.AttributeTypeCodeNumber || 0),
options: Array.isArray(r?.Options) ? r.Options.length : 0,
allOptions: Array.isArray(r?.AllOptions) ? r.AllOptions.length : 0,
selected: String(r?.AttributeCode || '').trim()
}))
})
for (const row of (attributeRows.value || [])) {
if (!Array.isArray(row.AllOptions)) {
row.AllOptions = Array.isArray(row.Options) ? [...row.Options] : []
}
if (!Array.isArray(row.Options)) {
row.Options = [...row.AllOptions]
}
}
attributeDialogOpen.value = true
}
function saveAttributeDraft () {
const code = String(attributeTargetCode.value || '').trim().toUpperCase()
if (!code) return
const rows = JSON.parse(JSON.stringify(attributeRows.value || []))
for (const row of rows) {
const selected = String(row?.AttributeCode || '').trim()
if (!selected) {
$q.notify({ type: 'negative', message: `Urun ozelliklerinde secim zorunlu: ${row?.TypeLabel || ''}` })
return
}
}
store.setProductAttributeDraft(code, rows)
console.info('[OrderProductionUpdate] saveAttributeDraft', {
code,
rowCount: rows.length,
selectedCount: rows.length,
selected: rows.map(r => ({
type: Number(r?.AttributeTypeCodeNumber || 0),
code: String(r?.AttributeCode || '').trim()
}))
})
attributeDialogOpen.value = false
$q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' })
}
watch(
cdItemDraftForm,
() => {
if (!cdItemDialogOpen.value) return
persistCdItemDraft()
},
{ deep: true }
)
async function collectProductAttributesFromSelectedRows (selectedRows) {
const codeSet = [...new Set(
(selectedRows || [])
.map(r => String(r?.NewItemCode || '').trim().toUpperCase())
.filter(Boolean)
)]
const out = []
for (const code of codeSet) {
const modeInfo = store.classifyItemCode(code)
let rows = store.getProductAttributeDraft(code)
const dbCurrent = await store.fetchProductItemAttributes(code, 1, true)
const dbMap = new Map(
(dbCurrent || []).map(x => [
Number(x?.attribute_type_code || x?.AttributeTypeCode || 0),
String(x?.attribute_code || x?.AttributeCode || '').trim()
]).filter(x => x[0] > 0)
)
const hasDbAttributes = dbMap.size > 0
const effectiveMode = hasDbAttributes ? 'existing' : modeInfo.mode
console.info('[OrderProductionUpdate] collectProductAttributes start', {
code,
mode: modeInfo.mode,
effectiveMode,
hasDbAttributes,
draftRowCount: Array.isArray(rows) ? rows.length : 0
})
if (effectiveMode === 'existing') {
// Existing kodda kullanıcı değişiklik yaptıysa draftı koru.
// Draft yoksa DB'den zorunlu/fresh çek.
if (!Array.isArray(rows) || !rows.length) {
const lookup = await store.fetchProductAttributes(1)
const baseRows = buildAttributeRowsFromLookup(lookup)
console.info('[OrderProductionUpdate] collectProductAttributes existing refetch', {
code,
lookupCount: Array.isArray(lookup) ? lookup.length : 0,
baseRowCount: baseRows.length
})
rows = baseRows.map(row => {
const currentCode = dbMap.get(Number(row.AttributeTypeCodeNumber || 0)) || ''
const currentOptions = Array.isArray(row.AllOptions)
? [...row.AllOptions]
: (Array.isArray(row.Options) ? [...row.Options] : [])
if (currentCode && !currentOptions.some(opt => String(opt?.value || '').trim() === currentCode)) {
currentOptions.unshift({ value: currentCode, label: currentCode })
}
return {
...row,
AttributeCode: currentCode,
OriginalAttributeCode: currentCode,
AllOptions: currentOptions,
Options: [...currentOptions]
}
})
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(rows)))
}
} else if (!Array.isArray(rows) || !rows.length) {
return { errMsg: `${code} icin urun ozellikleri taslagi kaydedilmedi`, productAttributes: [] }
}
if (!Array.isArray(rows) || !rows.length) {
return { errMsg: `${code} icin urun ozellikleri secilmedi`, productAttributes: [] }
}
for (const row of rows) {
const attributeTypeCode = Number(row?.AttributeTypeCodeNumber || 0)
const attributeCode = String(row?.AttributeCode || '').trim()
if (!attributeTypeCode) {
return { errMsg: `${code} icin urun ozellikleri eksik`, productAttributes: [] }
}
if (effectiveMode === 'existing') {
const originalCode =
dbMap.get(attributeTypeCode) ||
String(row?.OriginalAttributeCode || '').trim()
const changed = attributeCode !== originalCode
if (!changed) continue
if (!attributeCode) {
return { errMsg: `${code} icin urun ozellikleri eksik`, productAttributes: [] }
}
} else if (!attributeCode) {
return { errMsg: `${code} icin urun ozellikleri eksik`, productAttributes: [] }
}
out.push({
ItemTypeCode: 1,
ItemCode: code,
AttributeTypeCode: attributeTypeCode,
AttributeCode: attributeCode
})
}
console.info('[OrderProductionUpdate] collectProductAttributes done', {
code,
mode: modeInfo.mode,
effectiveMode,
outCount: out.filter(x => x.ItemCode === code).length,
rowCount: rows.length,
optionCounts: rows.map(r => ({
type: Number(r?.AttributeTypeCodeNumber || 0),
options: Array.isArray(r?.Options) ? r.Options.length : 0,
allOptions: Array.isArray(r?.AllOptions) ? r.AllOptions.length : 0
}))
})
}
return { errMsg: '', productAttributes: out }
}
async function collectCdItemsFromSelectedRows (selectedRows) {
const codes = [...new Set(
(selectedRows || [])
.filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim())
.map(r => String(r.NewItemCode).trim().toUpperCase())
)]
if (!codes.length) return { errMsg: '', cdItems: [] }
const out = []
for (const code of codes) {
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: [] }
}
out.push(normalizeCdItemDraftForPayload(draft))
}
return { errMsg: '', cdItems: out }
}
function buildMailLineLabelFromRow (row) {
const item = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase()
const color1 = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase()
const color2 = String(row?.NewDim2 || row?.OldDim2 || '').trim().toUpperCase()
const desc = String(row?.NewDesc || row?.OldDesc || '').trim()
if (!item) return ''
const colorPart = color2 ? `${color1}-${color2}` : color1
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(buildUpdateMailLineLabelFromRow)
.filter(Boolean)
)
]
return {
operation: 'update',
deletedItems: [],
updatedItems,
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
const host = String(window?.location?.hostname || '').trim().toLowerCase()
const isLocalHost = host === 'localhost' || host === '127.0.0.1'
if (isLocalHost) {
console.info('[OrderProductionUpdate] sendUpdateMailAfterApply skipped (localhost)', { orderHeaderID: orderId, host })
return
}
try {
const t0 = nowMs()
const payload = buildProductionUpdateMailPayload(selectedRows)
console.info('[OrderProductionUpdate] sendUpdateMailAfterApply start', {
orderHeaderID: orderId,
updatedItems: payload?.updatedItems?.length || 0
})
const res = await api.post('/order/send-market-mail', {
orderHeaderID: orderId,
operation: payload.operation,
deletedItems: payload.deletedItems,
updatedItems: payload.updatedItems,
addedItems: payload.addedItems,
dueDateChanges: payload.dueDateChanges,
extraRecipients: ['urun@baggi.com.tr']
})
const sentCount = Number(res?.data?.sentCount || 0)
console.info('[OrderProductionUpdate] sendUpdateMailAfterApply done', {
orderHeaderID: orderId,
sentCount,
durationMs: Math.round(nowMs() - t0)
})
$q.notify({
type: 'positive',
message: sentCount > 0
? `Guncelleme maili gonderildi (${sentCount} alici)`
: 'Guncelleme maili gonderildi'
})
} catch (err) {
console.error('[OrderProductionUpdate] sendUpdateMailAfterApply failed', {
orderHeaderID: orderId,
status: err?.response?.status,
data: err?.response?.data,
message: err?.message
})
$q.notify({
type: 'warning',
message: 'Guncelleme kaydedildi, mail gonderilemedi.'
})
}
}
function buildGroupKey (item) {
const parts = [
String(item?.OldItemCode || '').trim(),
String(item?.OldColor || '').trim(),
String(item?.OldDim2 || '').trim(),
String(item?.OldDesc || '').trim(),
String(item?.OldDim3 || '').trim()
]
return parts.join('||')
}
function formatSizes (sizeMap) {
const entries = Object.entries(sizeMap || {})
if (!entries.length) return { list: [], label: '-' }
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 || []) {
if (!r?.RowKey) continue
prevMap.set(r.RowKey, {
NewDesc: String(r.NewDesc || '').trim(),
NewItemCode: String(r.NewItemCode || '').trim().toUpperCase(),
NewColor: String(r.NewColor || '').trim().toUpperCase(),
NewDim2: String(r.NewDim2 || '').trim().toUpperCase(),
NewItemMode: String(r.NewItemMode || '').trim(),
NewItemSource: String(r.NewItemSource || '').trim(),
NewItemEntryMode: String(r.NewItemEntryMode || '').trim(),
NewDueDate: String(r.NewDueDate || '').trim()
})
}
const map = new Map()
for (const it of items) {
const key = buildGroupKey(it)
if (!map.has(key)) {
const prev = prevMap.get(key) || {}
const prevDesc = prev.NewDesc || ''
const fallbackDesc = String((it?.NewDesc || it?.OldDesc) || '').trim()
map.set(key, {
RowKey: key,
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 || '',
NewDesc: prevDesc || fallbackDesc,
NewItemMode: prev.NewItemMode || 'empty',
NewItemSource: prev.NewItemSource || '',
NewItemEntryMode: prev.NewItemEntryMode || '',
IsVariantMissing: !!it.IsVariantMissing,
yasPayloadMap: {}
})
}
const g = map.get(key)
if (it?.OrderLineID) g.OrderLineIDs.push(it.OrderLineID)
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
}
const out = []
for (const g of map.values()) {
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
if (info.mode === 'empty') g.NewItemSource = ''
if (!g.NewItemEntryMode && g.NewItemCode) {
g.NewItemEntryMode = g.NewItemSource === 'selected' ? 'selected' : 'typed'
}
delete g.__sizeMap
delete g.__oldQtyTotal
out.push(g)
}
return out
}
async function refreshAll () {
await store.fetchHeader(orderHeaderID.value)
await store.fetchItems(orderHeaderID.value)
await store.fetchProducts()
}
async function onBulkSubmit () {
if (isBulkSubmitting.value || store.saving) {
console.info('[OrderProductionUpdate] onBulkSubmit ignored (already running)', {
orderHeaderID: orderHeaderID.value,
isBulkSubmitting: isBulkSubmitting.value,
storeSaving: store.saving
})
return
}
isBulkSubmitting.value = true
const flowStart = nowMs()
try {
suppressAutoSetupDialogs.value = true
const selectedRows = rows.value.filter(r => !!selectedMap.value[r.RowKey])
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
}
const prepStart = nowMs()
const { errMsg, lines } = collectLinesFromRows(selectedRows)
if (errMsg) {
$q.notify({ type: 'negative', message: errMsg })
return
}
if (!lines.length && !headerDateChanged) {
$q.notify({ type: 'warning', message: 'Secili satirlarda degisiklik yok.' })
return
}
if (lines.length > 0) {
const changedRows = selectedRows.filter(hasRowChange)
const confirmed = await confirmOptionalColorWarnings(changedRows)
if (!confirmed) 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 })
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().toUpperCase()
if (isValidBaggiModelCode(firstCode)) {
await openAttributeDialog(firstCode)
}
return
}
productAttributes = nextProductAttributes
}
console.info('[OrderProductionUpdate] onBulkSubmit prepared', {
orderHeaderID: orderHeaderID.value,
selectedRowCount: selectedRows.length,
lineCount: lines.length,
cdItemCount: cdItems.length,
attributeCount: productAttributes.length,
headerAverageDueDate: headerAverageDueDateValue,
headerDateChanged,
prepDurationMs: Math.round(nowMs() - prepStart)
})
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)
})
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.' })
}
}
if (lines.length > 0) {
const validateStart = nowMs()
const validate = await store.validateUpdates(orderHeaderID.value, lines, cdItems)
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.' })
} finally {
isBulkSubmitting.value = false
suppressAutoSetupDialogs.value = false
console.info('[OrderProductionUpdate] onBulkSubmit total', {
orderHeaderID: orderHeaderID.value,
durationMs: Math.round(nowMs() - flowStart)
})
}
}
</script>
<style scoped>
.prod-table :deep(th) {
font-weight: 700;
letter-spacing: 0.2px;
}
.prod-table :deep(td) {
vertical-align: middle;
}
.prod-table :deep(.q-table__container) {
width: 100%;
}
.prod-table :deep(.q-table) {
font-size: 11px;
}
.order-prod-page {
--header-height: 56px;
--filter-bar-height: 72px;
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
}
.page-header {
position: sticky;
top: 0;
z-index: 8;
background: #fff;
margin-top: -8px;
margin-bottom: 8px;
padding-top: 4px;
padding-bottom: 6px;
}
.filter-bar {
position: sticky;
top: var(--header-height);
z-index: 7;
background: #fff;
padding-top: 4px;
padding-bottom: 8px;
}
.table-wrap {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
width: 100%;
overflow: auto;
}
.prod-table :deep(.q-table__middle) {
max-height: calc(100vh - var(--header-height) - var(--filter-bar-height) - 140px);
overflow: auto;
}
.prod-table :deep(.q-table__container) {
height: 100%;
display: flex;
flex-direction: column;
}
.prod-table :deep(.q-table thead tr th) {
position: sticky;
top: 0;
z-index: 6;
background: #fff;
}
.prod-table :deep(th.col-old),
.prod-table :deep(td.col-old) {
background: #fff0d9;
}
.prod-table :deep(th.col-new),
.prod-table :deep(td.col-new) {
background: #e3f3ff;
}
.prod-table :deep(th.col-old) {
color: #8a5a00;
}
.prod-table :deep(th.col-new) {
color: #0d4f7a;
}
.prod-table :deep(td.col-old) {
border-left: 4px solid #f0a500;
}
.prod-table :deep(td.col-new) {
border-left: 4px solid #2d9cdb;
}
.prod-table :deep(th.col-new-first),
.prod-table :deep(td.col-new-first) {
border-left: 6px solid #1b7cc8;
}
.prod-table :deep(td.cell-new) {
background: #e3f3ff;
}
.prod-table :deep(.new-item-existing .q-field__control) {
background: #eaf9ef !important;
border-left: 3px solid #21ba45;
}
.prod-table :deep(.new-item-new .q-field__control) {
background: #fff5e9 !important;
border-left: 3px solid #f2a100;
}
.prod-table :deep(td.col-desc),
.prod-table :deep(th.col-desc),
.prod-table :deep(td.col-wrap),
.prod-table :deep(th.col-wrap) {
white-space: normal;
word-break: break-word;
line-height: 1.2;
}
.prod-table :deep(.q-field),
.prod-table :deep(.q-field__control),
.prod-table :deep(.q-select),
.prod-table :deep(.q-input) {
width: 100%;
min-width: 0;
}
</style>