feat: add description field to orders and support marking menu items as sold out

This commit is contained in:
Firman Ramdhani
2026-09-11 10:40:23 +07:00
parent e3c8f980f7
commit f4358eb00b
14 changed files with 435 additions and 211 deletions
+66 -10
View File
@@ -312,9 +312,16 @@ export default function MyOrdersPage() {
{/* Left: Info */}
<div className="w-full md:flex-1 p-5 md:border-r border-slate-100 dark:border-slate-800/80 flex flex-col justify-center">
<div className="flex justify-between items-start gap-4">
<h3 className="text-xl font-bold text-slate-900 dark:text-white line-clamp-2 leading-snug flex-1 min-w-0">
{order.title}
</h3>
<div className="flex-1 min-w-0">
<h3 className="text-xl font-bold text-slate-900 dark:text-white line-clamp-2 leading-snug">
{order.title}
</h3>
{order.description && (
<p className="text-xs text-slate-500 mt-1 line-clamp-2 font-medium">
{order.description}
</p>
)}
</div>
<div className="flex flex-col sm:flex-row items-end sm:items-center gap-2 shrink-0">
<span className="text-[11px] text-slate-500 font-semibold bg-slate-100/80 dark:bg-slate-800 px-2.5 py-1 rounded-full whitespace-nowrap">
{format(date, 'EEEE, dd MMM yyyy', { locale: idLocale })}
@@ -538,6 +545,7 @@ export default function MyOrdersPage() {
function CreateOrderModal({ userId, onSuccess, initialData }: { userId: string, onSuccess: () => void, initialData?: any }) {
const [title, setTitle] = useState(initialData ? `${initialData.title} (Copy)` : '')
const [description, setDescription] = useState(initialData ? (initialData.description || '') : '')
const [allowCustom, setAllowCustom] = useState(initialData ? initialData.allow_custom : false)
const [items, setItems] = useState<{ id: string; name: string }[]>(
initialData && initialData.available_items?.length > 0
@@ -567,6 +575,7 @@ function CreateOrderModal({ userId, onSuccess, initialData }: { userId: string,
const res = await createOrder({
creator_id: userId,
title: title.trim(),
description: description.trim(),
date: new Date(),
allow_custom: allowCustom,
available_items: validItems
@@ -609,6 +618,19 @@ function CreateOrderModal({ userId, onSuccess, initialData }: { userId: string,
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="order-desc" className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">
Deskripsi <span className="text-slate-400 font-normal lowercase">(opsional)</span>
</Label>
<Input
id="order-desc"
placeholder="Keterangan tambahan tentang PO ini"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="h-11 rounded-xl border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1]"
/>
</div>
{/* Custom Item Checkbox */}
<div className="flex items-start space-x-3 p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/70 dark:bg-slate-800/40">
<Checkbox
@@ -689,18 +711,20 @@ function CreateOrderModal({ userId, onSuccess, initialData }: { userId: string,
function EditOrderModal({ order, onClose, onSuccess }: { order: any, onClose: () => void, onSuccess: () => void }) {
const [title, setTitle] = useState(order.title || '')
const [description, setDescription] = useState(order.description || '')
const [allowCustom, setAllowCustom] = useState(order.allow_custom || false)
const [items, setItems] = useState<Array<{ id: string, name: string }>>(
const [items, setItems] = useState<Array<{ id: string, name: string, is_sold_out: boolean }>>(
order.available_items?.length > 0
? order.available_items.map((ai: any) => ({ id: ai.id, name: ai.name }))
: [{ id: '1', name: '' }]
? order.available_items.map((ai: any) => ({ id: ai.id, name: ai.name, is_sold_out: ai.is_sold_out || false }))
: [{ id: '1', name: '', is_sold_out: false }]
)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const addItem = () => setItems([...items, { id: Math.random().toString(), name: '' }])
const addItem = () => setItems([...items, { id: Math.random().toString(), name: '', is_sold_out: false }])
const removeItem = (id: string) => setItems(items.filter(i => i.id !== id))
const updateItem = (id: string, name: string) => setItems(items.map(i => i.id === id ? { ...i, name } : i))
const toggleSoldOut = (id: string, is_sold_out: boolean) => setItems(items.map(i => i.id === id ? { ...i, is_sold_out } : i))
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -708,7 +732,7 @@ function EditOrderModal({ order, onClose, onSuccess }: { order: any, onClose: ()
if (!title.trim()) return setError('Judul PO wajib diisi.')
const validItems = items.filter(i => i.name.trim()).map(i => i.name.trim())
const validItems = items.filter(i => i.name.trim()).map(i => ({ name: i.name.trim(), is_sold_out: i.is_sold_out }))
if (!allowCustom && validItems.length === 0) {
return setError('Wajib menambahkan minimal 1 menu pilihan jika tidak mengizinkan custom item.')
}
@@ -717,6 +741,7 @@ function EditOrderModal({ order, onClose, onSuccess }: { order: any, onClose: ()
const res = await updateOrder(order.id, {
title: title.trim(),
description: description.trim(),
allow_custom: allowCustom,
available_items: validItems
})
@@ -758,6 +783,19 @@ function EditOrderModal({ order, onClose, onSuccess }: { order: any, onClose: ()
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="edit-order-desc" className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">
Deskripsi <span className="text-slate-400 font-normal lowercase">(opsional)</span>
</Label>
<Input
id="edit-order-desc"
placeholder="Keterangan tambahan tentang PO ini"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="h-11 rounded-xl border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1]"
/>
</div>
{/* Custom Item Checkbox */}
<div className="flex items-start space-x-3 p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/70 dark:bg-slate-800/40">
<Checkbox
@@ -800,16 +838,34 @@ function EditOrderModal({ order, onClose, onSuccess }: { order: any, onClose: ()
<Input
placeholder="Nama menu (mis: Es Kopi Susu Tetangga)"
value={item.name}
disabled={item.is_sold_out}
onChange={(e) => updateItem(item.id, e.target.value)}
className="flex-1 h-9 rounded-lg bg-white dark:bg-slate-900 text-xs font-medium"
className={cn(
"flex-1 h-9 rounded-lg bg-white dark:bg-slate-900 text-xs font-medium transition-all",
item.is_sold_out && "opacity-60 cursor-not-allowed line-through decoration-rose-500/50"
)}
/>
<Button
type="button"
variant={item.is_sold_out ? "default" : "outline"}
size="sm"
onClick={() => toggleSoldOut(item.id, !item.is_sold_out)}
className={cn(
"h-9 px-3 rounded-lg text-xs font-bold transition-all whitespace-nowrap",
item.is_sold_out
? "bg-rose-600 hover:bg-rose-700 text-white border-rose-600"
: "text-slate-500 hover:text-rose-600 hover:border-rose-300"
)}
>
Sold Out
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeItem(item.id)}
disabled={items.length === 1 && !allowCustom}
className="h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg"
className="h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg shrink-0"
>
<MinusCircle className="h-4 w-4" />
</Button>