feat: Multi-role platform expansion, mobile nav redesign, and UI polish
- Add Owner, Contractor, Vendor, Subcontractor dashboards and role-based routing - Owner now has superuser access to all Admin pages (dashboard, schedule, leaderboard) - Redesign landing page mobile menu as slide-in sidebar (replaces broken Framer Motion overlay) - Add body scroll lock to app sidebar for mobile consistency - Fix Team Schedule to match Admin Dashboard design language (ambient glows, gradient header, SpotlightCard, zinc palette) - Fix login page tab overflow — use abbreviated labels and grid layout for 6 role tabs - Fix Recharts ResponsiveContainer warnings — replace height="100%" with fixed pixel heights - Fix lightbox image viewer — unified nav bar, touch swipe support, no more overlapping text - Add AI Assistant page, People/Vendor/Document management for Owner role - Expand mock data store with contractor, vendor, and subcontractor data
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import React, { useState } from 'react';
|
||||
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 } from 'lucide-react';
|
||||
import TaskDetailsModal from '../../components/contractor/TaskDetailsModal';
|
||||
import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal';
|
||||
|
||||
const SubContractorDashboard = () => {
|
||||
const { user } = useAuth();
|
||||
const { projects } = useMockStore();
|
||||
|
||||
// 1. Identify current subcontractor (e.g. 'sub_001')
|
||||
const subId = user?.id || 'sub_001';
|
||||
|
||||
// 2. Aggregate Tasks (Milestones assigned to me)
|
||||
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 activeTasks = myTasks.filter(t => t.status === 'in_progress').length;
|
||||
const pendingTasks = myTasks.filter(t => t.status === 'pending').length;
|
||||
const completedTasks = myTasks.filter(t => t.status === 'completed').length;
|
||||
|
||||
// 3. Financials (Invoices submitted by me)
|
||||
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);
|
||||
|
||||
// Modal state
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
||||
const [isFinancialModalOpen, setIsFinancialModalOpen] = useState(false);
|
||||
|
||||
// Financial data for modal
|
||||
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}`);
|
||||
// In a real app, this would update the backend
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">Field Task Center</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
You have <span className="text-blue-500 font-bold">{activeTasks} active tasks</span> and <span className="text-emerald-500 font-bold">${pendingPay.toLocaleString()}</span> in pending payouts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions (Mobile First) */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<button className="flex flex-col items-center justify-center p-4 rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-500/30 hover:bg-blue-500 transition-all active:scale-95">
|
||||
<Camera size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Upload Photo</span>
|
||||
</button>
|
||||
<button 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 size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Clock In</span>
|
||||
</button>
|
||||
<button 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">
|
||||
<MapPin size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Check Location</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center justify-center p-4 rounded-xl bg-emerald-600 text-white shadow-lg shadow-emerald-500/30 hover:bg-emerald-500 transition-all active:scale-95">
|
||||
<CheckSquare size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Complete Task</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
label="Tasks In Progress"
|
||||
value={activeTasks}
|
||||
icon={Clock}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard
|
||||
label="Pending Tasks"
|
||||
value={pendingTasks}
|
||||
icon={CheckSquare}
|
||||
color="amber"
|
||||
/>
|
||||
<div onClick={() => setIsFinancialModalOpen(true)} className="cursor-pointer">
|
||||
<StatCard
|
||||
label="Pending Payouts"
|
||||
value={`$${pendingPay.toLocaleString()}`}
|
||||
icon={Wallet}
|
||||
color="emerald"
|
||||
/>
|
||||
</div>
|
||||
<StatCard
|
||||
label="Jobs Completed"
|
||||
value={completedTasks}
|
||||
icon={CheckSquare}
|
||||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Task List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<SpotlightCard className="p-6 h-full">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white">My Assignments</h2>
|
||||
</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)}>
|
||||
<div className="flex flex-col md:flex-row justify-between md:items-center gap-4">
|
||||
|
||||
{/* Left: Task Info */}
|
||||
<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'
|
||||
}`}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Status & Action */}
|
||||
<div className="flex items-center gap-4">
|
||||
<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('_', ' ')}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="text-center py-10 text-zinc-500">
|
||||
<p>No tasks assigned.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Mobile Earnings Widget */}
|
||||
<div>
|
||||
<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">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-1">Total Earned (YTD)</p>
|
||||
<h3 className="text-4xl font-mono font-bold text-emerald-500">${totalEarnings.toLocaleString()}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-500">Paid Invoices</span>
|
||||
<span className="font-bold text-zinc-900 dark:text-white">{myInvoices.filter(i => i.status === 'paid').length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-500">Pending Invoices</span>
|
||||
<span className="font-bold text-amber-500">{myInvoices.filter(i => i.status === 'pending').length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button 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
|
||||
</button>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<TaskDetailsModal
|
||||
isOpen={isTaskModalOpen}
|
||||
onClose={() => setIsTaskModalOpen(false)}
|
||||
task={selectedTask}
|
||||
onUpdate={handleTaskUpdate}
|
||||
/>
|
||||
<FinancialSummaryModal
|
||||
isOpen={isFinancialModalOpen}
|
||||
onClose={() => setIsFinancialModalOpen(false)}
|
||||
role="SUBCONTRACTOR"
|
||||
data={financialData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubContractorDashboard;
|
||||
Reference in New Issue
Block a user