feat: enhance media preview components by adding stock information support, improving user experience in managing media items

This commit is contained in:
Yoga Pangestu 2026-07-13 19:43:27 +07:00
parent 62beb309a8
commit bed22579b5
16 changed files with 56 additions and 16 deletions

View File

@ -22,11 +22,13 @@ const props = withDefaults(
urls?: string[];
title?: string;
titles?: string[];
stocks?: string[];
}>(),
{
urls: () => [],
title: 'Pratinjau Foto',
titles: () => [],
stocks: () => [],
}
);
@ -74,6 +76,14 @@ const activeTitle = computed(() => {
return props.title ?? 'Pratinjau Foto';
});
const activeStock = computed(() => {
if (props.stocks && props.stocks.length > 0 && currentIndex.value >= 0) {
return props.stocks[currentIndex.value] ?? null;
}
return null;
});
function nextImage() {
if (!hasMultiple.value) {
return;
@ -142,6 +152,7 @@ function handleTouchEnd(e: TouchEvent) {
<DialogContent class="sm:max-w-2xl p-4 overflow-hidden gap-4">
<DialogHeader class="pb-2 border-b">
<DialogTitle class="text-base font-semibold">{{ activeTitle }}</DialogTitle>
<p v-if="activeStock" class="text-sm text-muted-foreground">{{ activeStock }}</p>
</DialogHeader>
<div class="relative flex items-center justify-center min-h-[300px] select-none"

View File

@ -15,6 +15,7 @@ const props = defineProps<{
title?: string;
allUrls?: string[];
allTitles?: string[];
allStocks?: string[];
}>();
const previewOpen = ref(false);
@ -121,5 +122,5 @@ function openGallery() {
</DialogContent>
</Dialog>
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="allUrls ?? items.map(item => item.url)" :title="title" :titles="allTitles" @close="onPreviewClose" />
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="allUrls ?? items.map(item => item.url)" :title="title" :titles="allTitles" :stocks="allStocks" @close="onPreviewClose" />
</template>

View File

@ -195,6 +195,7 @@ const groupedResults = computed<GroupedResults[]>(() => {
?.images ?? []
"
:max-visible="1"
:all-stocks="(result.product_variant?.images ?? []).map(() => `${result.cutting_result} pcs`)"
/>
</TableCell>
<TableCell
@ -288,6 +289,7 @@ const groupedResults = computed<GroupedResults[]>(() => {
<MediaThumbnailCell
:items="material.images ?? []"
:max-visible="1"
:all-stocks="(material.images ?? []).map(() => `Pemakaian: ${material.material_usage_formatted}`)"
/>
</TableCell>
<TableCell

View File

@ -38,15 +38,17 @@ const props = defineProps<{
const previewOpen = ref(false);
const previewUrl = ref<string | null>(null);
const previewTitle = ref<string>('');
const previewStock = ref<string>('');
const allVariantImages = computed(() => {
const list: { priceId: number; title: string; url: string }[] = [];
const list: { priceId: number; title: string; stock: string; url: string }[] = [];
props.filteredRawMaterials.forEach((rawMaterial) => {
rawMaterial.prices.forEach((price) => {
if (price.images && price.images.length > 0) {
list.push({
priceId: price.id,
title: `${rawMaterial.name} - ${price.variant}`,
stock: `Stok: ${price.stock_formatted}`,
url: price.images[0].url,
});
}
@ -64,6 +66,7 @@ function handleThumbClick(priceId: number) {
if (idx !== -1) {
previewUrl.value = allVariantImages.value[idx].url;
previewTitle.value = allVariantImages.value[idx].title;
previewStock.value = allVariantImages.value[idx].stock;
previewOpen.value = true;
}
}
@ -73,6 +76,7 @@ watch(previewUrl, (newUrl) => {
if (matched) {
previewTitle.value = matched.title;
previewStock.value = matched.stock;
}
});
@ -206,5 +210,5 @@ function openCombination(rawMaterial: CuttingRawMaterialCatalogItem, price: Cutt
:existing-cart-items="materialCart" :initial-variant="combinationInitialVariant"
@combination-created="emit('combination-created', $event)" />
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="previewUrls" :title="previewTitle" />
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="previewUrls" :title="previewTitle" :stocks="previewStock ? [previewStock] : []" />
</template>

View File

@ -138,6 +138,7 @@ function setCombinationResult(combinationId: number | undefined, value: string)
v-if="item.images?.length"
:items="item.images"
:max-visible="1"
:all-stocks="item.images.map(() => `Stok: ${item.stock_input} ${item.unit_abbreviation}`)"
/>
<div class="min-w-0">
<p class="truncate text-sm font-medium">
@ -180,6 +181,7 @@ function setCombinationResult(combinationId: number | undefined, value: string)
v-if="item.images?.length"
:items="item.images"
:max-visible="1"
:all-stocks="item.images.map(() => `Stok: ${item.stock_input} ${item.unit_abbreviation}`)"
/>
<div class="min-w-0">
<p class="truncate text-sm font-medium">

View File

@ -30,15 +30,17 @@ const props = defineProps<{
const previewOpen = ref(false);
const previewUrl = ref<string | null>(null);
const previewTitle = ref<string>('');
const previewStock = ref<string>('');
const allVariantImages = computed(() => {
const list: { variantId: number; title: string; url: string }[] = [];
const list: { variantId: number; title: string; stock: string; url: string }[] = [];
props.filteredProducts.forEach((product) => {
product.variants.forEach((variant) => {
if (variant.images && variant.images.length > 0) {
list.push({
variantId: variant.id,
title: `${product.name} - ${variant.name}`,
stock: `Stok: ${variant.stock} pcs`,
url: variant.images[0].url,
});
}
@ -56,6 +58,7 @@ function handleThumbClick(variantId: number) {
if (idx !== -1) {
previewUrl.value = allVariantImages.value[idx].url;
previewTitle.value = allVariantImages.value[idx].title;
previewStock.value = allVariantImages.value[idx].stock;
previewOpen.value = true;
}
}
@ -65,6 +68,7 @@ watch(previewUrl, (newUrl) => {
if (matched) {
previewTitle.value = matched.title;
previewStock.value = matched.stock;
}
});
@ -168,5 +172,5 @@ const quickCreateOpen = ref(false);
<QuickCreateProductModal v-model:open="quickCreateOpen" :categories="categories" :selected-materials="materialCart"
@created="emit('product-created', $event)" />
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="previewUrls" :title="previewTitle" />
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="previewUrls" :title="previewTitle" :stocks="previewStock ? [previewStock] : []" />
</template>

View File

@ -50,6 +50,7 @@ const emit = defineEmits<{
v-if="item.images?.length"
:items="item.images"
:max-visible="1"
:all-stocks="item.images.map(() => `Stok: ${item.stock} pcs`)"
/>
<div class="min-w-0">
<p class="truncate text-sm font-medium">

View File

@ -265,7 +265,7 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
{{ material.variant }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" :title="`${group.rawMaterialName} - ${material.variant}`" />
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" :title="`${group.rawMaterialName} - ${material.variant}`" :all-stocks="(material.images ?? []).map(() => `Pemakaian: ${material.material_usage_formatted}`)" />
</TableCell>
<TableCell class="tabular-nums">
{{
@ -322,7 +322,7 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
{{ result.product_variant?.name }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" :title="`${group.productName} - ${result.product_variant?.name}`" />
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" :title="`${group.productName} - ${result.product_variant?.name}`" :all-stocks="(result.product_variant?.images ?? []).map(() => `Hasil: ${result.cutting_result} pcs`)" />
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs

View File

@ -201,7 +201,7 @@ function submitVerify() {
<div v-for="material in cutting.materials" :key="material.id"
class="flex items-center gap-3 rounded-lg border p-3 bg-muted/10">
<div class="shrink-0">
<MediaThumbnailCell :items="material.raw_material_price?.images ?? []" :max-visible="1" />
<MediaThumbnailCell :items="material.raw_material_price?.images ?? []" :max-visible="1" :all-stocks="(material.raw_material_price?.images ?? []).map(() => `Pemakaian: ${material.material_usage_formatted}`)" />
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">

View File

@ -26,6 +26,7 @@ const props = defineProps<{
const previewOpen = ref(false);
const previewUrl = ref<string | null>(null);
const previewTitle = ref('');
const previewStock = ref('');
const productSearch = defineModel<string>('search', { required: true });
@ -40,6 +41,7 @@ function handleThumbClick(variantId: number) {
if (variant && variant.images && variant.images.length > 0) {
previewUrl.value = variant.images[0].url;
previewTitle.value = `${product.name} - ${variant.name}`;
previewStock.value = `Stok: ${variant.stock} pcs`;
previewOpen.value = true;
break;
}
@ -117,5 +119,5 @@ function handleThumbClick(variantId: number) {
</CardContent>
</Card>
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :title="previewTitle" />
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :title="previewTitle" :stocks="previewStock ? [previewStock] : []" />
</template>

View File

@ -200,7 +200,7 @@ function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
</TableCell>
<TableCell>
<MediaThumbnailCell :items="result.product_variant?.images ?? []"
:max-visible="1" />
:max-visible="1" :all-stocks="(result.product_variant?.images ?? []).map(() => `${result.cutting_result} pcs`)" />
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs

View File

@ -131,7 +131,7 @@ function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
</TableCell>
<TableCell>
<MediaThumbnailCell :items="result.product_variant?.images ?? []"
:max-visible="1" />
:max-visible="1" :all-stocks="(result.product_variant?.images ?? []).map(() => `${result.cutting_result} pcs`)" />
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs
@ -189,7 +189,7 @@ function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
{{ material.variant }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" />
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" :all-stocks="(material.images ?? []).map(() => `${material.material_result ?? '-'} pcs`)" />
</TableCell>
<TableCell class="tabular-nums">
<template

View File

@ -124,7 +124,7 @@ const showingCount = computed(() => props.cuttings.length);
{{ result.product_variant?.name }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" />
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" :all-stocks="(result.product_variant?.images ?? []).map(() => `${result.cutting_result} pcs`)" />
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs

View File

@ -221,7 +221,7 @@ function submitVerify() {
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3 min-w-0">
<div class="shrink-0">
<MediaThumbnailCell :items="cutting.results[index]?.product_variant?.images ?? []" :max-visible="1" />
<MediaThumbnailCell :items="cutting.results[index]?.product_variant?.images ?? []" :max-visible="1" :all-stocks="(cutting.results[index]?.product_variant?.images ?? []).map(() => `${result.cutting_result} pcs`)" />
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm">{{ result.name }}</p>

View File

@ -67,6 +67,12 @@ function allVariantImageTitles(product: ProductListItem): string[] {
);
}
function allVariantImageStocks(product: ProductListItem): string[] {
return product.variants.flatMap((variant) =>
(variant.images ?? []).map(() => `Stok Bagus: ${variant.stock_formatted} | Reject: ${variant.reject_stock_formatted} | Ecer: ${variant.retail_stock_formatted}`)
);
}
const verificationModalOpen = ref(false);
const selectedRequestId = ref<number | null>(null);
@ -172,7 +178,7 @@ function openEditModal(variant: Variant, product: ProductListItem) {
{{ variant.name }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="variant.images ?? []" :max-visible="1" :title="`${product.name} - ${variant.name}`" :all-urls="allVariantImageUrls(product)" :all-titles="allVariantImageTitles(product)" />
<MediaThumbnailCell :items="variant.images ?? []" :max-visible="1" :title="`${product.name} - ${variant.name}`" :all-urls="allVariantImageUrls(product)" :all-titles="allVariantImageTitles(product)" :all-stocks="allVariantImageStocks(product)" />
</TableCell>
<TableCell class="tabular-nums">
{{ variant.stock_formatted }}

View File

@ -61,6 +61,12 @@ function allVariantImageTitles(material: RawMaterialListItem): string[] {
);
}
function allVariantImageStocks(material: RawMaterialListItem): string[] {
return material.prices.flatMap((price) =>
(price.images ?? []).map(() => `Stok: ${price.stock_formatted}`)
);
}
const verificationModalOpen = ref(false);
const selectedRequestId = ref<number | null>(null);
@ -167,7 +173,8 @@ function openEditModal(price: RawMaterialPrice, material: RawMaterialListItem) {
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1"
:title="`${material.name} - ${price.variant}`"
:all-urls="allVariantImageUrls(material)"
:all-titles="allVariantImageTitles(material)" />
:all-titles="allVariantImageTitles(material)"
:all-stocks="allVariantImageStocks(material)" />
</TableCell>
<TableCell class="tabular-nums">
{{ price.stock_formatted }}