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
+49 -92
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { useParams, useRouter, notFound } from 'next/navigation'
import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder, getSessionUser, getBalancesAsCreator } from '@/app/actions'
import { Card, CardContent } from '@/components/ui/card'
import { SummaryGenerator } from '@/components/SummaryGenerator'
import { Button, buttonVariants } from '@/components/ui/button'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
@@ -109,40 +110,11 @@ export default function OrderDetailPage() {
notFound()
}
const isCreator = userId === order.creator_id
const isCreator = order.creator_id === userId
const isClosed = order.status === 'CLOSE'
const isDraft = order.status === 'DRAFT'
const canDelete = isDraft || isClosed
let summaryByPerson = `${order.title}\n`
const itemCounts: Record<string, number> = {}
order.submissions.forEach((sub: any) => {
const itemStrings = sub.items.map((i: any) => `${i.name} ${i.qty}x`)
summaryByPerson += `- ${sub.user.name} : ${itemStrings.join(', ')}\n`
sub.items.forEach((i: any) => {
itemCounts[i.name] = (itemCounts[i.name] || 0) + i.qty
})
})
let summaryByItem = `${order.title}\n`
Object.entries(itemCounts).forEach(([name, qty]) => {
summaryByItem += `- ${name} : ${qty} pcs\n`
})
const handleCopyPerson = () => {
navigator.clipboard.writeText(summaryByPerson)
setCopiedPerson(true)
setTimeout(() => setCopiedPerson(false), 2000)
}
const handleCopyItem = () => {
navigator.clipboard.writeText(summaryByItem)
setCopiedItem(true)
setTimeout(() => setCopiedItem(false), 2000)
}
return (
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
{/* Top Breadcrumb Header */}
@@ -255,6 +227,11 @@ export default function OrderDetailPage() {
<h1 className="text-xl sm:text-2xl font-black text-slate-900 dark:text-white tracking-tight leading-tight">
{order.title}
</h1>
{order.description && (
<p className="text-sm text-slate-600 dark:text-slate-400 mt-1.5 font-medium leading-relaxed">
{order.description}
</p>
)}
<p className="text-xs text-slate-500 font-medium pt-0.5">
Oleh <span className="text-slate-800 dark:text-slate-200 font-bold">{order.creator.name}</span>
</p>
@@ -434,61 +411,7 @@ export default function OrderDetailPage() {
</div>
{/* Right 1 Col: Summary Generators */}
<div className="space-y-6">
<div className="px-1">
<h3 className="text-lg font-black text-slate-900 dark:text-white flex items-center gap-2">
<Receipt className="w-5 h-5 text-[#1B2CC1]" />
<span>Generator Rekap</span>
</h3>
<p className="text-xs text-slate-500 mt-0.5">Salin format teks siap kirim ke WhatsApp / grup.</p>
</div>
{/* Rekap Per Orang */}
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
<div className="p-4 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50 dark:bg-slate-800/40">
<span className="text-xs font-bold text-slate-800 dark:text-slate-200">
Rekap per Orang
</span>
<Button
variant="outline"
size="sm"
onClick={handleCopyPerson}
className="h-8 text-xs font-bold rounded-lg border-slate-200 gap-1.5 hover:bg-[#1B2CC1] hover:text-white transition-all"
>
{copiedPerson ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5" />}
<span>{copiedPerson ? 'Tersalin!' : 'Copy Text'}</span>
</Button>
</div>
<CardContent className="p-4">
<pre className="text-xs font-mono bg-slate-50 dark:bg-slate-800/80 p-3.5 rounded-xl overflow-x-auto whitespace-pre-wrap border border-slate-200/80 dark:border-slate-700 text-slate-700 dark:text-slate-300 leading-relaxed">
{summaryByPerson}
</pre>
</CardContent>
</Card>
{/* Rekap Per Item */}
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
<div className="p-4 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50 dark:bg-slate-800/40">
<span className="text-xs font-bold text-slate-800 dark:text-slate-200">
Rekap per Item (Akumulasi)
</span>
<Button
variant="outline"
size="sm"
onClick={handleCopyItem}
className="h-8 text-xs font-bold rounded-lg border-slate-200 gap-1.5 hover:bg-[#1B2CC1] hover:text-white transition-all"
>
{copiedItem ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5" />}
<span>{copiedItem ? 'Tersalin!' : 'Copy Text'}</span>
</Button>
</div>
<CardContent className="p-4">
<pre className="text-xs font-mono bg-slate-50 dark:bg-slate-800/80 p-3.5 rounded-xl overflow-x-auto whitespace-pre-wrap border border-slate-200/80 dark:border-slate-700 text-slate-700 dark:text-slate-300 leading-relaxed">
{summaryByItem}
</pre>
</CardContent>
</Card>
</div>
<SummaryGenerator order={order} />
</div>
</div>
)
@@ -728,18 +651,20 @@ function SubmissionRow({
function EditDetailOrderModal({ 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()
@@ -747,7 +672,7 @@ function EditDetailOrderModal({ order, onClose, onSuccess }: { order: any, onClo
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.')
}
@@ -756,6 +681,7 @@ function EditDetailOrderModal({ order, onClose, onSuccess }: { order: any, onClo
const res = await updateOrder(order.id, {
title: title.trim(),
description: description.trim(),
allow_custom: allowCustom,
available_items: validItems
})
@@ -794,6 +720,19 @@ function EditDetailOrderModal({ order, onClose, onSuccess }: { order: any, onClo
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="detail-edit-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="detail-edit-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
@@ -836,16 +775,34 @@ function EditDetailOrderModal({ order, onClose, onSuccess }: { order: any, onClo
<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>