adds subcontractor project history and payments

This commit is contained in:
Satyam Rastogi
2026-05-20 19:23:53 +05:30
parent 5dd781ca45
commit 6838d6c38e
@@ -5,7 +5,7 @@ import { useAuth } from '../../context/AuthContext';
import { useMockStore } from '../../data/mockStore';
import { StatCard } from '../../components/StatCard';
import { SpotlightCard } from '../../components/SpotlightCard';
import { CheckSquare, Clock, Wallet, MapPin, Camera, X, Calendar, DollarSign, ChevronRight, AlertCircle } from 'lucide-react';
import { CheckSquare, Clock, Wallet, MapPin, Camera, X, Calendar, DollarSign, ChevronRight, AlertCircle, ArrowDown, ArrowUp, FileText, Receipt } from 'lucide-react';
import TaskDetailsModal from '../../components/contractor/TaskDetailsModal';
import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal';
import { NotificationsPanel } from '../../components/subcontractor/NotificationsPanel';
@@ -239,6 +239,56 @@ const SubContractorDashboard = () => {
{ date: '2026-02-14', project: '2612 Dunwick Dr — Interior Renovation', clockIn: '7:30 AM', clockOut: '5:00 PM', hours: '9.5' },
], []);
// Project history & payment tracking
const [historyFilter, setHistoryFilter] = useState('all'); // all | Paid | Requested | Pending
const [historySort, setHistorySort] = useState('desc'); // desc = newest first
const [noteModal, setNoteModal] = useState(null); // { title, body, project, requestedAt, receivedAt, status }
const projectById = useMemo(() => {
const m = new Map();
for (const p of projects) m.set(p.id, p);
return m;
}, [projects]);
const paymentHistoryRows = useMemo(() => {
const items = myAssignments.map(t => {
const project = t.projectId ? projectById.get(t.projectId) : null;
const requestedAt = t.paymentRequestedAt || null;
const receivedAt = t.paidAt || null;
const status = t.paymentStatus === 'Paid'
? 'Paid'
: (requestedAt ? 'Requested' : 'Pending');
return {
id: t.id,
projectId: t.projectId || null,
projectName: project ? (project.projectType || project.address) : 'Standalone Task',
projectAddress: project?.address || t.location || '',
requestedAt,
receivedAt,
note: t.title || '',
description: t.description || '',
status,
};
});
const filtered = historyFilter === 'all' ? items : items.filter(r => r.status === historyFilter);
const sorted = [...filtered].sort((a, b) => {
const av = a.requestedAt || '';
const bv = b.requestedAt || '';
if (av === bv) return 0;
if (!av) return 1; // missing requested dates sink to bottom
if (!bv) return -1;
return historySort === 'desc' ? (bv > av ? 1 : -1) : (av > bv ? 1 : -1);
});
return sorted;
}, [myAssignments, projectById, historyFilter, historySort]);
const historyCounts = useMemo(() => ({
all: myAssignments.length,
Paid: myAssignments.filter(t => t.paymentStatus === 'Paid').length,
Requested: myAssignments.filter(t => t.paymentStatus !== 'Paid' && t.paymentRequestedAt).length,
Pending: myAssignments.filter(t => !t.paymentRequestedAt).length,
}), [myAssignments]);
// Modal states
const [detailModal, setDetailModal] = useState({ isOpen: false, type: null, title: '' });
const [selectedTask, setSelectedTask] = useState(null);
@@ -324,7 +374,7 @@ const SubContractorDashboard = () => {
{/* Main Task List */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
<div className="lg:col-span-2">
<div className="lg:col-span-2 space-y-6">
<SpotlightCard className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-lg font-bold text-zinc-900 dark:text-white">My Assignments</h2>
@@ -383,6 +433,151 @@ const SubContractorDashboard = () => {
)}
</div>
</SpotlightCard>
{/* Project History & Payment Tracking */}
<SpotlightCard className="p-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h2 className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
<Receipt size={18} className="text-amber-500 dark:text-amber-400" />
Project History & Payments
</h2>
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
Track payment requests and receipts across every project you've worked on.
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
{[
{ key: 'all', label: 'All', tone: 'bg-zinc-100 dark:bg-white/10 text-zinc-700 dark:text-zinc-200' },
{ key: 'Paid', label: 'Paid', tone: 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400' },
{ key: 'Requested', label: 'Requested', tone: 'bg-blue-100 dark:bg-blue-500/15 text-blue-700 dark:text-blue-400' },
{ key: 'Pending', label: 'Pending', tone: 'bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400' },
].map(p => {
const active = historyFilter === p.key;
return (
<button
key={p.key}
type="button"
onClick={() => setHistoryFilter(p.key)}
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider transition-colors border ${active ? `${p.tone} border-transparent shadow-sm` : 'bg-transparent border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
{p.label}
<span className="ml-1.5 opacity-70">{historyCounts[p.key] ?? 0}</span>
</button>
);
})}
<button
type="button"
onClick={() => setHistorySort(d => d === 'desc' ? 'asc' : 'desc')}
className="px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors flex items-center gap-1.5"
aria-label={`Sort by requested date ${historySort === 'desc' ? 'descending' : 'ascending'}`}
>
{historySort === 'desc' ? <ArrowDown size={12} /> : <ArrowUp size={12} />}
{historySort === 'desc' ? 'Newest' : 'Oldest'}
</button>
</div>
</div>
{paymentHistoryRows.length === 0 ? (
<div className="py-10 text-center">
<FileText size={28} className="mx-auto mb-3 text-zinc-400" />
<p className="text-sm text-zinc-500">No project history yet.</p>
<p className="text-xs text-zinc-400 mt-1">
{historyFilter === 'all' ? 'Tasks assigned to you will appear here.' : `No projects with status "${historyFilter}".`}
</p>
</div>
) : (
<div className="overflow-x-auto rounded-xl border border-zinc-200 dark:border-white/5">
<table className="w-full text-left min-w-[840px]">
<thead>
<tr className="border-b border-zinc-200 dark:border-white/5 bg-zinc-50 dark:bg-white/[0.02]">
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Project ID</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Project Name</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">
<button
type="button"
onClick={() => setHistorySort(d => d === 'desc' ? 'asc' : 'desc')}
className="inline-flex items-center gap-1 hover:text-zinc-900 dark:hover:text-white transition-colors"
title="Sort by requested date"
>
Requested
{historySort === 'desc' ? <ArrowDown size={10} /> : <ArrowUp size={10} />}
</button>
</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Received</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Note</th>
<th className="px-4 py-2.5 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">Status</th>
</tr>
</thead>
<tbody>
{paymentHistoryRows.map(row => {
const unpaid = row.status !== 'Paid';
const statusTone = {
Paid: 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-400 dark:border-emerald-500/20',
Requested: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-500/15 dark:text-blue-400 dark:border-blue-500/20',
Pending: 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-500/15 dark:text-amber-400 dark:border-amber-500/20',
}[row.status] || 'bg-zinc-100 text-zinc-600 border-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:border-white/10';
return (
<tr
key={row.id}
role="button"
tabIndex={0}
onClick={() => navigate(`/subcontractor/tasks/${row.id}`)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') navigate(`/subcontractor/tasks/${row.id}`); }}
className={`border-b border-zinc-100 dark:border-white/[0.03] hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors cursor-pointer group ${unpaid ? 'border-l-2 border-l-amber-400/60 dark:border-l-amber-500/40' : ''}`}
>
<td className="px-4 py-3">
<span className="text-[11px] font-mono text-zinc-500 dark:text-zinc-400">
{row.projectId || ''}
</span>
</td>
<td className="px-4 py-3">
<div className="min-w-0">
<div className="text-xs font-semibold text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors truncate max-w-[220px]" title={row.projectName}>
{row.projectName}
</div>
{row.projectAddress && (
<div className="text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[220px]" title={row.projectAddress}>
{row.projectAddress}
</div>
)}
</div>
</td>
<td className="px-4 py-3">
<span className="text-[11px] font-mono text-zinc-600 dark:text-zinc-300 whitespace-nowrap">
{row.requestedAt ? new Date(row.requestedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : ''}
</span>
</td>
<td className="px-4 py-3">
<span className={`text-[11px] font-mono whitespace-nowrap ${row.receivedAt ? 'text-emerald-600 dark:text-emerald-400 font-semibold' : 'text-amber-600 dark:text-amber-400/80'}`}>
{row.receivedAt ? new Date(row.receivedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : 'Not received'}
</span>
</td>
<td className="px-4 py-3">
<button
type="button"
onClick={(e) => { e.stopPropagation(); setNoteModal(row); }}
className="inline-flex items-center gap-1.5 text-[11px] text-zinc-600 dark:text-zinc-300 hover:text-blue-600 dark:hover:text-blue-400 max-w-[220px] truncate"
title={row.note}
>
<FileText size={11} className="shrink-0" />
<span className="truncate">{row.note || 'View note'}</span>
</button>
</td>
<td className="px-4 py-3">
<span className={`text-[11px] font-bold rounded-md px-1.5 py-0.5 border whitespace-nowrap ${statusTone}`}>
{row.status}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</SpotlightCard>
</div>
{/* Right column: Earnings + Notifications */}
@@ -421,6 +616,64 @@ const SubContractorDashboard = () => {
</div>
</div>
{/* Note detail modal */}
{noteModal && createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4" role="dialog" aria-modal="true">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setNoteModal(null)} />
<div className="relative w-full max-w-lg bg-white dark:bg-[#121214] rounded-2xl shadow-2xl border border-zinc-200 dark:border-white/10 overflow-hidden animate-in zoom-in-95 duration-200">
<div className="px-6 py-5 border-b border-zinc-200 dark:border-white/10 flex justify-between items-center bg-zinc-50/50 dark:bg-white/5">
<h2 className="text-lg font-bold text-zinc-900 dark:text-white">Note Detail</h2>
<button onClick={() => setNoteModal(null)} className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:text-red-500 transition-colors"><X size={18} /></button>
</div>
<div className="p-6 space-y-4">
<div>
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mb-1">Project</div>
<div className="font-semibold text-zinc-900 dark:text-white">{noteModal.projectName}</div>
{noteModal.projectAddress && (
<div className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">{noteModal.projectAddress}</div>
)}
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="p-3 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5">
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mb-1">Requested</div>
<div className="font-mono text-zinc-900 dark:text-white">
{noteModal.requestedAt ? new Date(noteModal.requestedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : ''}
</div>
</div>
<div className="p-3 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5">
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mb-1">Received</div>
<div className={`font-mono ${noteModal.receivedAt ? 'text-emerald-600 dark:text-emerald-400' : 'text-amber-600 dark:text-amber-400'}`}>
{noteModal.receivedAt ? new Date(noteModal.receivedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : 'Not received'}
</div>
</div>
</div>
<div>
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mb-1">Note</div>
<div className="text-sm text-zinc-900 dark:text-white font-semibold">{noteModal.note || ''}</div>
{noteModal.description && (
<p className="text-sm text-zinc-600 dark:text-zinc-300 mt-2 leading-relaxed whitespace-pre-line">{noteModal.description}</p>
)}
</div>
</div>
<div className="px-6 py-4 border-t border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5 flex justify-end gap-2">
<button
onClick={() => setNoteModal(null)}
className="px-4 py-2 text-sm font-bold rounded-xl border border-zinc-200 dark:border-white/10 hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors"
>
Close
</button>
<button
onClick={() => { const id = noteModal.id; setNoteModal(null); navigate(`/subcontractor/tasks/${id}`); }}
className="px-4 py-2 text-sm font-bold rounded-xl bg-blue-600 hover:bg-blue-500 text-white transition-colors"
>
Open Task
</button>
</div>
</div>
</div>,
document.body
)}
{/* Modals */}
<TaskDetailsModal isOpen={isTaskModalOpen} onClose={() => setIsTaskModalOpen(false)} task={selectedTask} onUpdate={handleTaskUpdate} />
<FinancialSummaryModal isOpen={isFinancialModalOpen} onClose={() => setIsFinancialModalOpen(false)} role="SUBCONTRACTOR" data={financialData} />