379 lines
14 KiB
TypeScript
379 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="flex max-h-[90vh] flex-col overflow-hidden rounded-3xl border-slate-200/90 p-0 shadow-2xl sm:max-w-[520px] dark:border-slate-800">
|
|
<div className="bg-gradient-to-br from-[#1B2CC1] to-[#121E85] p-6 text-white">
|
|
<span className="text-[11px] font-bold tracking-wider text-blue-200 uppercase">
|
|
Form Titipan
|
|
</span>
|
|
<DialogTitle className="mt-1 text-2xl font-black tracking-tight text-white">
|
|
{liveOrder.title}
|
|
</DialogTitle>
|
|
<p className="mt-1 text-xs text-blue-100">
|
|
Pilih menu yang tersedia di bawah atau tambahkan item khusus jika diizinkan.
|
|
</p>
|
|
</div>
|
|
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="flex-1 space-y-6 overflow-y-auto bg-white p-6 dark:bg-slate-900"
|
|
>
|
|
{/* Standard Items */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-bold tracking-wider text-slate-400 uppercase">
|
|
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 rounded-xl border p-3.5 transition-all',
|
|
isSelected
|
|
? 'border-[#1B2CC1]/40 bg-blue-50/20 shadow-sm dark:bg-blue-950/10'
|
|
: 'border-slate-200/90 bg-slate-50/70 dark:border-slate-800 dark:bg-slate-800/40'
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
<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 disabled:opacity-50 data-[state=checked]:border-[#1B2CC1] data-[state=checked]:bg-[#1B2CC1]"
|
|
/>
|
|
<Label
|
|
htmlFor={`item-${item.id}`}
|
|
className={cn(
|
|
'flex items-center gap-2 truncate text-sm font-bold',
|
|
item.is_sold_out
|
|
? 'cursor-not-allowed text-slate-400 dark:text-slate-500'
|
|
: 'cursor-pointer text-slate-800 dark:text-slate-200'
|
|
)}
|
|
>
|
|
<span className={item.is_sold_out ? 'line-through' : ''}>{item.name}</span>
|
|
{item.is_sold_out && (
|
|
<span className="rounded border border-rose-200 bg-rose-100 px-1.5 py-0.5 text-[9px] font-black tracking-wider text-rose-600 uppercase dark:border-rose-900/50 dark:bg-rose-950/30 dark:text-rose-400">
|
|
Sold Out
|
|
</span>
|
|
)}
|
|
</Label>
|
|
</div>
|
|
{isSelected && (
|
|
<div className="flex shrink-0 items-center gap-1 rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
|
<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="mt-1 pl-7">
|
|
<Input
|
|
placeholder="Catatan (opsional), misal: 1/2 porsi"
|
|
className="h-8 border-slate-200/60 bg-slate-50/50 text-xs dark:border-slate-700/60 dark:bg-slate-900/50"
|
|
value={items[item.id]?.note || ''}
|
|
disabled={loading}
|
|
onChange={(e) => handleStandardItemNote(item.id, e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
{liveOrder.available_items.length === 0 && (
|
|
<p className="rounded-xl border border-dashed border-slate-200 bg-slate-50 py-3 text-center text-xs text-slate-400 dark:border-slate-800 dark:bg-slate-800/40">
|
|
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 tracking-wider text-slate-400 uppercase">
|
|
Item Tambahan (Kustom)
|
|
</Label>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={loading}
|
|
onClick={addCustomItem}
|
|
className="h-8 gap-1.5 rounded-lg border-dashed border-[#1B2CC1]/40 text-xs font-bold text-[#1B2CC1] hover:bg-[#1B2CC1]/10"
|
|
>
|
|
<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 items-center gap-2 rounded-xl border border-slate-200/90 bg-slate-50/70 p-3 dark:border-slate-800 dark:bg-slate-800/40"
|
|
>
|
|
<span className="w-4 text-xs font-bold text-slate-400">{idx + 1}.</span>
|
|
<Input
|
|
placeholder="Nama Menu Kustom"
|
|
value={ci.name}
|
|
disabled={loading}
|
|
onChange={(e) => handleCustomItemChange(ci.id, 'name', e.target.value)}
|
|
className="h-9 flex-1 rounded-lg bg-white text-xs font-medium dark:bg-slate-900"
|
|
/>
|
|
<Input
|
|
type="number"
|
|
min="1"
|
|
value={ci.qty}
|
|
disabled={loading}
|
|
onChange={(e) =>
|
|
handleCustomItemChange(ci.id, 'qty', parseInt(e.target.value) || 1)
|
|
}
|
|
className="h-9 w-16 rounded-lg bg-white text-center text-xs font-bold dark:bg-slate-900"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
disabled={loading}
|
|
onClick={() => removeCustomItem(ci.id)}
|
|
className="h-8 w-8 shrink-0 rounded-lg text-red-500 hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/30"
|
|
>
|
|
<MinusCircle className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div
|
|
onClick={addCustomItem}
|
|
className="group cursor-pointer rounded-2xl border-2 border-dashed border-slate-200 bg-slate-50/50 p-4 text-center transition-all hover:border-[#1B2CC1]/50 dark:border-slate-800 dark:bg-slate-800/30"
|
|
>
|
|
<PlusCircle className="mx-auto mb-1 h-5 w-5 text-slate-400 transition-colors group-hover:text-[#1B2CC1]" />
|
|
<p className="text-xs font-bold text-slate-600 dark:text-slate-300">
|
|
Klik untuk Tambah Item Custom
|
|
</p>
|
|
<p className="mt-0.5 text-[11px] text-slate-400">
|
|
Ingin titip menu lain? Masukkan nama dan kuantitasnya di sini.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="rounded-xl border border-red-200 bg-red-50 p-3 text-xs font-semibold text-red-600 dark:border-red-900 dark:bg-red-950/30 dark:text-red-400">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3 border-t border-slate-100 pt-2 dark:border-slate-800">
|
|
<Button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="h-11 w-full rounded-xl bg-[#1B2CC1] font-bold text-white shadow-md shadow-[#1B2CC1]/25 hover:bg-[#15229E]"
|
|
>
|
|
{loading ? 'Menyimpan Titipan...' : 'Kirim Titip Pesanan'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
)
|
|
}
|