feat: add functionality for setting cart item quantity and improve cart detail dialog with empty state handling
This commit is contained in:
parent
55ca1521f9
commit
9e0188c9ef
@ -5,6 +5,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import type { OrderCartItem } from '@/types/order';
|
||||
|
||||
@ -15,18 +21,40 @@ defineProps<{
|
||||
totalAmount: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [index: number];
|
||||
'adjust-quantity': [index: number, delta: number];
|
||||
'set-quantity': [index: number, value: string];
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true });
|
||||
|
||||
function onQtyInput(index: number, event: Event) {
|
||||
emit('set-quantity', index, (event.target as HTMLInputElement).value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogContent class="sm:max-w-lg min-w-0 max-h-[90dvh] flex flex-col overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail Keranjang</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="scrollbar-thin max-h-96 space-y-3 overflow-y-auto overscroll-y-contain">
|
||||
|
||||
<div v-if="cart.length === 0" class="rounded-lg border border-dashed text-center text-sm text-muted-foreground">
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Keranjang masih kosong</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Pilih varian produk untuk menambahkan ke keranjang.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</div>
|
||||
|
||||
<div v-else class="scrollbar-thin max-h-96 space-y-3 overflow-y-auto overscroll-y-contain">
|
||||
<div
|
||||
v-for="item in cart"
|
||||
v-for="(item, index) in cart"
|
||||
:key="`detail-${item.product_variant_id}-${item.stock_quality}`"
|
||||
class="rounded-lg border p-3"
|
||||
>
|
||||
@ -38,15 +66,50 @@ const open = defineModel<boolean>('open', { required: true });
|
||||
· {{ item.stock_quality_label ?? stockQualityLabel(item.stock_quality) }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 text-xs font-medium tabular-nums">{{ item.quantity }} pcs</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
@click="emit('remove', index)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="mt-2 block text-xs text-muted-foreground">
|
||||
Jumlah (pcs)
|
||||
</label>
|
||||
<div class="mt-1 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded border text-sm transition-colors hover:bg-muted"
|
||||
@click="emit('adjust-quantity', index, -1)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
:value="item.quantity"
|
||||
class="h-8 w-full rounded border bg-transparent px-2 text-center text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
@change="onQtyInput(index, $event)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded border text-sm transition-colors hover:bg-muted"
|
||||
@click="emit('adjust-quantity', index, 1)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</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 v-if="cart.length > 0" 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>
|
||||
|
||||
@ -115,6 +115,7 @@ const {
|
||||
removeFromCart,
|
||||
adjustQuantity,
|
||||
syncCartItemQuantity,
|
||||
setCartItemQuantity,
|
||||
decreaseVariantQty,
|
||||
} = useOrderPosCart({
|
||||
catalog: () => props.catalog,
|
||||
@ -227,7 +228,7 @@ function buildFormData(): FormData {
|
||||
formData.append('discount', parseRupiah(form.discount));
|
||||
formData.append('nego_price', parseRupiah(form.nego_price));
|
||||
formData.append('notes', form.notes);
|
||||
|
||||
|
||||
if (isCashierUser.value) {
|
||||
formData.append('status', OrderStatus.COMPLETED);
|
||||
} else {
|
||||
@ -362,5 +363,20 @@ function submit() {
|
||||
:stock-quality-label="stockQualityLabel"
|
||||
:line-subtotal="lineSubtotal"
|
||||
:total-amount="totalAmount"
|
||||
@remove="removeFromCart"
|
||||
@adjust-quantity="adjustQuantity"
|
||||
@set-quantity="setCartItemQuantity"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="cart.length > 0"
|
||||
type="button"
|
||||
class="fixed top-50 right-6 z-50 flex size-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-105 active:scale-95 xl:hidden"
|
||||
@click="cartDetailOpen = true"
|
||||
>
|
||||
<ShoppingCart class="size-6" />
|
||||
<span class="absolute -right-1 -top-1 flex size-6 items-center justify-center rounded-full bg-destructive text-[11px] font-bold text-destructive-foreground">
|
||||
{{ cart.length > 99 ? '99+' : cart.length }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@ -287,6 +287,31 @@ export function useOrderPosCart(options: {
|
||||
}
|
||||
}
|
||||
|
||||
async function setCartItemQuantity(index: number, value: string) {
|
||||
const item = cart.value[index];
|
||||
const nextQty = Number.parseFloat(value.replace(',', '.'));
|
||||
|
||||
if (Number.isNaN(nextQty) || nextQty < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextQty < 1) {
|
||||
await removeFromCart(index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
item.quantity = String(nextQty);
|
||||
|
||||
if (toValue(options.isCreateMode)) {
|
||||
try {
|
||||
await syncDraftItem(item.product_variant_id, nextQty, item.stock_quality);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
search,
|
||||
selectedStockQuality,
|
||||
@ -303,6 +328,7 @@ export function useOrderPosCart(options: {
|
||||
removeFromCart,
|
||||
adjustQuantity,
|
||||
syncCartItemQuantity,
|
||||
setCartItemQuantity,
|
||||
decreaseVariantQty,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user