663 lines
34 KiB
TypeScript
663 lines
34 KiB
TypeScript
import { Head, Link, router, useForm } from '@inertiajs/react';
|
||
import { ArrowLeft, Check, ImagePlus, Loader, Minus, Plus, Save, Search, ShoppingCart, Tag, Trash2, X } from 'lucide-react';
|
||
import React, { useMemo, useState } from 'react';
|
||
import { NumericFormat } from 'react-number-format';
|
||
import { toast } from 'sonner';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
|
||
import { Field, FieldError } from "@/components/ui/field";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import {
|
||
Sheet,
|
||
SheetContent,
|
||
SheetTrigger
|
||
} from "@/components/ui/sheet";
|
||
import { cn } from '@/lib/utils';
|
||
import { formatCurrency, formatNumber } from '@/lib/formatters';
|
||
import * as orderRoutes from '@/routes/order';
|
||
import type { Product } from '@/types';
|
||
import type { OrderItem } from '@/types/order';
|
||
|
||
type CartItem = OrderItem & { id: number };
|
||
type EnumOption = { value: string, label: string };
|
||
|
||
export default function OrderCreate({ products, cartItems, orderStatus, orderChannels, paymentMethods, priceTypes }: {
|
||
products: Product[],
|
||
cartItems: CartItem[],
|
||
orderStatus: EnumOption[],
|
||
orderChannels: EnumOption[],
|
||
paymentMethods: EnumOption[],
|
||
priceTypes: EnumOption[]
|
||
}) {
|
||
const [search, setSearch] = useState('');
|
||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||
const [qtyDialogIndex, setQtyDialogIndex] = useState<number | null>(null);
|
||
const [qtyInputValue, setQtyInputValue] = useState('');
|
||
|
||
// Default price type for the catalog
|
||
const [globalPriceType, setGlobalPriceType] = useState<string>('retail');
|
||
|
||
const categories = useMemo(() => {
|
||
const map = new Map<number, string>();
|
||
products.forEach(p => p.categories?.forEach(c => map.set(c.id, c.name)));
|
||
|
||
return Array.from(map.entries()).map(([id, name]) => ({ id, name }));
|
||
}, [products]);
|
||
|
||
const { data, setData, post, processing, errors, transform } = useForm({
|
||
customer_name: '',
|
||
subtotal: 0,
|
||
discount: 0,
|
||
payment: 0,
|
||
payment_method: 'cash',
|
||
order_status: 'delivered',
|
||
order_channel: 'store',
|
||
items: [] as any[],
|
||
});
|
||
|
||
const filteredProducts = products.filter(p => {
|
||
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
|
||
const matchesCategory = selectedCategory === 'all' || p.categories?.some(c => String(c.id) === selectedCategory);
|
||
|
||
return matchesSearch && matchesCategory;
|
||
});
|
||
|
||
const getCartItem = (productId: number, priceType: string) =>
|
||
cartItems.find(item => item.product_id === productId && item.price_type === priceType);
|
||
|
||
const getProductPrice = (product: Product, priceType: string) => {
|
||
return product.prices?.find(p => p.price_type === priceType)?.price || 0;
|
||
};
|
||
|
||
const addToCart = (product: Product, priceType: string) => {
|
||
const price = getProductPrice(product, priceType);
|
||
router.post(orderRoutes.addToCart().url, {
|
||
product_id: product.id,
|
||
qty: 1,
|
||
price: price,
|
||
price_type: priceType
|
||
}, {
|
||
preserveScroll: true,
|
||
});
|
||
};
|
||
|
||
const decreaseQuantity = (item: CartItem, e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
router.post(orderRoutes.addToCart().url, {
|
||
product_id: item.product_id,
|
||
qty: -1,
|
||
price: item.price,
|
||
price_type: item.price_type
|
||
}, {
|
||
preserveScroll: true,
|
||
});
|
||
};
|
||
|
||
const increaseQuantity = (item: CartItem, e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
router.post(orderRoutes.addToCart().url, {
|
||
product_id: item.product_id,
|
||
qty: 1,
|
||
price: item.price,
|
||
price_type: item.price_type
|
||
}, {
|
||
preserveScroll: true,
|
||
});
|
||
};
|
||
|
||
const removeFromCart = (itemId: number) => {
|
||
router.delete(orderRoutes.removeFromCart(itemId).url, {
|
||
preserveScroll: true,
|
||
});
|
||
};
|
||
|
||
const updateCartQuantity = (itemId: number, qty: number) => {
|
||
if (qty < 1) {
|
||
return;
|
||
}
|
||
|
||
router.patch(orderRoutes.updateCartItem(itemId).url, {
|
||
qty
|
||
}, {
|
||
preserveScroll: true,
|
||
});
|
||
};
|
||
|
||
const openQtyDialog = (item: CartItem, e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
setQtyInputValue(String(item.qty));
|
||
setQtyDialogIndex(item.id);
|
||
};
|
||
|
||
const confirmQty = () => {
|
||
const val = parseInt(qtyInputValue);
|
||
|
||
if (!isNaN(val) && val >= 1 && qtyDialogIndex !== null) {
|
||
updateCartQuantity(qtyDialogIndex, val);
|
||
}
|
||
|
||
setQtyDialogIndex(null);
|
||
};
|
||
|
||
const onSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (cartItems.length === 0) {
|
||
toast.error('Pilih minimal satu produk');
|
||
|
||
return;
|
||
}
|
||
|
||
transform((data) => ({
|
||
...data,
|
||
subtotal: subtotal,
|
||
items: cartItems.map(item => ({
|
||
product_id: item.product_id,
|
||
qty: item.qty,
|
||
price: item.price,
|
||
total: item.total,
|
||
price_type: item.price_type
|
||
}))
|
||
}));
|
||
|
||
post(orderRoutes.store().url, {
|
||
onSuccess: (response: any) => {
|
||
toast.success(response.props.flash.success);
|
||
},
|
||
});
|
||
};
|
||
|
||
const subtotal = useMemo(() => {
|
||
return cartItems.reduce((acc, item) => acc + item.total, 0);
|
||
}, [cartItems]);
|
||
|
||
const total = subtotal - (Number(data.discount) || 0);
|
||
const change = (Number(data.payment) || 0) - total;
|
||
const cartTotalItems = cartItems.reduce((a, i) => a + i.qty, 0);
|
||
|
||
const PriceTypeLabel = ({ type }: { type: string }) => {
|
||
const option = priceTypes.find(opt => opt.value === type);
|
||
|
||
return option ? option.label : type;
|
||
};
|
||
|
||
const CartFormContent = () => (
|
||
<div className="flex flex-col h-full">
|
||
<CardHeader className="border-b py-4 flex-row items-center justify-between shrink-0">
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<ShoppingCart className="h-4 w-4" />
|
||
Keranjang
|
||
</CardTitle>
|
||
<Badge className="rounded-full font-bold">
|
||
{cartTotalItems} pcs
|
||
</Badge>
|
||
</CardHeader>
|
||
|
||
<ScrollArea className="flex-1 min-h-0">
|
||
<CardContent className="p-0">
|
||
{cartItems.length === 0 ? (
|
||
<div className="py-16 text-center text-muted-foreground">
|
||
<ShoppingCart className="h-10 w-10 mx-auto mb-3 opacity-20" />
|
||
<p className="text-sm">Keranjang masih kosong</p>
|
||
<p className="text-xs mt-1 opacity-60">Klik produk untuk menambahkan</p>
|
||
</div>
|
||
) : (
|
||
<div className="divide-y divide-border">
|
||
{cartItems.map((item) => (
|
||
<div key={item.id} className="p-4 space-y-2.5">
|
||
<div className="flex gap-3">
|
||
<div className="h-11 w-11 rounded-md bg-muted overflow-hidden shrink-0 border">
|
||
{item.product?.thumbnail_url ? (
|
||
<img src={item.product.thumbnail_url} className="h-full w-full object-cover" alt={item.product.name} />
|
||
) : (
|
||
<div className="h-full w-full flex items-center justify-center">
|
||
<ImagePlus className="h-4 w-4 text-muted-foreground/50" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<h4 className="text-sm font-semibold leading-tight line-clamp-1">{item.product?.name}</h4>
|
||
<div className="flex items-center gap-1.5 mt-0.5">
|
||
<Badge variant="outline" className="text-[10px] px-1 py-0 h-4 bg-primary/5 border-primary/20 text-primary">
|
||
<PriceTypeLabel type={item.price_type} />
|
||
</Badge>
|
||
<p className="text-xs text-primary font-medium">
|
||
{formatCurrency(item.total)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-7 w-7 shrink-0 text-muted-foreground hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||
onClick={() => removeFromCart(item.id)}
|
||
>
|
||
<Trash2 className="h-3.5 w-3.5" />
|
||
</Button>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||
<div className="flex items-center bg-muted/50 rounded-lg border px-1">
|
||
<button type="button" onClick={(e) => decreaseQuantity(item, e)} className="p-1 hover:text-primary"><Minus className="h-3 w-3" /></button>
|
||
<span className="px-2 font-medium text-foreground">{item.qty}</span>
|
||
<button type="button" onClick={(e) => increaseQuantity(item, e)} className="p-1 hover:text-primary"><Plus className="h-3 w-3" /></button>
|
||
</div>
|
||
<span>×</span>
|
||
<span className="font-mono">
|
||
{formatCurrency(item.price)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</ScrollArea>
|
||
|
||
<div className="p-4 border-t bg-muted/10 space-y-3 shrink-0">
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<Field>
|
||
<Label htmlFor='customer_name' className="text-xs text-muted-foreground" required>Pelanggan</Label>
|
||
<Input id="customer_name" className="h-8 text-xs" value={data.customer_name} onChange={e => setData('customer_name', e.target.value)} autoComplete="off" placeholder="Nama Pelanggan" />
|
||
<FieldError error={errors.customer_name} label="Pelanggan" className="text-xs" />
|
||
</Field>
|
||
<Field>
|
||
<Label className="text-xs text-muted-foreground" required>Channel</Label>
|
||
<Select value={data.order_channel} onValueChange={val => setData('order_channel', val as any)}>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{orderChannels.map(opt => (
|
||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FieldError error={errors.order_channel} label="Channel" className="text-xs" />
|
||
</Field>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<Field>
|
||
<Label className="text-xs text-muted-foreground" required>Metode Bayar</Label>
|
||
<Select value={data.payment_method} onValueChange={val => setData('payment_method', val as any)}>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{paymentMethods.map(opt => (
|
||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FieldError error={errors.payment_method} label="Metode Bayar" className="text-xs" />
|
||
</Field>
|
||
<Field>
|
||
<Label className="text-xs text-muted-foreground" required>Status</Label>
|
||
<Select value={data.order_status} onValueChange={val => setData('order_status', val as any)}>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{orderStatus.map(opt => (
|
||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FieldError error={errors.order_status} label="Status" className="text-xs" />
|
||
</Field>
|
||
</div>
|
||
|
||
<div className="space-y-1.5 pt-2 border-t border-dashed">
|
||
<div className="flex justify-between text-xs">
|
||
<span className="text-muted-foreground">Subtotal</span>
|
||
<span className="font-medium">{formatCurrency(subtotal)}</span>
|
||
</div>
|
||
<div className="flex justify-between items-center text-xs">
|
||
<span className="text-muted-foreground">Potongan / Diskon</span>
|
||
<div className="flex items-center gap-2">
|
||
<NumericFormat
|
||
id="discount"
|
||
customInput={Input}
|
||
thousandSeparator="."
|
||
decimalSeparator=","
|
||
prefix="Rp "
|
||
value={data.discount}
|
||
onValueChange={(values) => {
|
||
setData('discount', values.floatValue || 0)
|
||
}}
|
||
placeholder="Rp 0"
|
||
autoComplete='off'
|
||
/>
|
||
<FieldError error={errors.discount} label="Potongan / Diskon" className="text-xs" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-between text-lg font-bold border-t pt-2">
|
||
<span className="">Total</span>
|
||
<span className="font-medium">{formatCurrency(total)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Field>
|
||
<Label className="text-xs text-muted-foreground">Uang Bayar</Label>
|
||
<NumericFormat
|
||
id="payment"
|
||
customInput={Input}
|
||
thousandSeparator="."
|
||
decimalSeparator=","
|
||
prefix="Rp "
|
||
value={data.payment}
|
||
onValueChange={(values) => {
|
||
setData('payment', values.floatValue || 0)
|
||
}}
|
||
placeholder="Rp 0"
|
||
autoComplete='off'
|
||
/>
|
||
</Field>
|
||
{change >= 0 && (Number(data.payment) || 0) > 0 && (
|
||
<div className="flex justify-between items-center p-2 rounded bg-green-500/10 border border-green-500/20 text-green-600 dark:text-green-400">
|
||
<span className="text-xs font-bold uppercase">Kembalian</span>
|
||
<span className="text-lg font-bold">{formatCurrency(change)}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<Button
|
||
type="submit"
|
||
className="w-full h-10 font-semibold shadow"
|
||
disabled={processing || cartItems.length === 0}
|
||
onClick={() => {
|
||
if (cartItems.length > 0) {
|
||
onSubmit({ preventDefault: () => { } } as any)
|
||
}
|
||
}}
|
||
>
|
||
{processing ? <Loader className="h-4 w-4 animate-spin" /> : <Save
|
||
className="h-4 w-4" />}
|
||
{processing ? 'Menyimpan...' : 'Simpan'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6 p-6 lg:h-[calc(100vh-64px)] lg:overflow-hidden">
|
||
<Head title="Buat Pesanan" />
|
||
|
||
<div className="flex items-center justify-between shrink-0">
|
||
<h1 className="text-3xl font-bold tracking-tight">Tambah Pesanan</h1>
|
||
<Link href={orderRoutes.index().url}>
|
||
<Button variant='outline' className="gap-2">
|
||
<ArrowLeft className="size-4" />
|
||
Kembali
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-12 gap-6 pb-20 lg:pb-0">
|
||
<div className="lg:col-span-8 flex flex-col gap-5 h-full min-h-0">
|
||
<div className="flex flex-col sm:flex-row gap-2 shrink-0">
|
||
<div className="relative flex-1">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||
<Input
|
||
placeholder="Cari nama produk..."
|
||
className="pl-10 h-10"
|
||
value={search}
|
||
onChange={e => setSearch(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
||
<SelectTrigger className="w-full sm:w-40 h-10">
|
||
<SelectValue placeholder="Kategori" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">Semua</SelectItem>
|
||
{categories.map(cat => (
|
||
<SelectItem key={cat.id} value={String(cat.id)}>{cat.name}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<Select value={globalPriceType} onValueChange={setGlobalPriceType}>
|
||
<SelectTrigger className="w-full sm:w-40 h-10 bg-primary/5 text-primary border-primary/20 font-bold">
|
||
<Tag className="mr-2 h-4 w-4" />
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{priceTypes.filter(t => t.value !== 'purchase').map(opt => (
|
||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<ScrollArea className="flex-1 h-full pr-4">
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-4 pb-10">
|
||
{filteredProducts.map((product) => {
|
||
const cartItem = getCartItem(product.id, globalPriceType);
|
||
const currentPrice = getProductPrice(product, globalPriceType);
|
||
|
||
return (
|
||
<Card
|
||
key={product.id}
|
||
className={cn(
|
||
"group cursor-pointer transition-all duration-200 bg-card border p-0",
|
||
"hover:shadow-lg",
|
||
cartItem
|
||
? "border-primary shadow-md"
|
||
: "shadow-sm hover:border-primary/50"
|
||
)}
|
||
onClick={() => addToCart(product, globalPriceType)}
|
||
>
|
||
<CardContent className="p-4 flex flex-col gap-3">
|
||
<div className="-mx-4 -mt-4 aspect-[4/3] overflow-hidden bg-muted/50 rounded-t-xl relative">
|
||
{product.thumbnail_url ? (
|
||
<img
|
||
src={product.thumbnail_url}
|
||
alt={product.name}
|
||
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||
/>
|
||
) : (
|
||
<div className="h-full w-full flex items-center justify-center">
|
||
<ImagePlus className="h-10 w-10 text-muted-foreground/30" />
|
||
</div>
|
||
)}
|
||
{cartItem && (
|
||
<div className="absolute top-2 right-2 flex items-center justify-center bg-primary text-primary-foreground text-[10px] font-bold h-6 min-w-6 px-1 rounded-full shadow-lg">
|
||
{cartItem.qty}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<div className="flex flex-wrap gap-1 mb-1.5">
|
||
{product.categories?.map(cat => (
|
||
<Badge key={cat.id} variant="outline" className="text-[10px] h-4 px-1.5 bg-muted/50">
|
||
{cat.name}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
<h3 className="font-bold text-sm leading-snug line-clamp-2 min-h-[2.5rem]">
|
||
{product.name}
|
||
</h3>
|
||
</div>
|
||
|
||
{/* Price + Stepper */}
|
||
<div
|
||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mt-auto pt-2 border-t"
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
<div className="flex flex-col flex-1 min-w-0">
|
||
<span className="text-[10px] uppercase font-bold text-muted-foreground">Harga</span>
|
||
<span className="text-sm font-bold text-primary tabular-nums truncate block">
|
||
{formatCurrency(currentPrice)}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-1 bg-muted/50 rounded-xl sm:rounded-full p-1 sm:p-0.5 shrink-0 w-full sm:w-auto justify-between sm:justify-end border border-transparent hover:border-primary/20 transition-colors">
|
||
<button
|
||
type="button"
|
||
className={cn(
|
||
"h-8 w-8 sm:h-6 sm:w-6 rounded-lg sm:rounded-full flex items-center justify-center transition-all",
|
||
cartItem
|
||
? "bg-background text-primary shadow-sm hover:bg-primary hover:text-primary-foreground"
|
||
: "text-muted-foreground/30 cursor-not-allowed"
|
||
)}
|
||
disabled={!cartItem}
|
||
onClick={(e) => cartItem && decreaseQuantity(cartItem, e)}
|
||
>
|
||
<Minus className="h-3.5 w-3.5 sm:h-3 sm:w-3" />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={cn(
|
||
"flex-1 sm:flex-none sm:min-w-[36px] px-1 text-center text-sm font-bold tabular-nums transition-colors",
|
||
cartItem ? "text-foreground hover:text-primary cursor-pointer" : "text-muted-foreground/40 cursor-default"
|
||
)}
|
||
onClick={(e) => {
|
||
if (!cartItem) {
|
||
return;
|
||
}
|
||
|
||
openQtyDialog(cartItem, e);
|
||
}}
|
||
>
|
||
{cartItem?.qty ?? 0}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="h-8 w-8 sm:h-6 sm:w-6 rounded-lg sm:rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/80 transition-all shadow-sm"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
cartItem ? increaseQuantity(cartItem, e) : addToCart(product, globalPriceType);
|
||
}}
|
||
>
|
||
<Plus className="h-3.5 w-3.5 sm:h-3 sm:w-3" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
{filteredProducts.length === 0 && (
|
||
<div className="col-span-full py-20 text-center">
|
||
<Empty>
|
||
<EmptyHeader>
|
||
<EmptyTitle>Produk tidak ditemukan</EmptyTitle>
|
||
<EmptyDescription>Coba kata kunci lain atau kategori berbeda.</EmptyDescription>
|
||
</EmptyHeader>
|
||
</Empty>
|
||
</div>
|
||
)}
|
||
</ScrollArea>
|
||
</div>
|
||
|
||
<div className="hidden lg:block lg:col-span-4 h-full min-h-0">
|
||
<Card className="border shadow-2xl bg-card overflow-hidden h-full flex flex-col border-primary/10">
|
||
{CartFormContent()}
|
||
</Card>
|
||
</div>
|
||
|
||
<div className="lg:hidden fixed bottom-6 left-0 right-0 px-6 z-50 pointer-events-none">
|
||
<div className="max-w-md mx-auto pointer-events-auto">
|
||
<Sheet>
|
||
<SheetTrigger asChild>
|
||
<Button
|
||
size="lg"
|
||
className="w-full rounded-full shadow-2xl h-14 flex items-center justify-between px-6 bg-primary animate-in fade-in slide-in-from-bottom-4 duration-300"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<div className="bg-primary-foreground/20 rounded-full h-8 w-8 flex items-center justify-center">
|
||
<ShoppingCart className="h-4 w-4" />
|
||
</div>
|
||
<div className="text-left leading-tight">
|
||
<p className="text-[10px] opacity-80 uppercase font-bold tracking-wider">Keranjang</p>
|
||
<p className="text-sm font-bold">{cartTotalItems} Item</p>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-[10px] opacity-80 uppercase font-bold tracking-wider">Total</p>
|
||
<p className="text-sm font-bold">{formatCurrency(total)}</p>
|
||
</div>
|
||
</Button>
|
||
</SheetTrigger>
|
||
<SheetContent side="bottom" showCloseButton={false} className="p-0 !h-[90vh] rounded-t-[2.5rem] flex flex-col overflow-hidden">
|
||
{CartFormContent()}
|
||
</SheetContent>
|
||
</Sheet>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quantity Input Dialog */}
|
||
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => {
|
||
if (!open) {
|
||
setQtyDialogIndex(null);
|
||
}
|
||
}}>
|
||
<DialogContent className="max-w-xs">
|
||
<DialogHeader>
|
||
<DialogTitle>Ubah Jumlah</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="py-2">
|
||
<Label className="text-sm mb-2 block">
|
||
{qtyDialogIndex !== null ? cartItems.find(i => i.id === qtyDialogIndex)?.product?.name : ''}
|
||
</Label>
|
||
<Input
|
||
type="number"
|
||
min="1"
|
||
value={qtyInputValue}
|
||
onChange={e => setQtyInputValue(e.target.value)}
|
||
onKeyDown={e => {
|
||
if (e.key === 'Enter') {
|
||
confirmQty();
|
||
}
|
||
}}
|
||
className="text-lg font-bold"
|
||
autoFocus
|
||
/>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" className="gap-2" onClick={() => setQtyDialogIndex(null)}>
|
||
<X className="size-4" />
|
||
Batal
|
||
</Button>
|
||
<Button className="gap-2" onClick={confirmQty}>
|
||
<Check className="size-4" />
|
||
Konfirmasi
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
OrderCreate.layout = {
|
||
breadcrumbs: [
|
||
{ title: 'Kelola' },
|
||
],
|
||
};
|