dress/resources/js/pages/admin/manage/purchase/create.tsx

852 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Head, Link, useForm, router } from '@inertiajs/react';
import { format } from 'date-fns';
import { Plus, Trash2, CalendarIcon, Minus, ShoppingCart, Search, ImagePlus, X, Loader, Save, ArrowLeft } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} 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 { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Sheet,
SheetContent,
SheetTrigger,
SheetClose,
} from "@/components/ui/sheet";
import { cn } from '@/lib/utils';
import { formatCurrency, formatNumber } from '@/lib/formatters';
import purchaseRoutes from '@/routes/purchase';
import type { Product, ProductPrice } from '@/types';
type CartItem = { product_id: number; quantity: number; unit_price: number; product: Product };
function getPurchasePrice(prices: ProductPrice[] | undefined): number {
return prices?.find(p => p.price_type === 'purchase')?.price ?? 0;
}
function getPurchasePriceLabel(prices: ProductPrice[] | undefined): string {
return prices?.find(p => p.price_type === 'purchase')?.price_formatted ?? 'Rp 0';
}
export default function PurchaseCreate({ products, cartItems }: { products: Product[], cartItems: (CartItem & { id: number })[] }) {
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
const [search, setSearch] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [qtyDialogIndex, setQtyDialogIndex] = useState<number | null>(null);
const [qtyInputValue, setQtyInputValue] = useState('');
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({
purchase_date: format(new Date(), 'yyyy-MM-dd'),
note: '',
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) =>
cartItems.find(item => item.product_id === productId);
const addToCart = (product: Product) => {
const unitPrice = getPurchasePrice(product.prices);
router.post(purchaseRoutes.addToCart().url, {
product_id: product.id,
quantity: 1,
unit_price: unitPrice
}, {
preserveScroll: true,
});
};
const decreaseQuantity = (product: Product, e: React.MouseEvent) => {
e.stopPropagation();
const item = getCartItem(product.id);
if (!item) {
return;
}
router.post(purchaseRoutes.addToCart().url, {
product_id: product.id,
quantity: -1,
unit_price: item.unit_price
}, {
preserveScroll: true,
});
};
const increaseQuantity = (product: Product, e: React.MouseEvent) => {
e.stopPropagation();
addToCart(product);
};
const removeFromCart = (itemId: number) => {
router.delete(purchaseRoutes.removeFromCart(itemId).url, {
preserveScroll: true,
});
};
const updateCartQuantity = (itemId: number, quantity: number) => {
if (quantity < 1) {
return;
}
router.patch(purchaseRoutes.updateCartItem(itemId).url, {
quantity
}, {
preserveScroll: true,
});
};
const openQtyDialog = (item: CartItem & { id: number }, e: React.MouseEvent) => {
e.stopPropagation();
setQtyInputValue(String(item.quantity));
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,
items: cartItems.map(item => ({
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price
}))
}));
post(purchaseRoutes.store().url, {
onSuccess: (response: any) => {
const flash = response.props.flash;
if (flash?.error) {
toast.error(flash.error);
} else if (flash?.success) {
toast.success(flash.success);
}
},
});
};
const total = useMemo(() => {
return cartItems.reduce((acc, item) => acc + (item.quantity * item.unit_price), 0);
}, [cartItems]);
const cartTotalItems = cartItems.reduce((a, i) => a + i.quantity, 0);
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.product_id} className="p-4 space-y-2.5">
{/* Product info row */}
<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>
<p className="text-xs text-primary font-medium mt-0.5">
{formatCurrency(item.quantity * item.unit_price)}
</p>
</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>
{/* Qty + price text */}
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<div className="px-2 py-0.5 rounded bg-muted/50 border font-medium text-foreground">
{item.quantity} pcs
</div>
<span>×</span>
<span className="font-mono">
{formatCurrency(item.unit_price)}
</span>
</div>
</div>
))}
</div>
)}
</CardContent>
</ScrollArea>
{/* Footer */}
<div className="p-4 border-t bg-muted/10 space-y-3 shrink-0">
<Field>
<Label htmlFor='purchase_date' className="text-xs text-muted-foreground">Tanggal</Label>
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
<PopoverTrigger asChild>
<Button
id='purchase_date'
variant="outline"
size="sm"
className={cn("w-full justify-start text-left font-normal h-8 text-xs")}
>
<CalendarIcon className="mr-1 h-3 w-3" />
{data.purchase_date ? (
new Intl.DateTimeFormat("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
}).format(new Date(data.purchase_date))
) : (
<span className="text-muted-foreground">Pilih Tanggal</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={data.purchase_date ? new Date(data.purchase_date) : undefined}
defaultMonth={data.purchase_date ? new Date(data.purchase_date) : new Date()}
captionLayout="dropdown"
onSelect={(selectedDate: Date | undefined) => {
if (selectedDate) {
setData('purchase_date', format(selectedDate, 'yyyy-MM-dd'));
} else {
setData('purchase_date', '');
}
setIsCalendarOpen(false);
}}
/>
</PopoverContent>
</Popover>
<FieldError error={errors.purchase_date} label="Tanggal" className="text-xs" />
</Field>
<Field>
<Label htmlFor='note' className="text-xs text-muted-foreground">Catatan</Label>
<Input id='note' className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">Total Belanja</p>
<p className="text-xl font-bold text-primary">{formatCurrency(total)}</p>
</div>
<div className="text-right text-xs text-muted-foreground">
<p>{cartItems.length} produk</p>
<p>{cartTotalItems} pcs</p>
</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="Tambah Belanja" />
<div className="flex items-center justify-between shrink-0">
<h1 className="text-3xl font-bold tracking-tight">Tambah Belanja</h1>
<Link href={purchaseRoutes.index().url}>
<Button variant='outline'>
<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">
{/* ── Product Grid (Scrollable) ── */}
<div className="lg:col-span-8 flex flex-col gap-5 h-full min-h-0">
{/* Search + Category Filter */}
<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 produk..."
className="pl-10"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="w-full sm:w-40 shrink-0">
<SelectValue placeholder="Semua Kategori" />
</SelectTrigger>
<SelectContent position='item-aligned'>
<SelectItem value="all">Semua</SelectItem>
{categories.map(cat => (
<SelectItem key={cat.id} value={String(cat.id)}>{cat.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Scrollable Area for Product Cards */}
<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);
const displayPrice = getPurchasePriceLabel(product.prices);
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)}
>
<CardContent className="p-4 flex flex-col gap-3">
{/* Full Image */}
<div className="-mx-4 -mt-4 aspect-[4/3] overflow-hidden bg-muted/50 rounded-t-xl">
{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-105"
/>
) : (
<div className="h-full w-full flex items-center justify-center">
<ImagePlus className="h-10 w-10 text-muted-foreground/30" />
</div>
)}
</div>
{/* Categories + Name */}
<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">
{cat.name}
</Badge>
))}
</div>
<h3 className="font-semibold text-sm leading-snug line-clamp-2">
{product.name}
</h3>
</div>
{/* Price + Stepper */}
<div
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mt-auto"
onClick={e => e.stopPropagation()}
>
<div className="flex-1 min-w-0">
<span className="text-sm font-bold text-foreground tabular-nums truncate block">
{displayPrice}
</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) => decreaseQuantity(product, 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?.quantity ?? 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) => increaseQuantity(product, e)}
>
<Plus className="h-3.5 w-3.5 sm:h-3 sm:w-3" />
</button>
</div>
</div>
</CardContent>
</Card>
);
})}
{filteredProducts.length === 0 && (
<div className="col-span-full py-16 text-center text-muted-foreground">
<Empty>
<EmptyHeader>
<EmptyTitle>Ooops...</EmptyTitle>
<EmptyDescription>
Tidak ada data yang ditemukan.
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
)}
</div>
</ScrollArea>
</div>
{/* ── Cart Sidebar (Fixed on Desktop) ── */}
<div className="hidden lg:block lg:col-span-4 h-full min-h-0">
<form onSubmit={onSubmit} className="h-full">
<Card className="border shadow-xl bg-card overflow-hidden h-full flex flex-col">
<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.product_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>
<p className="text-xs text-primary font-medium mt-0.5">
{formatCurrency(item.quantity * item.unit_price)}
</p>
</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="px-2 py-0.5 rounded bg-muted/50 border font-medium text-foreground">
{item.quantity} pcs
</div>
<span>×</span>
<span className="font-mono">
{formatCurrency(item.unit_price)}
</span>
</div>
</div>
))}
</div>
)}
</CardContent>
</ScrollArea>
<div className="p-4 border-t bg-muted/10 space-y-3 shrink-0">
<Field>
<Label htmlFor="purchase_date" className="text-xs text-muted-foreground" required>Tanggal</Label>
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
<PopoverTrigger asChild>
<Button
id="purchase_date"
variant="outline"
size="sm"
className={cn("w-full justify-start text-left font-normal h-8 text-xs")}
>
<CalendarIcon className="mr-1 h-3 w-3" />
{data.purchase_date ? (
new Intl.DateTimeFormat("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
}).format(new Date(data.purchase_date))
) : (
<span className="text-muted-foreground">Pilih Tanggal</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={data.purchase_date ? new Date(data.purchase_date) : undefined}
defaultMonth={data.purchase_date ? new Date(data.purchase_date) : new Date()}
captionLayout="dropdown"
onSelect={(selectedDate: Date | undefined) => {
if (selectedDate) {
setData('purchase_date', format(selectedDate, 'yyyy-MM-dd'));
} else {
setData('purchase_date', '');
}
setIsCalendarOpen(false);
}}
/>
</PopoverContent>
</Popover>
</Field>
<Field>
<Label htmlFor="note" className="text-xs text-muted-foreground">Catatan</Label>
<Input id="note" className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} autoComplete='off' placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">Total Belanja</p>
<p className="text-xl font-bold text-primary">{formatCurrency(total)}</p>
</div>
<div className="text-right text-xs text-muted-foreground">
<p>{cartItems.length} produk</p>
<p>{cartTotalItems} pcs</p>
</div>
</div>
<Button
type="submit"
className="w-full h-10 font-semibold shadow"
disabled={processing || cartItems.length === 0}
>
{processing ? <Loader className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</div>
</div>
</Card>
</form>
</div>
{/* ── Floating Mobile Cart Trigger ── */}
<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-[85vh] rounded-t-[2.5rem] flex flex-col overflow-hidden gap-0"
>
{/* Header */}
<div className="flex items-center justify-between px-5 pt-5 pb-4 border-b shrink-0">
<div className="flex items-center gap-2 font-semibold text-base">
<ShoppingCart className="h-4 w-4" />
Keranjang
</div>
<div className="flex items-center gap-2">
<Badge className="rounded-full font-bold">
{cartTotalItems} pcs
</Badge>
<SheetClose asChild>
<button
className="h-8 w-8 rounded-full flex items-center justify-center text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
aria-label="Tutup"
>
<X className="h-4 w-4" />
</button>
</SheetClose>
</div>
</div>
{/* Scrollable cart items — min-h-0 wajib agar flex-1 bisa scroll */}
<div className="flex-1 min-h-0 overflow-y-auto overscroll-contain">
{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.product_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>
<p className="text-xs text-primary font-medium mt-0.5">
{formatCurrency(item.quantity * item.unit_price)}
</p>
</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="px-2 py-0.5 rounded bg-muted/50 border font-medium text-foreground">
{item.quantity} pcs
</div>
<span>×</span>
<span className="font-mono">
{formatCurrency(item.unit_price)}
</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t bg-muted/10 space-y-3 shrink-0">
<Field>
<Label htmlFor="purchase_date_mobile" className="text-xs text-muted-foreground">Tanggal</Label>
<Popover>
<PopoverTrigger asChild>
<Button
id="purchase_date_mobile"
variant="outline"
size="sm"
className={cn("w-full justify-start text-left font-normal h-8 text-xs")}
>
<CalendarIcon className="mr-1 h-3 w-3" />
{data.purchase_date ? (
new Intl.DateTimeFormat("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
}).format(new Date(data.purchase_date))
) : (
<span className="text-muted-foreground">Pilih Tanggal</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={data.purchase_date ? new Date(data.purchase_date) : undefined}
defaultMonth={data.purchase_date ? new Date(data.purchase_date) : new Date()}
captionLayout="dropdown"
onSelect={(selectedDate: Date | undefined) => {
if (selectedDate) {
setData('purchase_date', format(selectedDate, 'yyyy-MM-dd'));
} else {
setData('purchase_date', '');
}
}}
/>
</PopoverContent>
</Popover>
</Field>
<Field>
<Label htmlFor="note_mobile" className="text-xs text-muted-foreground">Catatan</Label>
<Input id="note_mobile" className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">Total Belanja</p>
<p className="text-xl font-bold text-primary">{formatCurrency(total)}</p>
</div>
<div className="text-right text-xs text-muted-foreground">
<p>{cartItems.length} produk</p>
<p>{cartTotalItems} pcs</p>
</div>
</div>
<Button
type="button"
className="w-full h-10 font-semibold shadow"
disabled={processing || cartItems.length === 0}
onClick={() => onSubmit({ preventDefault: () => { } } as any)}
>
{processing ? <Loader className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</div>
</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" onClick={() => setQtyDialogIndex(null)}>Batal</Button>
<Button onClick={confirmQty}>Konfirmasi</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
PurchaseCreate.layout = {
breadcrumbs: [{ title: 'Kelola' }],
};