diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f9fa417..a7eca3e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,6 +52,7 @@ model Submission { order_id String user_id String bill Int? + paid_amount Int? payment_status String @default("BELUM_BAYAR") order Order @relation(fields: [order_id], references: [id], onDelete: Cascade) user User @relation(fields: [user_id], references: [id], onDelete: Cascade) diff --git a/src/app/(app)/balances/page.tsx b/src/app/(app)/balances/page.tsx new file mode 100644 index 0000000..08f082f --- /dev/null +++ b/src/app/(app)/balances/page.tsx @@ -0,0 +1,176 @@ +'use client' + +import { useEffect, useState } from 'react' +import { getBalancesAsCreator, getBalancesAsSubmittor, getSessionUser } from '@/app/actions' +import { Card, CardContent } from '@/components/ui/card' +import { Wallet, ArrowDownRight, ArrowUpRight, Loader2 } from 'lucide-react' +import { cn } from '@/lib/utils' + +export default function BalancesPage() { + const [activeTab, setActiveTab] = useState<'SUBMITTOR' | 'CREATOR'>('SUBMITTOR') + const [userId, setUserId] = useState(null) + const [submittorBalances, setSubmittorBalances] = useState([]) + const [creatorBalances, setCreatorBalances] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const init = async () => { + const user = await getSessionUser() + if (user?.id) { + setUserId(user.id) + loadData(user.id) + } + } + init() + }, []) + + const loadData = async (id: string) => { + setLoading(true) + const [subRes, creRes] = await Promise.all([ + getBalancesAsSubmittor(id), + getBalancesAsCreator(id) + ]) + setSubmittorBalances(subRes) + setCreatorBalances(creRes) + setLoading(false) + } + + const formatRupiah = (n: number) => + new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(n) + + return ( +
+
+ Keuangan / Saldo +

+ + Buku Saldo +

+

+ Pantau riwayat saldo lebih atau kurang dari transaksi pesanan. +

+
+ +
+ + +
+ + {loading ? ( +
+ + Memuat saldo... +
+ ) : activeTab === 'SUBMITTOR' ? ( +
+
+

Saldo Anda di Kreator Lain

+

+ Jika saldo positif (hijau), Anda memiliki deposit yang bisa digunakan untuk pesanan berikutnya di kreator tersebut. Jika negatif (merah), Anda berhutang. +

+
+ + {submittorBalances.length === 0 ? ( +
+

Belum ada catatan saldo.

+
+ ) : ( +
+ {submittorBalances.map((b, i) => ( + +
+ {b.creator.photo ? ( + {b.creator.name} + ) : ( +
+ {b.creator.name.charAt(0).toUpperCase()} +
+ )} +
+

{b.creator.name}

+

Kreator

+
+
+ +
+ Total Saldo +
0 ? "text-emerald-600" : "text-rose-600")}> + {b.amount > 0 ? : } + {formatRupiah(b.amount)} +
+
+
+
+ ))} +
+ )} +
+ ) : ( +
+
+

Saldo Orang Lain di Anda

+

+ Jika saldo positif (merah bagi Anda), artinya Anda memegang uang lebih milik penitip (Hutang Anda ke mereka). Jika negatif (hijau), mereka berhutang ke Anda. +

+
+ + {creatorBalances.length === 0 ? ( +
+

Belum ada penitip yang memiliki catatan saldo dengan Anda.

+
+ ) : ( +
+ {creatorBalances.map((b, i) => ( + +
+ {b.user.photo ? ( + {b.user.name} + ) : ( +
+ {b.user.name.charAt(0).toUpperCase()} +
+ )} +
+

{b.user.name}

+

Penitip

+
+
+ +
+ Saldo Penitip + {/* From Creator perspective: positive balance of submittor is bad for creator (creator holds their money) */} +
0 ? "text-rose-600" : "text-emerald-600")}> + {b.amount > 0 ? : } + {formatRupiah(b.amount)} +
+
+
+
+ ))} +
+ )} +
+ )} +
+ ) +} diff --git a/src/app/(app)/my-orders/[id]/page.tsx b/src/app/(app)/my-orders/[id]/page.tsx index 65d74d3..2b8b538 100644 --- a/src/app/(app)/my-orders/[id]/page.tsx +++ b/src/app/(app)/my-orders/[id]/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' -import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder, getSessionUser } from '@/app/actions' +import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder, getSessionUser, getBalancesAsCreator } from '@/app/actions' import { Card, CardContent } from '@/components/ui/card' import { Button, buttonVariants } from '@/components/ui/button' import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog' @@ -11,7 +11,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { format } from 'date-fns' +import { format, isToday } from 'date-fns' import { Copy, Loader2, @@ -23,6 +23,7 @@ import { ToggleLeft, AlertCircle, Pencil, + ShoppingBag, PlusCircle, MinusCircle, Trash2, @@ -38,6 +39,7 @@ export default function OrderDetailPage() { const [userId, setUserId] = useState(null) const [order, setOrder] = useState(null) const [loading, setLoading] = useState(true) + const [balances, setBalances] = useState([]) const [copiedPerson, setCopiedPerson] = useState(false) const [copiedItem, setCopiedItem] = useState(false) const [updatingStatus, setUpdatingStatus] = useState(false) @@ -52,15 +54,22 @@ export default function OrderDetailPage() { if (user?.id) { setUserId(user.id) } - if (orderId) loadOrder() + if (orderId) loadOrder(user?.id) } init() }, [orderId]) - const loadOrder = async () => { + const loadOrder = async (currentUserId?: string) => { setLoading(true) const data = await getOrderDetail(orderId) setOrder(data) + + const idToUse = currentUserId || userId + if (data && idToUse === data.creator_id) { + const bals = await getBalancesAsCreator(idToUse) + setBalances(bals) + } + setLoading(false) } @@ -230,46 +239,52 @@ export default function OrderDetailPage() { )} {/* Hero Overview Card */} - -
-
-
+ +
+
+
- ● {order.status} + {order.status} - {format(new Date(order.date), 'dd MMMM yyyy')} + {format(new Date(order.date), 'dd MMM yyyy')}
-

+

{order.title}

-

- Dibuat oleh: {order.creator.name} +

+ Oleh {order.creator.name}

-
-
- Total Pemesan - {order.submissions.length} +
+
+
+ Pemesan + {order.submissions.length} +
+
-
-
- Total Menu - {order.available_items.length} + +
+
+ Menu + {order.available_items.length} +
+
{/* Creator Control Toolbar (Status, Edit, Delete) */} {isCreator && ( -
+
@@ -278,8 +293,8 @@ export default function OrderDetailPage() {
- {/* Edit Button (Available when not CLOSE) */} - {!isClosed && ( + {/* Edit Button (Available when not CLOSE and date is today) */} + {!isClosed && isToday(new Date(order.date)) && ( )} - {order.status === 'CLOSE' && ( + {order.status === 'CLOSE' && isToday(new Date(order.date)) && (
)} @@ -474,43 +493,88 @@ function SubmissionRow({ sub, isCreator, isClosed, + currentBalance, onUpdate }: { sub: any isCreator: boolean isClosed: boolean + currentBalance?: number onUpdate: () => void }) { const [bill, setBill] = useState(sub.bill ?? '') + const [paidAmount, setPaidAmount] = useState(sub.paid_amount ?? '') const [status, setStatus] = useState(sub.payment_status || 'BELUM_BAYAR') const [saving, setSaving] = useState(false) - const handleSave = async () => { + // Sync state when props change after save + useEffect(() => { + setBill(sub.bill ?? '') + setPaidAmount(sub.paid_amount ?? '') + setStatus(sub.payment_status || 'BELUM_BAYAR') + }, [sub]) + + const handleSave = async (overrideStatus?: string, overridePaid?: string | number) => { setSaving(true) - await updateSubmissionPayment(sub.id, bill !== '' ? parseInt(bill as string) : null, status) + const finalBill = bill !== '' ? parseInt(bill as string) : null + let finalStatus = overrideStatus || status + + let finalPaid: number | null = null + if (overridePaid !== undefined) { + finalPaid = overridePaid !== '' ? parseInt(overridePaid as string) : null + } else { + finalPaid = paidAmount !== '' ? parseInt(paidAmount as string) : null + } + + // AUTO-LUNAS LOGIC: Jika Uang Diterima >= Tagihan, otomatis set jadi LUNAS + if (finalPaid !== null && finalBill !== null && finalPaid >= finalBill && finalBill > 0) { + finalStatus = 'LUNAS' + setStatus('LUNAS') + } + + const res = await updateSubmissionPayment(sub.id, finalBill, finalStatus, finalPaid) + + // Tampilkan pesan error jika gagal (contoh: Prisma error) + if (res && res.error) { + alert("Gagal menyimpan: " + res.error + "\n\nPastikan Anda sudah me-restart server (npm run dev) jika baru ada perubahan database.") + } + onUpdate() setSaving(false) } + const handleUseBalance = () => { + setPaidAmount('0') + setStatus('LUNAS') + handleSave('LUNAS', '0') + } + const formatRupiah = (angka: number) => { return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(angka) } return ( -
+
{/* Left: User & Order items */} -
+
{sub.user.photo ? ( - {sub.user.name} + {sub.user.name} ) : ( -
+
{sub.user.name.charAt(0).toUpperCase()}
)}
-

{sub.user.name}

+
+

{sub.user.name}

+ {currentBalance !== undefined && currentBalance !== 0 && ( + 0 ? "bg-emerald-50 text-emerald-700 border border-emerald-200" : "bg-rose-50 text-rose-700 border border-rose-200")}> + Saldo: {formatRupiah(currentBalance)} + + )} +
-
+
    {sub.items.map((item: any) => (
  • @@ -545,43 +609,103 @@ function SubmissionRow({ {/* Right: Bill input if Creator */} {isCreator && ( -
    +
    {isClosed ? ( <> -
    - - setBill(e.target.value)} - className="h-9 rounded-lg font-bold text-xs" - /> +
    +
    + + { + const val = e.target.value + setBill(val) + const pBill = val !== '' ? parseInt(val) : 0 + const pPaid = paidAmount !== '' ? parseInt(paidAmount as string) : 0 + if (val !== '' && pPaid >= pBill && pBill > 0) { + setStatus('LUNAS') + } else { + setStatus('BELUM_BAYAR') + } + }} + className="h-8 rounded-md font-bold text-xs" + /> +
    +
    + + { + const val = e.target.value + setPaidAmount(val) + const pBill = bill !== '' ? parseInt(bill as string) : 0 + const pPaid = val !== '' ? parseInt(val) : 0 + if (val !== '' && pPaid >= pBill && pBill > 0) { + setStatus('LUNAS') + } else { + setStatus('BELUM_BAYAR') + } + }} + className="h-8 rounded-md font-bold text-xs" + /> +
    +
    + + +
    -
    - - + +
    + {sub.payment_status !== 'LUNAS' ? ( + <> + + + {currentBalance !== undefined && currentBalance > 0 && status !== 'LUNAS' && ( + + )} + + ) : ( +
    + Telah Lunas +
    + )}
    - ) : (
    diff --git a/src/app/(app)/my-orders/page.tsx b/src/app/(app)/my-orders/page.tsx index ed74848..20c1c94 100644 --- a/src/app/(app)/my-orders/page.tsx +++ b/src/app/(app)/my-orders/page.tsx @@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' -import { format } from 'date-fns' +import { format, isToday } from 'date-fns' import { id as idLocale } from 'date-fns/locale' import Link from 'next/link' import { @@ -306,10 +306,10 @@ export default function MyOrdersPage() { return (
    {/* Left: Info */} -
    +

    {order.title} @@ -358,14 +358,14 @@ export default function MyOrdersPage() {

    {/* Right: Actions */} -
    -
    +
    +
    {/* Detail Link */} @@ -373,12 +373,18 @@ export default function MyOrdersPage() { {/* Status Action Button */} -
    +
    {order.status === 'DRAFT' && (
    {/* Utility Tools Row */} -
    +
    diff --git a/src/app/(app)/reports/page.tsx b/src/app/(app)/reports/page.tsx index 65fa46a..afd7f6e 100644 --- a/src/app/(app)/reports/page.tsx +++ b/src/app/(app)/reports/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { getCreatorReport, getSubmittorReport, getSessionUser } from '@/app/actions' +import { getCreatorReport, getSubmittorReport, getSessionUser, getBalancesAsCreator } from '@/app/actions' import { Card } from '@/components/ui/card' import { Loader2, TrendingUp, TrendingDown, Wallet, ArrowRightLeft, Calendar as CalendarIcon } from 'lucide-react' import { cn } from '@/lib/utils' @@ -60,6 +60,7 @@ export default function ReportsPage() { const [userId, setUserId] = useState(null) const [submittorData, setSubmittorData] = useState([]) const [creatorData, setCreatorData] = useState([]) + const [creatorBalances, setCreatorBalances] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { @@ -79,12 +80,14 @@ export default function ReportsPage() { const loadData = async (id: string) => { setLoading(true) const interval = getIntervalFromFilter(filterType, filterValue) - const [subRes, creRes] = await Promise.all([ + const [subRes, creRes, balRes] = await Promise.all([ getSubmittorReport(id, interval?.start, interval?.end), - getCreatorReport(id, interval?.start, interval?.end) + getCreatorReport(id, interval?.start, interval?.end), + getBalancesAsCreator(id) ]) setSubmittorData(subRes) setCreatorData(creRes) + setCreatorBalances(balRes) setLoading(false) } @@ -284,6 +287,8 @@ export default function ReportsPage() { ) : ( { if (userId) loadData(userId) }} /> )} diff --git a/src/app/actions.ts b/src/app/actions.ts index c24ab58..20eb85b 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -381,36 +381,151 @@ export async function getOrderDetail(order_id: string) { }) } -export async function updateSubmissionPayment(submission_id: string, bill: number | null, payment_status: string) { +export async function updateSubmissionPayment(submission_id: string, bill: number | null, payment_status: string, paid_amount: number | null = null) { try { await prisma.submission.update({ where: { id: submission_id }, - data: { bill, payment_status } + data: { bill, payment_status, paid_amount } }) revalidatePath(`/my-orders`) revalidatePath(`/my-purchases`) revalidatePath(`/reports`) + revalidatePath(`/balances`) return { success: true } } catch (e) { console.error(e); return { success: false, error: 'Gagal menyimpan tagihan.' } } } -export async function updateBulkSubmissionPayment(submission_ids: string[], payment_status: string) { +export async function updateBulkSubmissionPayment(submission_ids: string[], payment_status: string, use_balance: boolean = false) { try { - await prisma.submission.updateMany({ - where: { id: { in: submission_ids } }, - data: { payment_status } - }) + if (payment_status === 'LUNAS') { + const submissions = await prisma.submission.findMany({ where: { id: { in: submission_ids } } }) + const ops = submissions.map(sub => prisma.submission.update({ + where: { id: sub.id }, + data: { + payment_status, + paid_amount: use_balance ? 0 : (sub.paid_amount != null ? sub.paid_amount : sub.bill) + } + })) + await prisma.$transaction(ops) + } else { + await prisma.submission.updateMany({ + where: { id: { in: submission_ids } }, + data: { payment_status } + }) + } revalidatePath(`/my-orders`) revalidatePath(`/my-purchases`) revalidatePath(`/reports`) + revalidatePath(`/balances`) return { success: true } } catch (e) { + console.error(e) return { success: false, error: 'Gagal mengubah status tagihan massal.' } } } +export async function processBulkPayment(data: { + submission_ids: string[]; + cash_amount: number; + use_balance: boolean; + creator_id: string; + user_id: string; +}) { + try { + let currentBalance = 0 + if (data.use_balance) { + const balanceData = await getBalancesAsCreator(data.creator_id) + const userBalance = balanceData.find(b => b.user.id === data.user_id) + if (userBalance) currentBalance = userBalance.amount + } + + const submissions = await prisma.submission.findMany({ + where: { id: { in: data.submission_ids } }, + include: { order: { select: { date: true } } } + }) + + // Sort by order date ascending (oldest first) + submissions.sort((a, b) => new Date(a.order.date).getTime() - new Date(b.order.date).getTime()) + + let remainingBalance = currentBalance + let remainingCash = data.cash_amount + let totalAvailable = remainingBalance + remainingCash + + const ops = [] + + for (let i = 0; i < submissions.length; i++) { + const sub = submissions[i] + const isLast = i === submissions.length - 1 + + const subBill = sub.bill || 0 + const prevPaid = sub.paid_amount || 0 + const amountToCover = Math.max(0, subBill - prevPaid) + + if (totalAvailable >= amountToCover && amountToCover > 0) { + // Fully covered -> LUNAS + let balanceToUse = Math.min(remainingBalance, amountToCover) + remainingBalance -= balanceToUse + + let cashToUse = amountToCover - balanceToUse + remainingCash -= cashToUse + totalAvailable -= amountToCover + + let finalPaid = prevPaid + cashToUse + if (isLast && remainingCash > 0) { + finalPaid += remainingCash + remainingCash = 0 + } + + ops.push(prisma.submission.update({ + where: { id: sub.id }, + data: { payment_status: 'LUNAS', paid_amount: finalPaid } + })) + } else if (totalAvailable >= amountToCover && amountToCover === 0) { + // It's already fully paid somehow, just mark LUNAS. Give excess cash if last. + let finalPaid = prevPaid + if (isLast && remainingCash > 0) { + finalPaid += remainingCash + remainingCash = 0 + } + ops.push(prisma.submission.update({ + where: { id: sub.id }, + data: { payment_status: 'LUNAS', paid_amount: finalPaid } + })) + } else if (totalAvailable > 0) { + // Partially covered -> BELUM_BAYAR. Only use cash. + let finalPaid = prevPaid + remainingCash + remainingCash = 0 + totalAvailable = remainingBalance // only balance left, which can't be used + + ops.push(prisma.submission.update({ + where: { id: sub.id }, + data: { payment_status: 'BELUM_BAYAR', paid_amount: finalPaid } + })) + } else { + // totalAvailable == 0. No more money. Just leave it as is, or update to BELUM_BAYAR. + ops.push(prisma.submission.update({ + where: { id: sub.id }, + data: { payment_status: 'BELUM_BAYAR' } + })) + } + } + + await prisma.$transaction(ops) + + revalidatePath(`/my-orders`) + revalidatePath(`/my-purchases`) + revalidatePath(`/reports`) + revalidatePath(`/balances`) + + return { success: true } + } catch (e) { + console.error(e) + return { success: false, error: 'Gagal memproses pembayaran massal cerdas.' } + } +} + // === SUBMISSION (PESANAN SAYA) === export async function getUserSubmission(order_id: string, user_id: string) { return await prisma.submission.findFirst({ @@ -524,3 +639,61 @@ export async function getCreatorReport(creator_id: string, startDate?: Date, end orderBy: { date: 'desc' } }) } + +// === BALANCE ACTIONS === +export async function getBalancesAsCreator(creator_id: string) { + const submissions = await prisma.submission.findMany({ + where: { + order: { creator_id }, + payment_status: 'LUNAS' + }, + include: { + user: { select: { id: true, name: true, photo: true } } + } + }) + + const balanceMap = new Map() + + submissions.forEach(sub => { + if (sub.paid_amount == null || sub.bill == null) return + const diff = sub.paid_amount - sub.bill + if (!balanceMap.has(sub.user.id)) { + balanceMap.set(sub.user.id, { user: sub.user, amount: diff }) + } else { + balanceMap.get(sub.user.id)!.amount += diff + } + }) + + return Array.from(balanceMap.values()).filter(b => b.amount !== 0) +} + +export async function getBalancesAsSubmittor(user_id: string) { + const submissions = await prisma.submission.findMany({ + where: { + user_id, + payment_status: 'LUNAS' + }, + include: { + order: { + include: { + creator: { select: { id: true, name: true, photo: true } } + } + } + } + }) + + const balanceMap = new Map() + + submissions.forEach(sub => { + if (sub.paid_amount == null || sub.bill == null) return + const diff = sub.paid_amount - sub.bill + const creator = sub.order.creator + if (!balanceMap.has(creator.id)) { + balanceMap.set(creator.id, { creator: creator, amount: diff }) + } else { + balanceMap.get(creator.id)!.amount += diff + } + }) + + return Array.from(balanceMap.values()).filter(b => b.amount !== 0) +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c5a15f4..3f409f1 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -10,6 +10,7 @@ const navItems = [ { name: 'Jasa Order Saya', href: '/my-orders' }, { name: 'Pesanan Saya', href: '/my-purchases' }, { name: 'Laporan', href: '/reports' }, + { name: 'Buku Saldo', href: '/balances' }, { name: 'Profile', href: '/profile' }, ] diff --git a/src/components/ReportByPersonGrid.tsx b/src/components/ReportByPersonGrid.tsx index d7f85d6..6c47cc0 100644 --- a/src/components/ReportByPersonGrid.tsx +++ b/src/components/ReportByPersonGrid.tsx @@ -6,7 +6,7 @@ import { ChevronDown, ChevronUp, Users, CheckCircle2, Package, InboxIcon, Chevro import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog' -import { updateBulkSubmissionPayment } from '@/app/actions' +import { processBulkPayment } from '@/app/actions' import { format } from 'date-fns' const ITEMS_PER_PAGE = 5 @@ -14,13 +14,14 @@ const ITEMS_PER_PAGE = 5 const formatRupiah = (value: number) => new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(value) -export default function ReportByPersonGrid({ data, onUpdate }: { data: any[]; onUpdate: () => void }) { +export default function ReportByPersonGrid({ data, balancesData = [], creatorId, onUpdate }: { data: any[]; balancesData?: any[]; creatorId: string; onUpdate: () => void }) { const [expandedRows, setExpandedRows] = useState>({}) const [currentPage, setCurrentPage] = useState(1) const [searchQuery, setSearchQuery] = useState('') const [modalOpen, setModalOpen] = useState(false) const [saving, setSaving] = useState(false) - const [target, setTarget] = useState<{ ids: string[]; label: string; amount: number }>({ ids: [], label: '', amount: 0 }) + const [target, setTarget] = useState<{ ids: string[]; label: string; amount: number; userId: string }>({ ids: [], label: '', amount: 0, userId: '' }) + const [cashInput, setCashInput] = useState('') // Reset page + search when data changes (e.g. filter changed) useEffect(() => { @@ -75,16 +76,24 @@ export default function ReportByPersonGrid({ data, onUpdate }: { data: any[]; on const totalPages = Math.ceil(filteredUserList.length / ITEMS_PER_PAGE) const paginatedList = filteredUserList.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE) - const openModal = (e: React.MouseEvent, ids: string[], label: string, amount: number) => { + const openModal = (e: React.MouseEvent, ids: string[], label: string, amount: number, userId: string) => { e.stopPropagation() - setTarget({ ids, label, amount }) + setTarget({ ids, label, amount, userId }) + setCashInput('') setModalOpen(true) } const handleConfirm = async () => { if (!target.ids.length) return setSaving(true) - const res = await updateBulkSubmissionPayment(target.ids, 'LUNAS') + const cash = Number(cashInput.replace(/\D/g, '')) || 0 + const res = await processBulkPayment({ + submission_ids: target.ids, + cash_amount: cash, + use_balance: true, + creator_id: creatorId, + user_id: target.userId + }) setSaving(false) if (res.success) { setModalOpen(false) @@ -92,6 +101,9 @@ export default function ReportByPersonGrid({ data, onUpdate }: { data: any[]; on } } + const targetUserBalanceObj = balancesData.find(b => b.user.id === target.userId) + const targetUserBalance = targetUserBalanceObj ? targetUserBalanceObj.amount : 0 + return ( <> @@ -182,7 +194,7 @@ export default function ReportByPersonGrid({ data, onUpdate }: { data: any[]; on {unpaid.length > 0 ? ( - diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 1e12e7a..7618797 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -19,13 +19,15 @@ import { X, ArrowUpRight, Info, - BarChart2 + BarChart2, + Wallet } from 'lucide-react' const menuItems = [ { name: 'Open Order', href: '/', icon: Store, desc: 'Daftar PO live hari ini' }, { name: 'Jasa Order Saya', href: '/my-orders', icon: ClipboardList, desc: 'Kelola PO buatan Anda' }, { name: 'Pesanan Saya', href: '/my-purchases', icon: ShoppingBag, desc: 'Riwayat titipan Anda' }, + { name: 'Buku Saldo', href: '/balances', icon: Wallet, desc: 'Pantau riwayat saldo' }, { name: 'Laporan', href: '/reports', icon: BarChart2, desc: 'Ringkasan transaksi' }, ]