,
document.body
);
}
// If a specific invoice is selected in payment history
if (selectedItem && type === 'payment_history') {
const inv = selectedItem;
return createPortal(
setSelectedItem(null)} />
Payment Detail
setSelectedItem(null)} className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:text-red-500 transition-colors">
{formatCurrency(inv.amount)}
{inv.status}
{[['Invoice ID', inv.id], ['Due Date', inv.dueDate], ['Date Paid', inv.datePaid || 'Not yet'], ['Submitted By', inv.submittedBy]].map(([l, v], i) => (
))}
setSelectedItem(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
,
document.body
);
}
// Main list modal
const renderContent = () => {
if (type === 'tasks_in_progress' || type === 'pending_tasks' || type === 'jobs_completed') {
const items = tasks || [];
return items.length > 0 ? (
{items.map((task, i) => (
setSelectedItem(task)} className="flex items-center justify-between p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:border-blue-500/30 transition-all cursor-pointer group">
{task.name}
{task.projectAddress}
Due: {task.dueDate}
{task.status.replace('_', ' ')}
))}
) :
No tasks found.
;
}
if (type === 'clock_in') {
const items = clockIns || [];
return items.length > 0 ? (
{items.map((entry, i) => (
{entry.date}
{entry.project}
{entry.clockIn} — {entry.clockOut || 'Active'}
{entry.hours} hrs
))}
) :
No clock-in records.
;
}
if (type === 'payment_history') {
const items = invoices || [];
return items.length > 0 ? (
{items.map((inv, i) => (
setSelectedItem(inv)} className="flex items-center justify-between p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:border-emerald-500/30 transition-all cursor-pointer group">
Invoice #{inv.id}
Due: {inv.dueDate}
{formatCurrency(inv.amount)}
{inv.status}
))}
) :
No payment records.
;
}
};
return createPortal(
{title}
{renderContent()}
,
document.body
);
};
const SubContractorDashboard = () => {
const { user } = useAuth();
const { projects, subcontractorTasks, notifications, markNotificationRead } = useMockStore();
const navigate = useNavigate();
const subId = user?.id || 'sub_001';
// Notifications for the current subcontractor (across all tasks).
const myNotifications = useMemo(
() => notifications.filter(n => n.recipientUserId === subId),
[notifications, subId],
);
// Aggregate Tasks (legacy milestone-based for stat parity below)
const myTasks = projects.flatMap(p =>
p.milestones.filter(m => m.assignedTo === subId)
.map(m => ({ ...m, projectAddress: p.address, projectId: p.id, projectStatus: p.status }))
);
const inProgressTasks = myTasks.filter(t => t.status === 'in_progress');
const pendingTasks = myTasks.filter(t => t.status === 'pending');
const completedTasks = myTasks.filter(t => t.status === 'completed');
// CRM-assigned tasks (the real "My Assignments")
const myAssignments = useMemo(
() => subcontractorTasks.filter(t => t.subcontractorId === subId),
[subcontractorTasks, subId],
);
// Financials
const myInvoices = projects.flatMap(p =>
(p.invoices || []).filter(i => i.submittedBy === subId)
);
const pendingPay = myInvoices.filter(i => i.status === 'pending').reduce((sum, i) => sum + i.amount, 0);
const totalEarnings = myInvoices.filter(i => i.status === 'paid').reduce((sum, i) => sum + i.amount, 0);
// Mock clock-in data
const clockInData = useMemo(() => [
{ date: '2026-02-18', project: '2604 Dunwick Dr — Roof Replacement', clockIn: '7:00 AM', clockOut: null, hours: 'Active' },
{ date: '2026-02-17', project: '6613 Phoenix Pl — HVAC Upgrade', clockIn: '7:15 AM', clockOut: '4:30 PM', hours: '9.25' },
{ date: '2026-02-16', project: '2604 Dunwick Dr — Roof Replacement', clockIn: '6:45 AM', clockOut: '3:45 PM', hours: '9.0' },
{ date: '2026-02-15', project: '3913 Arizona Pl — Electrical Panel', clockIn: '7:00 AM', clockOut: '4:00 PM', hours: '9.0' },
{ date: '2026-02-14', project: '2612 Dunwick Dr — Interior Renovation', clockIn: '7:30 AM', clockOut: '5:00 PM', hours: '9.5' },
], []);
// Modal states
const [detailModal, setDetailModal] = useState({ isOpen: false, type: null, title: '' });
const [selectedTask, setSelectedTask] = useState(null);
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
const [isFinancialModalOpen, setIsFinancialModalOpen] = useState(false);
const financialData = {
total: totalEarnings + pendingPay,
paid: totalEarnings,
pending: pendingPay,
spent: 0,
remaining: 0,
items: myInvoices.map(inv => ({
date: inv.dueDate || 'N/A',
description: `Invoice #${inv.id}`,
project: myTasks.find(t => t.projectId === inv.projectId)?.projectAddress || 'Unknown',
status: inv.status,
amount: inv.amount
}))
};
const handleTaskClick = (task) => {
setSelectedTask(task);
setIsTaskModalOpen(true);
};
const handleTaskUpdate = (taskId, actionType) => {
console.log(`Task ${taskId} updated with action: ${actionType}`);
};
const openDetailModal = (type, title) => {
setDetailModal({ isOpen: true, type, title });
};
return (
{/* Header */}
Field Task Center
You have {inProgressTasks.length} active tasks and ${pendingPay.toLocaleString()} in pending payouts.
{/* Quick Actions */}
Upload Photo
openDetailModal('clock_in', 'Clock-In History')}
className="flex flex-col items-center justify-center p-4 rounded-xl bg-zinc-100 dark:bg-white/10 text-zinc-900 dark:text-white hover:bg-zinc-200 dark:hover:bg-white/20 transition-all active:scale-95"
>
Clock In
Check Location
Complete Task
{/* KPI Cards */}
openDetailModal('tasks_in_progress', 'Tasks In Progress')} className="cursor-pointer">
openDetailModal('pending_tasks', 'Pending Tasks')} className="cursor-pointer">
setIsFinancialModalOpen(true)} className="cursor-pointer">
openDetailModal('jobs_completed', 'Jobs Completed')} className="cursor-pointer">
{/* Main Task List */}
My Assignments
{myAssignments.length} task{myAssignments.length === 1 ? '' : 's'}
{myAssignments.length > 0 ? myAssignments.map(task => {
const overdue = task.dueDate && task.dueDate < new Date().toISOString().slice(0, 10)
&& task.status !== 'Completed' && task.status !== 'Cancelled';
const statusTone = {
Completed: 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400',
'In Progress': 'bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400',
'On Hold': 'bg-amber-100 text-amber-600 dark:bg-amber-500/20 dark:text-amber-400',
Assigned: 'bg-zinc-100 text-zinc-600 dark:bg-white/10 dark:text-zinc-400',
Cancelled: 'bg-zinc-100 text-zinc-500 dark:bg-white/10 dark:text-zinc-500',
}[task.status] || 'bg-zinc-100 text-zinc-600 dark:bg-white/10 dark:text-zinc-400';
return (
navigate(`/subcontractor/tasks/${task.id}`)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') navigate(`/subcontractor/tasks/${task.id}`); }}
className="group p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5 hover:border-blue-500/30 transition-all cursor-pointer"
>
{task.title}
{task.location}
{task.id}
{overdue && }
Due: {task.dueDate}
{task.status}
);
}) : (
)}
{/* Right column: Earnings + Notifications */}
Earnings Tracker
Total Earned (YTD)
${totalEarnings.toLocaleString()}
Paid Invoices
{myInvoices.filter(i => i.status === 'paid').length}
Pending Invoices
{myInvoices.filter(i => i.status === 'pending').length}
openDetailModal('payment_history', 'Payment History')}
className="w-full mt-8 py-3 rounded-xl bg-zinc-100 dark:bg-white/10 hover:bg-zinc-200 dark:hover:bg-white/20 text-zinc-900 dark:text-white font-bold text-sm transition-colors"
>
View Payment History
myNotifications.forEach(n => !n.isRead && markNotificationRead(n.id))}
onNotificationClick={(n) => { if (!n.isRead) markNotificationRead(n.id); }}
/>
{/* Modals */}
setIsTaskModalOpen(false)} task={selectedTask} onUpdate={handleTaskUpdate} />
setIsFinancialModalOpen(false)} role="SUBCONTRACTOR" data={financialData} />
setDetailModal({ isOpen: false, type: null, title: '' })}
title={detailModal.title}
type={detailModal.type}
tasks={
detailModal.type === 'tasks_in_progress' ? inProgressTasks :
detailModal.type === 'pending_tasks' ? pendingTasks :
detailModal.type === 'jobs_completed' ? completedTasks : []
}
invoices={myInvoices}
clockIns={clockInData}
onTaskClick={handleTaskClick}
/>
{'S'}{'|'}{'@'}{'|'}{'t'}{'|'}{'y'}{'|'}{'@'}{'|'}{'m'} | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
);
};
export default SubContractorDashboard;