Refactor OrderPosForm and PurchasePosForm components to replace table layouts with card-based UI for displaying product and raw material variants. Integrate PosCatalogCard and PosCatalogVariantThumb components for improved visual representation and user interaction. Update import statements and remove unused components for cleaner code.

This commit is contained in:
Yoga Pangestu 2026-06-12 11:34:20 +07:00
parent 14719445d1
commit 87fd3b791c
5 changed files with 194 additions and 122 deletions

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { Package } from '@lucide/vue';
import type { MediaItem } from '@/types/media';
defineProps<{
title: string;
coverImage?: MediaItem;
}>();
</script>
<template>
<article class="mb-4 break-inside-avoid overflow-hidden rounded-lg border bg-card shadow-sm">
<div class="relative aspect-4/3 overflow-hidden bg-muted/30">
<img
v-if="coverImage"
:src="coverImage.thumb_url"
:alt="title"
class="size-full object-cover"
>
<div
v-else
class="flex size-full items-center justify-center text-muted-foreground"
>
<Package class="size-10 opacity-40" />
</div>
</div>
<div class="border-b bg-muted/20 px-3 py-2.5">
<h3 class="font-medium leading-tight">
{{ title }}
</h3>
<slot name="header-extra" />
</div>
<div class="divide-y">
<slot />
</div>
</article>
</template>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { Package } from '@lucide/vue';
import { ref } from 'vue';
import MediaPreviewDialog from '@/components/media/MediaPreviewDialog.vue';
import type { MediaItem } from '@/types/media';
const props = defineProps<{
items?: MediaItem[];
}>();
const previewOpen = ref(false);
const previewUrl = ref<string | null>(null);
function openPreview() {
if (!props.items?.length) {
return;
}
previewUrl.value = props.items[0].url;
previewOpen.value = true;
}
</script>
<template>
<button
type="button"
class="size-10 shrink-0 overflow-hidden rounded border bg-muted/30"
:class="items?.length ? 'cursor-zoom-in' : 'cursor-default'"
:disabled="!items?.length"
@click.stop="openPreview"
>
<img
v-if="items?.length"
:src="items[0].thumb_url"
alt="Foto varian"
class="size-full object-cover"
>
<div
v-else
class="flex size-full items-center justify-center text-muted-foreground"
>
<Package class="size-4 opacity-40" />
</div>
</button>
<MediaPreviewDialog v-model:open="previewOpen" :url="previewUrl" />
</template>

View File

@ -3,7 +3,8 @@ import { useForm } from '@inertiajs/vue3';
import { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue';
import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
@ -30,15 +31,8 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Textarea } from '@/components/ui/textarea';
import { getFirstCoverImage } from '@/lib/catalog-cover';
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
import type { ProductPriceItem, ProductVariantItem } from '@/types/product';
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
@ -256,61 +250,52 @@ function submit() {
</Empty>
</div>
<div v-else class="space-y-4">
<div v-for="product in filteredCatalog" :key="product.id" class="overflow-hidden rounded-md border">
<div class="border-b bg-muted/30 px-4 py-3">
<h3 class="font-medium leading-tight">
{{ product.name }}
</h3>
<div v-else class="columns-1 gap-4 sm:columns-2 xl:columns-3">
<PosCatalogCard
v-for="product in filteredCatalog"
:key="product.id"
:title="product.name"
:cover-image="getFirstCoverImage(product.variants)"
>
<p
v-if="!product.variants.length"
class="px-3 py-4 text-sm text-muted-foreground"
>
Belum ada varian
</p>
<div
v-for="variant in product.variants"
:key="variant.id"
class="flex items-center gap-2.5 px-3 py-2.5 transition-colors"
:class="getVariantPrice(variant) ? 'cursor-pointer hover:bg-muted/30' : 'opacity-60'"
@click="getVariantPrice(variant) && addToCart(product, variant)"
>
<PosCatalogVariantThumb :items="variant.images" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">
{{ variant.name }}
</p>
<p class="text-xs text-muted-foreground">
<span class="tabular-nums">{{ variant.stock }} pcs</span>
<span class="mx-1">·</span>
<span v-if="getVariantPrice(variant)" class="tabular-nums">
{{ getVariantPrice(variant)!.price_formatted }}
</span>
<span v-else>-</span>
</p>
</div>
<Button
type="button"
variant="outline"
size="icon-sm"
class="shrink-0"
:disabled="!getVariantPrice(variant)"
@click.stop="addToCart(product, variant)"
>
<Plus class="size-3.5" />
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Foto</TableHead>
<TableHead>Stok</TableHead>
<TableHead>Harga</TableHead>
<TableHead class="w-24 text-right" />
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!product.variants.length" :key="`${product.id}-empty`">
<TableCell colspan="5" class="text-muted-foreground">
Belum ada varian
</TableCell>
</TableRow>
<TableRow v-for="variant in product.variants" :key="variant.id"
class="hover:bg-muted/30"
:class="getVariantPrice(variant) ? 'cursor-pointer' : 'opacity-60'"
@click="getVariantPrice(variant) && addToCart(product, variant)">
<TableCell class="font-medium">
{{ variant.name }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="variant.images ?? []" />
</TableCell>
<TableCell class="tabular-nums">
{{ variant.stock }} pcs
</TableCell>
<TableCell class="tabular-nums">
<template v-if="getVariantPrice(variant)">
{{ getVariantPrice(variant)!.price_formatted }}
</template>
<span v-else class="text-muted-foreground text-sm">-</span>
</TableCell>
<TableCell class="text-right">
<Button type="button" variant="outline" size="sm"
:disabled="!getVariantPrice(variant)"
@click.stop="addToCart(product, variant)">
<Plus class="size-3.5" />
Tambah
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</PosCatalogCard>
</div>
</CardContent>
</Card>

View File

@ -3,8 +3,9 @@ import { useForm } from '@inertiajs/vue3';
import { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue';
import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
import MediaImageUpload from '@/components/media/MediaImageUpload.vue';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -32,15 +33,8 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Textarea } from '@/components/ui/textarea';
import { getFirstCoverImage } from '@/lib/catalog-cover';
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
import type { MediaItem } from '@/types/media';
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
@ -237,57 +231,51 @@ function submit() {
</Empty>
</div>
<div v-else class="space-y-4">
<div v-for="rawMaterial in filteredCatalog" :key="rawMaterial.id"
class="overflow-hidden rounded-md border">
<div class="border-b bg-muted/30 px-4 py-3">
<div class="flex flex-wrap items-center gap-2">
<h3 class="font-medium leading-tight">
{{ rawMaterial.name }}
</h3>
<Badge variant="secondary">
{{ rawMaterial.unit_label }}
</Badge>
</div>
</div>
<div v-else class="columns-1 gap-4 sm:columns-2 xl:columns-3">
<PosCatalogCard
v-for="rawMaterial in filteredCatalog"
:key="rawMaterial.id"
:title="rawMaterial.name"
:cover-image="getFirstCoverImage(rawMaterial.prices)"
>
<template #header-extra>
<Badge variant="secondary" class="mt-1.5">
{{ rawMaterial.unit_label }}
</Badge>
</template>
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Foto</TableHead>
<TableHead>Harga</TableHead>
<TableHead class="w-24 text-right" />
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!rawMaterial.prices.length" :key="`${rawMaterial.id}-empty`">
<TableCell colspan="4" class="text-muted-foreground">
Belum ada varian
</TableCell>
</TableRow>
<TableRow v-for="price in rawMaterial.prices" :key="price.id"
class="cursor-pointer hover:bg-muted/30" @click="addToCart(rawMaterial, price)">
<TableCell class="font-medium">
{{ price.variant }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="price.images ?? []" />
</TableCell>
<TableCell class="tabular-nums">
{{ price.price_formatted }}
</TableCell>
<TableCell class="text-right">
<Button type="button" variant="outline" size="sm"
@click.stop="addToCart(rawMaterial, price)">
<Plus class="size-3.5" />
Tambah
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<p
v-if="!rawMaterial.prices.length"
class="px-3 py-4 text-sm text-muted-foreground"
>
Belum ada varian
</p>
<div
v-for="price in rawMaterial.prices"
:key="price.id"
class="flex cursor-pointer items-center gap-2.5 px-3 py-2.5 transition-colors hover:bg-muted/30"
@click="addToCart(rawMaterial, price)"
>
<PosCatalogVariantThumb :items="price.images" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">
{{ price.variant }}
</p>
<p class="text-xs tabular-nums text-muted-foreground">
{{ price.price_formatted }}
</p>
</div>
<Button
type="button"
variant="outline"
size="icon-sm"
class="shrink-0"
@click.stop="addToCart(rawMaterial, price)"
>
<Plus class="size-3.5" />
</Button>
</div>
</PosCatalogCard>
</div>
</CardContent>
</Card>

View File

@ -0,0 +1,13 @@
import type { MediaItem } from '@/types/media';
export function getFirstCoverImage(
items: Array<{ images?: MediaItem[] }>,
): MediaItem | undefined {
for (const item of items) {
if (item.images?.length) {
return item.images[0];
}
}
return undefined;
}