adds sections as list tasks,log expenses. fees, notifications for sub contrcator

This commit is contained in:
Satyam Rastogi
2026-05-19 18:17:36 +05:30
parent c7a9849d1f
commit 2589975074
5 changed files with 2003 additions and 60 deletions
@@ -1,12 +1,14 @@
import React, { useState, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
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 } from 'lucide-react';
import { CheckSquare, Clock, Wallet, MapPin, Camera, X, Calendar, DollarSign, ChevronRight, AlertCircle } from 'lucide-react';
import TaskDetailsModal from '../../components/contractor/TaskDetailsModal';
import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal';
import { NotificationsPanel } from '../../components/subcontractor/NotificationsPanel';
// --- Reusable list modal for filtered tasks / clock-in / payment history ---
const SubDetailModal = ({ isOpen, onClose, title, type, tasks, invoices, clockIns, onTaskClick }) => {
@@ -194,11 +196,18 @@ const SubDetailModal = ({ isOpen, onClose, title, type, tasks, invoices, clockIn
const SubContractorDashboard = () => {
const { user } = useAuth();
const { projects } = useMockStore();
const { projects, subcontractorTasks, notifications, markNotificationRead } = useMockStore();
const navigate = useNavigate();
const subId = user?.id || 'sub_001';
// Aggregate Tasks
// 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 }))
@@ -208,6 +217,12 @@ const SubContractorDashboard = () => {
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)
@@ -308,58 +323,70 @@ const SubContractorDashboard = () => {
</div>
{/* Main Task List */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
<div className="lg:col-span-2">
<SpotlightCard className="p-6 h-full">
<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>
<span className="text-[11px] font-bold uppercase tracking-wider text-zinc-500">{myAssignments.length} task{myAssignments.length === 1 ? '' : 's'}</span>
</div>
<div className="space-y-4">
{myTasks.length > 0 ? myTasks.map((task, idx) => (
<div key={idx} 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" onClick={() => handleTaskClick(task)}>
{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 (
<div
key={task.id}
role="button"
tabIndex={0}
onClick={() => 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"
>
<div className="flex flex-col md:flex-row justify-between md:items-center gap-4">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-lg ${task.status === 'completed' ? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400' :
task.status === 'in_progress' ? 'bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400' :
'bg-zinc-100 text-zinc-600 dark:bg-white/10 dark:text-zinc-400'
}`}>
<div className="flex items-start gap-4 min-w-0">
<div className={`p-3 rounded-lg shrink-0 ${statusTone}`}>
<CheckSquare size={20} />
</div>
<div>
<h4 className="font-bold text-zinc-900 dark:text-white">{task.name}</h4>
<div className="flex items-center text-xs text-zinc-500 dark:text-zinc-400 mt-1">
<MapPin size={12} className="mr-1" />
{task.projectAddress}
<div className="min-w-0">
<h4 className="font-bold text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors truncate">{task.title}</h4>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-zinc-500 dark:text-zinc-400 mt-1">
<span className="flex items-center gap-1"><MapPin size={12} />{task.location}</span>
<span className="font-mono">{task.id}</span>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 md:gap-4 shrink-0">
<div className="text-right">
<p className="text-xs font-mono font-medium text-zinc-500 mb-1">Due: {task.dueDate}</p>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${task.status === 'completed' ? 'bg-emerald-100 text-emerald-600' :
task.status === 'in_progress' ? 'bg-blue-100 text-blue-600' : 'bg-amber-100 text-amber-600'
}`}>
{task.status.replace('_', ' ')}
<p className={`text-xs font-mono font-medium mb-1 flex items-center justify-end gap-1 ${overdue ? 'text-red-500 font-bold' : 'text-zinc-500'}`}>
{overdue && <AlertCircle size={11} />}
Due: {task.dueDate}
</p>
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${statusTone}`}>
{task.status}
</span>
</div>
{task.status !== 'completed' && (
<button className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold transition-colors"
onClick={(e) => { e.stopPropagation(); handleTaskClick(task); }}>
Update
</button>
)}
<ChevronRight size={16} className="text-zinc-400 group-hover:text-blue-500 transition-colors" />
</div>
</div>
</div>
)) : (
<div className="text-center py-10 text-zinc-500"><p>No tasks assigned.</p></div>
);
}) : (
<div className="text-center py-10 text-zinc-500"><p>No tasks assigned yet.</p></div>
)}
</div>
</SpotlightCard>
</div>
{/* Earnings Widget */}
<div>
{/* Right column: Earnings + Notifications */}
<div className="space-y-6">
<SpotlightCard className="p-6">
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Earnings Tracker</h2>
<div className="relative pt-4 pb-8">
@@ -385,6 +412,12 @@ const SubContractorDashboard = () => {
</button>
</div>
</SpotlightCard>
<NotificationsPanel
notifications={myNotifications}
onMarkAllRead={() => myNotifications.forEach(n => !n.isRead && markNotificationRead(n.id))}
onNotificationClick={(n) => { if (!n.isRead) markNotificationRead(n.id); }}
/>
</div>
</div>
File diff suppressed because it is too large Load Diff