diff --git a/src/components/shared/StatusBadge.tsx b/src/components/shared/StatusBadge.tsx index 8f7b893..28f162a 100644 --- a/src/components/shared/StatusBadge.tsx +++ b/src/components/shared/StatusBadge.tsx @@ -2,10 +2,14 @@ import { cn } from '@/lib/utils' const statusStyles: Record = { Completed: 'bg-green-100 text-green-700', + COMPLETED: 'bg-green-100 text-green-700', 'Partially Authorized': 'bg-orange-100 text-orange-700', Rejected: 'bg-red-100 text-red-700', Pending: 'bg-yellow-100 text-yellow-700', + IN_PROGRESS: 'bg-blue-100 text-blue-700', Failed: 'bg-red-100 text-red-700', + FAILED: 'bg-red-100 text-red-700', + Unknown: 'bg-gray-100 text-gray-600', // Voucher statuses Active: 'bg-green-100 text-green-700', Inactive: 'bg-gray-100 text-gray-600', @@ -20,9 +24,17 @@ export default function StatusBadge({ status, label, }: { - status: string + status: string | null | undefined label?: string }) { + if (!status) { + return ( + + - + + ) + } + return ( { - console.log('Fetching batches...') const response = await apiClient.get('/batches') - console.log('Batches response:', response.data) - return response.data + return { data: response.data.data } } export const fetchTransfers = async () => { @@ -13,3 +11,8 @@ export const fetchTransfers = async () => { }) return response.data } + +export const fetchSubBatches = async (batchId: string) => { + const response = await apiClient.get(`/batches/${batchId}/subBatches`) + return response.data +} diff --git a/src/main.tsx b/src/main.tsx index 3cad75a..bace96c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,6 +9,7 @@ import SplashScreen from '@/pages/SplashScreen' import LoginPage from '@/pages/LoginPage' import Dashboard from '@/pages/Dashboard' import PaymentHub from '@/pages/PaymentHub' +import BatchDetail from '@/pages/BatchDetail' import Vouchers from '@/pages/Vouchers' import AccountMapper from '@/pages/AccountMapper' import G2PConfig from '@/pages/G2PConfig' @@ -47,6 +48,7 @@ const router = createBrowserRouter([ children: [ { index: true, element: }, { path: 'payment-hub', element: }, + { path: 'payment-hub/batch/:batchId', element: }, { path: 'vouchers', element: }, { path: 'account-mapper', element: }, { path: 'g2p-config', element: }, diff --git a/src/modules/payment-hub/BatchDetail.tsx b/src/modules/payment-hub/BatchDetail.tsx new file mode 100644 index 0000000..7ec3303 --- /dev/null +++ b/src/modules/payment-hub/BatchDetail.tsx @@ -0,0 +1,176 @@ +import { Link, useNavigate, useParams } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { ArrowLeft } from 'lucide-react' +import { fetchMainBatches, fetchSubBatches } from '@/lib/api/paymentHub' +import { mainBatches as mockBatches } from './mocks/mainBatches.mock' +import { subBatches as mockSubBatches } from './mocks/subBatches.mock' +import StatusBadge from '@/components/shared/StatusBadge' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Table, TableBody, TableCell, TableHead, TableHeader, TableRow, +} from '@/components/ui/table' +import { Button } from '@/components/ui/button' + +const SKELETON_ROWS = 5 + +const formatAmount = (amount: number | null) => { + if (!amount) return '0' + return Math.abs(amount / 100).toLocaleString() +} + +const formatDate = (ts: number | null) => (ts ? new Date(ts).toLocaleString() : '-') + +const formatField = (value: string | null | undefined) => + !value || value === 'null' ? '-' : value + +function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ) +} + +function StatCard({ label, value }: { label: string; value: React.ReactNode }) { + return ( + + +

{label}

+ {value} +
+
+ ) +} + +export default function BatchDetail() { + const { batchId } = useParams() + const navigate = useNavigate() + + const { data: batchData, isError: isBatchesError } = useQuery({ + queryKey: ['mainBatches'], + queryFn: fetchMainBatches, + }) + + const batches = isBatchesError ? mockBatches : (batchData?.data ?? []) + const batch = batches.find((b) => b.batchId === batchId) + + const { data: subBatchData, isLoading: isSubBatchesLoading, isError: isSubBatchesError } = useQuery({ + queryKey: ['subBatches', batchId], + queryFn: () => fetchSubBatches(batchId!), + enabled: !!batchId, + }) + + const subBatches = isSubBatchesError + ? mockSubBatches.filter((sb) => sb.batchId === batchId) + : (subBatchData?.content ?? []) + + return ( +
+ {/* Breadcrumb */} + + + {/* Header */} +
+ +

{batchId}

+ +
+ + {/* Info cards */} +
+ + + + +
+ + {/* Batch info + Sub batches */} +
+ {/* Batch Info */} + + + Batch Info + + + + + + + + + + + + {/* Sub Batches */} + + + Sub Batches + + + {isSubBatchesError && ( +
+ Could not reach the API — showing cached data. +
+ )} + + + + Sub Batch ID + Transactions + Completed + Failed + Amount + Status + + + + {isSubBatchesLoading + ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( + + {Array.from({ length: 6 }).map((__, j) => ( + +
+ + ))} + + )) + : subBatches.length > 0 + ? subBatches.map((sb) => ( + + {sb.subBatchId ?? '-'} + {sb.totalTransactions} + {sb.completed} + {sb.failed} + {formatAmount(sb.totalAmount)} + + + )) + : ( + + + No sub batches found. + + + ) + } + +
+
+
+
+
+ ) +} diff --git a/src/modules/payment-hub/MainBatchesTab.tsx b/src/modules/payment-hub/MainBatchesTab.tsx index 8f2ed78..7616f6b 100644 --- a/src/modules/payment-hub/MainBatchesTab.tsx +++ b/src/modules/payment-hub/MainBatchesTab.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { useNavigate } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { fetchMainBatches } from '@/lib/api/paymentHub' import { mainBatches as mockBatches } from './mocks/mainBatches.mock' @@ -15,10 +16,23 @@ import { ChevronLeft, ChevronRight, Download, FileText, AlertCircle } from 'luci import { exportCsv, csvDate } from '@/lib/exportCsv' import { exportPdf } from '@/lib/exportPdf' -const STATUSES: MainBatch['status'][] = ['Completed', 'Partially Authorized', 'Rejected'] +const STATUSES = [ + { label: 'Completed', value: 'COMPLETED' }, + { label: 'Partially Authorized', value: 'IN_PROGRESS' }, + { label: 'Rejected', value: 'FAILED' }, +] const SKELETON_ROWS = 5 +const formatAmount = (amount: number | null) => { + if (!amount) return '0' + return Math.abs(amount / 100).toLocaleString() +} + +const formatInstitutionId = (id: string | null) => + !id || id === 'null' ? '-' : id + export default function MainBatchesTab() { + const navigate = useNavigate() const [statusFilter, setStatusFilter] = useState('all') const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(10) @@ -28,8 +42,8 @@ export default function MainBatchesTab() { queryFn: fetchMainBatches, }) - // Fall back to mock data when API returns empty or errors - const rows: MainBatch[] = (apiData?.data?.length ? apiData.data : mockBatches) + // Fall back to mock data only when the API call fails + const rows: MainBatch[] = isError ? mockBatches : (apiData?.data ?? []) const totalCount: number = apiData?.totalBatches ?? rows.length const filtered = rows.filter( @@ -38,7 +52,16 @@ export default function MainBatchesTab() { const totalPages = Math.max(1, Math.ceil(filtered.length / perPage)) const paginated = filtered.slice((page - 1) * perPage, page * perPage) - const exportRows = filtered as unknown as Record[] + const exportRows: Record[] = filtered.map((b) => ({ + 'Batch Reference': b.batchId, + 'Start Time': b.startedAt ? new Date(b.startedAt).toLocaleString() : '-', + 'Completed Time': b.completedAt ? new Date(b.completedAt).toLocaleString() : '-', + 'Institution ID': formatInstitutionId(b.registeringInstitutionId), + Instructions: b.totalTransactions, + Amount: formatAmount(b.totalAmount), + 'Payer FSP': b.payerFsp ?? '-', + Status: b.status ?? 'Unknown', + })) return (
@@ -57,15 +80,15 @@ export default function MainBatchesTab() { {STATUSES.map((s) => ( ))}
@@ -80,8 +103,8 @@ export default function MainBatchesTab() { variant="outline" size="sm" className="gap-1.5 text-xs" onClick={() => exportPdf( 'Main Batches', - ['Batch Reference', 'Start Time', 'Completed Time', 'Institution ID', 'Source Ministry', 'Instructions', 'Amount', 'Payer FSP', 'Status'], - filtered.map((b) => [b.batchReferenceNumber, b.startTime, b.completedTime, b.registeringInstitutionId, b.sourceMinistry, b.numberOfInstructions, b.amount.toLocaleString(), b.payerFSP, b.status]), + ['Batch Reference', 'Start Time', 'Completed Time', 'Institution ID', 'Instructions', 'Amount', 'Payer FSP', 'Status'], + filtered.map((b) => [b.batchId, b.startedAt ? new Date(b.startedAt).toLocaleString() : '-', b.completedAt ? new Date(b.completedAt).toLocaleString() : '-', formatInstitutionId(b.registeringInstitutionId), b.totalTransactions, formatAmount(b.totalAmount), b.payerFsp ?? '-', b.status ?? 'Unknown']), `main-batches-${csvDate()}.pdf`, )} > @@ -107,7 +130,6 @@ export default function MainBatchesTab() { Start Time Completed Time Institution ID - Source Ministry Instructions Amount Payer FSP @@ -118,7 +140,7 @@ export default function MainBatchesTab() { {isLoading ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( - {Array.from({ length: 9 }).map((__, j) => ( + {Array.from({ length: 8 }).map((__, j) => (
@@ -127,21 +149,24 @@ export default function MainBatchesTab() { )) : paginated.length > 0 ? paginated.map((batch) => ( - - {batch.batchReferenceNumber} - {batch.startTime} - {batch.completedTime} - {batch.registeringInstitutionId} - {batch.sourceMinistry} - {batch.numberOfInstructions} - {batch.amount.toLocaleString()} - {batch.payerFSP} + navigate(`/payment-hub/batch/${batch.batchId}`)} + > + {batch.batchId} + {batch.startedAt ? new Date(batch.startedAt).toLocaleString() : '-'} + {batch.completedAt ? new Date(batch.completedAt).toLocaleString() : '-'} + {formatInstitutionId(batch.registeringInstitutionId)} + {batch.totalTransactions} + {formatAmount(batch.totalAmount)} + {batch.payerFsp ?? '-'} )) : ( - + No records found. diff --git a/src/modules/payment-hub/SubBatchesTab.tsx b/src/modules/payment-hub/SubBatchesTab.tsx index 8843594..80f63be 100644 --- a/src/modules/payment-hub/SubBatchesTab.tsx +++ b/src/modules/payment-hub/SubBatchesTab.tsx @@ -1,6 +1,9 @@ -import { useState } from 'react' -import { subBatches } from './mocks/subBatches.mock' -import type { MainBatch } from './types' +import { useEffect, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { fetchMainBatches, fetchSubBatches } from '@/lib/api/paymentHub' +import { mainBatches as mockBatches } from './mocks/mainBatches.mock' +import { subBatches as mockSubBatches } from './mocks/subBatches.mock' +import type { MainBatch, SubBatch } from './types' import StatusBadge from '@/components/shared/StatusBadge' import { Table, @@ -18,69 +21,84 @@ import { SelectValue, } from '@/components/ui/select' import { Button } from '@/components/ui/button' -import { ChevronLeft, ChevronRight, Download, FileText } from 'lucide-react' +import { ChevronLeft, ChevronRight, Download, FileText, AlertCircle } from 'lucide-react' import { exportCsv, csvDate } from '@/lib/exportCsv' import { exportPdf } from '@/lib/exportPdf' -const statuses: MainBatch['status'][] = [ - 'Completed', - 'Partially Authorized', - 'Rejected', -] +const SKELETON_ROWS = 5 + +const formatAmount = (amount: number | null) => { + if (!amount) return '0' + return Math.abs(amount / 100).toLocaleString() +} export default function SubBatchesTab() { - const [statusFilter, setStatusFilter] = useState('all') const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(10) - const filtered = subBatches.filter( - (b) => statusFilter === 'all' || b.status === statusFilter - ) + const { data: mainBatchesData, isError: isBatchesError } = useQuery({ + queryKey: ['mainBatches'], + queryFn: fetchMainBatches, + }) + + const batchOptions: MainBatch[] = isBatchesError ? mockBatches : (mainBatchesData?.data ?? []) + const [batchId, setBatchId] = useState('') + + useEffect(() => { + if (!batchId && batchOptions.length > 0) { + setBatchId(batchOptions[0].batchId) + } + }, [batchId, batchOptions]) + + const { data: apiData, isLoading, isError } = useQuery({ + queryKey: ['subBatches', batchId], + queryFn: () => fetchSubBatches(batchId), + enabled: !!batchId, + }) + + const rows: SubBatch[] = isError + ? mockSubBatches.filter((sb) => sb.batchId === batchId) + : (apiData?.content ?? []) + const totalCount: number = apiData?.totalElements ?? rows.length - const totalPages = Math.max(1, Math.ceil(filtered.length / perPage)) - const paginated = filtered.slice((page - 1) * perPage, page * perPage) + const totalPages = Math.max(1, Math.ceil(rows.length / perPage)) + const paginated = rows.slice((page - 1) * perPage, page * perPage) + + const exportRows: Record[] = rows.map((b) => ({ + 'Sub Batch ID': b.subBatchId ?? '-', + 'Start Time': b.startedAt ? new Date(b.startedAt).toLocaleString() : '-', + 'Completed Time': b.completedAt ? new Date(b.completedAt).toLocaleString() : '-', + 'Total Transactions': b.totalTransactions, + Completed: b.completed, + Failed: b.failed, + Amount: formatAmount(b.totalAmount), + Status: b.status ?? 'Unknown', + })) return (
- {/* Filters + Export */} + {/* Batch selector + Export */}
-
- - {statuses.map((s) => ( -
+ {/* Error banner */} + {isError && ( +
+ + Could not reach the API — showing cached data. +
+ )} + {/* Table */}
- Batch Reference + Sub Batch ID Start Time Completed Time - Institution ID - Source Ministry - Instructions + Total Transactions + Completed + Failed Amount - Payer FSP Status - {paginated.map((batch) => ( - - - {batch.batchReferenceNumber} - - {batch.startTime} - {batch.completedTime} - {batch.registeringInstitutionId} - {batch.sourceMinistry} - - {batch.numberOfInstructions} - - - {batch.amount.toLocaleString()} - - {batch.payerFSP} - - - - - ))} - {paginated.length === 0 && ( - - - No records found. - - - )} + {isLoading + ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( + + {Array.from({ length: 8 }).map((__, j) => ( + +
+ + ))} + + )) + : paginated.length > 0 + ? paginated.map((batch) => ( + + {batch.subBatchId ?? '-'} + {batch.startedAt ? new Date(batch.startedAt).toLocaleString() : '-'} + {batch.completedAt ? new Date(batch.completedAt).toLocaleString() : '-'} + {batch.totalTransactions} + {batch.completed} + {batch.failed} + {formatAmount(batch.totalAmount)} + + + )) + : ( + + + No records found. + + + ) + }
@@ -173,8 +201,10 @@ export default function SubBatchesTab() {
- {(page - 1) * perPage + 1}– - {Math.min(page * perPage, filtered.length)} of {filtered.length} + {rows.length === 0 + ? '0–0 of 0' + : `${(page - 1) * perPage + 1}–${Math.min(page * perPage, rows.length)} of ${totalCount}` + }