Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-31 12:45:22 +03:00
parent 44439f7908
commit d7d871fb8a
19 changed files with 1608 additions and 158 deletions

View File

@@ -323,12 +323,12 @@
======================================================== -->
<div class="order-scroll-y" :class="{ 'compact-grid-header': compactGridHeader }"> <!-- ✅ YENİ: Grid + Editor ortak dikey scroll -->
<div class="order-grid-body">
<template v-for="grp in groupedRows" :key="grp.name">
<template v-for="grp in groupedRows" :key="grp.groupKey">
<div :class="['summary-group', grp.open ? 'open' : 'closed']">
<!-- 🟡 Sub-header -->
<div class="order-sub-header" @click="toggleGroup(grp.name)">
<div class="sub-left">{{ grp.name }}</div>
<div class="order-sub-header" @click="toggleGroup(grp.groupKey)">
<div class="sub-left">{{ grp.displayName }}</div>
<div class="sub-center">
<div
@@ -348,10 +348,10 @@
<div class="order-text-caption">
Toplam {{ grp.name }} Adet: {{ grp.toplamAdet }}
Toplam {{ grp.displayName }} Adet: {{ grp.toplamAdet }}
</div>
<div class="order-text-caption">
Toplam {{ grp.name }} Tutar:
Toplam {{ grp.displayName }} Tutar:
{{ Number(grp.toplamTutar || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
{{ form.pb || aktifPB }}
</div>
@@ -747,6 +747,14 @@
@click="removeSelected"
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="canMutateRows"
flat
color="warning"
label="Bedenleri Sıfırla"
@click="onZeroBedenClick"
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="canMutateRows"
flat
@@ -1362,6 +1370,21 @@ const selectedRow = computed(() => {
=========================================================== */
const groupOpen = reactive({})
function normalizeYetiskinGarsonToken (row) {
const raw = String(
row?.yetiskinGarson ||
row?.YETISKIN_GARSON ||
row?.YetiskinGarson ||
row?.kategori ||
row?.Kategori ||
''
).toUpperCase()
if (raw.includes('GARSON')) return 'GARSON'
if (raw.includes('YETISKIN') || raw.includes('YETİSKİN')) return 'YETISKIN'
return 'GENEL'
}
const groupedRows = computed(() => {
const rows = Array.isArray(summaryRows.value) ? summaryRows.value : []
const buckets = {}
@@ -1376,33 +1399,44 @@ const groupedRows = computed(() => {
const ana = (row?.urunAnaGrubu || 'GENEL')
.toUpperCase()
.trim()
const yg = normalizeYetiskinGarsonToken(row)
const grpKey = String(row?.grpKey || 'tak').trim() || 'tak'
const bucketKey = `${yg}::${ana}::${grpKey}`
if (!buckets[ana]) {
buckets[ana] = {
if (!buckets[bucketKey]) {
buckets[bucketKey] = {
name: ana,
yg,
displayName: `${yg} ${ana}`,
rows: [],
toplamAdet: 0,
toplamTutar: 0,
open: groupOpen[ana] ?? true,
open: groupOpen[bucketKey] ?? true,
// 🔑 TEK KAYNAK
grpKey: row.grpKey
grpKey
}
order.push(ana)
order.push(bucketKey)
}
const bucket = buckets[ana]
const bucket = buckets[bucketKey]
bucket.rows.push(row)
bucket.toplamAdet += Number(row.adet || 0)
bucket.toplamTutar += Number(row.tutar || 0)
}
return order.map(name => {
const grp = buckets[name]
return order.map(bucketKey => {
const grp = buckets[bucketKey]
const schema = schemaMap?.[grp.grpKey]
const schemaTitle = String(schema?.title || grp.grpKey || '').trim()
const displayName = schemaTitle
? `${grp.yg} ${grp.name} (${schemaTitle})`
: `${grp.yg} ${grp.name}`
return {
...grp,
displayName,
groupKey: bucketKey,
bedenValues: schema?.values || []
}
})
@@ -3016,6 +3050,25 @@ const onSaveAndNextColor = async () => {
})
}
function onZeroBedenClick () {
if (!Array.isArray(form.bedenLabels) || !form.bedenLabels.length) {
$q.notify({
type: 'warning',
message: 'Sıfırlanacak beden alanı bulunamadı.'
})
return
}
form.bedenler = form.bedenLabels.map(() => 0)
updateTotals(form)
$q.notify({
type: 'info',
message: 'Beden adetleri sıfırlandı.',
position: 'top-right'
})
}

View File

@@ -114,7 +114,8 @@
filled
maxlength="13"
label="Yeni Urun"
@update:model-value="val => onNewItemChange(props.row, val)"
:class="newItemInputClass(props.row)"
@update:model-value="val => onNewItemChange(props.row, val, 'typed')"
>
<template #append>
<q-icon name="arrow_drop_down" class="cursor-pointer" />
@@ -145,6 +146,32 @@
</div>
</q-menu>
</q-input>
<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="cdItem Bilgisi"
@click="openCdItemDialog(props.row.NewItemCode)"
/>
<q-btn
v-if="props.row.NewItemMode === 'new'"
class="q-ml-xs"
dense
flat
size="sm"
color="primary"
label="Urun Ozellikleri"
@click="openAttributeDialog(props.row.NewItemCode)"
/>
</div>
</q-td>
</template>
@@ -157,13 +184,10 @@
option-value="color_code"
emit-value
map-options
use-input
new-value-mode="add-unique"
dense
filled
label="Yeni Renk"
@update:model-value="() => onNewColorChange(props.row)"
@new-value="(val, done) => onCreateColorValue(props.row, val, done)"
/>
</q-td>
</template>
@@ -173,16 +197,13 @@
<q-select
v-model="props.row.NewDim2"
:options="getSecondColorOptions(props.row)"
option-label="item_dim2_code"
option-label="item_dim2_label"
option-value="item_dim2_code"
emit-value
map-options
use-input
new-value-mode="add-unique"
dense
filled
label="Yeni 2. Renk"
@new-value="(val, done) => onCreateSecondColorValue(props.row, val, done)"
/>
</q-td>
</template>
@@ -205,6 +226,127 @@
<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">Yeni Kod cdItem Bilgileri</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">
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ItemDimTypeCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemDimTypeCodes')" label="ItemDimTypeCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ProductTypeCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('productTypeCodes')" label="ProductTypeCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ProductHierarchyID" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('productHierarchyIDs')" label="ProductHierarchyID" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.UnitOfMeasureCode1" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('unitOfMeasureCode1List')" label="UnitOfMeasureCode1" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ItemAccountGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemAccountGrCodes')" label="ItemAccountGrCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ItemTaxGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemTaxGrCodes')" label="ItemTaxGrCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ItemPaymentPlanGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemPaymentPlanGrCodes')" label="ItemPaymentPlanGrCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ItemDiscountGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemDiscountGrCodes')" label="ItemDiscountGrCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ItemVendorGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('itemVendorGrCodes')" label="ItemVendorGrCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.PromotionGroupCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('promotionGroupCodes')" label="PromotionGroupCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.ProductCollectionGrCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('productCollectionGrCodes')" label="ProductCollectionGrCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.StorePriceLevelCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('storePriceLevelCodes')" label="StorePriceLevelCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.PerceptionOfFashionCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('perceptionOfFashionCodes')" label="PerceptionOfFashionCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.CommercialRoleCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('commercialRoleCodes')" label="CommercialRoleCode" />
</div>
<div class="col-12 col-md-4">
<q-select v-model="cdItemDraftForm.StoreCapacityLevelCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('storeCapacityLevelCodes')" label="StoreCapacityLevelCode" />
</div>
<div class="col-12 col-md-6">
<q-select v-model="cdItemDraftForm.CustomsTariffNumberCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('customsTariffNumbers')" label="CustomsTariffNumberCode" />
</div>
<div class="col-12 col-md-6">
<q-select v-model="cdItemDraftForm.CompanyCode" dense filled emit-value map-options option-label="label" option-value="value" :options="lookupOptions('companyCodes')" label="CompanyCode" />
</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 style="max-height: 68vh; overflow: auto;">
<div class="text-caption text-grey-7 q-mb-sm">
Ilk etap dummy: isBlocked=0 kabul edilmis satirlar gibi listelenir.
</div>
<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
emit-value
map-options
option-label="label"
option-value="value"
:options="row.Options"
label="AttributeCode - AttributeDescription"
/>
</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 Kaydet" @click="saveAttributeDraft" />
</q-card-actions>
</q-card>
</q-dialog>
</q-page>
</template>
@@ -219,6 +361,8 @@ 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'
const orderHeaderID = computed(() => String(route.params.orderHeaderID || '').trim())
const header = computed(() => store.header || {})
@@ -235,6 +379,12 @@ const descFilter = ref('')
const productOptions = ref([])
const productSearch = ref('')
const selectedMap = ref({})
const cdItemDialogOpen = ref(false)
const cdItemTargetCode = ref('')
const cdItemDraftForm = ref(createEmptyCdItemDraft(''))
const attributeDialogOpen = ref(false)
const attributeTargetCode = ref('')
const attributeRows = ref([])
const columns = [
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false, style: 'width:44px;', headerStyle: 'width:44px;' },
@@ -300,24 +450,70 @@ const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(k => !!s
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
function onSelectProduct (row, code) {
productSearch.value = ''
onNewItemChange(row, code)
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 onNewItemChange (row, val) {
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) {
productSearch.value = ''
onNewItemChange(row, code, 'selected')
}
function onNewItemChange (row, val, source = 'typed') {
const prevCode = String(row?.NewItemCode || '').trim().toUpperCase()
const next = String(val || '').trim().toUpperCase()
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 = ''
if (row.NewItemCode) {
store.fetchColors(row.NewItemCode)
}
if (row.NewItemMode === 'new' && isValidBaggiModelCode(row.NewItemCode) && row.NewItemCode !== prevCode) {
openCdItemDialog(row.NewItemCode)
}
}
function onNewColorChange (row) {
@@ -341,7 +537,11 @@ function getSecondColorOptions (row) {
const code = row?.NewItemCode || ''
const color = row?.NewColor || ''
const key = `${code}::${color}`
return store.secondColorOptionsByKey[key] || []
const list = store.secondColorOptionsByKey[key] || []
return list.map(c => ({
...c,
item_dim2_label: `${c.item_dim2_code} - ${c.color_description || ''}`.trim()
}))
}
function toggleRowSelection (rowKey, checked) {
@@ -360,33 +560,12 @@ function toggleSelectAllVisible (checked) {
selectedMap.value = next
}
function onCreateColorValue (row, val, done) {
const code = normalizeShortCode(val, 3)
if (!code) {
done(null)
return
}
row.NewColor = code
onNewColorChange(row)
done(code, 'add-unique')
}
function onCreateSecondColorValue (row, val, done) {
const code = normalizeShortCode(val, 3)
if (!code) {
done(null)
return
}
row.NewDim2 = code
done(code, 'add-unique')
}
function normalizeShortCode (value, maxLen) {
return String(value || '').trim().toUpperCase().slice(0, maxLen)
}
function isValidBaggiModelCode (code) {
return /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/.test(code)
return BAGGI_CODE_PATTERN.test(code)
}
function validateRowInput (row) {
@@ -398,7 +577,7 @@ function validateRowInput (row) {
if (!newItemCode) return 'Yeni model kodu zorunludur.'
if (!isValidBaggiModelCode(newItemCode)) {
return 'Girdiginiz yapi BAGGI kod yapisina uygun degildir. Format: X999-XXX99999'
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.'
@@ -437,6 +616,238 @@ function collectLinesFromRows (selectedRows) {
return { errMsg: '', lines }
}
function createEmptyCdItemDraft (itemCode) {
return {
ItemTypeCode: '1',
ItemCode: String(itemCode || '').trim().toUpperCase(),
ItemDimTypeCode: '',
ProductTypeCode: '',
ProductHierarchyID: '',
UnitOfMeasureCode1: '',
ItemAccountGrCode: '',
ItemTaxGrCode: '',
ItemPaymentPlanGrCode: '',
ItemDiscountGrCode: '',
ItemVendorGrCode: '',
PromotionGroupCode: '',
ProductCollectionGrCode: '',
StorePriceLevelCode: '',
PerceptionOfFashionCode: '',
CommercialRoleCode: '',
StoreCapacityLevelCode: '',
CustomsTariffNumberCode: '',
CompanyCode: ''
}
}
function lookupOptions (key) {
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
}
})
}
async function openCdItemDialog (itemCode) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return
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 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),
ProductTypeCode: toIntOrNil(d.ProductTypeCode),
ProductHierarchyID: toIntOrNil(d.ProductHierarchyID),
UnitOfMeasureCode1: toStrOrNil(d.UnitOfMeasureCode1),
ItemAccountGrCode: toStrOrNil(d.ItemAccountGrCode),
ItemTaxGrCode: toStrOrNil(d.ItemTaxGrCode),
ItemPaymentPlanGrCode: toStrOrNil(d.ItemPaymentPlanGrCode),
ItemDiscountGrCode: toStrOrNil(d.ItemDiscountGrCode),
ItemVendorGrCode: toStrOrNil(d.ItemVendorGrCode),
PromotionGroupCode: toStrOrNil(d.PromotionGroupCode),
ProductCollectionGrCode: toStrOrNil(d.ProductCollectionGrCode),
StorePriceLevelCode: toStrOrNil(d.StorePriceLevelCode),
PerceptionOfFashionCode: toStrOrNil(d.PerceptionOfFashionCode),
CommercialRoleCode: toStrOrNil(d.CommercialRoleCode),
StoreCapacityLevelCode: toStrOrNil(d.StoreCapacityLevelCode),
CustomsTariffNumberCode: toStrOrNil(d.CustomsTariffNumberCode),
CompanyCode: toStrOrNil(d.CompanyCode)
}
}
function saveCdItemDraft () {
const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value)
if (!payload.ItemCode) {
$q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' })
return
}
store.setCdItemDraft(payload.ItemCode, payload)
cdItemDialogOpen.value = false
}
function createDummyAttributeRows () {
const sharedOptions = [
{ value: 'DAMATLIK', label: 'DAMATLIK - DAMATLIK' },
{ value: 'TAKIM', label: 'TAKIM - TAKIM ELBISE' },
{ value: 'CEKET', label: 'CEKET - CEKET' },
{ value: 'PANTOLON', label: 'PANTOLON - PANTOLON' }
]
const rows = [{
AttributeTypeCodeNumber: 1,
TypeLabel: '1-001 Urun Ana Grubu',
AttributeCode: '',
Options: sharedOptions
}]
for (let i = 2; i <= 50; i++) {
const code = String(i).padStart(3, '0')
rows.push({
AttributeTypeCodeNumber: i,
TypeLabel: `1-${code} Dummy Ozellik ${i}`,
AttributeCode: '',
Options: sharedOptions
})
}
return rows
}
function buildAttributeRowsFromLookup (list) {
const grouped = new Map()
for (const it of (list || [])) {
const typeCode = Number(it?.attribute_type_code || 0)
if (!typeCode) continue
if (!grouped.has(typeCode)) {
grouped.set(typeCode, {
typeCode,
typeDesc: String(it?.attribute_type_description || '').trim() || String(typeCode),
options: []
})
}
const g = grouped.get(typeCode)
const code = String(it?.attribute_code || '').trim()
const desc = String(it?.attribute_description || '').trim()
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: '',
Options: g.options
}))
return rows
}
async function openAttributeDialog (itemCode) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return
attributeTargetCode.value = code
const existing = store.getProductAttributeDraft(code)
const fetched = await store.fetchProductAttributes(1)
const fromLookup = buildAttributeRowsFromLookup(fetched)
const baseRows = fromLookup.length ? fromLookup : createDummyAttributeRows()
attributeRows.value = Array.isArray(existing) && existing.length
? JSON.parse(JSON.stringify(existing))
: baseRows
attributeDialogOpen.value = true
}
function saveAttributeDraft () {
const code = String(attributeTargetCode.value || '').trim().toUpperCase()
if (!code) return
for (const row of (attributeRows.value || [])) {
const selected = String(row?.AttributeCode || '').trim()
if (!selected) {
$q.notify({ type: 'negative', message: `Urun ozelliklerinde secim zorunlu: ${row?.TypeLabel || ''}` })
return
}
}
store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(attributeRows.value || [])))
attributeDialogOpen.value = false
$q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' })
}
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 rows = store.getProductAttributeDraft(code)
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 || !attributeCode) {
return { errMsg: `${code} icin urun ozellikleri eksik`, productAttributes: [] }
}
out.push({
ItemTypeCode: 1,
ItemCode: code,
AttributeTypeCode: attributeTypeCode,
AttributeCode: attributeCode
})
}
}
return { errMsg: '', productAttributes: out }
}
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) {
const draft = store.getCdItemDraft(code)
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()
@@ -516,14 +927,23 @@ function formatSizes (sizeMap) {
function groupItems (items, prevRows = []) {
const prevMap = new Map()
for (const r of prevRows || []) {
if (r?.RowKey) prevMap.set(r.RowKey, String(r.NewDesc || '').trim())
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()
})
}
const map = new Map()
for (const it of items) {
const key = buildGroupKey(it)
if (!map.has(key)) {
const prevDesc = prevMap.get(key) || ''
const prev = prevMap.get(key) || {}
const prevDesc = prev.NewDesc || ''
const fallbackDesc = String((it?.NewDesc || it?.OldDesc) || '').trim()
map.set(key, {
RowKey: key,
@@ -536,10 +956,12 @@ function groupItems (items, prevRows = []) {
OrderLineIDs: [],
OldSizes: [],
OldSizesLabel: '',
NewItemCode: '',
NewColor: '',
NewDim2: '',
NewItemCode: prev.NewItemCode || '',
NewColor: prev.NewColor || '',
NewDim2: prev.NewDim2 || '',
NewDesc: prevDesc || fallbackDesc,
NewItemMode: prev.NewItemMode || 'empty',
NewItemSource: prev.NewItemSource || '',
IsVariantMissing: !!it.IsVariantMissing
})
}
@@ -560,6 +982,10 @@ function groupItems (items, prevRows = []) {
const sizes = formatSizes(g.__sizeMap || {})
g.OldSizes = sizes.list
g.OldSizesLabel = sizes.label
const info = store.classifyItemCode(g.NewItemCode)
g.NewItemCode = info.normalized
g.NewItemMode = info.mode
if (info.mode === 'empty') g.NewItemSource = ''
delete g.__sizeMap
out.push(g)
}
@@ -589,6 +1015,20 @@ async function onBulkSubmit () {
$q.notify({ type: 'negative', message: 'Secili satirlarda guncellenecek kayit bulunamadi.' })
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 } = collectProductAttributesFromSelectedRows(selectedRows)
if (attrErrMsg) {
$q.notify({ type: 'negative', message: attrErrMsg })
const firstCode = String(attrErrMsg.split(' ')[0] || '').trim()
if (firstCode) openAttributeDialog(firstCode)
return
}
try {
const validate = await store.validateUpdates(orderHeaderID.value, lines)
@@ -604,7 +1044,7 @@ async function onBulkSubmit () {
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
cancel: { label: 'Vazgec', flat: true }
}).onOk(async () => {
await store.applyUpdates(orderHeaderID.value, lines, true)
await store.applyUpdates(orderHeaderID.value, lines, true, cdItems, productAttributes)
await store.fetchItems(orderHeaderID.value)
selectedMap.value = {}
await sendUpdateMailAfterApply(selectedRows)
@@ -612,7 +1052,7 @@ async function onBulkSubmit () {
return
}
await store.applyUpdates(orderHeaderID.value, lines, false)
await store.applyUpdates(orderHeaderID.value, lines, false, cdItems, productAttributes)
await store.fetchItems(orderHeaderID.value)
selectedMap.value = {}
await sendUpdateMailAfterApply(selectedRows)
@@ -738,6 +1178,16 @@ async function onBulkSubmit () {
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),

View File

@@ -2,40 +2,6 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
function normalizeTextForMatch (v) {
return String(v || '')
.trim()
.toUpperCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
// Production ekranlari icin beden grup tespiti helper'i.
// Ozel kural:
// YETISKIN/GARSON = GARSON ve URUN ANA GRUBU "GOMLEK ATA YAKA" veya "GOMLEK KLASIK" ise => yas
export function detectProductionBedenGroup (bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '') {
const list = Array.isArray(bedenList) ? bedenList : []
const hasLetterSizes = list
.map(v => String(v || '').trim().toUpperCase())
.some(v => ['XS', 'S', 'M', 'L', 'XL', '2XL', '3XL', '4XL', '5XL', '6XL', '7XL'].includes(v))
const ana = normalizeTextForMatch(urunAnaGrubu)
const kat = normalizeTextForMatch(urunKategori)
const yg = normalizeTextForMatch(yetiskinGarson)
if ((kat.includes('GARSON') || yg.includes('GARSON')) &&
(ana.includes('GOMLEK ATAYAKA') || ana.includes('GOMLEK ATA YAKA') || ana.includes('GOMLEK KLASIK'))) {
return 'yas'
}
if (hasLetterSizes) return 'gom'
if ((ana.includes('AYAKKABI') || kat.includes('AYAKKABI')) && (kat.includes('GARSON') || yg.includes('GARSON'))) return 'ayk_garson'
if (kat.includes('GARSON') || yg.includes('GARSON') || ana.includes('GARSON')) return 'yas'
if (ana.includes('PANTOLON') && kat.includes('YETISKIN')) return 'pan'
if (ana.includes('AKSESUAR')) return 'aksbir'
return 'tak'
}
function extractApiErrorMessage (err, fallback) {
const data = err?.response?.data
if (typeof data === 'string' && data.trim()) return data
@@ -70,12 +36,40 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
products: [],
colorOptionsByCode: {},
secondColorOptionsByKey: {},
productAttributesByItemType: {},
cdItemLookups: null,
cdItemDraftsByCode: {},
productAttributeDraftsByCode: {},
loading: false,
saving: false,
error: null
}),
getters: {
productCodeSet (state) {
const set = new Set()
for (const p of (state.products || [])) {
const code = String(p?.ProductCode || '').trim().toUpperCase()
if (code) set.add(code)
}
return set
}
},
actions: {
classifyItemCode (value) {
const normalized = String(value || '').trim().toUpperCase()
if (!normalized) {
return { normalized: '', mode: 'empty', exists: false }
}
const exists = this.productCodeSet.has(normalized)
return {
normalized,
mode: exists ? 'existing' : 'new',
exists
}
},
async fetchHeader (orderHeaderID) {
if (!orderHeaderID) {
this.header = null
@@ -166,6 +160,62 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
return []
}
},
async fetchProductAttributes (itemTypeCode = 1) {
const key = String(itemTypeCode || 1)
if (this.productAttributesByItemType[key]) {
return this.productAttributesByItemType[key]
}
try {
const res = await api.get('/product-attributes', { params: { itemTypeCode } })
const list = Array.isArray(res?.data) ? res.data : []
this.productAttributesByItemType[key] = list
return list
} catch (err) {
this.error = err?.response?.data || err?.message || 'Urun ozellikleri alinamadi'
return []
}
},
async fetchCdItemLookups (force = false) {
if (this.cdItemLookups && !force) return this.cdItemLookups
try {
const res = await api.get('/orders/production-items/cditem-lookups')
this.cdItemLookups = res?.data || null
return this.cdItemLookups
} catch (err) {
this.error = err?.response?.data || err?.message || 'cdItem lookup listesi alinamadi'
return null
}
},
setCdItemDraft (itemCode, draft) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return
this.cdItemDraftsByCode = {
...this.cdItemDraftsByCode,
[code]: {
...(draft || {}),
ItemCode: code,
ItemTypeCode: Number(draft?.ItemTypeCode || 1)
}
}
},
getCdItemDraft (itemCode) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return null
return this.cdItemDraftsByCode[code] || null
},
setProductAttributeDraft (itemCode, rows) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return
this.productAttributeDraftsByCode = {
...this.productAttributeDraftsByCode,
[code]: Array.isArray(rows) ? rows : []
}
},
getProductAttributeDraft (itemCode) {
const code = String(itemCode || '').trim().toUpperCase()
if (!code) return []
return this.productAttributeDraftsByCode[code] || []
},
async validateUpdates (orderHeaderID, lines) {
if (!orderHeaderID) return { missingCount: 0, missing: [] }
@@ -186,7 +236,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
this.saving = false
}
},
async applyUpdates (orderHeaderID, lines, insertMissing) {
async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = []) {
if (!orderHeaderID) return { updated: 0, inserted: 0 }
this.saving = true
@@ -195,7 +245,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
try {
const res = await api.post(
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
{ lines, insertMissing }
{ lines, insertMissing, cdItems, productAttributes }
)
return res?.data || { updated: 0, inserted: 0 }
} catch (err) {

View File

@@ -42,20 +42,21 @@ export function buildComboKey(row, beden) {
export const BEDEN_SCHEMA = [
{ key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
{ key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] },
{ key: 'ayk_garson', title: 'AYAKKABI GARSON', values: ['22','23','24','25','26','27','28','29','30','31','32','33','34','35','STD'] },
{ key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] },
{ key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] },
{ key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] }
]
const SIZE_GROUP_TITLES = {
tak: 'TAKIM ELBISE',
ayk: 'AYAKKABI',
ayk_garson: 'AYAKKABI GARSON',
yas: 'YAS',
pan: 'PANTOLON',
gom: 'GOMLEK',
aksbir: 'AKSESUAR'
}
export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
m[g.key] = g
return m
}, {})
const FALLBACK_SCHEMA_MAP = {
tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] }
}
export const schemaByKey = { ...FALLBACK_SCHEMA_MAP }
const productSizeMatchCache = {
loaded: false,
@@ -111,6 +112,23 @@ function setProductSizeMatchCache(payload) {
productSizeMatchCache.schemas = normalizedSchemas
}
function buildSchemaMapFromCacheSchemas() {
const out = {}
const src = productSizeMatchCache.schemas || {}
for (const [keyRaw, valuesRaw] of Object.entries(src)) {
const key = String(keyRaw || '').trim()
if (!key) continue
const values = Array.isArray(valuesRaw) ? valuesRaw : []
out[key] = {
key,
title: SIZE_GROUP_TITLES[key] || key.toUpperCase(),
values: values.map(v => String(v == null ? '' : v))
}
}
if (!out.tak) out.tak = { ...FALLBACK_SCHEMA_MAP.tak }
return out
}
export const stockMap = ref({})
export const bedenStock = ref([])
@@ -257,25 +275,17 @@ export const useOrderEntryStore = defineStore('orderentry', {
,
/* ===========================================================
🧩 initSchemaMap — BEDEN ŞEMA İNİT
- TEK SOURCE OF TRUTH: BEDEN_SCHEMA
- TEK SOURCE OF TRUTH: SQL mk_size_group (cache)
=========================================================== */
initSchemaMap() {
if (this.schemaMap && Object.keys(this.schemaMap).length > 0) {
return
}
const map = {}
for (const g of BEDEN_SCHEMA) {
map[g.key] = {
key: g.key,
title: g.title,
values: [...g.values]
}
this.schemaMap = buildSchemaMapFromCacheSchemas()
if (!Object.keys(this.schemaMap).length) {
this.schemaMap = { ...FALLBACK_SCHEMA_MAP }
}
this.schemaMap = map
console.log(
'🧩 schemaMap INIT edildi:',
Object.keys(this.schemaMap)
@@ -284,17 +294,20 @@ export const useOrderEntryStore = defineStore('orderentry', {
async ensureProductSizeMatchRules($q = null, force = false) {
if (!force && productSizeMatchCache.loaded && productSizeMatchCache.rules.length > 0) {
this.schemaMap = buildSchemaMapFromCacheSchemas()
return true
}
try {
const res = await api.get('/product-size-match/rules')
setProductSizeMatchCache(res?.data || {})
this.schemaMap = buildSchemaMapFromCacheSchemas()
return true
} catch (err) {
if (force) {
resetProductSizeMatchCache()
}
this.schemaMap = { ...FALLBACK_SCHEMA_MAP }
console.warn('⚠ product-size-match rules alınamadı:', err)
$q?.notify?.({
type: 'warning',
@@ -4044,6 +4057,8 @@ export function toSummaryRowFromForm(form) {
urunAnaGrubu: form.urunAnaGrubu || '',
urunAltGrubu: form.urunAltGrubu || '',
kategori: form.kategori || '',
yetiskinGarson: form.yetiskinGarson || form.askiliyan || '',
aciklama: form.aciklama || '',
fiyat: Number(form.fiyat || 0),