Files
bssapp/ui/src/pages/ProductStockQuery.vue
2026-03-16 00:13:02 +03:00

2095 lines
63 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<q-page
v-if="canReadOrder"
class="order-page q-pa-md"
:style="{ '--grid-header-h': gridHeaderHeight }"
>
<div class="sticky-stack">
<div class="filter-bar row q-col-gutter-md q-mb-sm">
<div class="col-12 col-md-6">
<q-select
v-model="selectedProductCode"
:options="filteredProductOptions"
label="Ürün Kodu"
filled
dense
clearable
use-input
input-debounce="250"
emit-value
map-options
option-value="value"
option-label="label"
:loading="loadingProducts"
@filter="filterProducts"
@keyup.enter="fetchStockByCode"
/>
</div>
<div class="col-auto">
<q-btn
color="primary"
icon="search"
label="Sorgula"
:loading="loadingStock"
:disable="!selectedProductCode"
@click="fetchStockByCode"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="grey-8"
icon="restart_alt"
label="Sıfırla"
@click="resetForm"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="primary"
icon="unfold_more"
:label="allDetailsExpanded ? 'Tüm Depoları Kapat' : 'Tüm Depoları Göster'"
:disable="!level1Groups.length"
@click="toggleAllDetails"
/>
</div>
</div>
<div class="save-toolbar">
<div class="text-subtitle2 text-weight-bold">Ürün Kodundan Stok Sorgula</div>
</div>
<div
v-if="showGridHeader"
class="order-grid-header"
>
<div class="col-fixed model">MODEL</div>
<div class="col-fixed renk">RENK</div>
<div class="col-fixed ana">ÜRÜN ANA GRUBU</div>
<div class="col-fixed alt">ÜRÜN ALT GRUBU</div>
<div class="col-fixed aciklama-col">AÇIKLAMA</div>
<div class="beden-block">
<div class="grp-row">
<div class="grp-title">{{ activeSchema?.title || 'BEDEN' }}</div>
<div class="grp-body">
<div
v-for="v in sizeLabels"
:key="'hdr-' + activeGrpKey + '-' + v"
class="grp-cell hdr"
>
{{ v }}
</div>
</div>
</div>
</div>
<div class="total-row">
<div class="total-cell">ADET</div>
<div class="total-cell">FOTO</div>
</div>
</div>
</div>
<q-banner
v-if="errorMessage"
class="bg-red-1 text-negative q-my-sm rounded-borders"
dense
>
{{ errorMessage }}
</q-banner>
<q-banner
v-else-if="!loadingStock && !level1Groups.length"
class="bg-blue-1 text-primary q-my-sm rounded-borders"
dense
>
Ürün kodu seçip sorgulayın.
</q-banner>
<div class="order-scroll-y">
<div v-if="level1Groups.length" class="order-grid-body">
<template v-for="grp1 in level1Groups" :key="grp1.key">
<div class="summary-group open">
<div class="order-sub-header level-1" @click="toggleOpen(grp1.key)">
<div class="sub-col level1-merged">
<div class="text-weight-bold">{{ grp1.productCode }}</div>
<div class="text-caption">{{ grp1.productDesc || '-' }}</div>
</div>
<div class="sub-center level1-center">
<div class="beden-row values-top">
<div v-for="sz in sizeLabels" :key="`v1-${grp1.key}-${sz}`" class="beden-cell">
{{ Number(grp1.sizeTotals[sz] || 0) > 0 ? formatNumber(grp1.sizeTotals[sz]) : '' }}
</div>
</div>
<div class="beden-row headers">
<div v-for="sz in sizeLabels" :key="`h1-${grp1.key}-${sz}`" class="beden-cell">
{{ sz }}
</div>
</div>
</div>
<div class="sub-right level1-right">
<div class="top-total">{{ formatNumber(grp1.totalQty) }}</div>
<div class="bottom-row">
<div class="bottom-label">ADET</div>
</div>
<div class="icon-row">
<q-icon :name="isOpen(grp1.key) ? 'expand_less' : 'expand_more'" size="18px" />
</div>
</div>
</div>
<template v-if="isOpen(grp1.key)">
<template v-for="grp2 in grp1.children" :key="grp2.key">
<div class="order-sub-header level-2" @click="onLevel2Click(grp1.productCode, grp2)">
<div class="sub-col model">{{ grp1.productCode || '-' }}</div>
<div class="sub-col renk">
<div class="renk-kodu">{{ grp2.colorCode || '-' }}{{ grp2.secondColor ? '-' + grp2.secondColor : '' }}</div>
<div class="renk-aciklama">{{ grp2.colorDesc || '-' }}</div>
</div>
<div class="sub-col ana">{{ grp2.urunAnaGrubu || '-' }}</div>
<div class="sub-col alt">{{ grp2.urunAltGrubu || '-' }}</div>
<div class="sub-col aciklama">{{ grp2.aciklama || '-' }}</div>
<div class="sub-center level2-center">
<div class="beden-row values-top">
<div v-for="sz in sizeLabels" :key="`top2-${grp2.key}-${sz}`" class="beden-cell">
{{ Number(grp2.sizeTotals[sz] || 0) > 0 ? formatNumber(grp2.sizeTotals[sz]) : '' }}
</div>
</div>
<div class="beden-row headers">
<div v-for="sz in sizeLabels" :key="`h2-${grp2.key}-${sz}`" class="beden-cell">
{{ sz }}
</div>
</div>
</div>
<div class="sub-right level2-right">
<div class="top-total">{{ formatNumber(grp2.totalQty) }}</div>
<div class="bottom-row">
<div class="bottom-label">ADET</div>
<q-icon :name="isOpen(grp2.key) ? 'expand_less' : 'expand_more'" size="18px" />
</div>
</div>
<div class="sub-image level2-image">
<q-card flat bordered class="product-image-card cursor-pointer" @click.stop="openProductCard(grp1, grp2)">
<q-card-section class="q-pa-xs product-image-wrap">
<q-img
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.photoDim3 || grp2.secondColor, grp2.photoDim1ID || '', grp2.photoDim3ID || '')"
:src="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.photoDim3 || grp2.secondColor, grp2.photoDim1ID || '', grp2.photoDim3ID || '')"
fit="contain"
class="product-image"
loading="lazy"
@error="onProductImageError(grp1.productCode, grp2.colorCode, grp2.photoDim3 || grp2.secondColor, grp2.photoDim1ID || '', grp2.photoDim3ID || '')"
/>
<div v-else class="product-image-placeholder">
<q-icon name="image_not_supported" size="22px" color="grey-6" />
</div>
</q-card-section>
</q-card>
<q-btn
dense
flat
color="primary"
label="Urun Detayi Gor"
class="detail-open-btn"
@click.stop="openProductCard(grp1, grp2)"
/>
</div>
</div>
<template v-if="isOpen(grp2.key)">
<div class="detail-table-wrap">
<div
v-for="row in buildLevel2Rows(grp2)"
:key="row.rowKey"
class="summary-row"
>
<div class="cell depo-merged">{{ row.depoAdi || '-' }}</div>
<div class="grp-area">
<div class="grp-row">
<div
v-for="v in sizeLabels"
:key="row.rowKey + '-sz-' + v"
class="cell beden"
>
{{ resolveBedenValue(row.bedenMap, row.grpKey, v) }}
</div>
</div>
</div>
<div class="cell adet">{{ formatNumber(row.adet) }}</div>
<div class="cell img-placeholder"></div>
</div>
</div>
</template>
</template>
</template>
</div>
</template>
</div>
</div>
<q-dialog v-model="productCardDialog" maximized>
<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.colorCode || '-' }}{{ productCardData.secondColor ? '-' + productCardData.secondColor : '' }}</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">Urun Renk</span><span class="v">{{ productCardData.colorCode || '-' }}</span></div>
<div class="field-row"><span class="k">Urun 2.Renk</span><span class="v">{{ productCardData.secondColor || '-' }}</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 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">Urun Icerigi</span><span class="v">{{ productCardData.urunIcerigi || '-' }}</span></div>
<div class="field-row"><span class="k">Fit</span><span class="v">{{ productCardData.fit || '-' }}</span></div>
<div class="field-row"><span class="k">Drop</span><span class="v">{{ productCardData.drop || '-' }}</span></div>
<div class="field-row"><span class="k">Kumas</span><span class="v">{{ productCardData.kumas || '-' }}</span></div>
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
<div class="product-card-stock-inline q-mt-md">
<div class="text-subtitle2 text-weight-bold">Stok Ozet</div>
<div class="text-caption">Toplam Stok: {{ formatNumber(productCardData.totalQty || 0) }}</div>
<div class="stock-size-grid q-mt-sm">
<div v-for="sz in sizeLabels" :key="'dlg-sz-' + sz" class="stock-size-chip">
<span class="label">{{ sz }}</span>
<span class="value">{{ Number(productCardData.sizeTotals?.[sz] || 0) > 0 ? formatNumber(productCardData.sizeTotals[sz]) : '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="productImageFullscreenDialog" maximized @hide="onFullscreenMouseUp">
<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"
@update:model-value="onFullscreenSlideChange"
>
<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"
@wheel.prevent="onFullscreenWheel"
@mousedown="onFullscreenMouseDown"
@dblclick="toggleFullscreenImageZoom"
>
<q-img
:src="img"
fit="contain"
class="image-fullscreen-img"
:style="fullscreenImageStyle"
/>
</div>
</q-carousel-slide>
</q-carousel>
</q-card-section>
</q-card>
</q-dialog>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">Bu modüle erişim yetkiniz yok.</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useQuasar } from 'quasar'
import api from 'src/services/api'
import { usePermission } from 'src/composables/usePermission'
import { normalizeSearchText } from 'src/utils/searchText'
import {
detectBedenGroup,
normalizeBedenLabel,
schemaByKey as storeSchemaByKey,
useOrderEntryStore
} from 'src/stores/orderentryStore'
const $q = useQuasar()
const { canRead } = usePermission()
const canReadOrder = canRead('order')
const orderStore = useOrderEntryStore()
const loadingProducts = ref(false)
const loadingStock = ref(false)
const errorMessage = ref('')
const selectedProductCode = ref('')
const productOptions = ref([])
const filteredProductOptions = ref([])
const rawRows = ref([])
const productImageCache = ref({})
const productImageLoading = ref({})
const productImageListByCode = ref({})
const productImageListLoading = ref({})
const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({})
const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0)
const productCardDialog = ref(false)
const productCardData = ref({})
const productCardImages = ref([])
const productCardSlide = ref(0)
const productImageFullscreenDialog = ref(false)
const productImageFullscreenSrc = ref('')
const productImageFullscreenSlide = ref(0)
const productImageFullscreenZoom = ref(1)
const productImageFullscreenOffsetX = ref(0)
const productImageFullscreenOffsetY = ref(0)
const productImageFullscreenDragging = ref(false)
const productImageFullscreenDragStartX = ref(0)
const productImageFullscreenDragStartY = ref(0)
const productImageFullscreenDragOriginX = ref(0)
const productImageFullscreenDragOriginY = ref(0)
const IMAGE_LIST_CONCURRENCY = 8
let imageListActiveRequests = 0
const imageListWaitQueue = []
const activeSchema = ref(storeSchemaByKey.tak)
const activeGrpKey = ref('tak')
const openState = ref({})
const sizeLabels = computed(() => activeSchema.value?.values || [])
const showGridHeader = computed(() =>
!loadingStock.value && level1Groups.value.length > 0
)
const allDetailsExpanded = computed(() => {
const groups = level1Groups.value || []
if (!groups.length) return false
const detailKeys = []
for (const g1 of groups) {
for (const g2 of g1.children || []) {
detailKeys.push(g2.key)
for (const g3 of g2.children || []) detailKeys.push(g3.key)
}
}
if (!detailKeys.length) return false
return detailKeys.every((k) => openState.value[k] === true)
})
const gridHeaderHeight = computed(() =>
showGridHeader.value ? '56px' : '0px'
)
const fullscreenImageStyle = computed(() => ({
transform: `translate(${productImageFullscreenOffsetX.value}px, ${productImageFullscreenOffsetY.value}px) scale(${productImageFullscreenZoom.value})`,
transformOrigin: 'center center',
transition: productImageFullscreenDragging.value ? 'none' : 'transform 0.1s ease-out',
cursor: productImageFullscreenZoom.value > 1 ? (productImageFullscreenDragging.value ? 'grabbing' : 'grab') : 'zoom-in'
}))
const fullscreenImages = computed(() => {
const arr = Array.isArray(productCardImages.value) ? productCardImages.value : []
if (arr.length) return arr
const single = String(productImageFullscreenSrc.value || '').trim()
return single ? [single] : []
})
function emptySizeTotals() {
const map = {}
for (const s of sizeLabels.value) map[s] = 0
return map
}
function parseNumber(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 firstText(...values) {
for (const value of values) {
const s = String(value || '').trim()
if (s) return s
}
return ''
}
function resolveKumasValue(item) {
return firstText(
item?.BIRINCI_PARCA_KUMAS,
item?.ProductAtt26Desc,
item?.PRODUCTATT26DESC,
item?.ProductAtt29Desc,
item?.PRODUCTATT29DESC
)
}
function resolveKarisimValue(item) {
return firstText(
item?.BIRINCI_PARCA_KARISIM,
item?.ProductAtt29Desc,
item?.PRODUCTATT29DESC,
item?.ProductAtt26Desc,
item?.PRODUCTATT26DESC
)
}
function sortByColorCodeAsc(a, b) {
const compareCodeLike = (va, vb) => {
const sa = String(va || '').trim()
const sb = String(vb || '').trim()
const pa = sa.match(/^(\d+)(?:_(\d+))?$/)
const pb = sb.match(/^(\d+)(?:_(\d+))?$/)
if (pa && pb) {
const a1 = Number.parseInt(pa[1], 10)
const b1 = Number.parseInt(pb[1], 10)
if (a1 !== b1) return a1 - b1
const a2 = Number.parseInt(pa[2] || '0', 10)
const b2 = Number.parseInt(pb[2] || '0', 10)
if (a2 !== b2) return a2 - b2
}
return sa.localeCompare(sb, 'tr', { sensitivity: 'base' })
}
const ca = String(a?.colorCode || '').trim()
const cb = String(b?.colorCode || '').trim()
const na = Number.parseInt(ca, 10)
const nb = Number.parseInt(cb, 10)
const aNum = Number.isFinite(na)
const bNum = Number.isFinite(nb)
if (aNum && bNum && na !== nb) return na - nb
if (aNum !== bNum) return aNum ? -1 : 1
const cmp = compareCodeLike(ca, cb)
if (cmp !== 0) return cmp
return compareCodeLike(a?.secondColor, b?.secondColor)
}
function buildImageKey(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}::${String(secondColor || '').trim().toUpperCase()}::${String(dim1Id || '').trim().toUpperCase()}::${String(dim3Id || '').trim().toUpperCase()}`
}
function normalizeImageDim3(value) {
const s = String(value || '').trim().toUpperCase()
if (!s) return ''
if (/^\d{3}(?:_\d+)?$/.test(s)) return s
return ''
}
function resolvePhotoDim3(item, secondColorDisplay = '') {
return (
normalizeImageDim3(item?.Renk2) ||
normalizeImageDim3(item?.ItemDim3Code) ||
normalizeImageDim3(secondColorDisplay) ||
''
)
}
function resolvePhotoDim1ID(item) {
const candidates = [
item?.PhotoDim1ID,
item?.photoDim1ID,
item?.Dim1ID,
item?.dim1ID,
item?.ColorID,
item?.colorID,
item?.RenkID
]
for (const value of candidates) {
const s = String(value || '').trim()
if (/^\d+$/.test(s)) return s
}
return ''
}
function resolvePhotoDim3ID(item) {
const candidates = [
item?.PhotoDim3ID,
item?.photoDim3ID,
item?.Dim3ID,
item?.dim3ID,
item?.SecondColorID,
item?.secondColorID,
item?.Renk2ID
]
for (const value of candidates) {
const s = String(value || '').trim()
if (/^\d+$/.test(s)) return s
}
return ''
}
function buildDim3Candidates(secondColor) {
const secondTrim = normalizeImageDim3(secondColor)
if (!secondTrim) return ['']
const set = new Set([secondTrim])
if (/^\d{3}$/.test(secondTrim)) set.add(`${secondTrim}_1`)
return Array.from(set)
}
async function fetchProductImageList(codeTrim, colorTrim, secondTrim, dim1IdTrim = '', dim3IdTrim = '') {
const dim3Candidates = buildDim3Candidates(secondTrim)
for (const dim3Candidate of dim3Candidates) {
const params = { code: codeTrim, dim1: colorTrim }
if (String(dim1IdTrim || '').trim()) params.dim1_id = String(dim1IdTrim || '').trim()
if (String(dim3IdTrim || '').trim()) params.dim3_id = String(dim3IdTrim || '').trim()
if (dim3Candidate) params.dim3 = dim3Candidate
const res = await api.get('/product-images', { params })
const list = Array.isArray(res?.data) ? res.data : []
if (list.length) return list
}
return []
}
function normalizeUploadsPath(storagePath) {
const raw = String(storagePath || '').trim()
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 { contentUrl: '', publicUrl: '', thumbUrl: '', fullUrl: '' }
}
let contentUrl = ''
const imageId = Number(item.id || item.ID || 0)
if (Number.isFinite(imageId) && imageId > 0) {
contentUrl = `/api/product-images/${imageId}/content`
} else {
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
}
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
let publicUrl = ''
if (uploadsPath) {
publicUrl = uploadsPath
} else {
const fileName = String(item.file_name || item.FileName || '').trim()
if (fileName) publicUrl = `/uploads/image/${fileName}`
}
const thumbUrl = String(item.thumb_url || item.thumbUrl || '').trim()
const fullUrl = String(item.full_url || item.fullUrl || '').trim()
return { contentUrl, publicUrl, thumbUrl, fullUrl }
}
function extractImageOrder(fileName, fallbackIndex) {
const name = String(fileName || '').trim()
const m = name.match(/\((\d+)\)(?=\.[a-z0-9]+$)/i)
if (m) return Number(m[1] || 999999)
const m2 = name.match(/[-_ ](\d+)(?=\.[a-z0-9]+$)/i)
if (m2) return Number(m2[1] || 999999)
return 1000000 + Number(fallbackIndex || 0)
}
function sortImagesForDisplay(list) {
return (Array.isArray(list) ? list : [])
.map((item, idx) => ({ item, idx, order: extractImageOrder(item?.file_name || item?.FileName || '', idx) }))
.sort((a, b) => {
if (a.order !== b.order) return a.order - b.order
return a.idx - b.idx
})
.map((x) => x.item)
}
async function resolveProductImageUrlForCarousel(item) {
const resolved = resolveProductImageUrl(item)
const contentUrl = String(resolved.contentUrl || '').trim()
if (contentUrl) {
try {
const blobRes = await api.get(contentUrl, { baseURL: '', responseType: 'blob' })
const blob = blobRes?.data
if (blob instanceof Blob) {
const objectUrl = URL.createObjectURL(blob)
productImageBlobUrls.value.push(objectUrl)
return objectUrl
}
} catch {
// fall through to public url
}
}
const fullUrl = String(resolved.fullUrl || '').trim()
if (fullUrl) return fullUrl
const publicUrl = String(resolved.publicUrl || '').trim()
return String(publicUrl || fullUrl || contentUrl || '').trim()
}
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const existing = productImageCache.value[key]
if (existing !== undefined) return existing || ''
void ensureProductImage(code, color, secondColor, dim1Id, dim3Id)
return ''
}
async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim()
}
async function ensureProductImage(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const codeTrim = String(code || '').trim().toUpperCase()
const colorTrim = String(color || '').trim().toUpperCase()
const secondTrim = String(secondColor || '').trim().toUpperCase()
const dim1IDTrim = String(dim1Id || '').trim().toUpperCase()
const dim3IDTrim = String(dim3Id || '').trim().toUpperCase()
const listKey = buildImageKey(codeTrim, colorTrim, secondTrim, dim1IDTrim, dim3IDTrim)
if (!codeTrim) {
productImageCache.value[key] = ''
return ''
}
if (Date.now() < Number(productImageListBlockedUntil.value || 0)) {
productImageCache.value[key] = ''
return ''
}
if (productImageCache.value[key] !== undefined) return productImageCache.value[key] || ''
if (productImageLoading.value[key]) return ''
productImageLoading.value[key] = true
try {
if (!productImageListByCode.value[listKey]) {
if (!productImageListLoading.value[listKey]) {
productImageListLoading.value[listKey] = true
try {
if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) {
await new Promise((resolve) => imageListWaitQueue.push(resolve))
}
imageListActiveRequests++
productImageListByCode.value[listKey] = await fetchProductImageList(codeTrim, colorTrim, secondTrim, dim1IDTrim, dim3IDTrim)
} catch (err) {
productImageListByCode.value[listKey] = []
const status = Number(err?.response?.status || 0)
if (status >= 500 || status === 403 || status === 0) {
productImageListBlockedUntil.value = Date.now() + 30 * 1000
}
console.warn('[ProductStockQuery] product image list fetch failed', { code: codeTrim, color: colorTrim, secondColor: secondTrim, err })
} finally {
imageListActiveRequests = Math.max(0, imageListActiveRequests - 1)
const nextInQueue = imageListWaitQueue.shift()
if (nextInQueue) nextInQueue()
delete productImageListLoading.value[listKey]
}
} else {
while (productImageListLoading.value[listKey]) {
await new Promise((resolve) => setTimeout(resolve, 25))
}
}
}
const rawList = Array.isArray(productImageListByCode.value[listKey]) ? productImageListByCode.value[listKey] : []
const primaryItem = rawList[0] || null
const secondaryItem = rawList.length > 1 ? rawList[rawList.length - 1] : null
const primaryResolved = resolveProductImageUrl(primaryItem)
let preferredCardUrl = primaryItem ? await resolveProductImageUrlForCarousel(primaryItem) : ''
if (!preferredCardUrl && secondaryItem) {
preferredCardUrl = await resolveProductImageUrlForCarousel(secondaryItem)
}
const secondaryResolved = resolveProductImageUrl(secondaryItem)
productImageCache.value[key] = String(
preferredCardUrl ||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl ||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl ||
''
).trim()
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.contentUrl || ''
} catch (err) {
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
productImageCache.value[key] = ''
productImageFallbackByKey.value[key] = ''
} finally {
delete productImageLoading.value[key]
}
return productImageCache.value[key] || ''
}
function formatNumber(v) {
return parseNumber(v).toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
}
function normalizeSize(v) {
return normalizeBedenLabel(v)
}
function resolveBedenValue(bedenMap, grpKey, bedenLabel) {
const map = bedenMap?.[grpKey]
if (!map || typeof map !== 'object') return 0
return Number(map[bedenLabel] || 0)
}
function isOpen(key) {
return openState.value[key] !== false
}
function toggleOpen(key) {
openState.value[key] = !isOpen(key)
}
function initOpenState(groups, expandDetails = false) {
const next = {}
for (const g1 of groups) {
next[g1.key] = true
for (const g2 of g1.children) {
next[g2.key] = expandDetails
for (const g3 of g2.children) {
next[g3.key] = expandDetails
}
}
}
openState.value = next
}
function expandAllDetails() {
initOpenState(level1Groups.value || [], true)
}
function collapseAllDetails() {
initOpenState(level1Groups.value || [], false)
}
function toggleAllDetails() {
if (allDetailsExpanded.value) {
collapseAllDetails()
return
}
expandAllDetails()
}
function buildLevel3Rows(grp3) {
const byKey = new Map()
const gk = activeGrpKey.value || 'tak'
for (const item of grp3.items || []) {
const model = String(item.Urun_Kodu || '').trim()
const renk = String(item.Renk_Kodu || '').trim()
const renk2 = String(item.Yaka || '').trim()
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim()
const aciklama = String(item.Madde_Aciklamasi || '').trim()
const beden = normalizeSize(item.Beden || '')
const qty = parseNumber(item.Kullanilabilir_Envanter)
const rowKey = [model, renk, renk2, grp3.depoKodu || '', grp3.depoAdi || ''].join('|')
if (!byKey.has(rowKey)) {
byKey.set(rowKey, {
rowKey,
model,
renk,
renk2,
urunAnaGrubu,
urunAltGrubu,
aciklama,
grpKey: gk,
bedenMap: { [gk]: {} },
adet: 0,
depoAdi: grp3.depoAdi || '-'
})
}
const row = byKey.get(rowKey)
row.bedenMap[gk][beden] = Number(row.bedenMap[gk][beden] || 0) + qty
row.adet += qty
}
return Array.from(byKey.values())
}
function buildLevel2Rows(grp2) {
const merged = []
for (const grp3 of grp2.children || []) {
merged.push(...buildLevel3Rows(grp3))
}
return merged
}
const level1Groups = computed(() => {
const l1Map = new Map()
for (const item of rawRows.value) {
const productCode = String(item.Urun_Kodu || '').trim()
const productDesc = String(item.Madde_Aciklamasi || '').trim()
const colorCode = String(item.Renk_Kodu || '').trim()
const colorDesc = String(item.Renk_Aciklamasi || '').trim()
const secondColor = String(item.Yaka || '').trim()
const photoDim3 = resolvePhotoDim3(item, secondColor)
const photoDim1ID = resolvePhotoDim1ID(item)
const photoDim3ID = resolvePhotoDim3ID(item)
const depoKodu = String(item.Depo_Kodu || '').trim()
const depoAdi = String(item.Depo_Adi || '').trim()
const kategori = String(item.YETISKIN_GARSON || '').trim()
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim()
const urunIcerigi = String(item.URUN_ICERIGI || item.KISA_KAR || '').trim()
const fit = String(item.BIRINCI_PARCA_FIT || '').trim()
const drop = String(item.DR || '').trim()
const kumas = resolveKumasValue(item)
const karisim = resolveKarisimValue(item)
const aciklama = String(item.Madde_Aciklamasi || '').trim()
const beden = normalizeSize(item.Beden || '')
const qty = parseNumber(item.Kullanilabilir_Envanter)
if (!l1Map.has(productCode)) {
l1Map.set(productCode, {
key: `L1|${productCode}`,
productCode,
productDesc,
sizeTotals: emptySizeTotals(),
totalQty: 0,
childrenMap: new Map()
})
}
const l1 = l1Map.get(productCode)
if (Object.prototype.hasOwnProperty.call(l1.sizeTotals, beden)) {
l1.sizeTotals[beden] += qty
}
l1.totalQty += qty
const l2Key = `${colorCode}|${secondColor}|${photoDim3}|${photoDim1ID}|${photoDim3ID}`
if (!l1.childrenMap.has(l2Key)) {
l1.childrenMap.set(l2Key, {
key: `L2|${productCode}|${l2Key}`,
colorCode,
colorDesc,
secondColor,
photoDim3,
photoDim1ID,
photoDim3ID,
kategori,
urunAnaGrubu,
urunAltGrubu,
urunIcerigi,
fit,
drop,
kumas,
karisim,
aciklama,
sizeTotals: emptySizeTotals(),
totalQty: 0,
childrenMap: new Map()
})
}
const l2 = l1.childrenMap.get(l2Key)
if (Object.prototype.hasOwnProperty.call(l2.sizeTotals, beden)) {
l2.sizeTotals[beden] += qty
}
l2.totalQty += qty
const l3Key = `${depoKodu}|${depoAdi}`
if (!l2.childrenMap.has(l3Key)) {
l2.childrenMap.set(l3Key, {
key: `L3|${productCode}|${l2Key}|${l3Key}`,
depoKodu,
depoAdi,
sizeTotals: emptySizeTotals(),
totalQty: 0,
items: []
})
}
const l3 = l2.childrenMap.get(l3Key)
if (Object.prototype.hasOwnProperty.call(l3.sizeTotals, beden)) {
l3.sizeTotals[beden] += qty
}
l3.totalQty += qty
l3.items.push({
...item,
_rowKey: `${productCode}|${colorCode}|${secondColor}|${depoKodu}|${beden}`
})
}
return Array.from(l1Map.values()).map((l1) => ({
...l1,
children: Array.from(l1.childrenMap.values())
.map((l2) => ({
...l2,
children: Array.from(l2.childrenMap.values())
}))
.sort(sortByColorCodeAsc)
}))
})
function filterProducts(val, update) {
if (!val) {
update(() => {
filteredProductOptions.value = [...productOptions.value]
})
return
}
const needle = normalizeSearchText(val)
update(() => {
filteredProductOptions.value = productOptions.value.filter(opt =>
normalizeSearchText(opt.label).includes(needle)
)
})
}
async function loadProductOptions() {
loadingProducts.value = true
try {
const res = await api.get('/products')
const arr = Array.isArray(res?.data) ? res.data : []
productOptions.value = arr
.map((x) => String(x?.ProductCode || '').trim())
.filter((code) => code.length === 13)
.sort((a, b) => a.localeCompare(b, 'tr'))
.map((code) => ({ label: code, value: code }))
filteredProductOptions.value = [...productOptions.value]
} catch (err) {
errorMessage.value = 'Ürün kodları alınamadı.'
console.error('loadProductOptions error:', err)
} finally {
loadingProducts.value = false
}
}
async function fetchStockByCode() {
const code = String(selectedProductCode.value || '').trim()
if (!code) return
loadingStock.value = true
errorMessage.value = ''
try {
if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) {
orderStore.initSchemaMap()
}
const res = await api.get('/product-stock-query', { params: { code } })
const list = Array.isArray(res?.data) ? res.data : []
if (!list.length) {
rawRows.value = []
openState.value = {}
return
}
const first = list[0] || {}
const grpKey = detectBedenGroup(
list.map((x) => x?.Beden || ''),
first?.URUN_ANA_GRUBU || '',
first?.YETISKIN_GARSON || ''
)
const schemaMap = Object.keys(orderStore.schemaMap || {}).length
? orderStore.schemaMap
: storeSchemaByKey
activeGrpKey.value = grpKey || 'tak'
activeSchema.value = schemaMap?.[grpKey] || storeSchemaByKey.tak
rawRows.value = list
productImageCache.value = {}
productImageLoading.value = {}
productImageListByCode.value = {}
productImageListLoading.value = {}
productImageFallbackByKey.value = {}
productImageContentLoading.value = {}
productImageListBlockedUntil.value = 0
initOpenState(level1Groups.value)
} catch (err) {
console.error('fetchStockByCode error:', err)
rawRows.value = []
openState.value = {}
errorMessage.value = 'Stok sorgulama sırasında hata oluştu.'
$q.notify({
type: 'negative',
position: 'top-right',
message: 'Stok sorgusu başarısız.'
})
} finally {
loadingStock.value = false
}
}
function onLevel2Click(productCode, grp2) {
toggleOpen(grp2.key)
if (isOpen(grp2.key)) {
void ensureProductImage(productCode, grp2.colorCode, grp2.photoDim3 || grp2.secondColor, grp2.photoDim1ID || '', grp2.photoDim3ID || '')
}
}
async function openProductCard(grp1, grp2) {
const productCode = String(grp1?.productCode || '').trim()
const colorCode = String(grp2?.colorCode || '').trim()
const secondColor = String(grp2?.secondColor || '').trim()
const photoDim3 = String(grp2?.photoDim3 || secondColor).trim()
const photoDim1ID = String(grp2?.photoDim1ID || '').trim()
const photoDim3ID = String(grp2?.photoDim3ID || '').trim()
const listKey = buildImageKey(productCode, colorCode, photoDim3, photoDim1ID, photoDim3ID)
const codeTrim = String(productCode || '').trim().toUpperCase()
const colorTrim = String(colorCode || '').trim().toUpperCase()
const secondTrim = String(photoDim3 || '').trim().toUpperCase()
const dim1IDTrim = String(photoDim1ID || '').trim().toUpperCase()
const dim3IDTrim = String(photoDim3ID || '').trim().toUpperCase()
await ensureProductImage(productCode, colorCode, photoDim3, photoDim1ID, photoDim3ID)
let list = Array.isArray(productImageListByCode.value[listKey]) ? productImageListByCode.value[listKey] : []
console.info('[ProductStockQuery][openProductCard] request', {
productCode,
colorCode,
secondColor,
dim1ID: dim1IDTrim,
dim3ID: dim3IDTrim,
listKey,
cachedListCount: list.length
})
if (!list.length && codeTrim) {
try {
list = await fetchProductImageList(codeTrim, colorTrim, secondTrim, dim1IDTrim, dim3IDTrim)
productImageListByCode.value[listKey] = list
console.info('[ProductStockQuery][openProductCard] refetch', {
productCode: codeTrim,
dim1: colorTrim,
dim1ID: dim1IDTrim,
dim3: secondTrim,
dim3ID: dim3IDTrim,
fetchedCount: list.length,
fileNames: list.map((x) => String(x?.file_name || x?.FileName || '').trim()).filter(Boolean)
})
} catch (err) {
console.warn('[ProductStockQuery] product card image list refetch failed', {
code: codeTrim,
color: colorTrim,
secondColor: secondTrim,
err
})
}
}
const sortedList = sortImagesForDisplay(list)
const imageCandidates = await Promise.all(
sortedList.map((item) => resolveProductImageUrlForCarousel(item))
)
const images = imageCandidates.filter((x) => String(x || '').trim() !== '')
console.info('[ProductStockQuery][openProductCard] render', {
productCode,
colorCode,
secondColor,
candidateCount: imageCandidates.length,
imageCount: images.length,
firstImages: images.slice(0, 3)
})
const uniqueImages = Array.from(new Set(images))
if (!uniqueImages.length) {
const single = getProductImageUrl(productCode, colorCode, photoDim3, photoDim1ID, photoDim3ID)
if (single) uniqueImages.push(single)
}
productCardImages.value = uniqueImages
productCardSlide.value = 0
productCardData.value = {
productCode,
colorCode,
secondColor,
kategori: String(grp2?.kategori || '').trim(),
urunAnaGrubu: String(grp2?.urunAnaGrubu || '').trim(),
urunAltGrubu: String(grp2?.urunAltGrubu || '').trim(),
urunIcerigi: String(grp2?.urunIcerigi || '').trim(),
fit: String(grp2?.fit || '').trim(),
drop: String(grp2?.drop || '').trim(),
kumas: String(grp2?.kumas || '').trim(),
karisim: String(grp2?.karisim || '').trim(),
sizeTotals: grp2?.sizeTotals || {},
totalQty: Number(grp2?.totalQty || 0)
}
productCardDialog.value = true
}
function openProductImageFullscreen(src) {
const value = String(src || '').trim()
if (!value) return
productImageFullscreenSrc.value = value
const idx = Math.max(0, fullscreenImages.value.findIndex((x) => String(x || '').trim() === value))
productImageFullscreenSlide.value = idx
productImageFullscreenZoom.value = 1
productImageFullscreenOffsetX.value = 0
productImageFullscreenOffsetY.value = 0
productImageFullscreenDragging.value = false
productImageFullscreenDialog.value = true
}
function toggleFullscreenImageZoom() {
const current = Number(productImageFullscreenZoom.value || 1)
if (current < 1.5) productImageFullscreenZoom.value = 1.8
else if (current < 2.3) productImageFullscreenZoom.value = 2.6
else if (current < 3.2) productImageFullscreenZoom.value = 3.2
else productImageFullscreenZoom.value = 1
if (productImageFullscreenZoom.value <= 1) {
productImageFullscreenOffsetX.value = 0
productImageFullscreenOffsetY.value = 0
}
}
function onFullscreenWheel(evt) {
if (!evt) return
evt.preventDefault()
const delta = Number(evt.deltaY || 0)
if (productImageFullscreenZoom.value > 1.01 && !evt.ctrlKey) {
productImageFullscreenOffsetY.value -= delta * 0.45
return
}
const current = Number(productImageFullscreenZoom.value || 1)
const next = delta < 0 ? current + 0.2 : current - 0.2
productImageFullscreenZoom.value = Math.min(4, Math.max(1, Number(next.toFixed(2))))
if (productImageFullscreenZoom.value <= 1) {
productImageFullscreenOffsetX.value = 0
productImageFullscreenOffsetY.value = 0
}
}
function onFullscreenMouseDown(evt) {
if (productImageFullscreenZoom.value <= 1) return
productImageFullscreenDragging.value = true
productImageFullscreenDragStartX.value = Number(evt?.clientX || 0)
productImageFullscreenDragStartY.value = Number(evt?.clientY || 0)
productImageFullscreenDragOriginX.value = Number(productImageFullscreenOffsetX.value || 0)
productImageFullscreenDragOriginY.value = Number(productImageFullscreenOffsetY.value || 0)
}
function onFullscreenMouseMove(evt) {
if (!productImageFullscreenDragging.value) return
const dx = Number(evt?.clientX || 0) - productImageFullscreenDragStartX.value
const dy = Number(evt?.clientY || 0) - productImageFullscreenDragStartY.value
productImageFullscreenOffsetX.value = productImageFullscreenDragOriginX.value + dx
productImageFullscreenOffsetY.value = productImageFullscreenDragOriginY.value + dy
}
function onFullscreenMouseUp() {
productImageFullscreenDragging.value = false
}
function onFullscreenSlideChange() {
productImageFullscreenZoom.value = 1
productImageFullscreenOffsetX.value = 0
productImageFullscreenOffsetY.value = 0
productImageFullscreenDragging.value = false
}
function resetForm() {
selectedProductCode.value = ''
rawRows.value = []
errorMessage.value = ''
openState.value = {}
activeSchema.value = storeSchemaByKey.tak
productImageCache.value = {}
productImageLoading.value = {}
productImageListByCode.value = {}
productImageListLoading.value = {}
productImageFallbackByKey.value = {}
productImageContentLoading.value = {}
productImageListBlockedUntil.value = 0
productCardDialog.value = false
productCardData.value = {}
productCardImages.value = []
productCardSlide.value = 0
productImageFullscreenDialog.value = false
productImageFullscreenSrc.value = ''
productImageFullscreenSlide.value = 0
productImageFullscreenZoom.value = 1
productImageFullscreenOffsetX.value = 0
productImageFullscreenOffsetY.value = 0
productImageFullscreenDragging.value = false
}
onUnmounted(() => {
window.removeEventListener('mousemove', onFullscreenMouseMove)
window.removeEventListener('mouseup', onFullscreenMouseUp)
for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {}
}
productImageBlobUrls.value = []
})
onMounted(() => {
loadProductOptions()
window.addEventListener('mousemove', onFullscreenMouseMove)
window.addEventListener('mouseup', onFullscreenMouseUp)
})
</script>
<style scoped>
.order-page {
--psq-sticky-offset: 12px;
--grp-title-w: 44px;
--psq-header-h: 56px;
--psq-col-adet: calc(var(--col-adet) + var(--beden-w));
--psq-col-img: 190px;
--psq-l1-lift: 42px;
}
.order-grid-header {
top: calc(var(--header-h) + var(--filter-h) + var(--save-h) + var(--psq-sticky-offset)) !important;
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count)))
var(--psq-col-adet)
var(--psq-col-img) !important;
}
.order-grid-header .col-fixed,
.order-grid-header .total-cell {
writing-mode: horizontal-tb !important;
transform: none !important;
height: var(--psq-header-h) !important;
font-size: 10px !important;
line-height: 1 !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px !important;
}
.order-grid-header .beden-block {
height: var(--psq-header-h) !important;
}
.order-grid-header .grp-row {
height: var(--psq-header-h) !important;
align-items: center;
}
.order-grid-header .grp-title {
width: var(--grp-title-w) !important;
text-align: center !important;
padding-right: 0 !important;
font-size: 10px !important;
}
.order-grid-header .grp-cell.hdr {
height: var(--psq-header-h) !important;
font-size: 10px !important;
}
.order-grid-header .total-row {
grid-column: 7 / -1 !important;
}
.order-grid-header .total-cell {
width: var(--psq-col-adet) !important;
}
.order-grid-header .total-cell:last-child {
width: var(--psq-col-img) !important;
}
.order-sub-header {
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count)))
var(--psq-col-adet)
var(--psq-col-img) !important;
top: calc(
var(--header-h)
+ var(--filter-h)
+ var(--save-h)
+ var(--grid-header-h)
+ var(--psq-sticky-offset)
) !important;
}
.order-sub-header.level-2 {
min-height: 252px !important;
height: 252px !important;
background: #fff9c4 !important;
border-top: 1px solid #d4c79f !important;
border-bottom: 1px solid #d4c79f !important;
}
.order-sub-header.level-1 {
min-height: 84px !important;
height: 84px !important;
top: calc(
var(--header-h)
+ var(--filter-h)
+ var(--save-h)
+ var(--grid-header-h)
+ var(--psq-sticky-offset)
- var(--psq-l1-lift)
) !important;
background: var(--q-primary, #1976d2) !important;
border-top: 1px solid #145ea8 !important;
border-bottom: 1px solid #145ea8 !important;
color: #fff !important;
}
.order-sub-header.level-1 .sub-col.level1-merged {
grid-column: 1 / 6;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
padding: 0 10px;
border-right: 1px solid rgba(255, 255, 255, 0.45);
min-width: 0;
}
.order-sub-header.level-1 .sub-col.level1-merged .text-caption {
color: #fff !important;
opacity: 0.95;
}
.order-sub-header.level-1 .sub-center.level1-center {
grid-column: 6;
display: grid;
grid-template-rows: 42px 42px;
align-items: stretch;
height: 100%;
overflow: hidden;
padding-left: calc(var(--grp-title-w) + var(--grp-title-gap));
margin-left: 0;
}
.order-sub-header.level-1 .beden-row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
height: 42px;
min-height: 42px;
}
.order-sub-header.level-1 .beden-row .beden-cell {
display: flex;
align-items: center;
justify-content: center;
height: 42px;
min-height: 42px;
background: var(--q-primary, #1976d2) !important;
color: #fff !important;
border-right: 1px solid rgba(255, 255, 255, 0.45);
border-bottom: 1px solid rgba(255, 255, 255, 0.45);
border-top: none;
border-left: none;
font-size: 12px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
overflow: hidden;
}
.order-sub-header.level-1 .beden-row.values-top .beden-cell {
background: #fff9c4 !important;
color: var(--q-primary, #1976d2) !important;
font-weight: 700;
}
.order-sub-header.level-1 .beden-row.headers .beden-cell {
font-weight: 500;
color: #fff !important;
}
.order-sub-header.level-1 .sub-right.level1-right {
grid-column: 7 / 8;
display: grid;
grid-template-columns: var(--psq-col-adet);
grid-template-rows: 30px 30px 24px;
align-items: stretch;
justify-items: stretch;
height: 100%;
overflow: hidden;
padding: 0 8px 0 6px;
border-left: 1px solid rgba(255, 255, 255, 0.45);
color: #fff;
}
.order-sub-header.level-1 .sub-right .top-total {
grid-column: 1;
grid-row: 1;
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
background: #fff9c4 !important;
color: var(--q-primary, #1976d2) !important;
padding: 0 6px;
border-radius: 4px;
}
.order-sub-header.level-1 .sub-right .bottom-label {
font-size: 12px;
font-weight: 700;
}
.order-sub-header.level-1 .sub-right .bottom-row {
grid-column: 1;
grid-row: 2;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
white-space: nowrap;
overflow: hidden;
}
.order-sub-header.level-1 .sub-right .icon-row {
grid-column: 1;
grid-row: 3;
display: flex;
align-items: center;
justify-content: flex-end;
}
.order-sub-header.level-2 .sub-col {
display: flex;
align-items: center;
padding: 0 8px;
font-size: 12px;
font-weight: 500;
color: #111;
min-width: 0;
border-right: 1px solid #d4c79f;
white-space: normal;
overflow: visible;
text-overflow: clip;
line-height: 1.2;
word-break: break-word;
overflow-wrap: anywhere;
}
.order-sub-header.level-2 .sub-col.model { grid-column: 1; }
.order-sub-header.level-2 .sub-col.renk { grid-column: 2; }
.order-sub-header.level-2 .sub-col.ana { grid-column: 3; }
.order-sub-header.level-2 .sub-col.alt { grid-column: 4; }
.order-sub-header.level-2 .sub-col.aciklama { grid-column: 5; }
.order-sub-header.level-2 .sub-col.model,
.order-sub-header.level-2 .sub-col.renk,
.order-sub-header.level-2 .sub-col.ana,
.order-sub-header.level-2 .sub-col.alt {
justify-content: center;
text-align: center;
}
.order-sub-header.level-2 .sub-col.renk {
flex-direction: column;
gap: 2px;
line-height: 1.1;
}
.order-sub-header.level-2 .sub-col.renk .renk-kodu {
font-weight: 700;
}
.order-sub-header.level-2 .sub-col.renk .renk-aciklama {
font-size: 11px;
opacity: 0.9;
}
.order-sub-header.level-2 .sub-col.aciklama {
justify-content: flex-start;
text-align: left;
}
.order-sub-header.level-2 .sub-center.level2-center {
grid-column: 6;
display: grid;
grid-template-rows: 1fr 1fr;
align-items: stretch;
height: 100%;
padding-left: var(--grp-title-w);
margin-left: var(--grp-title-gap);
}
.order-sub-header.level-2 .beden-row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
}
.order-sub-header.level-2 .beden-row.values-top .beden-cell {
border-right: 1px solid #d4c79f;
background: transparent;
font-size: 13px;
font-weight: 600;
color: #1f1f1f;
line-height: 1;
}
.order-sub-header.level-2 .beden-row.headers .beden-cell {
border-top: 1px solid #d4c79f;
border-right: 1px solid #d4c79f;
border-bottom: none;
background: var(--q-primary, #1976d2);
font-size: 12px;
font-weight: 700;
color: #fff;
line-height: 1;
}
.order-sub-header.level-2 .sub-right.level2-right {
grid-column: 7 / 8;
display: grid;
grid-template-columns: var(--psq-col-adet);
grid-template-rows: 1fr 1fr;
align-items: center;
justify-items: start;
padding-left: 8px;
padding-right: 0;
transform: none !important;
border-left: 1px solid #d4c79f;
}
.order-sub-header.level-2 .sub-image.level2-image {
grid-column: 8 / 9;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 10px;
border-left: 1px solid #d4c79f;
}
.product-image-card {
width: 162px;
height: 216px;
border-radius: 8px;
overflow: hidden;
}
.product-image-wrap {
width: 100%;
height: 100%;
}
.product-image {
width: 100%;
height: 100%;
background: #fff;
}
.product-image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f6f7;
}
.detail-open-btn {
margin-top: 4px;
font-size: 11px;
}
.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-stock {
background: linear-gradient(180deg, #f9f6ef 0%, #fffdf9 100%);
border: 1px solid #e4dac7;
border-radius: 12px;
padding: 14px;
}
.stock-size-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
gap: 8px;
}
.stock-size-chip {
border: 1px solid #e6dccb;
border-radius: 10px;
background: #ffffff;
padding: 6px 8px;
display: flex;
justify-content: space-between;
font-size: 12px;
}
.stock-size-chip .label {
font-weight: 700;
}
.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;
}
:deep(.product-card-carousel .q-carousel__navigation) {
bottom: 10px;
}
:deep(.product-card-carousel .q-carousel__navigation .q-btn) {
color: var(--q-secondary, #26a69a);
opacity: 0.7;
transform: scale(1);
transition: transform 0.14s ease, opacity 0.14s ease, color 0.14s ease;
}
:deep(.product-card-carousel .q-carousel__navigation .q-btn--active) {
color: var(--q-primary, #1976d2);
opacity: 1;
transform: scale(1.22);
}
.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;
}
.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);
}
:deep(.image-fullscreen-carousel .q-carousel__slide) {
background: linear-gradient(180deg, #f7f2e7 0%, #efe5d2 100%);
}
:deep(.image-fullscreen-carousel .q-carousel__navigation) {
bottom: 12px;
}
:deep(.image-fullscreen-carousel .q-carousel__navigation .q-btn) {
color: var(--q-secondary, #26a69a);
opacity: 0.72;
transform: scale(1);
transition: transform 0.14s ease, color 0.14s ease;
}
:deep(.image-fullscreen-carousel .q-carousel__navigation .q-btn--active) {
color: var(--q-primary, #1976d2);
opacity: 1;
transform: scale(1.28);
text-shadow: 0 0 0.5px currentColor;
filter: drop-shadow(0 0 2px rgba(38, 166, 154, 0.45));
}
:deep(.image-fullscreen-carousel .q-carousel__arrow .q-btn) {
color: var(--q-primary, #1976d2);
background: rgba(255, 255, 255, 0.88);
border: 1px solid #d7e2f3;
}
:deep(.image-fullscreen-carousel .q-carousel__arrow .q-btn:hover) {
color: var(--q-secondary, #26a69a);
border-color: rgba(38, 166, 154, 0.45);
background: rgba(255, 255, 255, 0.98);
}
.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%;
}
.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;
}
.product-card-stock-inline {
border-top: 1px solid #e6dccb;
padding-top: 10px;
}
.q-btn,
.q-icon,
.product-image-card,
.cursor-pointer {
cursor: pointer !important;
}
@media (max-width: 1024px) {
.product-card-content {
grid-template-columns: 1fr;
}
.product-card-images,
.product-card-fields {
grid-column: auto;
grid-row: auto;
}
.product-card-fields {
height: auto;
}
.product-card-dialog {
--pc-media-h: calc(100vh - 220px);
--pc-media-w: min(96vw, 900px);
}
}
.order-sub-header.level-2 .sub-right .top-total {
grid-column: 1 / 2;
grid-row: 1;
font-size: 14px;
font-weight: 700;
line-height: 1;
justify-self: start;
text-align: left;
}
.order-sub-header.level-2 .sub-right .bottom-label {
font-size: 12px;
font-weight: 700;
}
.order-sub-header.level-2 .sub-right .bottom-row {
grid-column: 1;
grid-row: 2;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
}
.order-grid-body .summary-row {
display: grid !important;
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
var(--psq-col-adet)
var(--psq-col-img) !important;
min-height: 56px;
height: 56px;
background: #fff;
border-top: 1px solid #d4c79f;
border-bottom: 1px solid #d4c79f;
align-items: stretch;
}
.order-grid-body .summary-row .cell {
min-height: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid #d4c79f;
font-size: 12px;
overflow: hidden !important;
}
.order-grid-body .summary-row .cell:last-child {
border-right: none;
}
.order-grid-body .summary-row .grp-area {
display: grid !important;
grid-template-columns: var(--grp-title-w) var(--grp-title-gap) 1fr;
width: 100% !important;
height: 56px;
padding-left: 0 !important;
transform: none !important;
border-right: 1px solid #d4c79f;
}
.order-grid-body .summary-row .grp-row {
grid-column: 3;
width: 100%;
height: 56px;
margin-left: 0 !important;
justify-content: start !important;
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
}
.order-grid-body .summary-row .grp-row .cell.beden {
width: var(--beden-w);
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
margin: 0 !important;
border-right: 1px solid #d4c79f;
border-left: none !important;
border-top: none !important;
border-bottom: none !important;
min-height: 56px;
height: 56px;
box-sizing: border-box;
}
.order-grid-body .summary-row .grp-row .cell.beden:first-child {
border-left: 1px solid #d4c79f !important;
}
.order-grid-body .summary-row .cell.model,
.order-grid-body .summary-row .cell.renk,
.order-grid-body .summary-row .cell.ana,
.order-grid-body .summary-row .cell.alt {
justify-content: center;
text-align: center;
}
.order-grid-body .summary-row .cell.aciklama {
grid-column: 5 / 6 !important;
position: static !important;
width: var(--col-aciklama) !important;
margin-right: 0 !important;
min-height: 56px !important;
z-index: auto !important;
background: #fff !important;
box-sizing: border-box !important;
border-right: 1px solid #d4c79f !important;
justify-content: flex-start !important;
text-align: left !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
line-height: 1 !important;
padding-left: 6px !important;
padding-right: 6px !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
}
.order-grid-body .summary-row .cell.depo-merged {
grid-column: 1 / 6 !important;
justify-content: center !important;
text-align: center !important;
font-weight: 600;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.order-grid-body .summary-row .cell.adet,
.order-grid-body .summary-row .cell.termin {
justify-content: flex-end;
text-align: right;
padding-right: 10px !important;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.order-grid-body .summary-row .cell.img-placeholder {
justify-content: center;
text-align: center;
}
.order-grid-body {
margin-top: 0 !important;
padding-top: 0 !important;
}
.order-grid-body > .summary-group,
.order-grid-body > .summary-group:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
}
.detail-table-wrap {
padding: 8px 0 12px 0;
background: #fff;
}
.detail-table :deep(th),
.detail-table :deep(td) {
white-space: nowrap;
}
</style>