feat: add cart detail dialog in Cutting, Order, and Purchase components for improved user experience and item overview

This commit is contained in:
Yoga Pangestu 2026-06-20 18:32:38 +07:00
parent e6f1fcc97e
commit 4d5901cc63
3 changed files with 199 additions and 15 deletions

View File

@ -69,6 +69,7 @@ const productSearch = ref('');
const materialCart = ref<CuttingMaterialCartItem[]>([]); const materialCart = ref<CuttingMaterialCartItem[]>([]);
const resultCart = ref<CuttingResultCartItem[]>([]); const resultCart = ref<CuttingResultCartItem[]>([]);
const costModalOpen = ref(false); const costModalOpen = ref(false);
const cartDetailOpen = ref(false);
const form = useForm({ const form = useForm({
description: '', description: '',
@ -723,13 +724,23 @@ function confirmSubmit() {
<Scissors class="size-4" /> <Scissors class="size-4" />
Ringkasan Cutting Ringkasan Cutting
</span> </span>
<Badge <span class="flex items-center gap-2">
v-if="totalResultPieces > 0" <button
variant="secondary" v-if="materialCart.length > 0 || resultCart.length > 0"
class="tabular-nums font-semibold" type="button"
> class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
Total: {{ totalResultPieces }} pcs @click="cartDetailOpen = true"
</Badge> >
Lihat Detail
</button>
<Badge
v-if="totalResultPieces > 0"
variant="secondary"
class="tabular-nums font-semibold"
>
Total: {{ totalResultPieces }} pcs
</Badge>
</span>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -762,7 +773,7 @@ function confirmSubmit() {
Belum ada bahan baku dipilih. Belum ada bahan baku dipilih.
</div> </div>
<div v-else class="space-y-3"> <div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
<div <div
v-for="(item, index) in materialCart" v-for="(item, index) in materialCart"
:key="item.raw_material_price_id" :key="item.raw_material_price_id"
@ -855,7 +866,7 @@ function confirmSubmit() {
Belum ada produk hasil dipilih. Belum ada produk hasil dipilih.
</div> </div>
<div v-else class="space-y-3"> <div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
<div <div
v-for="(item, index) in resultCart" v-for="(item, index) in resultCart"
:key="item.product_variant_id" :key="item.product_variant_id"
@ -1064,4 +1075,72 @@ function confirmSubmit() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog v-model:open="cartDetailOpen">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Detail Keranjang</DialogTitle>
</DialogHeader>
<div class="max-h-96 space-y-4 overflow-y-auto overscroll-y-contain scrollbar-thin">
<div v-if="materialCart.length > 0">
<p class="mb-2 text-xs font-medium text-muted-foreground">Bahan Baku</p>
<div class="space-y-2">
<div
v-for="item in materialCart"
:key="`detail-mat-${item.raw_material_price_id}`"
class="rounded-lg border p-3"
>
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="truncate text-sm font-medium">{{ item.raw_material_name }}</p>
<p class="truncate text-xs text-muted-foreground">
{{ item.variant }} · Stok {{ item.stock_input }} {{ item.unit_abbreviation }}
</p>
</div>
<span class="shrink-0 text-xs font-medium tabular-nums">
{{ item.material_usage }} {{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }}
</span>
</div>
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>Sisa: {{ item.remaining_material }} {{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }}</span>
<span class="font-medium text-foreground">{{ formatRupiah(materialLineCost(item)) }}</span>
</div>
</div>
</div>
</div>
<div v-if="resultCart.length > 0">
<p class="mb-2 text-xs font-medium text-muted-foreground">Hasil Produk</p>
<div class="space-y-2">
<div
v-for="item in resultCart"
:key="`detail-res-${item.product_variant_id}`"
class="rounded-lg border p-3"
>
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
<p class="truncate text-xs text-muted-foreground">
{{ item.variant_name }} · Stok {{ item.stock }} pcs
</p>
</div>
</div>
<div class="mt-1.5 flex items-center gap-3 text-xs tabular-nums">
<span>Hasil: <strong>{{ item.cutting_result }}</strong></span>
<span class="text-muted-foreground">·</span>
<span>Bagus: <strong>{{ item.warehouse_stock }}</strong></span>
<span class="text-muted-foreground">·</span>
<span>Reject: <strong>{{ item.cutting_reject }}</strong></span>
</div>
</div>
</div>
</div>
</div>
<div class="border-t pt-3">
<div class="flex items-center justify-between text-sm font-semibold">
<span>Total Harga Modal</span>
<span class="text-primary">{{ formatRupiah(totalProductionCost) }}</span>
</div>
</div>
</DialogContent>
</Dialog>
</template> </template>

View File

@ -10,6 +10,12 @@ import { NumberInput } from '@/components/form/number-input';
import { RupiahInput } from '@/components/form/rupiah-input'; import { RupiahInput } from '@/components/form/rupiah-input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { import {
Empty, Empty,
EmptyDescription, EmptyDescription,
@ -91,6 +97,7 @@ const search = ref('');
const selectedStockQuality = ref<'good' | 'reject'>('good'); const selectedStockQuality = ref<'good' | 'reject'>('good');
const cart = ref<OrderCartItem[]>([]); const cart = ref<OrderCartItem[]>([]);
const customerFormOpen = ref(false); const customerFormOpen = ref(false);
const cartDetailOpen = ref(false);
const printAfterSave = ref(false); const printAfterSave = ref(false);
const selectedPaperSize = ref<'58mm' | '80mm'>('80mm'); const selectedPaperSize = ref<'58mm' | '80mm'>('80mm');
@ -580,9 +587,19 @@ function submit() {
<Card class="h-fit xl:sticky xl:top-4"> <Card class="h-fit xl:sticky xl:top-4">
<CardHeader class="pb-3"> <CardHeader class="pb-3">
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center justify-between gap-2 text-base">
<ShoppingCart class="size-4" /> <span class="flex items-center gap-2">
Ringkasan Pesanan <ShoppingCart class="size-4" />
Ringkasan Pesanan
</span>
<button
v-if="cart.length > 0"
type="button"
class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
@click="cartDetailOpen = true"
>
Lihat Detail
</button>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -856,4 +873,40 @@ function submit() {
</div> </div>
<CustomerQuickCreateModal v-model:open="customerFormOpen" @created="onCustomerCreated" /> <CustomerQuickCreateModal v-model:open="customerFormOpen" @created="onCustomerCreated" />
<Dialog v-model:open="cartDetailOpen">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Detail Keranjang</DialogTitle>
</DialogHeader>
<div class="max-h-96 space-y-3 overflow-y-auto overscroll-y-contain scrollbar-thin">
<div
v-for="item in cart"
:key="`detail-${item.product_variant_id}-${item.stock_quality}`"
class="rounded-lg border p-3"
>
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
<p class="truncate text-xs text-muted-foreground">
{{ item.variant_name }}
· {{ item.stock_quality_label ?? (item.stock_quality === 'reject' ? 'Reject' : 'Bagus') }}
</p>
</div>
<span class="shrink-0 text-xs font-medium tabular-nums">{{ item.quantity }} pcs</span>
</div>
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>@ Rp {{ formatRupiah(item.unit_price) }}</span>
<span class="font-medium text-foreground">Rp {{ formatRupiah(lineSubtotal(item)) }}</span>
</div>
</div>
</div>
<div class="border-t pt-3">
<div class="flex items-center justify-between text-sm font-semibold">
<span>Total</span>
<span class="text-primary">Rp {{ formatRupiah(totalAmount) }}</span>
</div>
</div>
</DialogContent>
</Dialog>
</template> </template>

View File

@ -11,6 +11,12 @@ import { RupiahInput } from '@/components/form/rupiah-input';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { import {
Empty, Empty,
EmptyDescription, EmptyDescription,
@ -63,6 +69,7 @@ const props = defineProps<{
const isCreateMode = computed(() => props.method === 'post'); const isCreateMode = computed(() => props.method === 'post');
const search = ref(''); const search = ref('');
const cart = ref<PurchaseCartItem[]>([]); const cart = ref<PurchaseCartItem[]>([]);
const cartDetailOpen = ref(false);
const existingPhotoId = ref<number | null>(null); const existingPhotoId = ref<number | null>(null);
const currentPhotoUrl = computed(() => props.initialData?.photos?.url ?? null); const currentPhotoUrl = computed(() => props.initialData?.photos?.url ?? null);
@ -415,9 +422,19 @@ function submit() {
<Card class="h-fit xl:sticky xl:top-4"> <Card class="h-fit xl:sticky xl:top-4">
<CardHeader class="pb-3"> <CardHeader class="pb-3">
<CardTitle class="flex items-center gap-2 text-base"> <CardTitle class="flex items-center justify-between gap-2 text-base">
<ShoppingCart class="size-4" /> <span class="flex items-center gap-2">
Ringkasan Belanja <ShoppingCart class="size-4" />
Ringkasan Belanja
</span>
<button
v-if="cart.length > 0"
type="button"
class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
@click="cartDetailOpen = true"
>
Lihat Detail
</button>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -547,4 +564,39 @@ function submit() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<Dialog v-model:open="cartDetailOpen">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Detail Keranjang</DialogTitle>
</DialogHeader>
<div class="max-h-96 space-y-3 overflow-y-auto overscroll-y-contain scrollbar-thin">
<div
v-for="item in cart"
:key="`detail-${item.raw_material_price_id}`"
class="rounded-lg border p-3"
>
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="truncate text-sm font-medium">{{ item.raw_material_name }}</p>
<p class="truncate text-xs text-muted-foreground">{{ item.variant }}</p>
</div>
<span class="shrink-0 text-xs font-medium tabular-nums">
{{ item.quantity }} {{ item.unit_abbreviation }}
</span>
</div>
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>@ Rp {{ formatRupiah(item.unit_price) }}</span>
<span class="font-medium text-foreground">Rp {{ formatRupiah(lineSubtotal(item)) }}</span>
</div>
</div>
</div>
<div class="border-t pt-3">
<div class="flex items-center justify-between text-sm font-semibold">
<span>Total</span>
<span class="text-primary">Rp {{ formatRupiah(total) }}</span>
</div>
</div>
</DialogContent>
</Dialog>
</template> </template>