feat: comprehensive mobile optimization, role-based AI chatbot, and multi-role dashboard enhancements
- Rewrote AI chatbot context system with deep role-aware data injection for all 7 roles (Owner, Admin, Field Agent, Contractor, Subcontractor, Vendor, Customer) plus guest fallback - Added mobile-responsive bottom-sheet modals across all roles (ChangeOrderDrawer, InvoiceDetailModal, FinancialSummaryModal, VendorFinancialSummaryModal, FinancialDetailsModal) - Converted Owner Project List and Vendor Orders tables to mobile card views with touch-friendly layouts - Optimized Vendor Management and People Directory with mobile list/detail toggle and back navigation - Fixed Document Control mobile view: horizontally scrollable filter tabs, proper overflow chain for document visibility - Added responsive padding, text scaling, and flex-wrap across StatCard, TaskDetailsModal, OwnerSnapshot, OwnerProjectDetail, VendorDashboard, and DocumentManagement - Expanded mock data store with richer vendor invoices, orders, documents, and compliance records - Enhanced Owner Snapshot with financial KPI cards, urgent items panel, and Action Center modal - Built Contractor Dashboard with task management, financial summary, and performance metrics - Built Subcontractor Dashboard with clock-in tracking, task assignments, and invoice management - Enhanced Vendor Dashboard with earnings summary, active orders, compliance status, and performance rating - Added icon-only tab navigation on mobile for OwnerProjectDetail - Extended attribution signatures across all platform pages
This commit is contained in:
@@ -1,47 +1,235 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { createPortal } from 'react-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 } from 'lucide-react';
|
||||
import { CheckSquare, Clock, Wallet, MapPin, Camera, X, Calendar, DollarSign, ChevronRight } from 'lucide-react';
|
||||
import TaskDetailsModal from '../../components/contractor/TaskDetailsModal';
|
||||
import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal';
|
||||
|
||||
// --- Reusable list modal for filtered tasks / clock-in / payment history ---
|
||||
const SubDetailModal = ({ isOpen, onClose, title, type, tasks, invoices, clockIns, onTaskClick }) => {
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleEsc = (e) => { if (e.key === 'Escape') { if (selectedItem) setSelectedItem(null); else onClose(); } };
|
||||
if (isOpen) window.addEventListener('keydown', handleEsc);
|
||||
return () => window.removeEventListener('keydown', handleEsc);
|
||||
}, [isOpen, onClose, selectedItem]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const formatCurrency = (amt) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amt);
|
||||
|
||||
const getStatusColor = (s) => {
|
||||
if (s === 'completed' || s === 'paid') return 'text-emerald-600 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-500/10';
|
||||
if (s === 'in_progress') return 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-500/10';
|
||||
if (s === 'pending') return 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-500/10';
|
||||
return 'text-zinc-500 bg-zinc-100 dark:text-zinc-400 dark:bg-white/5';
|
||||
};
|
||||
|
||||
// If a specific task is selected, show detail view
|
||||
if (selectedItem && (type === 'tasks_in_progress' || type === 'pending_tasks' || type === 'jobs_completed')) {
|
||||
const task = selectedItem;
|
||||
return 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={() => setSelectedItem(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">Task Detail</h2>
|
||||
<button onClick={() => setSelectedItem(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>
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white">{task.name}</h3>
|
||||
<span className={`mt-2 inline-block px-3 py-1 rounded-full text-xs font-bold uppercase ${getStatusColor(task.status)}`}>{task.status.replace('_', ' ')}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{[['Project', task.projectAddress], ['Due Date', task.dueDate], ['Assigned To', task.assignedTo], ['Project Status', task.projectStatus]].map(([l, v], i) => (
|
||||
<div key={i} 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">{l}</div>
|
||||
<div className="font-semibold text-zinc-900 dark:text-white">{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</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">
|
||||
<button onClick={() => 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</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
// If a specific invoice is selected in payment history
|
||||
if (selectedItem && type === 'payment_history') {
|
||||
const inv = selectedItem;
|
||||
return 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={() => setSelectedItem(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">Payment Detail</h2>
|
||||
<button onClick={() => setSelectedItem(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 className="flex justify-between items-center">
|
||||
<div className="text-3xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(inv.amount)}</div>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-bold uppercase ${getStatusColor(inv.status)}`}>{inv.status}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{[['Invoice ID', inv.id], ['Due Date', inv.dueDate], ['Date Paid', inv.datePaid || 'Not yet'], ['Submitted By', inv.submittedBy]].map(([l, v], i) => (
|
||||
<div key={i} 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">{l}</div>
|
||||
<div className="font-semibold text-zinc-900 dark:text-white">{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</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">
|
||||
<button onClick={() => 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</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
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 ? (
|
||||
<div className="space-y-3">
|
||||
{items.map((task, i) => (
|
||||
<div key={i} onClick={() => 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">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`p-2 rounded-lg ${getStatusColor(task.status)}`}><CheckSquare size={16} /></div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white group-hover:text-blue-500 transition-colors">{task.name}</h4>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">{task.projectAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex items-center gap-2">
|
||||
<div>
|
||||
<p className="text-xs font-mono text-zinc-500">Due: {task.dueDate}</p>
|
||||
<span className={`text-[10px] font-bold uppercase ${getStatusColor(task.status)} px-2 py-0.5 rounded-full`}>{task.status.replace('_', ' ')}</span>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-zinc-400" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p className="text-center text-zinc-500 py-8">No tasks found.</p>;
|
||||
}
|
||||
|
||||
if (type === 'clock_in') {
|
||||
const items = clockIns || [];
|
||||
return items.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{items.map((entry, i) => (
|
||||
<div key={i} 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">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-500/10"><Clock size={16} /></div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white">{entry.date}</h4>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">{entry.project}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-mono font-bold text-zinc-900 dark:text-white">{entry.clockIn} — {entry.clockOut || 'Active'}</p>
|
||||
<p className="text-xs text-zinc-500">{entry.hours} hrs</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p className="text-center text-zinc-500 py-8">No clock-in records.</p>;
|
||||
}
|
||||
|
||||
if (type === 'payment_history') {
|
||||
const items = invoices || [];
|
||||
return items.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{items.map((inv, i) => (
|
||||
<div key={i} onClick={() => 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">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`p-2 rounded-lg ${getStatusColor(inv.status)}`}><DollarSign size={16} /></div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white group-hover:text-emerald-500 transition-colors">Invoice #{inv.id}</h4>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">Due: {inv.dueDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex items-center gap-2">
|
||||
<div>
|
||||
<p className="text-sm font-mono font-bold text-zinc-900 dark:text-white">{formatCurrency(inv.amount)}</p>
|
||||
<span className={`text-[10px] font-bold uppercase ${getStatusColor(inv.status)} px-2 py-0.5 rounded-full`}>{inv.status}</span>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-zinc-400" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p className="text-center text-zinc-500 py-8">No payment records.</p>;
|
||||
}
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[9999] flex items-end sm:items-center justify-center sm:p-6" role="dialog" aria-modal="true">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="relative w-full sm:max-w-2xl h-[85dvh] sm:h-auto sm:max-h-[80vh] bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-10 duration-200 border border-zinc-200 dark:border-white/10">
|
||||
<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-xl font-bold text-zinc-900 dark:text-white">{title}</h2>
|
||||
<button onClick={onClose} 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="flex-1 overflow-y-auto p-6 custom-scrollbar">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
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)
|
||||
// Aggregate Tasks
|
||||
const myTasks = projects.flatMap(p =>
|
||||
p.milestones.filter(m => m.assignedTo === subId)
|
||||
.map(m => ({
|
||||
...m,
|
||||
projectAddress: p.address,
|
||||
projectId: p.id,
|
||||
projectStatus: p.status
|
||||
}))
|
||||
.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;
|
||||
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');
|
||||
|
||||
// 3. Financials (Invoices submitted by me)
|
||||
// Financials
|
||||
const myInvoices = projects.flatMap(p =>
|
||||
p.invoices.filter(i => i.submittedBy === subId)
|
||||
(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
|
||||
// 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);
|
||||
|
||||
// Financial data for modal
|
||||
const financialData = {
|
||||
total: totalEarnings + pendingPay,
|
||||
paid: totalEarnings,
|
||||
@@ -64,7 +252,10 @@ const SubContractorDashboard = () => {
|
||||
|
||||
const handleTaskUpdate = (taskId, actionType) => {
|
||||
console.log(`Task ${taskId} updated with action: ${actionType}`);
|
||||
// In a real app, this would update the backend
|
||||
};
|
||||
|
||||
const openDetailModal = (type, title) => {
|
||||
setDetailModal({ isOpen: true, type, title });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -73,17 +264,20 @@ const SubContractorDashboard = () => {
|
||||
<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.
|
||||
You have <span className="text-blue-500 font-bold">{inProgressTasks.length} active tasks</span> and <span className="text-emerald-500 font-bold">${pendingPay.toLocaleString()}</span> in pending payouts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions (Mobile First) */}
|
||||
{/* Quick Actions */}
|
||||
<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">
|
||||
<button
|
||||
onClick={() => 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 size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Clock In</span>
|
||||
</button>
|
||||
@@ -99,32 +293,18 @@ const SubContractorDashboard = () => {
|
||||
|
||||
{/* 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 onClick={() => openDetailModal('tasks_in_progress', 'Tasks In Progress')} className="cursor-pointer">
|
||||
<StatCard label="Tasks In Progress" value={inProgressTasks.length} icon={Clock} color="blue" />
|
||||
</div>
|
||||
<div onClick={() => openDetailModal('pending_tasks', 'Pending Tasks')} className="cursor-pointer">
|
||||
<StatCard label="Pending Tasks" value={pendingTasks.length} icon={CheckSquare} color="amber" />
|
||||
</div>
|
||||
<div onClick={() => setIsFinancialModalOpen(true)} className="cursor-pointer">
|
||||
<StatCard label="Pending Payouts" value={`$${pendingPay.toLocaleString()}`} icon={Wallet} color="emerald" />
|
||||
</div>
|
||||
<div onClick={() => openDetailModal('jobs_completed', 'Jobs Completed')} className="cursor-pointer">
|
||||
<StatCard label="Jobs Completed" value={completedTasks.length} icon={CheckSquare} color="purple" />
|
||||
</div>
|
||||
<StatCard
|
||||
label="Jobs Completed"
|
||||
value={completedTasks}
|
||||
icon={CheckSquare}
|
||||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Task List */}
|
||||
@@ -134,13 +314,10 @@ const SubContractorDashboard = () => {
|
||||
<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' :
|
||||
@@ -156,26 +333,18 @@ const SubContractorDashboard = () => {
|
||||
</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 === '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);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
@@ -183,15 +352,13 @@ const SubContractorDashboard = () => {
|
||||
</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.</p></div>
|
||||
)}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Mobile Earnings Widget */}
|
||||
{/* Earnings Widget */}
|
||||
<div>
|
||||
<SpotlightCard className="p-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Earnings Tracker</h2>
|
||||
@@ -200,7 +367,6 @@ const SubContractorDashboard = () => {
|
||||
<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>
|
||||
@@ -211,8 +377,10 @@ const SubContractorDashboard = () => {
|
||||
<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">
|
||||
<button
|
||||
onClick={() => 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
|
||||
</button>
|
||||
</div>
|
||||
@@ -221,18 +389,23 @@ const SubContractorDashboard = () => {
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<TaskDetailsModal
|
||||
isOpen={isTaskModalOpen}
|
||||
onClose={() => setIsTaskModalOpen(false)}
|
||||
task={selectedTask}
|
||||
onUpdate={handleTaskUpdate}
|
||||
/>
|
||||
<FinancialSummaryModal
|
||||
isOpen={isFinancialModalOpen}
|
||||
onClose={() => setIsFinancialModalOpen(false)}
|
||||
role="SUBCONTRACTOR"
|
||||
data={financialData}
|
||||
<TaskDetailsModal isOpen={isTaskModalOpen} onClose={() => setIsTaskModalOpen(false)} task={selectedTask} onUpdate={handleTaskUpdate} />
|
||||
<FinancialSummaryModal isOpen={isFinancialModalOpen} onClose={() => setIsFinancialModalOpen(false)} role="SUBCONTRACTOR" data={financialData} />
|
||||
<SubDetailModal
|
||||
isOpen={detailModal.isOpen}
|
||||
onClose={() => 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}
|
||||
/>
|
||||
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user