addnew payments table

This commit is contained in:
Satyam-Rastogi
2026-05-11 18:02:29 +05:30
parent ebe7a585c0
commit 28f4ae57be
+231 -1
View File
@@ -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 = () => {
</NeoCard>
)}
{/* PAYMENTS TAB */}
{activeTab === 'payments' && (
<div className="space-y-6 max-w-7xl mx-auto">
{/* Summary Row */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<SpotlightCard className="p-4">
<div className={`text-xl font-extrabold ${NEON_GREEN}`}>{formatCurrency(totalReceived)}</div>
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Total Received</div>
</SpotlightCard>
<SpotlightCard className="p-4">
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{payments.length}</div>
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Payments</div>
</SpotlightCard>
<SpotlightCard className="p-4">
<div className="text-xl font-extrabold text-zinc-900 dark:text-white font-mono">
{payments.length > 0 ? [...payments].sort((a, b) => b.date.localeCompare(a.date))[0].date : '—'}
</div>
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Latest Payment</div>
</SpotlightCard>
</div>
{/* Payments Table */}
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" innerClassName="p-0 lg:p-0">
<div className="p-5 border-b border-zinc-200 dark:border-white/5 flex items-center justify-between">
<div className="flex items-center gap-3">
<Banknote className={NEON_GREEN} size={20} />
<h3 className="text-lg font-bold text-zinc-900 dark:text-white tracking-widest uppercase">Payments Received</h3>
<span className="text-xs font-bold bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/5 px-2 py-0.5 rounded-full text-zinc-500 dark:text-zinc-400">
{payments.length}
</span>
</div>
<button
onClick={() => setCreateModalConfig({
title: 'Record Payment', icon: Banknote, iconColor: 'text-[#39ff14]', iconBg: 'bg-[#39ff14]/10',
submitLabel: 'Save Payment', submitColor: 'bg-[#39ff14]/20 text-[#39ff14] border-[#39ff14]/30',
fields: [
{ name: 'from', label: 'From / Payer Name', type: 'text', required: true, placeholder: 'e.g. State Farm Insurance' },
{ name: 'method', label: 'Payment Method', type: 'select', options: [{ label: 'Cash', value: 'Cash' }, { label: 'Check', value: 'Check' }, { label: 'ACH', value: 'ACH' }, { label: 'Credit Card', value: 'Credit Card' }, { label: 'Wire Transfer', value: 'Wire Transfer' }], required: true },
{ name: 'amount', label: 'Amount ($)', type: 'number', required: true },
{ name: 'date', label: 'Payment Date', type: 'date', required: true },
{ name: 'refNumber', label: 'Check No. / Reference', type: 'text', placeholder: 'e.g. CHK-1234' },
{ name: 'memo', label: 'Memo / Notes', type: 'textarea' },
],
onSubmit: handleAddPayment
})}
className="bg-[#39ff14]/10 hover:bg-[#39ff14]/20 text-green-600 dark:text-[#39ff14] border border-[#39ff14]/30 px-4 py-2 rounded-lg text-xs font-bold transition flex items-center gap-2"
>
<Plus size={14} /> Record Payment
</button>
</div>
{payments.length > 0 ? (
<>
{/* Desktop Table */}
<div className="overflow-x-auto w-full hidden md:block">
<table className="w-full text-left border-collapse min-w-[800px]">
<thead className="bg-zinc-100 dark:bg-[#18181b]/80 border-b border-zinc-200 dark:border-white/10">
<tr>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">From / Method</th>
<th
className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors select-none"
onClick={() => togglePaymentSort('amount')}
>
Amount {paymentSort.field === 'amount' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''}
</th>
<th
className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors select-none"
onClick={() => togglePaymentSort('date')}
>
Date {paymentSort.field === 'date' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''}
</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Check No. / Ref</th>
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Memo</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
{paginatedPayments.map((pay) => (
<tr key={pay.id} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
<td className="px-5 py-4">
<div className="font-bold text-sm text-zinc-900 dark:text-white">{pay.from}</div>
<span className={`inline-block mt-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest ${
pay.method === 'ACH' ? 'text-sky-600 dark:text-[#00f0ff] bg-sky-50 dark:bg-blue-500/10 border border-sky-200 dark:border-blue-500/20' :
pay.method === 'Check' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' :
pay.method === 'Credit Card' ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-500/10 border border-purple-200 dark:border-purple-500/20' :
pay.method === 'Wire Transfer' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20' :
'text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'
}`}>
{pay.method}
</span>
</td>
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-green-600 dark:text-[#39ff14]">
{formatCurrency(pay.amount)}
</td>
<td className="px-5 py-4 text-sm text-zinc-500 dark:text-zinc-400 font-mono">{pay.date}</td>
<td className="px-5 py-4 font-mono text-xs text-zinc-500 dark:text-zinc-400">{pay.refNumber || '—'}</td>
<td className="px-5 py-4 text-xs text-zinc-500 dark:text-zinc-400 max-w-[200px] truncate">{pay.memo || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Card View */}
<div className="md:hidden p-4 space-y-3">
{paginatedPayments.map((pay) => (
<div key={pay.id} className="bg-zinc-50 dark:bg-white/[0.03] border border-zinc-100 dark:border-white/[0.06] rounded-2xl p-4 space-y-3">
<div className="flex items-start justify-between">
<div>
<p className="font-bold text-sm text-zinc-900 dark:text-white">{pay.from}</p>
<span className={`inline-block mt-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest ${
pay.method === 'ACH' ? 'text-sky-600 dark:text-[#00f0ff] bg-sky-50 dark:bg-blue-500/10 border border-sky-200 dark:border-blue-500/20' :
pay.method === 'Check' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' :
pay.method === 'Credit Card' ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-500/10 border border-purple-200 dark:border-purple-500/20' :
pay.method === 'Wire Transfer' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20' :
'text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'
}`}>
{pay.method}
</span>
</div>
<span className="font-mono text-lg font-black text-green-600 dark:text-[#39ff14]">{formatCurrency(pay.amount)}</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-zinc-500 dark:text-zinc-500">Date</span>
<p className="font-mono text-zinc-700 dark:text-zinc-300">{pay.date}</p>
</div>
<div>
<span className="text-zinc-500 dark:text-zinc-500">Ref #</span>
<p className="font-mono text-zinc-700 dark:text-zinc-300">{pay.refNumber || '—'}</p>
</div>
</div>
{pay.memo && (
<p className="text-xs text-zinc-500 dark:text-zinc-400 border-t border-zinc-100 dark:border-white/5 pt-2">{pay.memo}</p>
)}
</div>
))}
</div>
{/* Pagination */}
{totalPaymentPages > 1 && (
<div className="px-5 py-4 border-t border-zinc-200 dark:border-white/5 flex items-center justify-between">
<p className="text-xs text-zinc-500 dark:text-zinc-400 font-mono">
{paymentPage * PAYMENTS_PER_PAGE + 1}{Math.min((paymentPage + 1) * PAYMENTS_PER_PAGE, sortedPayments.length)} of {sortedPayments.length}
</p>
<div className="flex gap-2">
<button
onClick={() => setPaymentPage(p => Math.max(0, p - 1))}
disabled={paymentPage === 0}
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={14} />
</button>
<button
onClick={() => setPaymentPage(p => Math.min(totalPaymentPages - 1, p + 1))}
disabled={paymentPage >= totalPaymentPages - 1}
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight size={14} />
</button>
</div>
</div>
)}
</>
) : (
<div className="p-16 text-center text-zinc-500">
<Banknote size={48} className="mx-auto mb-4 text-zinc-400 dark:text-zinc-600" />
<p className="font-bold text-lg text-zinc-900 dark:text-white">No payments recorded yet.</p>
<p className="text-sm mt-2 text-zinc-400">Record your first payment to start tracking.</p>
</div>
)}
</NeoCard>
</div>
)}
{/* RISK & ISSUES TAB */}
{activeTab === 'risks' && (
<div className="space-y-6">