Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-18 23:44:19 +03:00
parent ef33a56a49
commit b59889bbdb

View File

@@ -70,6 +70,7 @@
</div> </div>
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions"> <div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions">
<div class="toolbar-group">
<q-btn-dropdown dense color="secondary" outline icon="view_module" label="Fiyat Secimi" :auto-close="false" :disable="pageBusy"> <q-btn-dropdown dense color="secondary" outline icon="view_module" label="Fiyat Secimi" :auto-close="false" :disable="pageBusy">
<q-list dense class="currency-menu-list"> <q-list dense class="currency-menu-list">
<q-item clickable @click="selectAllPrices"> <q-item clickable @click="selectAllPrices">
@@ -103,7 +104,9 @@
:disable="pageBusy" :disable="pageBusy"
@click="leftDetailsExpanded = !leftDetailsExpanded" @click="leftDetailsExpanded = !leftDetailsExpanded"
/> />
</div>
<div class="toolbar-group">
<q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :disable="pageBusy || filteredRows.length === 0"> <q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :disable="pageBusy || filteredRows.length === 0">
<q-list dense style="min-width: 220px;"> <q-list dense style="min-width: 220px;">
<q-item clickable @click="exportVisibleExcel"> <q-item clickable @click="exportVisibleExcel">
@@ -116,9 +119,11 @@
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
</div>
<q-space /> <q-space />
<div class="toolbar-group toolbar-group--paging">
<q-pagination <q-pagination
v-model="currentPage" v-model="currentPage"
color="primary" color="primary"
@@ -135,6 +140,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }"> <div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
<div v-if="showGuidanceOverlay" class="empty-overlay"> <div v-if="showGuidanceOverlay" class="empty-overlay">
@@ -340,10 +346,14 @@
<q-img <q-img
v-if="props.row.imageUrl" v-if="props.row.imageUrl"
:src="props.row.imageUrl" :src="props.row.imageUrl"
class="product-thumb" class="product-thumb cursor-pointer"
fit="cover" fit="cover"
no-spinner no-spinner
@click.stop="openProductCard(props.row)"
/> />
<div v-else class="product-thumb-placeholder cursor-pointer" @click.stop="openProductCard(props.row)">
<q-icon name="image_not_supported" size="24px" color="grey-6" />
</div>
</q-td> </q-td>
</template> </template>
@@ -379,6 +389,99 @@
</template> </template>
</q-table> </q-table>
</div> </div>
<q-dialog v-model="productCardDialog" maximized @hide="onProductCardDialogHide">
<q-card class="product-card-dialog">
<q-card-section class="row items-center q-pb-sm">
<div class="text-h6">Urun Karti</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section class="q-pt-md">
<div class="product-card-content">
<div class="product-card-images">
<q-carousel
v-if="productCardImages.length"
v-model="productCardSlide"
animated
swipeable
navigation
arrows
height="100%"
class="product-card-carousel rounded-borders"
>
<q-carousel-slide
v-for="(img, idx) in productCardImages"
:key="'img-' + idx"
:name="idx"
class="column no-wrap flex-center"
>
<div class="dialog-image-stage cursor-pointer" @click="openProductImageFullscreen(img)">
<q-img :src="img" fit="contain" class="dialog-image" />
</div>
</q-carousel-slide>
</q-carousel>
<div v-else class="dialog-image-empty">
<q-icon name="image_not_supported" size="36px" color="grey-6" />
</div>
</div>
<div class="product-card-fields">
<div class="field-row field-row-head"><span class="k">Urun</span><span class="v">{{ productCardData.productCode || '-' }} / {{ productCardData.variantCodes || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Kodu</span><span class="v">{{ productCardData.productCode || '-' }}</span></div>
<div class="field-row"><span class="k">Varyant</span><span class="v">{{ productCardData.variantCodes || '-' }}</span></div>
<div class="field-row"><span class="k">Marka</span><span class="v">{{ productCardData.marka || '-' }}</span></div>
<div class="field-row"><span class="k">Marka Grubu</span><span class="v">{{ productCardData.brandGroupSelection || '-' }}</span></div>
<div class="field-row"><span class="k">Kategori</span><span class="v">{{ productCardData.kategori || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Ilk Grubu</span><span class="v">{{ productCardData.urunIlkGrubu || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Ana Grubu</span><span class="v">{{ productCardData.urunAnaGrubu || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Alt Grubu</span><span class="v">{{ productCardData.urunAltGrubu || '-' }}</span></div>
<div class="field-row"><span class="k">Icerik</span><span class="v">{{ productCardData.icerik || '-' }}</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">Stok</span><span class="v">{{ formatStock(productCardData.stockQty || 0) }}</span></div>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="productImageFullscreenDialog" maximized>
<q-card class="image-fullscreen-dialog">
<q-card-section class="row items-center q-pb-sm">
<div class="text-h6">Urun Fotografi</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section class="image-fullscreen-body">
<q-carousel
v-if="fullscreenImages.length"
v-model="productImageFullscreenSlide"
animated
swipeable
navigation
arrows
height="calc(100vh - 120px)"
class="image-fullscreen-carousel"
>
<q-carousel-slide
v-for="(img, idx) in fullscreenImages"
:key="'full-img-' + idx"
:name="idx"
class="column no-wrap flex-center"
>
<div class="image-fullscreen-stage">
<q-img :src="img" fit="contain" class="image-fullscreen-img" />
</div>
</q-carousel-slide>
</q-carousel>
</q-card-section>
</q-card>
</q-dialog>
</q-page> </q-page>
</template> </template>
@@ -418,10 +521,19 @@ const serverFilterLoading = ref({})
const serverFilterLastQuery = ref({}) const serverFilterLastQuery = ref({})
const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' }) const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' })
const imageCache = new Map() const imageCache = new Map()
const imageListCache = new Map()
const mainTableRef = ref(null) const mainTableRef = ref(null)
const topScrollRef = ref(null) const topScrollRef = ref(null)
let syncingScroll = false let syncingScroll = false
const productCardDialog = ref(false)
const productCardData = ref({})
const productCardImages = ref([])
const productCardSlide = ref(0)
const productImageFullscreenDialog = ref(false)
const productImageFullscreenSlide = ref(0)
const fullscreenImages = computed(() => productCardImages.value || [])
const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || [])) const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || []))
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 || []))
@@ -739,13 +851,84 @@ async function loadImagesForRows (list) {
timeout: 15000 timeout: 15000
}) })
const first = Array.isArray(res?.data) ? res.data[0] : null const first = Array.isArray(res?.data) ? res.data[0] : null
const url = toText(first?.thumb_url || first?.content_url || first?.full_url) const url = resolveProductImageUrl(first)
imageCache.set(key, url) imageCache.set(key, url)
row.imageUrl = url row.imageUrl = url
imageListCache.set(key, Array.isArray(res?.data) ? res.data : [])
} catch { } catch {
imageCache.set(key, '') imageCache.set(key, '')
} }
})) }))
rows.value = [...rows.value]
}
function normalizeUploadsPath (storagePath) {
const raw = toText(storagePath)
if (!raw) return ''
const normalized = raw.replace(/\\/g, '/')
const idx = normalized.toLowerCase().indexOf('/uploads/')
if (idx >= 0) return normalized.slice(idx)
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
return ''
}
function resolveProductImageUrl (item) {
if (!item || typeof item !== 'object') return ''
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
const thumbUrl = toText(item.thumb_url || item.thumbUrl)
if (thumbUrl) return thumbUrl
const fullUrl = toText(item.full_url || item.fullUrl)
if (fullUrl) return fullUrl
const contentUrl = toText(item.content_url || item.ContentURL)
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage)
if (uploadsPath) return uploadsPath
const fileName = toText(item.file_name || item.FileName)
return fileName ? `/uploads/image/${fileName}` : ''
}
async function fetchImageListForRow (row) {
const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}`
if (imageListCache.has(key)) return imageListCache.get(key) || []
const res = await api.get('/product-images', {
params: {
code: row.productCode,
dim1_id: row.dim1 || '',
dim3_id: row.dim3 || ''
},
timeout: 15000
})
const list = Array.isArray(res?.data) ? res.data : []
imageListCache.set(key, list)
return list
}
async function openProductCard (row) {
if (!row) return
productCardData.value = { ...row }
productCardDialog.value = true
productCardSlide.value = 0
try {
const list = await fetchImageListForRow(row)
const images = list.map(resolveProductImageUrl).filter(Boolean)
if (row.imageUrl && !images.includes(row.imageUrl)) images.unshift(row.imageUrl)
productCardImages.value = Array.from(new Set(images))
} catch {
productCardImages.value = row.imageUrl ? [row.imageUrl] : []
}
}
function openProductImageFullscreen (src) {
const value = toText(src)
if (!value) return
const idx = Math.max(0, fullscreenImages.value.findIndex((x) => toText(x) === value))
productImageFullscreenSlide.value = idx
productImageFullscreenDialog.value = true
}
function onProductCardDialogHide () {
productImageFullscreenDialog.value = false
} }
function resetSelections () { function resetSelections () {
@@ -793,7 +976,7 @@ function col (name, label, field, width, extra = {}) {
} }
const allColumns = [ const allColumns = [
col('image', '', 'imageUrl', 58, { align: 'center', classes: 'image-col sticky-col' }), col('image', '', 'imageUrl', 108, { align: 'center', classes: 'image-col sticky-col' }),
col('brandGroupSelection', 'MARKA GRUBU', 'brandGroupSelection', 86, { classes: 'ps-col sticky-col' }), col('brandGroupSelection', 'MARKA GRUBU', 'brandGroupSelection', 86, { classes: 'ps-col sticky-col' }),
col('marka', 'MARKA', 'marka', 62, { sortable: true, classes: 'ps-col sticky-col' }), col('marka', 'MARKA', 'marka', 62, { sortable: true, classes: 'ps-col sticky-col' }),
col('productCode', 'URUN KODU', 'productCode', 112, { sortable: true, classes: 'ps-col product-code-col sticky-col' }), col('productCode', 'URUN KODU', 'productCode', 112, { sortable: true, classes: 'ps-col product-code-col sticky-col' }),
@@ -801,9 +984,6 @@ const allColumns = [
col('variantStocks', 'STOK', 'stockQty', 62, { align: 'right', sortable: true, classes: 'ps-col variant-stock-col sticky-col' }), col('variantStocks', 'STOK', 'stockQty', 62, { align: 'right', sortable: true, classes: 'ps-col variant-stock-col sticky-col' }),
col('campaignLabel', 'KAMPANYA', 'campaignLabel', 150, { classes: 'ps-col campaign-col sticky-col' }), col('campaignLabel', 'KAMPANYA', 'campaignLabel', 150, { classes: 'ps-col campaign-col sticky-col' }),
col('campaignRate', 'IND %', 'campaignRate', 58, { align: 'right', classes: 'ps-col campaign-rate-col sticky-col' }), col('campaignRate', 'IND %', 'campaignRate', 58, { align: 'right', classes: 'ps-col campaign-rate-col sticky-col' }),
col('stockEntryDate', 'STOK GIRIS', 'stockEntryDate', 86, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
col('lastPricingDate', 'SON FIYAT', 'lastPricingDate', 86, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
col('lastCampaignDate', 'SON KAMPANYA', 'lastCampaignDate', 98, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col' }), col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col' }),
col('kategori', 'KATEGORI', 'kategori', 58, { sortable: true, classes: 'ps-col' }), col('kategori', 'KATEGORI', 'kategori', 58, { sortable: true, classes: 'ps-col' }),
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 70, { sortable: true, classes: 'ps-col' }), col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 70, { sortable: true, classes: 'ps-col' }),
@@ -818,9 +998,6 @@ const allColumns = [
] ]
const hideableLeftDetailColumnNames = new Set([ const hideableLeftDetailColumnNames = new Set([
'stockEntryDate',
'lastPricingDate',
'lastCampaignDate',
'askiliYan', 'askiliYan',
'kategori', 'kategori',
'urunIlkGrubu', 'urunIlkGrubu',
@@ -906,7 +1083,7 @@ function exportCell (row, col) {
function exportVisibleExcel () { function exportVisibleExcel () {
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}" width="54" height="54"></td>` if (c.name === 'image' && row.imageUrl) return `<td><img src="${row.imageUrl}" width="100" height="100"></td>`
return `<td>${escapeHtml(exportCell(row, c))}</td>` return `<td>${escapeHtml(exportCell(row, c))}</td>`
}).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 || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></body></html>` const html = `<!doctype html><html><head><meta charset="utf-8"></head><body><table border="1"><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></body></html>`
@@ -935,7 +1112,7 @@ function printVisibleRows () {
th { background: #957116; color: #fff; } th { background: #957116; color: #fff; }
th, td { border: 1px solid #ccc; padding: 3px; vertical-align: middle; } th, td { border: 1px solid #ccc; padding: 3px; vertical-align: middle; }
.num { text-align: right; } .num { text-align: right; }
.thumb { width: 42px; height: 42px; object-fit: cover; } .thumb { width: 100px; height: 100px; object-fit: cover; }
</style></head><body><h1>Fiyat Listesi</h1><table><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table><script>window.onload=function(){window.print()}<\/script></body></html>` </style></head><body><h1>Fiyat Listesi</h1><table><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table><script>window.onload=function(){window.print()}<\/script></body></html>`
const win = window.open('', '_blank') const win = window.open('', '_blank')
if (!win) return if (!win) return
@@ -1006,7 +1183,7 @@ onMounted(() => {
height: calc(100vh - 58px); height: calc(100vh - 58px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
--pricing-row-height: 31px; --pricing-row-height: 108px;
--pricing-header-height: 72px; --pricing-header-height: 72px;
--pricing-table-height: calc(100vh - 156px); --pricing-table-height: calc(100vh - 156px);
} }
@@ -1015,12 +1192,53 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
align-items: flex-end; align-items: flex-start;
flex: 1 1 auto;
min-width: 0;
} }
.top-actions-row { .top-actions-row {
width: 100%; width: 100%;
justify-content: flex-end; justify-content: flex-start;
}
.top-bar {
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.top-actions-row--filters,
.top-actions-row--actions {
justify-content: flex-start;
}
.toolbar-group {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
background: #fff;
}
.toolbar-group--paging {
gap: 10px;
}
.toolbar-group :deep(.q-btn) {
min-height: 32px;
white-space: nowrap;
}
.toolbar-group :deep(.q-btn__content) {
gap: 6px;
padding: 0;
}
.toolbar-group :deep(.q-btn__wrapper) {
padding: 4px 10px;
} }
.table-wrap { .table-wrap {
@@ -1035,14 +1253,25 @@ onMounted(() => {
} }
.product-thumb { .product-thumb {
width: 46px; width: 100px;
height: 46px; height: 100px;
border-radius: 4px; border-radius: 4px;
background: #f4f4f4; background: #f4f4f4;
} }
.product-thumb-placeholder {
width: 100px;
height: 100px;
border-radius: 4px;
background: #f4f4f4;
border: 1px dashed rgba(0, 0, 0, 0.16);
display: flex;
align-items: center;
justify-content: center;
}
.image-cell { .image-cell {
padding: 2px 4px !important; padding: 4px !important;
} }
.top-x-scroll { .top-x-scroll {
@@ -1081,7 +1310,7 @@ onMounted(() => {
height: var(--pricing-row-height) !important; height: var(--pricing-row-height) !important;
min-height: var(--pricing-row-height) !important; min-height: var(--pricing-row-height) !important;
max-height: var(--pricing-row-height) !important; max-height: var(--pricing-row-height) !important;
line-height: var(--pricing-row-height); line-height: 1.25;
padding: 0 !important; padding: 0 !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important; border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
} }
@@ -1156,6 +1385,13 @@ onMounted(() => {
font-weight: 700; font-weight: 700;
} }
.pricing-table :deep(td.ps-col),
.pricing-table :deep(td.usd-col),
.pricing-table :deep(td.eur-col),
.pricing-table :deep(td.try-col) {
vertical-align: middle !important;
}
.order-price-list-table :deep(.campaign-price-col) { .order-price-list-table :deep(.campaign-price-col) {
background: #f6fbf7; background: #f6fbf7;
} }
@@ -1259,4 +1495,166 @@ onMounted(() => {
padding: 16px 20px; padding: 16px 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
} }
.product-card-dialog {
--pc-media-h: calc(100vh - 180px);
--pc-media-w: min(74vw, 1220px);
background: #f9f8f5;
height: 100vh;
display: flex;
flex-direction: column;
}
:deep(.product-card-dialog > .q-card__section:last-child) {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
.product-card-content {
display: grid;
grid-template-columns: minmax(360px, 420px) minmax(760px, 1fr);
gap: 14px;
align-items: stretch;
justify-content: start;
height: 100%;
}
.product-card-images {
grid-column: 2;
grid-row: 1;
height: var(--pc-media-h);
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
}
.product-card-carousel {
width: var(--pc-media-w);
height: 100%;
max-width: 100%;
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
border: 1px solid #e4dac7;
}
.dialog-image {
width: 100%;
height: 100%;
}
.dialog-image-stage {
width: var(--pc-media-w);
max-width: 100%;
height: 100%;
overflow: hidden;
border-radius: 10px;
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
display: flex;
align-items: center;
justify-content: center;
}
.dialog-image-empty {
width: var(--pc-media-w);
max-width: 100%;
height: var(--pc-media-h);
border: 1px dashed #c5b28d;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: #faf6ee;
}
.product-card-fields {
grid-column: 1;
grid-row: 1;
border: 1px solid #e4dac7;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff 0%, #fdfaf4 100%);
padding: 12px;
height: var(--pc-media-h);
overflow: auto;
}
.field-row {
display: grid;
grid-template-columns: 150px 1fr;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #efe5d5;
font-size: 13px;
}
.field-row.field-row-head {
background: #f8f3e9;
border: 1px solid #e6dccb;
border-radius: 8px;
padding: 8px 10px;
margin-bottom: 8px;
}
.field-row:last-child {
border-bottom: none;
}
.field-row .k {
color: #6b5a33;
font-weight: 700;
}
.field-row .v {
color: #1f1f1f;
word-break: break-word;
}
.image-fullscreen-dialog {
background: #f4f0e2;
}
.image-fullscreen-body {
height: calc(100vh - 72px);
display: flex;
align-items: center;
justify-content: center;
}
.image-fullscreen-carousel {
width: min(98vw, 1500px);
}
.image-fullscreen-stage {
width: min(96vw, 1400px);
height: calc(100vh - 120px);
border-radius: 10px;
background: linear-gradient(180deg, #f1e7d3 0%, #e9dcc4 100%);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.image-fullscreen-img {
width: 100%;
height: 100%;
}
.q-btn,
.q-icon,
.cursor-pointer {
cursor: pointer !important;
}
@media (max-width: 1024px) {
.product-card-content {
grid-template-columns: 1fr;
}
.product-card-images,
.product-card-fields {
grid-column: 1;
grid-row: auto;
}
}
</style> </style>