Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-19 15:09:55 +03:00
parent 539ca4b587
commit da9d7c2fd5
4 changed files with 338 additions and 20 deletions

View File

@@ -545,7 +545,7 @@ func csvEscape(value string) string {
} }
func csvFloat(value float64) string { func csvFloat(value float64) string {
return fmt.Sprintf("%.2f", value) return strings.ReplaceAll(fmt.Sprintf("%.2f", value), ".", ",")
} }
func exportPriceFieldTitle(field string) string { func exportPriceFieldTitle(field string) string {

View File

@@ -501,7 +501,11 @@
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]" :class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
:style="getBodyCellStyle(props.col)" :style="getBodyCellStyle(props.col)"
> >
<q-badge v-if="props.row.campaignLabel" color="primary" outline :label="props.row.campaignLabel" class="campaign-badge" /> <div class="campaign-cell-content">
<span v-if="props.row.campaignLabel" class="campaign-text" :title="props.row.campaignLabel">
{{ props.row.campaignLabel }}
</span>
</div>
</q-td> </q-td>
</template> </template>
@@ -511,7 +515,9 @@
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]" :class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
:style="getBodyCellStyle(props.col)" :style="getBodyCellStyle(props.col)"
> >
{{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }} <span class="cell-text campaign-rate-text" :title="String(props.row.campaignRate ?? '')">
{{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }}
</span>
</q-td> </q-td>
</template> </template>
@@ -521,7 +527,9 @@
:class="['text-right', props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]" :class="['text-right', props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
:style="getBodyCellStyle(props.col)" :style="getBodyCellStyle(props.col)"
> >
{{ formatPrice(props.row[name]) }} <span :class="['cell-text', 'price-cell-text', { 'campaign-price-text': name.endsWith('Campaign') }]" :title="formatPrice(props.row[name])">
{{ formatPrice(props.row[name]) }}
</span>
</q-td> </q-td>
</template> </template>
</q-table> </q-table>
@@ -580,6 +588,32 @@
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div> <div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
<div class="field-row"><span class="k">Kampanya</span><span class="v">{{ productCardData.campaignLabel || '-' }}</span></div> <div class="field-row"><span class="k">Kampanya</span><span class="v">{{ productCardData.campaignLabel || '-' }}</span></div>
<div class="field-row"><span class="k">Stok</span><span class="v">{{ formatStock(productCardData.stockQty || 0) }}</span></div> <div class="field-row"><span class="k">Stok</span><span class="v">{{ formatStock(productCardData.stockQty || 0) }}</span></div>
<div class="product-card-section">
<div class="product-card-section-title">Fiyat Bilgileri</div>
<div v-if="productCardPriceRows.length" class="price-info-grid">
<div v-for="item in productCardPriceRows" :key="item.key" class="price-info-row">
<span class="price-label">{{ item.label }}</span>
<span class="price-value">{{ item.price || '-' }}</span>
<span class="price-campaign">{{ item.campaignPrice || '-' }}</span>
</div>
</div>
<div v-else class="product-card-empty-text">Secili fiyat kolonu yok.</div>
</div>
<div class="product-card-section">
<div class="product-card-section-title">Beden Stoklari</div>
<q-inner-loading :showing="productCardStockLoading">
<q-spinner size="24px" color="primary" />
</q-inner-loading>
<div v-if="productCardSizeRows.length" class="size-stock-grid">
<div v-for="item in productCardSizeRows" :key="item.size" class="size-stock-cell">
<span class="size-label">{{ item.size }}</span>
<span class="size-qty">{{ formatStock(item.qty) }}</span>
</div>
</div>
<div v-else-if="!productCardStockLoading" class="product-card-empty-text">Beden stogu bulunamadi.</div>
</div>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
@@ -673,11 +707,24 @@ const productCardDialog = ref(false)
const productCardData = ref({}) const productCardData = ref({})
const productCardImages = ref([]) const productCardImages = ref([])
const productCardSlide = ref(0) const productCardSlide = ref(0)
const productCardStockLoading = ref(false)
const productCardSizeRows = ref([])
const productImageFullscreenDialog = ref(false) const productImageFullscreenDialog = ref(false)
const productImageFullscreenSlide = ref(0) const productImageFullscreenSlide = ref(0)
const fullscreenImages = computed(() => productCardImages.value || []) const fullscreenImages = computed(() => productCardImages.value || [])
const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || [])) const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || []))
const productCardPriceRows = computed(() => {
const row = productCardData.value || {}
return priceOptions
.filter((option) => selectedPriceSet.value.has(option.value))
.map((option) => ({
key: option.value,
label: option.label,
price: formatPrice(row?.[option.value]),
campaignPrice: formatPrice(row?.[`${option.value}Campaign`])
}))
})
const selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || [])) const selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || []))
const selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || [])) const selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || []))
const selectedVariantCodeSet = computed(() => new Set(selectedVariantCodes.value || [])) const selectedVariantCodeSet = computed(() => new Set(selectedVariantCodes.value || []))
@@ -745,6 +792,59 @@ function formatStock (value) {
return n.toLocaleString('tr-TR', { maximumFractionDigits: 2 }) return n.toLocaleString('tr-TR', { maximumFractionDigits: 2 })
} }
function parseStockNumber (value) {
if (typeof value === 'number') return Number.isFinite(value) ? value : 0
const text = String(value ?? '').trim()
if (!text) return 0
const normalized = text.replace(/\./g, '').replace(',', '.')
const n = Number.parseFloat(normalized)
return Number.isFinite(n) ? n : 0
}
function normalizeCardToken (value) {
return String(value ?? '').trim().toUpperCase()
}
function parseVariantTokens (variantCode) {
const parts = String(variantCode || '').split('-').map((x) => normalizeCardToken(x)).filter(Boolean)
return {
color: parts[0] || '',
dim2: parts.length > 1 ? parts.slice(1).join('-') : ''
}
}
function stockRowText (row, ...keys) {
for (const key of keys) {
const value = String(row?.[key] ?? '').trim()
if (value) return value
}
return ''
}
function matchesProductCardVariant (stockRow, cardRow) {
const tokens = parseVariantTokens(cardRow?.variantCodes)
if (!tokens.color && !tokens.dim2) return true
const color = normalizeCardToken(stockRowText(stockRow, 'Renk_Kodu', 'ColorCode', 'colorCode'))
const dim2 = normalizeCardToken(stockRowText(stockRow, 'Yaka', 'ItemDim2Code', 'itemDim2Code', 'Renk2'))
if (tokens.color && color !== tokens.color) return false
if (tokens.dim2 && dim2 !== tokens.dim2) return false
return true
}
function buildSizeStockRows (stockRows, cardRow) {
const totals = new Map()
for (const item of stockRows || []) {
if (!matchesProductCardVariant(item, cardRow)) continue
const size = stockRowText(item, 'Beden', 'Size', 'ItemDim1Code', 'itemDim1Code')
if (!size) continue
const qty = parseStockNumber(item?.Kullanilabilir_Envanter ?? item?.StockQty ?? item?.qty)
totals.set(size, (totals.get(size) || 0) + qty)
}
return Array.from(totals.entries())
.map(([size, qty]) => ({ size, qty }))
.sort((a, b) => variantCodeCollator.compare(a.size, b.size))
}
function mapProductRow (raw, index) { function mapProductRow (raw, index) {
const row = { const row = {
id: index + 1, id: index + 1,
@@ -1151,15 +1251,24 @@ async function fetchImageListForRow (row) {
async function openProductCard (row) { async function openProductCard (row) {
if (!row) return if (!row) return
productCardData.value = { ...row } productCardData.value = { ...row }
productCardSizeRows.value = []
productCardDialog.value = true productCardDialog.value = true
productCardSlide.value = 0 productCardSlide.value = 0
productCardStockLoading.value = true
try { try {
const list = await fetchImageListForRow(row) const [list, stockRes] = await Promise.all([
fetchImageListForRow(row),
api.get('/product-stock-query', { params: { code: row.productCode }, timeout: 30000 })
])
const images = list.map(resolveProductImageUrl).filter(Boolean) const images = list.map(resolveProductImageUrl).filter(Boolean)
if (row.imageUrl && !images.includes(row.imageUrl)) images.unshift(row.imageUrl) if (row.imageUrl && !images.includes(row.imageUrl)) images.unshift(row.imageUrl)
productCardImages.value = Array.from(new Set(images)) productCardImages.value = Array.from(new Set(images))
productCardSizeRows.value = buildSizeStockRows(Array.isArray(stockRes?.data) ? stockRes.data : [], row)
} catch { } catch {
productCardImages.value = row.imageUrl ? [row.imageUrl] : [] productCardImages.value = row.imageUrl ? [row.imageUrl] : []
productCardSizeRows.value = []
} finally {
productCardStockLoading.value = false
} }
} }
@@ -1173,6 +1282,8 @@ function openProductImageFullscreen (src) {
function onProductCardDialogHide () { function onProductCardDialogHide () {
productImageFullscreenDialog.value = false productImageFullscreenDialog.value = false
productCardStockLoading.value = false
productCardSizeRows.value = []
} }
function resetSelections () { function resetSelections () {
@@ -1373,12 +1484,30 @@ function exportCell (row, col) {
return toText(row[col.field]) return toText(row[col.field])
} }
function isExcelNumericColumn (col) {
return priceColumnNames.includes(col.name) || col.name === 'campaignRate'
}
function excelNumericCell (value) {
const n = Number(value)
if (!Number.isFinite(n) || n === 0) return '<td></td>'
const display = n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const raw = n.toFixed(2)
return `<td style="mso-number-format:'0.00';text-align:right;" x:num="${raw}">${escapeHtml(display)}</td>`
}
function exportExcelCellHtml (row, col) {
if (priceColumnNames.includes(col.name)) return excelNumericCell(row[col.field])
if (col.name === 'campaignRate') return excelNumericCell(row.campaignRate)
return `<td>${escapeHtml(exportCell(row, col))}</td>`
}
function exportVisibleExcel () { function exportVisibleExcel () {
const cols = visibleColumns.value.filter((c) => c.name !== 'image') const cols = visibleColumns.value.filter((c) => c.name !== 'image')
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => { const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
return `<td>${escapeHtml(exportCell(row, c))}</td>` return exportExcelCellHtml(row, c)
}).join('')}</tr>`).join('') }).join('')}</tr>`).join('')
const html = `<!doctype html><html><head><meta charset="utf-8"></head><body><table border="1"><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label)}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></body></html>` const html = `<!doctype html><html xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8"></head><body><table border="1"><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label)}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></body></html>`
const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' }) const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const a = document.createElement('a') const a = document.createElement('a')
@@ -1395,7 +1524,7 @@ function printVisibleRows () {
const cols = visibleColumns.value const cols = visibleColumns.value
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => { const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
if (c.name === 'image' && row.imageUrl) return `<td><img src="${row.imageUrl}" class="thumb"></td>` if (c.name === 'image' && row.imageUrl) return `<td><img src="${row.imageUrl}" class="thumb"></td>`
return `<td class="${priceColumnNames.includes(c.name) ? 'num' : ''}">${escapeHtml(exportCell(row, c))}</td>` return `<td class="${isExcelNumericColumn(c) ? 'num' : ''}">${escapeHtml(exportCell(row, c))}</td>`
}).join('')}</tr>`).join('') }).join('')}</tr>`).join('')
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Fiyat Listesi</title><style> const html = `<!doctype html><html><head><meta charset="utf-8"><title>Fiyat Listesi</title><style>
@page { size: A3 landscape; margin: 8mm; } @page { size: A3 landscape; margin: 8mm; }
@@ -1754,13 +1883,46 @@ onMounted(() => {
vertical-align: middle !important; vertical-align: middle !important;
} }
.pricing-table :deep(td.center-col) { .pricing-table :deep(th.usd-col),
text-align: center !important; .pricing-table :deep(td.usd-col) {
background: #ecf9f0;
color: #178a3e;
font-weight: 700;
} }
.pricing-table :deep(td.center-col .q-badge) { .pricing-table :deep(th.eur-col),
margin-left: auto; .pricing-table :deep(td.eur-col) {
margin-right: auto; background: #fdeeee;
color: #c62828;
font-weight: 700;
}
.pricing-table :deep(th.try-col),
.pricing-table :deep(td.try-col) {
background: #edf4ff;
color: #1e63c6;
font-weight: 700;
}
.pricing-table :deep(th.usd-col),
.pricing-table :deep(th.eur-col),
.pricing-table :deep(th.try-col),
.pricing-table :deep(td.usd-col),
.pricing-table :deep(td.eur-col),
.pricing-table :deep(td.try-col) {
font-size: 10px;
}
.pricing-table :deep(td.campaign-price-col),
.pricing-table :deep(th.campaign-price-col) {
background: #fff3f1;
color: #c62828;
font-weight: 800;
letter-spacing: 0;
}
.pricing-table :deep(td.center-col) {
text-align: center !important;
} }
.pricing-table :deep(td.karisim-wrap-col) { .pricing-table :deep(td.karisim-wrap-col) {
@@ -1784,10 +1946,6 @@ onMounted(() => {
overflow: hidden; overflow: hidden;
} }
.order-price-list-table :deep(.campaign-price-col) {
background: #f6fbf7;
}
.header-with-filter { .header-with-filter {
display: grid; display: grid;
grid-template-columns: 1fr 20px; grid-template-columns: 1fr 20px;
@@ -1813,10 +1971,55 @@ onMounted(() => {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.campaign-badge { .cell-text {
display: block;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
line-height: 1.1;
padding-top: 0;
}
.price-cell-text {
width: 100%;
text-align: right;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.campaign-price-text {
color: #c62828;
font-weight: 900;
}
.campaign-cell-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-height: 22px;
}
.campaign-text {
display: block;
max-width: 100%;
color: #c62828;
font-weight: 900;
line-height: 1.12;
text-align: center;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
.campaign-rate-text {
width: 100%;
color: #c62828;
font-weight: 900;
text-align: center;
font-variant-numeric: tabular-nums;
} }
.header-filter-btn { .header-filter-btn {
@@ -2008,6 +2211,94 @@ onMounted(() => {
word-break: break-word; word-break: break-word;
} }
.product-card-section {
position: relative;
margin-top: 12px;
border: 1px solid #e6dccb;
border-radius: 8px;
background: #fffdf8;
padding: 10px;
}
.product-card-section-title {
font-size: 13px;
font-weight: 800;
color: #5e4a22;
margin-bottom: 8px;
}
.price-info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
}
.price-info-row {
display: grid;
grid-template-columns: 70px 1fr 1fr;
gap: 6px;
align-items: center;
min-height: 26px;
padding: 4px 6px;
border: 1px solid #f0e5d2;
border-radius: 6px;
background: #fff;
font-size: 12px;
}
.price-label {
font-weight: 800;
color: #6b5a33;
}
.price-value,
.price-campaign {
text-align: right;
font-variant-numeric: tabular-nums;
}
.price-campaign {
color: #b13a2b;
font-weight: 700;
}
.size-stock-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
gap: 6px;
}
.size-stock-cell {
min-height: 42px;
border: 1px solid #eadfca;
border-radius: 6px;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
}
.size-label {
font-size: 11px;
font-weight: 800;
color: #6b5a33;
}
.size-qty {
font-size: 13px;
font-weight: 800;
color: #1f1f1f;
font-variant-numeric: tabular-nums;
}
.product-card-empty-text {
color: #7a6d55;
font-size: 12px;
padding: 6px 0;
}
.image-fullscreen-dialog { .image-fullscreen-dialog {
background: #f4f0e2; background: #f4f0e2;
} }

View File

@@ -1825,10 +1825,20 @@ function getOriginalCellValue (row, field) {
return row?.[`__orig_${field}`] ?? row?.[field] ?? 0 return row?.[`__orig_${field}`] ?? row?.[field] ?? 0
} }
function exportDecimalValue (value) {
const n = parseNumber(value)
if (!Number.isFinite(n)) return ''
return n.toFixed(2).replace('.', ',')
}
function isExportDecimalField (field) {
return editableColumnSet.has(field) || field === 'costPrice' || field === 'basePriceUsd' || field === 'basePriceTry'
}
function exportCellValue (row, field) { function exportCellValue (row, field) {
if (field === 'stockQty') return formatStock(row?.[field]) if (field === 'stockQty') return formatStock(row?.[field])
if (field === 'stockEntryDate' || field === 'lastCostingDate' || field === 'lastPricingDate') return formatDateDisplay(row?.[field]) if (field === 'stockEntryDate' || field === 'lastCostingDate' || field === 'lastPricingDate') return formatDateDisplay(row?.[field])
if (editableColumnSet.has(field)) return String(round2(row?.[field] || 0)) if (isExportDecimalField(field)) return exportDecimalValue(row?.[field])
return String(row?.[field] ?? '').trim() return String(row?.[field] ?? '').trim()
} }

View File

@@ -1926,10 +1926,27 @@ function getOriginalCellValue (row, field) {
return row?.[`__orig_${field}`] ?? row?.[field] ?? 0 return row?.[`__orig_${field}`] ?? row?.[field] ?? 0
} }
function exportDecimalValue (value) {
const n = parseNumber(value)
if (!Number.isFinite(n)) return ''
return n.toFixed(2).replace('.', ',')
}
function isExportDecimalField (field) {
return field === 'campaignRate' ||
field === 'belowBaseDiff' ||
field === 'costPrice' ||
field === 'basePriceUsd' ||
field === 'basePriceTry' ||
/^usd\d$/i.test(field) ||
/^eur\d$/i.test(field) ||
/^try\d$/i.test(field)
}
function exportCellValue (row, field) { function exportCellValue (row, field) {
if (field === 'stockQty') return formatStock(row?.[field]) if (field === 'stockQty') return formatStock(row?.[field])
if (field === 'stockEntryDate' || field === 'lastCampaignDate') return formatDateDisplay(row?.[field]) if (field === 'stockEntryDate' || field === 'lastCampaignDate') return formatDateDisplay(row?.[field])
if (editableColumnSet.has(field)) return String(round2(row?.[field] || 0)) if (isExportDecimalField(field)) return exportDecimalValue(row?.[field])
return String(row?.[field] ?? '').trim() return String(row?.[field] ?? '').trim()
} }