diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx
index 85407ba..3209cb8 100644
--- a/app/(app)/layout.tsx
+++ b/app/(app)/layout.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import Sidebar from '@/components/layout/sidebar';
@@ -9,16 +9,22 @@ import Topbar from '@/components/layout/topbar';
export default function AppLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const accessToken = useAuthStore((s) => s.accessToken);
+ // Wait for zustand to hydrate from localStorage before checking auth
+ const [mounted, setMounted] = useState(false);
useEffect(() => {
- if (!accessToken) {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (mounted && !accessToken) {
router.replace('/login');
}
- }, [accessToken, router]);
+ }, [mounted, accessToken, router]);
- if (!accessToken) {
- return null;
- }
+ // Show nothing until hydrated (prevents flash redirect)
+ if (!mounted) return null;
+ if (!accessToken) return null;
return (
diff --git a/app/(app)/remittances/page.tsx b/app/(app)/remittances/page.tsx
index b71ea35..e992521 100644
--- a/app/(app)/remittances/page.tsx
+++ b/app/(app)/remittances/page.tsx
@@ -1,103 +1,124 @@
'use client';
-import { useState } from 'react';
-import { useQuery } from '@tanstack/react-query';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { api } from '@/lib/api';
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 { api } from '@/lib/api';
+import { ChevronRight, CheckCircle } from 'lucide-react';
import { format } from 'date-fns';
interface Remittance {
id: string;
- totalAmount: number;
+ totalAmount: string;
+ notes: string | null;
status: string;
- notes?: string;
createdAt: string;
collectedBy?: { firstName: string; lastName: string };
confirmedBy?: { firstName: string; lastName: string };
+ payments?: Array<{ id: string; amount: string }>;
}
-const STATUS_COLORS: Record
= {
- PENDING: 'bg-yellow-100 text-yellow-700',
- CONFIRMED: 'bg-emerald-100 text-emerald-700',
- REJECTED: 'bg-red-100 text-red-700',
-};
+const peso = (v: string | number) =>
+ '₱' + Number(v ?? 0).toLocaleString('en-PH', { minimumFractionDigits: 2 });
export default function RemittancesPage() {
- const [page, setPage] = useState(1);
- const limit = 20;
+ const qc = useQueryClient();
- const { data, isLoading } = useQuery({
- queryKey: ['remittances', page],
+ const { data, isLoading } = useQuery<{ data: Remittance[]; total: number }>({
+ queryKey: ['remittances'],
queryFn: async () => {
- const res = await api.get(`/api/v1/remittances?page=${page}&limit=${limit}`);
+ const res = await api.get('/api/v1/remittances?limit=30');
return res.data;
},
+ staleTime: 30_000,
});
- const items: Remittance[] = Array.isArray(data) ? data : (data?.data ?? data?.items ?? []);
- const total = (data as any)?.meta?.total ?? (data as any)?.total ?? items.length;
+ const confirm = useMutation({
+ mutationFn: async (id: string) => {
+ await api.patch(`/api/v1/remittances/${id}/confirm`);
+ },
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['remittances'] }),
+ });
+
+ const remittances = data?.data ?? [];
return (
Remittances
-
Collector cash remittance records
+
Cash collections submitted by collectors
-
+
-
-
-
- Date
- Collector
- Status
- Total Amount
- Confirmed By
- Notes
-
-
-
- {isLoading ? (
- Array.from({ length: 6 }).map((_, i) => (
- {Array.from({ length: 6 }).map((_, j) => (
-
- ))}
- ))
- ) : items.length === 0 ? (
- No remittances yet
- ) : (
- items.map((r) => (
-
-
- {format(new Date(r.createdAt), 'MMM d, yyyy')}
-
-
- {r.collectedBy ? `${r.collectedBy.firstName} ${r.collectedBy.lastName}` : '—'}
-
-
-
- {r.status}
-
-
-
- ₱{Number(r.totalAmount).toLocaleString()}
-
-
- {r.confirmedBy ? `${r.confirmedBy.firstName} ${r.confirmedBy.lastName}` : '—'}
-
-
- {r.notes ?? '—'}
-
-
- ))
- )}
-
-
+
+
+
+
+ | Date |
+ Collector |
+ Amount |
+ Payments |
+ Status |
+ Confirmed By |
+ |
+
+
+
+ {isLoading
+ ? Array.from({ length: 6 }).map((_, i) => (
+
+ {[...Array(7)].map((_, j) => (
+ |
+ ))}
+
+ ))
+ : remittances.map((r) => (
+
+ |
+ {r.createdAt ? format(new Date(r.createdAt), 'MMM d, yyyy') : '—'}
+ |
+
+ {r.collectedBy ? `${r.collectedBy.firstName} ${r.collectedBy.lastName}` : '—'}
+ |
+
+ {peso(r.totalAmount)}
+ |
+
+ {r.payments?.length ?? '—'}
+ |
+
+
+ {r.status}
+
+ |
+
+ {r.confirmedBy ? `${r.confirmedBy.firstName} ${r.confirmedBy.lastName}` : '—'}
+ |
+
+ {r.status !== 'CONFIRMED' && (
+
+ )}
+ |
+
+ ))}
+
+
+
+ {!isLoading && remittances.length === 0 && (
+ No remittances yet
+ )}
diff --git a/app/(app)/reports/page.tsx b/app/(app)/reports/page.tsx
index f416ecd..bc9f68d 100644
--- a/app/(app)/reports/page.tsx
+++ b/app/(app)/reports/page.tsx
@@ -117,7 +117,7 @@ export default function ReportsPage() {
`₱${(v/1000).toFixed(0)}k`} />
- peso(v)} />
+ peso(v)} />
diff --git a/components/providers.tsx b/components/providers.tsx
index a896ad7..7452291 100644
--- a/components/providers.tsx
+++ b/components/providers.tsx
@@ -1,10 +1,19 @@
'use client';
-import { QueryClientProvider } from '@tanstack/react-query';
-import { queryClient } from '@/lib/query-client';
+import { useState } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from '@/components/ui/sonner';
export default function Providers({ children }: { children: React.ReactNode }) {
+ // Must be created inside component — NOT at module level
+ // Module-level singleton causes SSR crashes in Next.js App Router
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: { queries: { staleTime: 30_000, retry: 1 } },
+ })
+ );
+
return (
{children}