Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/docs/SwaggerUIWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function SwaggerUIWrapper({ specUrl }: SwaggerUIWrapperProps) {
RemitWise API Documentation
</h2>
<p className="text-gray-300 text-base sm:text-lg leading-relaxed mb-6">
Complete reference for integrating with RemitWise's remittance and financial planning services.
Complete reference for integrating with RemitWise&apos;s remittance and financial planning services.
Build secure, scalable applications with our comprehensive API.
</p>
<div className="flex flex-wrap gap-4">
Expand Down
86 changes: 77 additions & 9 deletions app/bills/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,57 @@
}
}, [toast]);

const [bills, setBills] = useState<Bill[]>([]);
const [stats, setStats] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

const fetchBillsData = async () => {
setIsLoading(true);
setError(null);
try {
const [billsRes, statsRes] = await Promise.all([
apiClient.get('/api/bills'),
apiClient.get('/api/bills/total-unpaid')
]);

if (!billsRes || !statsRes) throw new Error("Session expired");
if (!billsRes.ok || !statsRes.ok) throw new Error("Failed to load bills data");

const billsJson = await billsRes.json();
const statsJson = await statsRes.json();

const fetchedBills: Bill[] = billsJson.data?.bills || [];
const fetchedStats = statsJson.data;

setBills(fetchedBills);

const paidBills = fetchedBills.filter((b: Bill) => b.status === 'paid');
const paidAmount = paidBills.reduce((acc: number, b: Bill) => acc + b.amount, 0);
const overdueCount = fetchedBills.filter((b: Bill) => b.status === 'overdue' || b.status === 'urgent').length;

setStats({
totalUnpaid: {
amount: fetchedStats?.totalUnpaid?.toLocaleString() || '0',
pendingCount: fetchedStats?.count || 0
},
overdueCount,
paidThisMonth: {
amount: paidAmount.toLocaleString(),
paymentCount: paidBills.length
}
});
} catch (err) {
setError(err instanceof Error ? err : new Error("Unknown error"));
} finally {
setIsLoading(false);
}
};

useEffect(() => {
fetchBillsData();
}, []);

function handleAddBill() {
formSectionRef.current?.scrollIntoView({
behavior: "smooth",
Expand All @@ -147,17 +198,34 @@
/>

<main className='mx-auto max-w-7xl px-4 py-6 sm:px-6 sm:py-8 lg:px-8'>
<section className='mb-8'>
<BillPaymentsStatsCards />
</section>
{error ? (
<div className="mb-8">
<WidgetErrorState

Check failure on line 203 in app/bills/page.tsx

View workflow job for this annotation

GitHub Actions / Lint and test

'WidgetErrorState' is not defined
title="Failed to load bills"
message={error.message}
onRetry={fetchBillsData}
/>
</div>
) : isLoading ? (
<div className="mb-8 space-y-8">
<SkeletonList rows={3} variant="cards" />

Check failure on line 211 in app/bills/page.tsx

View workflow job for this annotation

GitHub Actions / Lint and test

'SkeletonList' is not defined
<SkeletonList rows={3} variant="table" />

Check failure on line 212 in app/bills/page.tsx

View workflow job for this annotation

GitHub Actions / Lint and test

'SkeletonList' is not defined
</div>
) : (
<>
<section className='mb-8'>
<BillPaymentsStatsCards stats={stats} />
</section>

<div className='mb-8'>
<UnpaidBillsSection />
</div>
<div className='mb-8'>
<UnpaidBillsSection bills={bills} />
</div>

<div className='mb-8'>
<RecentPaymentsSection />
</div>
<div className='mb-8'>
<RecentPaymentsSection bills={bills} />
</div>
</>
)}

<div className='grid gap-8 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start'>
<div
Expand Down
6 changes: 3 additions & 3 deletions components/Bills/BillsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const STATUS_STYLES: Record<
// ─── Status badge ─────────────────────────────────────────────────────────────

function StatusBadge({ status }: { status: Bill["status"] }) {
const s = STATUS_STYLES[status];
const s = getStatusStyles(status)!;
const label: Record<Bill["status"], string> = {
overdue: "Overdue",
urgent: "Due Soon",
Expand All @@ -79,7 +79,7 @@ function StatusBadge({ status }: { status: Bill["status"] }) {

return (
<span
className={`inline-flex items-center gap-1 rounded-[10px] border px-2 py-0.5 text-xs font-semibold ${s.badgeBg} ${s.badgeBorder} ${s.badgeText}`}
className={`inline-flex items-center gap-1 rounded-[10px] border px-2 py-0.5 text-xs font-semibold ${s.dueBg} ${s.border} text-white`}
aria-label={`Status: ${label[status]}`}
>
<Icon className="h-3 w-3" aria-hidden="true" />
Expand All @@ -88,7 +88,7 @@ function StatusBadge({ status }: { status: Bill["status"] }) {
);
}

// ─── Recurring chain badge ────────────────────────────────────────────────────
// ─── Exported Component ─────────────────────────────────────────────────────────

function RecurringBadge({
recurrenceLabel,
Expand Down
34 changes: 20 additions & 14 deletions components/Bills/RecentPaymentsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { BillCards } from "@/components/Bills/BillsCard";
import { mockPaidBills } from "@/lib/mockdata/bills";
import { useDensity } from "@/lib/context/DensityContext";
import { Bill } from '@/lib/contracts/bill-payments';
import { WidgetEmptyState } from '@/components/ui/WidgetStates';

export default function RecentPaymentsSection() {
export default function RecentPaymentsSection({ bills }: { bills: Bill[] }) {
const { density } = useDensity();
const paidBills = bills.filter((bill) => bill.status === 'paid');

return (
<section
Expand All @@ -19,18 +21,22 @@ export default function RecentPaymentsSection() {
</p>
</div>

<div
role="list"
className={
density === "compact"
? "flex flex-col gap-2"
: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
}
>
{mockPaidBills.map((bill) => (
<BillCards key={bill.id} bill={bill} density={density} />
))}
</div>
{paidBills.length > 0 ? (
<div
role="list"
className={
density === "compact"
? "flex flex-col gap-2"
: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
}
>
{paidBills.map((bill) => (
<BillCards key={bill.id} bill={bill} density={density} />
))}
</div>
) : (
<WidgetEmptyState title="No recent payments" message="Paid bills will appear here once processed." />
)}
</section>
);
}
5 changes: 3 additions & 2 deletions components/Bills/UnpaidBillsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from 'react';
import { BillCards } from './BillsCard';
import { mockBills } from '@/lib/mockdata/bills';
import { useDensity } from '@/lib/context/DensityContext';
import { Bill } from '@/lib/contracts/bill-payments';
import { WidgetEmptyState } from '@/components/ui/WidgetStates';

export function UnpaidBillsSection() {
const { density } = useDensity();
const unpaidStatuses: Bill['status'][] = ['overdue', 'urgent', 'upcoming'];

const unpaidBills = mockBills.filter((bill) =>
const unpaidBills = bills.filter((bill) =>
unpaidStatuses.includes(bill.status)
);
const recurringUnpaidCount = unpaidBills.filter((bill) => bill.isRecurring).length;
Expand Down
27 changes: 27 additions & 0 deletions components/ui/WidgetStates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { AlertCircle, AlertTriangle, RefreshCcw } from 'lucide-react';

export function WidgetEmptyState({ title, message }: { title: string; message?: string }) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center bg-[#010101] rounded-2xl border border-white/5">
<AlertCircle className="w-8 h-8 text-gray-500 mb-3" />
<h3 className="text-white font-medium">{title}</h3>
{message && <p className="text-gray-400 text-sm mt-1">{message}</p>}
</div>
);
}

export function WidgetErrorState({ title, message, onRetry }: { title: string; message?: string; onRetry?: () => void }) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center bg-[#010101] rounded-2xl border border-white/5">
<AlertTriangle className="w-8 h-8 text-red-500 mb-3" />
<h3 className="text-white font-medium">{title}</h3>
{message && <p className="text-gray-400 text-sm mt-1">{message}</p>}
{onRetry && (
<button onClick={onRetry} className="mt-4 flex items-center gap-2 text-sm text-red-400 hover:text-red-300 transition-colors">
<RefreshCcw className="w-4 h-4" /> Try again
</button>
)}
</div>
);
}
Loading