feat: add saldo balance tracking and item notes support for orders and reports
This commit is contained in:
@@ -13,8 +13,8 @@ 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 }>>({})
|
||||
const [customItems, setCustomItems] = useState<Array<{ id: string, name: string, qty: number }>>([])
|
||||
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('')
|
||||
|
||||
@@ -42,10 +42,15 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
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 }
|
||||
freshItems[stdItem.id] = { selected: true, qty: item.qty, note: item.note || '' }
|
||||
}
|
||||
} else {
|
||||
newCustoms.push({ id: Math.random().toString(), name: item.name, qty: item.qty })
|
||||
newCustoms.push({
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
name: item.name,
|
||||
qty: item.qty,
|
||||
note: item.note || ''
|
||||
})
|
||||
}
|
||||
})
|
||||
setItems(freshItems)
|
||||
@@ -53,30 +58,47 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
}
|
||||
}
|
||||
|
||||
const handleStandardItemToggle = (itemId: string, checked: boolean) => {
|
||||
const handleStandardItemToggle = (id: string, checked: boolean) => {
|
||||
setItems(prev => ({
|
||||
...prev,
|
||||
[itemId]: { selected: checked, qty: checked ? 1 : 0 }
|
||||
[id]: { selected: checked, qty: checked ? 1 : 0, note: prev[id]?.note || '' }
|
||||
}))
|
||||
}
|
||||
|
||||
const handleStandardItemQty = (itemId: string, qty: number) => {
|
||||
const handleStandardItemQty = (id: string, qty: number) => {
|
||||
if (qty < 1) {
|
||||
handleStandardItemToggle(itemId, false)
|
||||
handleStandardItemToggle(id, false)
|
||||
return
|
||||
}
|
||||
setItems(prev => ({
|
||||
...prev,
|
||||
[itemId]: { selected: true, qty }
|
||||
[id]: { ...prev[id], qty }
|
||||
}))
|
||||
}
|
||||
|
||||
const handleStandardItemNote = (id: string, note: string) => {
|
||||
setItems(prev => ({
|
||||
...prev,
|
||||
[id]: { ...prev[id], note }
|
||||
}))
|
||||
}
|
||||
|
||||
const addCustomItem = () => {
|
||||
setCustomItems([...customItems, { id: Math.random().toString(), name: '', qty: 1 }])
|
||||
setCustomItems(prev => [...prev, { id: Math.random().toString(36).substr(2, 9), name: '', qty: 1, note: '' }])
|
||||
}
|
||||
|
||||
const updateCustomItem = (id: string, field: 'name' | 'qty', value: any) => {
|
||||
setCustomItems(customItems.map(c => c.id === id ? { ...c, [field]: value } : c))
|
||||
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) => {
|
||||
@@ -91,15 +113,25 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
const payloadItems: any[] = []
|
||||
|
||||
liveOrder.available_items.forEach((ai: any) => {
|
||||
const state = items[ai.id]
|
||||
if (state?.selected && state.qty > 0) {
|
||||
payloadItems.push({ name: ai.name, qty: state.qty, is_custom: false })
|
||||
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 })
|
||||
payloadItems.push({
|
||||
name: ci.name.trim(),
|
||||
qty: ci.qty,
|
||||
is_custom: true,
|
||||
note: ci.note?.trim() || undefined
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -149,50 +181,63 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-3.5 rounded-xl border transition-all",
|
||||
isSelected
|
||||
? "border-[#1B2CC1] bg-[#1B2CC1]/5 dark:bg-blue-950/20 shadow-sm"
|
||||
: "border-slate-200/80 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50"
|
||||
"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 gap-3 flex-1 min-w-0">
|
||||
<Checkbox
|
||||
id={`item-${item.id}`}
|
||||
checked={isSelected && !item.is_sold_out}
|
||||
disabled={item.is_sold_out}
|
||||
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 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="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">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
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"
|
||||
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 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>
|
||||
@@ -217,6 +262,7 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
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"
|
||||
>
|
||||
@@ -230,24 +276,27 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
<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 / Catatan Khusus"
|
||||
placeholder="Nama Menu Kustom"
|
||||
value={ci.name}
|
||||
onChange={(e) => updateCustomItem(ci.id, 'name', e.target.value)}
|
||||
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}
|
||||
onChange={(e) => updateCustomItem(ci.id, 'qty', parseInt(e.target.value) || 1)}
|
||||
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"
|
||||
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>
|
||||
|
||||
@@ -51,10 +51,12 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
})
|
||||
}
|
||||
const obj = usersMap.get(sub.user.id)
|
||||
const amount = Number(sub.bill) || 0
|
||||
if (sub.payment_status !== 'LUNAS') obj.totalPiutang += amount
|
||||
else obj.totalPaid += amount
|
||||
obj.submissions.push({ ...sub, orderTitle: order.title, orderDate: order.date })
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
obj.totalPiutang += unpaid
|
||||
obj.totalPaid += paid
|
||||
obj.submissions.push({ ...sub, orderTitle: order.title, orderDate: order.date, paid, unpaid })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -150,7 +152,7 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{paginatedList.map((userObj) => {
|
||||
const isExpanded = !!expandedRows[userObj.user.id]
|
||||
const unpaid = userObj.submissions.filter((s: any) => s.payment_status !== 'LUNAS')
|
||||
const unpaid = userObj.submissions.filter((s: any) => s.unpaid > 0)
|
||||
|
||||
return (
|
||||
<React.Fragment key={userObj.user.id}>
|
||||
@@ -224,7 +226,7 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-700/50">
|
||||
{userObj.submissions.map((sub: any) => {
|
||||
const isUnpaid = sub.payment_status !== 'LUNAS'
|
||||
const isUnpaid = sub.unpaid > 0
|
||||
return (
|
||||
<tr key={sub.id}>
|
||||
<td className="px-4 py-2.5 font-bold text-slate-800 dark:text-slate-200">{sub.orderTitle}</td>
|
||||
@@ -241,8 +243,14 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
{isUnpaid ? 'Belum' : 'Lunas'}
|
||||
</span>
|
||||
</td>
|
||||
<td className={cn('px-4 py-2.5 text-right font-black', isUnpaid ? 'text-rose-600 dark:text-rose-400' : 'text-emerald-600 dark:text-emerald-400')}>
|
||||
{sub.bill ? formatRupiah(sub.bill) : 'Rp0'}
|
||||
<td className={cn('px-4 py-2.5 text-right flex flex-col', isUnpaid ? 'text-rose-600 dark:text-rose-400' : 'text-emerald-600 dark:text-emerald-400')}>
|
||||
<span className="font-black">{sub.bill ? formatRupiah(sub.bill) : 'Rp0'}</span>
|
||||
{sub.unpaid > 0 && sub.paid > 0 && (
|
||||
<div className="text-[10px] text-slate-500 mt-0.5 font-medium leading-tight">
|
||||
Masuk: {formatRupiah(sub.paid)}<br/>
|
||||
Sisa: {formatRupiah(sub.unpaid)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
{/* <td className="px-4 py-2.5 text-center">
|
||||
{isUnpaid ? (
|
||||
|
||||
@@ -16,12 +16,19 @@ export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREA
|
||||
chartData = [...data]
|
||||
.filter(sub => sub.order.status === 'CLOSE')
|
||||
.sort((a, b) => new Date(a.order.date).getTime() - new Date(b.order.date).getTime()) // oldest → newest
|
||||
.map(sub => ({
|
||||
name: sub.order.title.length > 15 ? sub.order.title.substring(0, 15) + '...' : sub.order.title,
|
||||
fullTitle: sub.order.title,
|
||||
Total: sub.bill || 0,
|
||||
status: sub.payment_status
|
||||
}))
|
||||
.map(sub => {
|
||||
const bill = sub.bill || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
return {
|
||||
name: sub.order.title.length > 15 ? sub.order.title.substring(0, 15) + '...' : sub.order.title,
|
||||
fullTitle: sub.order.title,
|
||||
Total: bill,
|
||||
Paid: paid,
|
||||
Unpaid: unpaid,
|
||||
status: unpaid > 0 ? (paid > 0 ? 'SEBAGIAN' : 'BELUM BAYAR') : 'LUNAS'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
chartData = [...data]
|
||||
.filter(order => order.status === 'CLOSE')
|
||||
@@ -50,14 +57,21 @@ export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREA
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900 p-3 border border-slate-200 dark:border-slate-800 rounded-xl shadow-lg">
|
||||
<p className="font-bold text-xs text-slate-800 dark:text-slate-200 mb-1">{payload[0].payload.fullTitle}</p>
|
||||
<p className="font-bold text-xs text-slate-800 dark:text-slate-200 mb-1">{data.fullTitle}</p>
|
||||
<p className="text-sm font-black text-[#1B2CC1]">
|
||||
{formatRupiah(payload[0].value)}
|
||||
Total: {formatRupiah(data.Total)}
|
||||
</p>
|
||||
{type === 'SUBMITTOR' && (
|
||||
<>
|
||||
<p className="text-[11px] text-emerald-600 font-bold mt-1">Dibayar: {formatRupiah(data.Paid)}</p>
|
||||
{data.Unpaid > 0 && <p className="text-[11px] text-rose-600 font-bold">Kurang: {formatRupiah(data.Unpaid)}</p>}
|
||||
</>
|
||||
)}
|
||||
<p className="text-[10px] text-slate-500 mt-1 uppercase font-bold tracking-wider">
|
||||
Status: {payload[0].payload.status}
|
||||
Status: {data.status}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
@@ -90,17 +104,14 @@ export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREA
|
||||
tickFormatter={(value) => `Rp${value / 1000}k`}
|
||||
/>
|
||||
<Tooltip cursor={{ fill: 'rgba(27, 44, 193, 0.05)' }} content={<CustomTooltip />} />
|
||||
<Bar dataKey="Total" radius={[4, 4, 0, 0]}>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={type === 'SUBMITTOR'
|
||||
? (entry.status === 'LUNAS' ? '#10b981' : '#f43f5e') // Emerald or Rose
|
||||
: '#1B2CC1' // Primary for creator
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
{type === 'SUBMITTOR' ? (
|
||||
<>
|
||||
<Bar dataKey="Paid" stackId="a" fill="#10b981" />
|
||||
<Bar dataKey="Unpaid" stackId="a" fill="#f43f5e" radius={[4, 4, 0, 0]} />
|
||||
</>
|
||||
) : (
|
||||
<Bar dataKey="Total" radius={[4, 4, 0, 0]} fill="#1B2CC1" />
|
||||
)}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
|
||||
@@ -160,8 +160,22 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
{sub.payment_status === 'LUNAS' ? 'LUNAS' : 'BELUM BAYAR'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right font-black text-slate-900 dark:text-white">
|
||||
{sub.bill ? formatRupiah(sub.bill) : '-'}
|
||||
<td className="px-6 py-4 text-right font-black text-slate-900 dark:text-white flex flex-col">
|
||||
<span>{sub.bill ? formatRupiah(sub.bill) : '-'}</span>
|
||||
{(() => {
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
if (unpaid > 0 && paid > 0) {
|
||||
return (
|
||||
<div className="text-[10px] text-slate-500 font-medium mt-1 leading-tight">
|
||||
Dibayar: {formatRupiah(paid)}<br/>
|
||||
Sisa: {formatRupiah(unpaid)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -238,7 +252,11 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
{paginatedData.map((order: any) => {
|
||||
const isExpanded = !!expandedRows[order.id]
|
||||
const totalOmzet = order.submissions.reduce((acc: number, sub: any) => acc + (Number(sub.bill) || 0), 0)
|
||||
const totalPiutang = order.submissions.reduce((acc: number, sub: any) => acc + (sub.payment_status !== 'LUNAS' ? (Number(sub.bill) || 0) : 0), 0)
|
||||
const totalPiutang = order.submissions.reduce((acc: number, sub: any) => {
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
return acc + Math.max(0, bill - paid)
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<React.Fragment key={order.id}>
|
||||
@@ -312,8 +330,22 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
{sub.payment_status === 'LUNAS' ? 'Lunas' : 'Belum'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right font-black text-slate-700 dark:text-slate-300">
|
||||
{sub.bill ? formatRupiah(sub.bill) : '-'}
|
||||
<td className="px-4 py-2.5 text-right font-black text-slate-700 dark:text-slate-300 flex flex-col">
|
||||
<span>{sub.bill ? formatRupiah(sub.bill) : '-'}</span>
|
||||
{(() => {
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
if (unpaid > 0 && paid > 0) {
|
||||
return (
|
||||
<div className="text-[10px] text-slate-500 font-medium mt-0.5 leading-tight">
|
||||
Masuk: {formatRupiah(paid)}<br/>
|
||||
Sisa: {formatRupiah(unpaid)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -14,11 +14,12 @@ export function SummaryGenerator({ order }: { order: any }) {
|
||||
const itemCounts: Record<string, number> = {}
|
||||
|
||||
order.submissions?.forEach((sub: any) => {
|
||||
const itemStrings = sub.items.map((i: any) => `${i.name} ${i.qty}x`)
|
||||
const itemStrings = sub.items.map((i: any) => `${i.name} ${i.qty}x${i.note ? ` (${i.note})` : ''}`)
|
||||
summaryByPerson += `- ${sub.user?.name || 'Unknown'} : ${itemStrings.join(', ')}\n`
|
||||
|
||||
sub.items.forEach((i: any) => {
|
||||
itemCounts[i.name] = (itemCounts[i.name] || 0) + i.qty
|
||||
const key = i.note ? `${i.name} (${i.note})` : i.name
|
||||
itemCounts[key] = (itemCounts[key] || 0) + i.qty
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user