Updated project changes

This commit is contained in:
2026-06-12 19:57:23 +05:30
parent 1c8d5538c4
commit aa87870350
4 changed files with 201 additions and 3 deletions
+1 -1
View File
@@ -209,7 +209,7 @@ const Layout = () => {
]; ];
default: // Customer or Fallback default: // Customer or Fallback
return [ return [
{ to: "/customer/profile", icon: LayoutDashboard, label: "Dashboard" }, { to: "/portal/profile", icon: LayoutDashboard, label: "Dashboard" },
{ to: "/portal/payments", icon: CreditCard, label: "Payments" }, { to: "/portal/payments", icon: CreditCard, label: "Payments" },
...commonItems, ...commonItems,
]; ];
@@ -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 (
<div className="p-10 text-center border border-dashed border-zinc-300 dark:border-zinc-700 rounded-xl text-zinc-500">
You have no outstanding balance everything is paid up. 🎉
</div>
);
}
return (
<div className="space-y-6">
{/* Total due summary */}
<SpotlightCard className="p-6 md:p-8">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<p className="text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
Total Outstanding
</p>
<p className="text-4xl md:text-5xl font-black text-zinc-900 dark:text-white mt-1">
{formatAmount(totalCents, currency, stripeConfig.locale)}
</p>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-2">
Across {due.length} invoice{due.length === 1 ? '' : 's'}
{overdueCount > 0 && (
<span className="text-red-500 dark:text-red-400 font-semibold">
{' '}· {overdueCount} overdue
</span>
)}
</p>
</div>
<div className="w-16 h-16 rounded-full bg-amber-50 dark:bg-amber-900/20 flex items-center justify-center text-amber-500 border border-amber-100 dark:border-amber-500/20 shrink-0">
<CalendarClock size={30} />
</div>
</div>
</SpotlightCard>
{/* Outstanding invoice list */}
<div className="grid gap-4">
{due.map((inv) => {
const busy = redirectingId === inv.id;
const overdue = isOverdue(inv);
return (
<SpotlightCard key={inv.id} className="p-5 md:p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-start gap-4 min-w-0">
<div className="w-11 h-11 rounded-xl bg-blue-50 dark:bg-blue-900/20 flex items-center justify-center text-blue-600 dark:text-blue-400 border border-blue-100 dark:border-blue-500/20 shrink-0">
<FileText size={20} />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h4 className="font-bold truncate">{inv.title}</h4>
<span className="text-[10px] font-mono text-zinc-400 shrink-0">
{inv.id}
</span>
</div>
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1 line-clamp-2">
{inv.description}
</p>
<p
className={`text-xs mt-1 flex items-center gap-1 ${
overdue
? 'text-red-500 dark:text-red-400 font-semibold'
: 'text-zinc-400'
}`}
>
{overdue && <AlertCircle size={12} />}
{overdue ? 'Overdue · ' : 'Due '}
{new Date(inv.dueDate).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex items-center justify-between md:flex-col md:items-end gap-3 md:gap-2 shrink-0">
<span className="text-xl font-black text-zinc-900 dark:text-white">
{formatAmount(inv.amount, inv.currency, stripeConfig.locale)}
</span>
<button
onClick={() => handlePay(inv)}
disabled={busy}
className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-bold shadow-lg shadow-blue-500/20 transition-all disabled:opacity-60 disabled:cursor-not-allowed"
>
{busy ? (
<><Loader2 className="animate-spin" size={15} /> Redirecting</>
) : (
<><Lock size={14} /> Pay Now</>
)}
</button>
</div>
</div>
</SpotlightCard>
);
})}
</div>
</div>
);
};
export default OutstandingPanel;
+36
View File
@@ -35,6 +35,42 @@ export const INVOICES = [
amount: 14900, amount: 14900,
currency: 'usd', 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. */ /** Format an integer cent amount into a localized currency string. */
+12 -2
View File
@@ -4,6 +4,7 @@ import { useAuth } from '../../../context/AuthContext';
import { usePayments } from '../context/PaymentProvider'; import { usePayments } from '../context/PaymentProvider';
import { stripeConfig } from '../config/stripeConfig'; import { stripeConfig } from '../config/stripeConfig';
import InvoiceList from '../components/InvoiceList'; import InvoiceList from '../components/InvoiceList';
import OutstandingPanel from '../components/OutstandingPanel';
import TransactionHistory from '../components/TransactionHistory'; import TransactionHistory from '../components/TransactionHistory';
import StripeConfigNotice from '../components/StripeConfigNotice'; import StripeConfigNotice from '../components/StripeConfigNotice';
@@ -58,18 +59,27 @@ const PaymentsPage = () => {
{/* Tabs */} {/* Tabs */}
<div className="flex space-x-1 border-b border-zinc-200 dark:border-zinc-800 mb-6 overflow-x-auto scrollbar-hide"> <div className="flex space-x-1 border-b border-zinc-200 dark:border-zinc-800 mb-6 overflow-x-auto scrollbar-hide">
<TabButton id="pay" label="Make a Payment" activeTab={activeTab} onSelect={setActiveTab} /> <TabButton id="pay" label="Make a Payment" activeTab={activeTab} onSelect={setActiveTab} />
<TabButton id="outstanding" label="Outstanding" activeTab={activeTab} onSelect={setActiveTab} />
<TabButton id="history" label="Transaction History" activeTab={activeTab} onSelect={setActiveTab} /> <TabButton id="history" label="Transaction History" activeTab={activeTab} onSelect={setActiveTab} />
</div> </div>
{/* Content */} {/* Content */}
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500"> <div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
{activeTab === 'pay' ? ( {activeTab === 'pay' && (
<InvoiceList <InvoiceList
invoices={invoices} invoices={invoices}
paidIds={paidIds} paidIds={paidIds}
customerEmail={user?.email || user?.username} customerEmail={user?.email || user?.username}
/> />
) : ( )}
{activeTab === 'outstanding' && (
<OutstandingPanel
invoices={invoices}
paidIds={paidIds}
customerEmail={user?.email || user?.username}
/>
)}
{activeTab === 'history' && (
<TransactionHistory transactions={transactions} /> <TransactionHistory transactions={transactions} />
)} )}
</div> </div>