338 lines
14 KiB
TypeScript
338 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { cn } from '@/lib/utils'
|
|
import { DialogContent, DialogTitle } from '@/components/ui/dialog'
|
|
import { Checkbox } from '@/components/ui/checkbox'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Input } from '@/components/ui/input'
|
|
import { submitOrder, getUserSubmission, getSessionUser, getOrderDetail } from '@/app/actions'
|
|
import { PlusCircle, MinusCircle, CheckCircle2 } from 'lucide-react'
|
|
|
|
export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: () => void }) {
|
|
const [liveOrder, setLiveOrder] = useState<any>(order)
|
|
const [userId, setUserId] = useState<string>('')
|
|
const [items, setItems] = useState<Record<string, { selected: boolean, qty: number, note?: string }>>({})
|
|
const [customItems, setCustomItems] = useState<Array<{ id: string, name: string, qty: number, note?: string }>>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
const freshOrder = await getOrderDetail(order.id)
|
|
if (freshOrder) setLiveOrder(freshOrder)
|
|
|
|
const user = await getSessionUser()
|
|
if (user?.id) {
|
|
setUserId(user.id)
|
|
loadExistingSubmission(user.id, freshOrder || order)
|
|
}
|
|
}
|
|
init()
|
|
}, [order.id])
|
|
|
|
const loadExistingSubmission = async (uid: string, currentOrder: any) => {
|
|
const sub = await getUserSubmission(currentOrder.id, uid)
|
|
if (sub) {
|
|
const freshItems: any = {}
|
|
const newCustoms: any[] = []
|
|
|
|
sub.items.forEach((item: any) => {
|
|
if (!item.is_custom) {
|
|
const stdItem = currentOrder.available_items.find((ai: any) => ai.name === item.name)
|
|
if (stdItem) {
|
|
freshItems[stdItem.id] = { selected: true, qty: item.qty, note: item.note || '' }
|
|
}
|
|
} else {
|
|
newCustoms.push({
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
name: item.name,
|
|
qty: item.qty,
|
|
note: item.note || ''
|
|
})
|
|
}
|
|
})
|
|
setItems(freshItems)
|
|
setCustomItems(newCustoms)
|
|
}
|
|
}
|
|
|
|
const handleStandardItemToggle = (id: string, checked: boolean) => {
|
|
setItems(prev => ({
|
|
...prev,
|
|
[id]: { selected: checked, qty: checked ? 1 : 0, note: prev[id]?.note || '' }
|
|
}))
|
|
}
|
|
|
|
const handleStandardItemQty = (id: string, qty: number) => {
|
|
if (qty < 1) {
|
|
handleStandardItemToggle(id, false)
|
|
return
|
|
}
|
|
setItems(prev => ({
|
|
...prev,
|
|
[id]: { ...prev[id], qty }
|
|
}))
|
|
}
|
|
|
|
const handleStandardItemNote = (id: string, note: string) => {
|
|
setItems(prev => ({
|
|
...prev,
|
|
[id]: { ...prev[id], note }
|
|
}))
|
|
}
|
|
|
|
const addCustomItem = () => {
|
|
setCustomItems(prev => [...prev, { id: Math.random().toString(36).substr(2, 9), name: '', qty: 1, note: '' }])
|
|
}
|
|
|
|
const handleCustomItemChange = (id: string, field: 'name' | 'qty' | 'note', value: string | number) => {
|
|
setCustomItems(prev => prev.map(item => {
|
|
if (item.id === id) {
|
|
return { ...item, [field]: value }
|
|
}
|
|
return item
|
|
}))
|
|
}
|
|
|
|
const handleCustomItemQty = (id: string, qty: number) => {
|
|
if (qty < 1) return
|
|
handleCustomItemChange(id, 'qty', qty)
|
|
}
|
|
|
|
const removeCustomItem = (id: string) => {
|
|
setCustomItems(customItems.filter(c => c.id !== id))
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setLoading(true)
|
|
|
|
const payloadItems: any[] = []
|
|
|
|
liveOrder.available_items.forEach((ai: any) => {
|
|
const selectedItem = items[ai.id]
|
|
if (selectedItem?.selected && selectedItem.qty > 0) {
|
|
payloadItems.push({
|
|
name: ai.name,
|
|
qty: selectedItem.qty,
|
|
is_custom: false,
|
|
note: selectedItem.note?.trim() || undefined
|
|
})
|
|
}
|
|
})
|
|
|
|
customItems.forEach(ci => {
|
|
if (ci.name.trim() && ci.qty > 0) {
|
|
payloadItems.push({
|
|
name: ci.name.trim(),
|
|
qty: ci.qty,
|
|
is_custom: true,
|
|
note: ci.note?.trim() || undefined
|
|
})
|
|
}
|
|
})
|
|
|
|
if (payloadItems.length === 0) {
|
|
setError('Harap pilih minimal 1 item atau tambahkan item lainnya.')
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
const res = await submitOrder({
|
|
order_id: liveOrder.id,
|
|
user_id: userId,
|
|
items: payloadItems
|
|
})
|
|
|
|
if (res.success) {
|
|
onSuccess()
|
|
} else {
|
|
setError(res.error || 'Gagal menyimpan pesanan.')
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
return (
|
|
<DialogContent className="sm:max-w-[520px] p-0 overflow-hidden rounded-3xl border-slate-200/90 dark:border-slate-800 shadow-2xl max-h-[90vh] flex flex-col">
|
|
<div className="bg-gradient-to-br from-[#1B2CC1] to-[#121E85] p-6 text-white">
|
|
<span className="text-[11px] font-bold uppercase tracking-wider text-blue-200">Form Titipan</span>
|
|
<DialogTitle className="text-2xl font-black tracking-tight text-white mt-1">
|
|
{liveOrder.title}
|
|
</DialogTitle>
|
|
<p className="text-xs text-blue-100 mt-1">
|
|
Pilih menu yang tersedia di bawah atau tambahkan item khusus jika diizinkan.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 overflow-y-auto space-y-6 flex-1 bg-white dark:bg-slate-900">
|
|
{/* Standard Items */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
|
Daftar Menu Tersedia
|
|
</Label>
|
|
<div className="space-y-2">
|
|
{liveOrder.available_items.map((item: any) => {
|
|
const isSelected = items[item.id]?.selected || false
|
|
const qty = items[item.id]?.qty || 0
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className={cn(
|
|
"flex flex-col gap-2 p-3.5 rounded-xl border transition-all",
|
|
isSelected ? "border-[#1B2CC1]/40 bg-blue-50/20 dark:bg-blue-950/10 shadow-sm" : "border-slate-200/90 dark:border-slate-800 bg-slate-50/70 dark:bg-slate-800/40"
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
<Checkbox
|
|
id={`item-${item.id}`}
|
|
checked={isSelected && !item.is_sold_out}
|
|
disabled={item.is_sold_out || loading}
|
|
onCheckedChange={(c) => !item.is_sold_out && handleStandardItemToggle(item.id, c as boolean)}
|
|
className="rounded-lg data-[state=checked]:bg-[#1B2CC1] data-[state=checked]:border-[#1B2CC1] disabled:opacity-50"
|
|
/>
|
|
<Label htmlFor={`item-${item.id}`} className={cn("text-sm font-bold truncate flex items-center gap-2", item.is_sold_out ? "text-slate-400 dark:text-slate-500 cursor-not-allowed" : "text-slate-800 dark:text-slate-200 cursor-pointer")}>
|
|
<span className={item.is_sold_out ? "line-through" : ""}>{item.name}</span>
|
|
{item.is_sold_out && (
|
|
<span className="text-[9px] bg-rose-100 dark:bg-rose-950/30 text-rose-600 dark:text-rose-400 px-1.5 py-0.5 rounded font-black uppercase tracking-wider border border-rose-200 dark:border-rose-900/50">
|
|
Sold Out
|
|
</span>
|
|
)}
|
|
</Label>
|
|
</div>
|
|
{isSelected && (
|
|
<div className="flex items-center gap-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-0.5 shadow-sm shrink-0">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
disabled={loading}
|
|
className="h-7 w-7 text-slate-500 hover:text-slate-800"
|
|
onClick={() => handleStandardItemQty(item.id, qty - 1)}
|
|
>
|
|
<MinusCircle className="h-4 w-4" />
|
|
</Button>
|
|
<span className="w-7 text-center text-xs font-extrabold text-[#1B2CC1] dark:text-blue-400">{qty}</span>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
disabled={loading}
|
|
className="h-7 w-7 text-slate-500 hover:text-slate-800"
|
|
onClick={() => handleStandardItemQty(item.id, qty + 1)}
|
|
>
|
|
<PlusCircle className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{isSelected && (
|
|
<div className="pl-7 mt-1">
|
|
<Input
|
|
placeholder="Catatan (opsional), misal: 1/2 porsi"
|
|
className="h-8 text-xs bg-slate-50/50 dark:bg-slate-900/50 border-slate-200/60 dark:border-slate-700/60"
|
|
value={items[item.id]?.note || ''}
|
|
disabled={loading}
|
|
onChange={(e) => handleStandardItemNote(item.id, e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
{liveOrder.available_items.length === 0 && (
|
|
<p className="text-xs text-slate-400 text-center py-3 bg-slate-50 dark:bg-slate-800/40 rounded-xl border border-dashed border-slate-200 dark:border-slate-800">
|
|
Tidak ada menu standar yang ditentukan.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Custom Items */}
|
|
{liveOrder.allow_custom && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
|
Item Tambahan (Kustom)
|
|
</Label>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={loading}
|
|
onClick={addCustomItem}
|
|
className="h-8 text-xs font-bold rounded-lg border-dashed border-[#1B2CC1]/40 text-[#1B2CC1] hover:bg-[#1B2CC1]/10 gap-1.5"
|
|
>
|
|
<PlusCircle className="h-3.5 w-3.5" /> Tambah Kustom
|
|
</Button>
|
|
</div>
|
|
|
|
{customItems.length > 0 ? (
|
|
<div className="space-y-2.5">
|
|
{customItems.map((ci, idx) => (
|
|
<div key={ci.id} className="flex gap-2 items-center p-3 rounded-xl border border-slate-200/90 dark:border-slate-800 bg-slate-50/70 dark:bg-slate-800/40">
|
|
<span className="text-xs font-bold text-slate-400 w-4">{idx + 1}.</span>
|
|
<Input
|
|
placeholder="Nama Menu Kustom"
|
|
value={ci.name}
|
|
disabled={loading}
|
|
onChange={(e) => handleCustomItemChange(ci.id, 'name', e.target.value)}
|
|
className="flex-1 h-9 rounded-lg bg-white dark:bg-slate-900 text-xs font-medium"
|
|
/>
|
|
<Input
|
|
type="number"
|
|
min="1"
|
|
value={ci.qty}
|
|
disabled={loading}
|
|
onChange={(e) => handleCustomItemChange(ci.id, 'qty', parseInt(e.target.value) || 1)}
|
|
className="w-16 h-9 rounded-lg bg-white dark:bg-slate-900 text-center font-bold text-xs"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
disabled={loading}
|
|
onClick={() => removeCustomItem(ci.id)}
|
|
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>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div
|
|
onClick={addCustomItem}
|
|
className="p-4 border-2 border-dashed border-slate-200 dark:border-slate-800 hover:border-[#1B2CC1]/50 rounded-2xl text-center bg-slate-50/50 dark:bg-slate-800/30 cursor-pointer transition-all group"
|
|
>
|
|
<PlusCircle className="w-5 h-5 text-slate-400 group-hover:text-[#1B2CC1] mx-auto mb-1 transition-colors" />
|
|
<p className="text-xs font-bold text-slate-600 dark:text-slate-300">Klik untuk Tambah Item Custom</p>
|
|
<p className="text-[11px] text-slate-400 mt-0.5">Ingin titip menu lain? Masukkan nama dan kuantitasnya di sini.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 rounded-xl text-xs text-red-600 dark:text-red-400 font-semibold">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3 pt-2 border-t border-slate-100 dark:border-slate-800">
|
|
<Button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full h-11 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold shadow-md shadow-[#1B2CC1]/25"
|
|
>
|
|
{loading ? 'Menyimpan Titipan...' : 'Kirim Titip Pesanan'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
)
|
|
}
|