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:
parent
14719445d1
commit
87fd3b791c
39
resources/js/components/admin/manage/PosCatalogCard.vue
Normal file
39
resources/js/components/admin/manage/PosCatalogCard.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
13
resources/js/lib/catalog-cover.ts
Normal file
13
resources/js/lib/catalog-cover.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user