diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx
index bd90f48..eb5190c 100644
--- a/src/components/Layout.jsx
+++ b/src/components/Layout.jsx
@@ -209,7 +209,7 @@ const Layout = () => {
];
default: // Customer or Fallback
return [
- { to: "/customer/profile", icon: LayoutDashboard, label: "Dashboard" },
+ { to: "/portal/profile", icon: LayoutDashboard, label: "Dashboard" },
{ to: "/portal/payments", icon: CreditCard, label: "Payments" },
...commonItems,
];
diff --git a/src/modules/payments/components/OutstandingPanel.jsx b/src/modules/payments/components/OutstandingPanel.jsx
new file mode 100644
index 0000000..6f18774
--- /dev/null
+++ b/src/modules/payments/components/OutstandingPanel.jsx
@@ -0,0 +1,152 @@
+import React, { useMemo } from 'react';
+import { AlertCircle, CalendarClock, FileText, Lock, Loader2 } from 'lucide-react';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'sonner';
+import { SpotlightCard } from '../../../components/SpotlightCard';
+import { formatAmount } from '../data/invoices';
+import { stripeConfig } from '../config/stripeConfig';
+import { startCheckout } from '../services/checkoutService';
+
+/**
+ * Outstanding tab — summarizes everything the customer still owes: a headline
+ * total-due figure plus the list of unpaid invoices, with overdue ones flagged.
+ * Reads the same invoice/paid data as the "Make a Payment" tab so the two views
+ * never disagree.
+ */
+const OutstandingPanel = ({ invoices, paidIds = [], customerEmail }) => {
+ const navigate = useNavigate();
+ const [redirectingId, setRedirectingId] = useState(null);
+
+ const due = useMemo(
+ () =>
+ invoices
+ .filter((i) => !(i.status === 'paid' || paidIds.includes(i.id)))
+ .sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)),
+ [invoices, paidIds]
+ );
+
+ // Total owed, assuming all unpaid invoices share a currency (they do here).
+ const totalCents = due.reduce((sum, i) => sum + (i.amount || 0), 0);
+ const currency = due[0]?.currency || 'usd';
+
+ // Midnight today, so an invoice due today is not counted as overdue.
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const isOverdue = (inv) => new Date(inv.dueDate) < today;
+ const overdueCount = due.filter(isOverdue).length;
+
+ const handlePay = async (invoice) => {
+ if (redirectingId) return;
+ setRedirectingId(invoice.id);
+
+ if (!stripeConfig.isConfigured) {
+ navigate(`/payments/demo-checkout?invoice=${encodeURIComponent(invoice.id)}`);
+ return;
+ }
+
+ try {
+ await startCheckout({ invoiceId: invoice.id, customerEmail });
+ } catch (err) {
+ toast.error('Unable to start checkout', { description: err.message });
+ setRedirectingId(null);
+ }
+ };
+
+ if (due.length === 0) {
+ return (
+
+ You have no outstanding balance — everything is paid up. 🎉
+
+ );
+ }
+
+ return (
+
+ {/* Total due summary */}
+
+
+
+
+ Total Outstanding
+
+
+ {formatAmount(totalCents, currency, stripeConfig.locale)}
+
+
+ Across {due.length} invoice{due.length === 1 ? '' : 's'}
+ {overdueCount > 0 && (
+
+ {' '}· {overdueCount} overdue
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Outstanding invoice list */}
+
+ {due.map((inv) => {
+ const busy = redirectingId === inv.id;
+ const overdue = isOverdue(inv);
+ return (
+
+
+
+
+
+
+
+
+
{inv.title}
+
+ {inv.id}
+
+
+
+ {inv.description}
+
+
+ {overdue && }
+ {overdue ? 'Overdue · ' : 'Due '}
+ {new Date(inv.dueDate).toLocaleDateString()}
+
+
+
+
+
+
+ {formatAmount(inv.amount, inv.currency, stripeConfig.locale)}
+
+
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default OutstandingPanel;
diff --git a/src/modules/payments/data/invoices.js b/src/modules/payments/data/invoices.js
index dcb42c9..30636c8 100644
--- a/src/modules/payments/data/invoices.js
+++ b/src/modules/payments/data/invoices.js
@@ -35,6 +35,42 @@ export const INVOICES = [
amount: 14900,
currency: 'usd',
},
+ {
+ id: 'INV-2055',
+ title: 'Siding Replacement — West Wall',
+ description: 'Storm-damaged vinyl siding replacement, 3 panels.',
+ property: '2612 Dunwick Dr, Plano, TX 75023',
+ dueDate: '2026-07-10',
+ amount: 95000,
+ currency: 'usd',
+ },
+ {
+ id: 'INV-2061',
+ title: 'Chimney Flashing Repair & Sealant',
+ description: 'Re-flashed chimney base and applied weatherproof sealant.',
+ property: '2612 Dunwick Dr, Plano, TX 75023',
+ dueDate: '2026-05-30',
+ amount: 42500,
+ currency: 'usd',
+ },
+ {
+ id: 'INV-2063',
+ title: 'Skylight Installation — Living Room',
+ description: 'Supply and install one energy-efficient skylight unit.',
+ property: '2612 Dunwick Dr, Plano, TX 75023',
+ dueDate: '2026-06-28',
+ amount: 128000,
+ currency: 'usd',
+ },
+ {
+ id: 'INV-2067',
+ title: 'Annual Roof Maintenance Plan',
+ description: 'Twice-yearly inspection and minor repair coverage.',
+ property: '2612 Dunwick Dr, Plano, TX 75023',
+ dueDate: '2026-08-01',
+ amount: 56000,
+ currency: 'usd',
+ },
];
/** Format an integer cent amount into a localized currency string. */
diff --git a/src/modules/payments/pages/PaymentsPage.jsx b/src/modules/payments/pages/PaymentsPage.jsx
index 412d1d4..785c23a 100644
--- a/src/modules/payments/pages/PaymentsPage.jsx
+++ b/src/modules/payments/pages/PaymentsPage.jsx
@@ -4,6 +4,7 @@ import { useAuth } from '../../../context/AuthContext';
import { usePayments } from '../context/PaymentProvider';
import { stripeConfig } from '../config/stripeConfig';
import InvoiceList from '../components/InvoiceList';
+import OutstandingPanel from '../components/OutstandingPanel';
import TransactionHistory from '../components/TransactionHistory';
import StripeConfigNotice from '../components/StripeConfigNotice';
@@ -58,18 +59,27 @@ const PaymentsPage = () => {
{/* Tabs */}
+
{/* Content */}
- {activeTab === 'pay' ? (
+ {activeTab === 'pay' && (
- ) : (
+ )}
+ {activeTab === 'outstanding' && (
+
+ )}
+ {activeTab === 'history' && (
)}