diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx index 73e3a23..1a61d7d 100644 --- a/src/pages/owner/OwnerProjectDetail.jsx +++ b/src/pages/owner/OwnerProjectDetail.jsx @@ -12,7 +12,7 @@ import { ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert, FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone, Shield, ChevronRight, Phone, MessageSquare, Mail, AlertOctagon, TrendingUp, Key, Zap, Camera, Plus, Map, User, Crosshair, Image as ImageIcon, MapPin, - Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote + Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon } from 'lucide-react'; import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer'; import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal'; @@ -67,6 +67,7 @@ const tabs = [ { id: 'rfis', label: 'RFIs', icon: FileText }, { id: 'milestones', label: 'Milestones', icon: Milestone }, { id: 'invoices', label: 'Invoices', icon: DollarSign }, + { id: 'payments', label: 'Payments', icon: Banknote }, { id: 'risks', label: 'Risk & Issues', icon: AlertCircle }, { id: 'activity', label: 'Activity', icon: Clock }, { id: 'docs', label: 'Docs', icon: FolderOpen }, @@ -105,6 +106,21 @@ function seedDocsMock(projectId) { return all.slice(0, 4 + seed); } +function seedPaymentsMock(projectId) { + const all = [ + { id: 'pay_001', from: 'State Farm Insurance', method: 'ACH', amount: 18500, date: '2026-01-28', refNumber: 'ACH-2026-4411', memo: 'Initial claim payout — roof replacement' }, + { id: 'pay_002', from: 'Justin Johnson', method: 'Check', amount: 2500, date: '2026-02-05', refNumber: 'CHK-8832', memo: 'Homeowner deductible payment' }, + { id: 'pay_003', from: 'State Farm Insurance', method: 'ACH', amount: 6200, date: '2026-02-18', refNumber: 'ACH-2026-4499', memo: 'Supplement approval — additional scope' }, + { id: 'pay_004', from: 'Justin Johnson', method: 'Credit Card', amount: 1800.50, date: '2026-03-01', refNumber: 'CC-9901', memo: 'Upgrade — premium underlayment' }, + { id: 'pay_005', from: 'State Farm Insurance', method: 'Check', amount: 4100, date: '2026-03-12', refNumber: 'CHK-7761', memo: 'Final supplement payout' }, + { id: 'pay_006', from: 'Justin Johnson', method: 'Cash', amount: 500, date: '2026-03-20', refNumber: '', memo: 'Gutter guard add-on' }, + ]; + const seed = projectId.charCodeAt(projectId.length - 1) % 3; + return all.slice(0, 3 + seed); +} + +const PAYMENTS_PER_PAGE = 5; + const OwnerProjectDetail = () => { const { projectId } = useParams(); const { user } = useAuth(); @@ -115,6 +131,11 @@ const OwnerProjectDetail = () => { const [selectedInvoice, setSelectedInvoice] = useState(null); const [createModalConfig, setCreateModalConfig] = useState(null); + // ── Payments state ───────────────────────────────────────────────────────── + const [payments, setPayments] = useState(() => seedPaymentsMock(projectId)); + const [paymentSort, setPaymentSort] = useState({ field: 'date', dir: 'desc' }); + const [paymentPage, setPaymentPage] = useState(0); + // ── Docs state ──────────────────────────────────────────────────────────── const [docs, setDocs] = useState(() => seedDocsMock(projectId)); const [viewDoc, setViewDoc] = useState(null); // doc object being viewed @@ -160,6 +181,41 @@ const OwnerProjectDetail = () => { alert("Action successful! (Mock interface)"); }; + // ── Payment handlers ──────────────────────────────────────────────────── + const handleAddPayment = (data) => { + const newPayment = { + id: `pay_${Date.now()}`, + from: data.from || '', + method: data.method || 'Cash', + amount: parseFloat(data.amount) || 0, + date: data.date || new Date().toISOString().split('T')[0], + refNumber: data.refNumber || '', + memo: data.memo || '', + }; + setPayments(prev => [newPayment, ...prev]); + setPaymentPage(0); + }; + + const togglePaymentSort = (field) => { + setPaymentSort(prev => prev.field === field ? { field, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field, dir: 'desc' }); + setPaymentPage(0); + }; + + const sortedPayments = useMemo(() => { + return [...payments].sort((a, b) => { + if (paymentSort.field === 'amount') return paymentSort.dir === 'asc' ? a.amount - b.amount : b.amount - a.amount; + if (paymentSort.field === 'date') return paymentSort.dir === 'asc' ? a.date.localeCompare(b.date) : b.date.localeCompare(a.date); + return 0; + }); + }, [payments, paymentSort]); + + // Pagination logic for payments + const paginatedPayments = sortedPayments.slice(paymentPage * PAYMENTS_PER_PAGE, (paymentPage + 1) * PAYMENTS_PER_PAGE); + // Calculate total number of pages for pagination + const totalPaymentPages = Math.ceil(sortedPayments.length / PAYMENTS_PER_PAGE); + // Calculate total received payments + const totalReceived = payments.reduce((sum, p) => sum + p.amount, 0); + const project = useMemo(() => projects.find(p => p.id === projectId && p.ownerId === user?.id) , [projects, projectId, user]); @@ -846,6 +902,180 @@ const OwnerProjectDetail = () => { )} + {/* PAYMENTS TAB */} + {activeTab === 'payments' && ( +
+ {/* Summary Row */} +
+ +
{formatCurrency(totalReceived)}
+
Total Received
+
+ +
{payments.length}
+
Payments
+
+ +
+ {payments.length > 0 ? [...payments].sort((a, b) => b.date.localeCompare(a.date))[0].date : '—'} +
+
Latest Payment
+
+
+ + {/* Payments Table */} + +
+
+ +

Payments Received

+ + {payments.length} + +
+ +
+ + {payments.length > 0 ? ( + <> + {/* Desktop Table */} +
+ + + + + + + + + + + + {paginatedPayments.map((pay) => ( + + + + + + + + ))} + +
From / Method togglePaymentSort('amount')} + > + Amount {paymentSort.field === 'amount' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''} + togglePaymentSort('date')} + > + Date {paymentSort.field === 'date' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''} + Check No. / RefMemo
+
{pay.from}
+ + {pay.method} + +
+ {formatCurrency(pay.amount)} + {pay.date}{pay.refNumber || '—'}{pay.memo || '—'}
+
+ + {/* Mobile Card View */} +
+ {paginatedPayments.map((pay) => ( +
+
+
+

{pay.from}

+ + {pay.method} + +
+ {formatCurrency(pay.amount)} +
+
+
+ Date +

{pay.date}

+
+
+ Ref # +

{pay.refNumber || '—'}

+
+
+ {pay.memo && ( +

{pay.memo}

+ )} +
+ ))} +
+ + {/* Pagination */} + {totalPaymentPages > 1 && ( +
+

+ {paymentPage * PAYMENTS_PER_PAGE + 1}–{Math.min((paymentPage + 1) * PAYMENTS_PER_PAGE, sortedPayments.length)} of {sortedPayments.length} +

+
+ + +
+
+ )} + + ) : ( +
+ +

No payments recorded yet.

+

Record your first payment to start tracking.

+
+ )} +
+
+ )} + {/* RISK & ISSUES TAB */} {activeTab === 'risks' && (