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,67 +70,73 @@
</div>
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions">
<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-item clickable @click="selectAllPrices">
<q-item-section>Tumunu Sec</q-item-section>
</q-item>
<q-item clickable @click="clearAllPrices">
<q-item-section>Tumunu Temizle</q-item-section>
</q-item>
<q-separator />
<q-item v-for="option in priceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
<q-item-section avatar>
<q-checkbox
dense
:model-value="selectedPriceSet.has(option.value)"
:disable="pageBusy"
@click.stop
@update:model-value="() => togglePriceOption(option.value)"
/>
</q-item-section>
<q-item-section>{{ option.label }}</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<div class="toolbar-group">
<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-item clickable @click="selectAllPrices">
<q-item-section>Tumunu Sec</q-item-section>
</q-item>
<q-item clickable @click="clearAllPrices">
<q-item-section>Tumunu Temizle</q-item-section>
</q-item>
<q-separator />
<q-item v-for="option in priceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
<q-item-section avatar>
<q-checkbox
dense
:model-value="selectedPriceSet.has(option.value)"
:disable="pageBusy"
@click.stop
@update:model-value="() => togglePriceOption(option.value)"
/>
</q-item-section>
<q-item-section>{{ option.label }}</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn
dense
flat
color="grey-8"
icon="view_sidebar"
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
:disable="pageBusy"
@click="leftDetailsExpanded = !leftDetailsExpanded"
/>
<q-btn
dense
flat
color="grey-8"
icon="view_sidebar"
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
:disable="pageBusy"
@click="leftDetailsExpanded = !leftDetailsExpanded"
/>
</div>
<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-item clickable @click="exportVisibleExcel">
<q-item-section avatar><q-icon name="grid_on" /></q-item-section>
<q-item-section>Excel'e Aktar</q-item-section>
</q-item>
<q-item clickable @click="printVisibleRows">
<q-item-section avatar><q-icon name="picture_as_pdf" /></q-item-section>
<q-item-section>PDF / Yazdir</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<div class="toolbar-group">
<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-item clickable @click="exportVisibleExcel">
<q-item-section avatar><q-icon name="grid_on" /></q-item-section>
<q-item-section>Excel'e Aktar</q-item-section>
</q-item>
<q-item clickable @click="printVisibleRows">
<q-item-section avatar><q-icon name="picture_as_pdf" /></q-item-section>
<q-item-section>PDF / Yazdir</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<q-space />
<q-pagination
v-model="currentPage"
color="primary"
:max="Math.max(1, totalPages || 1)"
:max-pages="8"
boundary-links
direction-links
:disable="pageBusy"
@update:model-value="onPageChange"
/>
<div class="text-caption text-grey-8">
Sayfa {{ currentPage }} / {{ Math.max(1, totalPages || 1) }} - {{ filteredRows.length }} satir
<div class="toolbar-group toolbar-group--paging">
<q-pagination
v-model="currentPage"
color="primary"
:max="Math.max(1, totalPages || 1)"
:max-pages="8"
boundary-links
direction-links
:disable="pageBusy"
@update:model-value="onPageChange"
/>
<div class="text-caption text-grey-8">
Sayfa {{ currentPage }} / {{ Math.max(1, totalPages || 1) }} - {{ filteredRows.length }} satir
</div>
</div>
</div>
</div>
@@ -340,10 +346,14 @@
<q-img
v-if="props.row.imageUrl"
:src="props.row.imageUrl"
class="product-thumb"
class="product-thumb cursor-pointer"
fit="cover"
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>
</template>
@@ -379,6 +389,99 @@
</template>
</q-table>
</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>
</template>
@@ -418,10 +521,19 @@ const serverFilterLoading = ref({})
const serverFilterLastQuery = ref({})
const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' })
const imageCache = new Map()
const imageListCache = new Map()
const mainTableRef = ref(null)
const topScrollRef = ref(null)
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 selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || []))
const selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || []))
@@ -739,13 +851,84 @@ async function loadImagesForRows (list) {
timeout: 15000
})
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)
row.imageUrl = url
imageListCache.set(key, Array.isArray(res?.data) ? res.data : [])
} catch {
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 () {
@@ -793,7 +976,7 @@ function col (name, label, field, width, extra = {}) {
}
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('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' }),
@@ -801,9 +984,6 @@ const allColumns = [
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('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('kategori', 'KATEGORI', 'kategori', 58, { 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([
'stockEntryDate',
'lastPricingDate',
'lastCampaignDate',
'askiliYan',
'kategori',
'urunIlkGrubu',
@@ -906,7 +1083,7 @@ function exportCell (row, col) {
function exportVisibleExcel () {
const cols = visibleColumns.value
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>`
}).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>`
@@ -935,7 +1112,7 @@ function printVisibleRows () {
th { background: #957116; color: #fff; }
th, td { border: 1px solid #ccc; padding: 3px; vertical-align: middle; }
.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>`
const win = window.open('', '_blank')
if (!win) return
@@ -1006,7 +1183,7 @@ onMounted(() => {
height: calc(100vh - 58px);
display: flex;
flex-direction: column;
--pricing-row-height: 31px;
--pricing-row-height: 108px;
--pricing-header-height: 72px;
--pricing-table-height: calc(100vh - 156px);
}
@@ -1015,12 +1192,53 @@ onMounted(() => {
display: flex;
flex-direction: column;
gap: 6px;
align-items: flex-end;
align-items: flex-start;
flex: 1 1 auto;
min-width: 0;
}
.top-actions-row {
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 {
@@ -1035,14 +1253,25 @@ onMounted(() => {
}
.product-thumb {
width: 46px;
height: 46px;
width: 100px;
height: 100px;
border-radius: 4px;
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 {
padding: 2px 4px !important;
padding: 4px !important;
}
.top-x-scroll {
@@ -1081,7 +1310,7 @@ onMounted(() => {
height: var(--pricing-row-height) !important;
min-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;
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
}
@@ -1156,6 +1385,13 @@ onMounted(() => {
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) {
background: #f6fbf7;
}
@@ -1259,4 +1495,166 @@ onMounted(() => {
padding: 16px 20px;
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>