diff --git a/app/(app)/invoices/page.tsx b/app/(app)/invoices/page.tsx index 744bfa6..9e5af2e 100644 --- a/app/(app)/invoices/page.tsx +++ b/app/(app)/invoices/page.tsx @@ -2,144 +2,167 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Card, CardContent } from '@/components/ui/card'; -import { Skeleton } from '@/components/ui/skeleton'; -import { - Table, TableBody, TableCell, TableHead, TableHeader, TableRow, -} from '@/components/ui/table'; -import { Search } from 'lucide-react'; import { api } from '@/lib/api'; -import { useDebounce } from '@/lib/hooks'; +import { Card, CardContent } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Search, ChevronRight } from 'lucide-react'; import { format } from 'date-fns'; interface Invoice { id: string; invoiceNumber: string; - status: string; - subtotal: number; - lateFee: number; - total: number; - amountPaid: number; - balance: number; dueDate: string; - createdAt: string; + subtotal: string; + lateFee: string; + total: string; + amountPaid: string; + balance: string; + status: string; client?: { firstName: string; lastName: string; accountNumber: string }; } -const STATUS_COLORS: Record = { - DRAFT: 'bg-gray-100 text-gray-600', +interface InvoicesResponse { + data: Invoice[]; + total: number; +} + +const statusColors: Record = { SENT: 'bg-blue-100 text-blue-700', PARTIAL: 'bg-yellow-100 text-yellow-700', - PAID: 'bg-emerald-100 text-emerald-700', + PAID: 'bg-green-100 text-green-700', OVERDUE: 'bg-red-100 text-red-700', + DRAFT: 'bg-gray-100 text-gray-500', VOID: 'bg-gray-100 text-gray-400', }; +const peso = (v: string | number) => + '₱' + Number(v ?? 0).toLocaleString('en-PH', { minimumFractionDigits: 2 }); + export default function InvoicesPage() { const [search, setSearch] = useState(''); - const [status, setStatus] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); const [page, setPage] = useState(1); - const debouncedSearch = useDebounce(search, 400); - const limit = 20; - const { data, isLoading } = useQuery({ - queryKey: ['invoices', debouncedSearch, status, page], + const { data, isLoading } = useQuery({ + queryKey: ['invoices', search, statusFilter, page], queryFn: async () => { - const params = new URLSearchParams({ page: String(page), limit: String(limit) }); - if (status) params.set('status', status); - if (debouncedSearch) params.set('search', debouncedSearch); + const params = new URLSearchParams({ page: String(page), limit: '20' }); + if (search) params.set('search', search); + if (statusFilter) params.set('status', statusFilter); const res = await api.get(`/api/v1/invoices?${params}`); return res.data; }, + staleTime: 30_000, }); - const invoices: Invoice[] = Array.isArray(data) ? data : (data?.data ?? data?.items ?? []); - const total = (data as any)?.meta?.total ?? (data as any)?.total ?? invoices.length; - - const STATUSES = ['', 'SENT', 'PARTIAL', 'PAID', 'OVERDUE', 'DRAFT', 'VOID']; + const invoices = data?.data ?? []; + const total = data?.total ?? 0; return (
-
-

Invoices

-

Track billing and payment status

+
+
+

Invoices

+

{total} invoices

+
- {/* Filters */} -
-
+
+
- { setSearch(e.target.value); setPage(1); }} /> + { setSearch(e.target.value); setPage(1); }} + />
-
- {STATUSES.map(s => ( - +
- + - - - - Invoice # - Client - Status - Total - Balance - Due Date - - - - {isLoading ? ( - Array.from({ length: 8 }).map((_, i) => ( - - {Array.from({ length: 6 }).map((_, j) => ( - +
+
+ + + + + + + + + + + + + {isLoading + ? Array.from({ length: 8 }).map((_, i) => ( + + {Array.from({ length: 7 }).map((_, j) => ( + + ))} + + )) + : invoices.map((inv) => ( + + + + + + + + + ))} - - )) - ) : invoices.length === 0 ? ( - - - No invoices found - - - ) : ( - invoices.map((inv) => ( - - {inv.invoiceNumber} - - {inv.client ? `${inv.client.firstName} ${inv.client.lastName}` : '—'} - {inv.client && #{inv.client.accountNumber}} - - - - {inv.status} - - - ₱{Number(inv.total).toLocaleString()} - 0 ? 'text-red-600 font-medium' : 'text-slate-400'}`}> - {Number(inv.balance) > 0 ? `₱${Number(inv.balance).toLocaleString()}` : '—'} - - - {format(new Date(inv.dueDate), 'MMM d, yyyy')} - - - )) - )} - -
Invoice #ClientDue DateTotalBalanceStatus
{inv.invoiceNumber} + {inv.client + ? `${inv.client.firstName} ${inv.client.lastName}` + : '—'} +
{inv.client?.accountNumber}
+
+ {inv.dueDate ? format(new Date(inv.dueDate), 'MMM d, yyyy') : '—'} + {peso(inv.total)} + {Number(inv.balance) > 0 ? ( + {peso(inv.balance)} + ) : ( + Paid + )} + + + {inv.status} + +
+ + +
+ + {!isLoading && invoices.length === 0 && ( +
No invoices found
+ )} + + {total > 20 && ( +
+

+ Showing {(page - 1) * 20 + 1}–{Math.min(page * 20, total)} of {total} +

+
+ + +
+
+ )}
diff --git a/app/(app)/payments/page.tsx b/app/(app)/payments/page.tsx index 3b715f4..34bd86c 100644 --- a/app/(app)/payments/page.tsx +++ b/app/(app)/payments/page.tsx @@ -2,139 +2,135 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; +import { api } from '@/lib/api'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Skeleton } from '@/components/ui/skeleton'; -import { - Table, TableBody, TableCell, TableHead, TableHeader, TableRow, -} from '@/components/ui/table'; import { Search } from 'lucide-react'; -import { api } from '@/lib/api'; import { format } from 'date-fns'; interface Payment { id: string; - amount: number; + amount: string; channel: string; paymentDate: string; - notes?: string; + notes: string | null; client?: { firstName: string; lastName: string; accountNumber: string }; - invoice?: { invoiceNumber: string }; recordedBy?: { firstName: string; lastName: string }; + invoice?: { invoiceNumber: string }; } -const CHANNEL_COLORS: Record = { - CASH: 'bg-emerald-100 text-emerald-700', +const channelColors: Record = { + CASH: 'bg-green-100 text-green-700', GCASH: 'bg-blue-100 text-blue-700', - MAYA: 'bg-green-100 text-green-700', - BANK_TRANSFER: 'bg-purple-100 text-purple-700', - CHECK: 'bg-orange-100 text-orange-700', + MAYA: 'bg-purple-100 text-purple-700', + BANK_TRANSFER: 'bg-orange-100 text-orange-700', + CHECK: 'bg-gray-100 text-gray-700', }; -export default function PaymentsPage() { - const [page, setPage] = useState(1); - const limit = 20; +const peso = (v: string | number) => + '₱' + Number(v ?? 0).toLocaleString('en-PH', { minimumFractionDigits: 2 }); - const { data, isLoading } = useQuery({ - queryKey: ['payments', page], +export default function PaymentsPage() { + const [search, setSearch] = useState(''); + const [channelFilter, setChannelFilter] = useState(''); + const [page, setPage] = useState(1); + + const { data, isLoading } = useQuery<{ data: Payment[]; total: number }>({ + queryKey: ['payments', search, channelFilter, page], queryFn: async () => { - const res = await api.get(`/api/v1/payments?page=${page}&limit=${limit}`); + const params = new URLSearchParams({ page: String(page), limit: '20' }); + if (channelFilter) params.set('channel', channelFilter); + const res = await api.get(`/api/v1/payments?${params}`); return res.data; }, + staleTime: 30_000, }); - const payments: Payment[] = Array.isArray(data) ? data : (data?.data ?? data?.items ?? []); - const total = (data as any)?.meta?.total ?? (data as any)?.total ?? payments.length; - - const totalAmount = payments.reduce((sum, p) => sum + Number(p.amount), 0); + const payments = data?.data ?? []; + const total = data?.total ?? 0; return (
-
+

Payments

-

Payment collection records

+

{total} payments

- {payments.length > 0 && ( -
-

Total (this page)

-

₱{totalAmount.toLocaleString()}

-
- )}
- +
+
+ + { setSearch(e.target.value); setPage(1); }} /> +
+ +
+ + - - - - Date - Client - Invoice - Channel - Amount - Recorded By - Notes - - - - {isLoading ? ( - Array.from({ length: 8 }).map((_, i) => ( - - {Array.from({ length: 7 }).map((_, j) => ( - +
+
+ + + + + + + + + + + + {isLoading + ? Array.from({ length: 8 }).map((_, i) => ( + + {Array.from({ length: 6 }).map((_, j) => ( + + ))} + + )) + : payments.map((p) => ( + + + + + + + + ))} - - )) - ) : payments.length === 0 ? ( - - No payments yet - - ) : ( - payments.map((p) => ( - - - {format(new Date(p.paymentDate), 'MMM d, yyyy')} - - - {p.client ? `${p.client.firstName} ${p.client.lastName}` : '—'} - - - {p.invoice?.invoiceNumber ?? '—'} - - - - {p.channel} - - - - ₱{Number(p.amount).toLocaleString()} - - - {p.recordedBy ? `${p.recordedBy.firstName} ${p.recordedBy.lastName}` : '—'} - - - {p.notes ?? '—'} - - - )) - )} - -
DateClientAmountChannelRecorded ByInvoice
+ {p.paymentDate ? format(new Date(p.paymentDate), 'MMM d, yyyy') : '—'} + + {p.client ? `${p.client.firstName} ${p.client.lastName}` : '—'} +
{p.client?.accountNumber}
+
{peso(p.amount)} + + {p.channel?.replace('_', ' ')} + + + {p.recordedBy ? `${p.recordedBy.firstName} ${p.recordedBy.lastName}` : '—'} + + {p.invoice?.invoiceNumber ?? '—'} +
+ + +
+ {!isLoading && payments.length === 0 && ( +
No payments found
+ )} - - {total > limit && ( -
- Page {page} of {Math.ceil(total / limit)} -
- - -
-
- )}
); }