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,209 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, DollarSign, TrendingUp, TrendingDown, Download, Search, Filter } from 'lucide-react';
|
||||
|
||||
const FinancialSummaryModal = ({ isOpen, onClose, role, data }) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortConfig, setSortConfig] = useState({ key: 'date', direction: 'desc' });
|
||||
|
||||
// Close on Escape key
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !data) return null;
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
|
||||
};
|
||||
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Filter and sort data
|
||||
const filteredData = (data.items || []).filter(item =>
|
||||
item.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.project?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const sortedData = [...filteredData].sort((a, b) => {
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed top-0 left-0 w-screen h-[100dvh] 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 transition-opacity"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative w-full sm:max-w-5xl h-[85dvh] sm:h-auto sm:max-h-[85vh] bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-10 sm:zoom-in-95 duration-300 sm:duration-200 border-t border-x sm:border border-zinc-200 dark:border-white/10">
|
||||
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden w-full flex justify-center pt-3 pb-1 bg-zinc-50 dark:bg-white/5 border-b-0 cursor-grab active:cursor-grabbing" onClick={onClose}>
|
||||
<div className="w-12 h-1.5 rounded-full bg-zinc-300 dark:bg-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 py-4 sm:py-5 border-b border-zinc-200 dark:border-white/10 flex justify-between items-start bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg sm:text-2xl font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<DollarSign className="text-emerald-500 shrink-0" size={20} />
|
||||
<span className="truncate">{role === 'CONTRACTOR' ? 'Budget Overview' : 'Earnings Summary'}</span>
|
||||
</h2>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-xs sm:text-sm mt-1">
|
||||
{role === 'CONTRACTOR' ? 'Total Budget Managed' : 'Total Earned (YTD)'}:
|
||||
<span className="font-mono font-bold text-zinc-900 dark:text-white ml-1">{formatCurrency(data.total || 0)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3 ml-2">
|
||||
<button className="hidden sm:block p-2 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors" title="Export CSV">
|
||||
<Download size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-white/20 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4 border-b border-zinc-200 dark:border-white/10 bg-white dark:bg-[#121214]">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div className="p-3 sm:p-4 rounded-xl bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-100 dark:border-emerald-500/20">
|
||||
<div className="flex items-center gap-2 mb-1 sm:mb-2">
|
||||
<TrendingUp size={16} className="text-emerald-600 dark:text-emerald-400" />
|
||||
<span className="text-[10px] sm:text-xs font-bold uppercase text-emerald-600 dark:text-emerald-400">
|
||||
{role === 'CONTRACTOR' ? 'Total Budget' : 'Paid'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(data.paid || data.total || 0)}</p>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 rounded-xl bg-amber-50 dark:bg-amber-500/5 border border-amber-100 dark:border-amber-500/20">
|
||||
<div className="flex items-center gap-2 mb-1 sm:mb-2">
|
||||
<TrendingDown size={16} className="text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-[10px] sm:text-xs font-bold uppercase text-amber-600 dark:text-amber-400">
|
||||
{role === 'CONTRACTOR' ? 'Spent' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(data.spent || data.pending || 0)}</p>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 rounded-xl bg-blue-50 dark:bg-blue-500/5 border border-blue-100 dark:border-blue-500/20">
|
||||
<div className="flex items-center gap-2 mb-1 sm:mb-2">
|
||||
<DollarSign size={16} className="text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-[10px] sm:text-xs font-bold uppercase text-blue-600 dark:text-blue-400">
|
||||
{role === 'CONTRACTOR' ? 'Remaining' : 'Total Earned'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(data.remaining || data.total || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4 border-b border-zinc-200 dark:border-white/5 flex flex-col sm:flex-row gap-3 sm:gap-4 justify-between bg-white dark:bg-[#121214]">
|
||||
<div className="relative w-full sm:w-96">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search transactions..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-9 pr-4 py-2 rounded-xl bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-sm text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
<button className="hidden sm:flex items-center gap-2 px-4 py-2 rounded-xl border border-zinc-200 dark:border-white/10 text-sm font-medium hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
|
||||
<Filter size={16} /> Filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-auto custom-scrollbar bg-white dark:bg-[#121214]">
|
||||
<table className="w-full text-left border-collapse min-w-[600px]">
|
||||
<thead className="sticky top-0 z-10 bg-zinc-50 dark:bg-[#18181b] border-b border-zinc-200 dark:border-white/10 shadow-sm">
|
||||
<tr>
|
||||
{[
|
||||
{ key: 'date', label: 'Date' },
|
||||
{ key: 'description', label: role === 'CONTRACTOR' ? 'Project' : 'Description' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'amount', label: 'Amount', align: 'right' }
|
||||
].map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
onClick={() => handleSort(col.key)}
|
||||
className={`px-3 sm:px-6 py-3 sm:py-4 text-[10px] sm:text-xs font-bold uppercase tracking-wider text-zinc-500 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors ${col.align === 'right' ? 'text-right' : ''}`}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
|
||||
{sortedData.length > 0 ? sortedData.map((item, idx) => (
|
||||
<tr key={idx} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4 text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 font-mono whitespace-nowrap">
|
||||
{item.date}
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4">
|
||||
<div className="font-semibold text-sm sm:text-base text-zinc-900 dark:text-white">{item.description || item.project}</div>
|
||||
{item.project && <div className="text-xs text-zinc-500">{item.project}</div>}
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4">
|
||||
<span className={`px-2 sm:px-2.5 py-0.5 rounded-full text-[10px] sm:text-xs font-bold uppercase tracking-wide whitespace-nowrap ${item.status === 'paid' || item.status === 'completed'
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400'
|
||||
: 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400'
|
||||
}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4 text-right font-mono font-medium text-sm sm:text-base text-zinc-900 dark:text-white whitespace-nowrap">
|
||||
{formatCurrency(item.amount)}
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr>
|
||||
<td colSpan="4" className="py-20 text-center text-zinc-500">
|
||||
No transactions found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4 border-t border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5 flex justify-between items-center text-xs sm:text-sm">
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Showing {sortedData.length} transactions</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Total:</span>
|
||||
<span className="text-base sm:text-lg font-bold text-zinc-900 dark:text-white">{formatCurrency(data.total || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default FinancialSummaryModal;
|
||||
@@ -0,0 +1,303 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, Briefcase, Calendar, DollarSign, Users, FileText, Activity, CheckCircle, Clock, AlertTriangle } from 'lucide-react';
|
||||
|
||||
const ProjectDetailsModal = ({ isOpen, onClose, project }) => {
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
|
||||
// Close on Escape key
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !project) return null;
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
|
||||
};
|
||||
|
||||
const getMilestoneIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'completed': return <CheckCircle size={18} className="text-emerald-500" />;
|
||||
case 'in_progress': return <Clock size={18} className="text-blue-500" />;
|
||||
default: return <AlertTriangle size={18} className="text-zinc-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getMilestoneColor = (status) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400';
|
||||
case 'in_progress': return 'bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400';
|
||||
default: return 'bg-zinc-100 text-zinc-600 dark:bg-white/5 dark:text-zinc-400';
|
||||
}
|
||||
};
|
||||
|
||||
const budgetUtilization = project.budget > 0 ? Math.round(((project.spent || 0) / project.budget) * 100) : 0;
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed top-0 left-0 w-screen h-[100dvh] z-[9999] flex items-end sm:items-center justify-center sm:p-6" role="dialog" aria-modal="true" aria-labelledby="project-modal-title">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative w-full sm:max-w-5xl h-[85dvh] sm:h-auto sm:max-h-[85vh] bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-10 sm:zoom-in-95 duration-300 sm:duration-200 border-t border-x sm:border border-zinc-200 dark:border-white/10">
|
||||
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden w-full flex justify-center pt-3 pb-1 bg-zinc-50 dark:bg-white/5 border-b-0 cursor-grab active:cursor-grabbing" onClick={onClose}>
|
||||
<div className="w-12 h-1.5 rounded-full bg-zinc-300 dark:bg-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 py-5 border-b border-zinc-200 dark:border-white/10 flex justify-between items-start bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="flex-1">
|
||||
<h2 id="project-modal-title" className="text-2xl font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<Briefcase className="text-blue-500" size={24} />
|
||||
{project.address}
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-3 mt-2">
|
||||
<span className="px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400">
|
||||
{project.projectType}
|
||||
</span>
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide ${project.status === 'active' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400' :
|
||||
project.status === 'completed' ? 'bg-zinc-100 text-zinc-700 dark:bg-white/5 dark:text-zinc-400' :
|
||||
'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400'
|
||||
}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
<span className="text-sm text-zinc-500 dark:text-zinc-400 font-mono">ID: {project.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-white/20 hover:text-red-500 transition-colors"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="px-6 py-3 border-b border-zinc-200 dark:border-white/10 bg-white dark:bg-[#121214] flex gap-2 overflow-x-auto">
|
||||
{[
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'milestones', label: 'Milestones' },
|
||||
{ id: 'financials', label: 'Financials' },
|
||||
{ id: 'team', label: 'Team' }
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 rounded-full text-xs font-bold uppercase tracking-wider whitespace-nowrap transition-all ${activeTab === tab.id
|
||||
? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900 shadow-md'
|
||||
: 'bg-transparent text-zinc-500 hover:bg-zinc-100 dark:hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 bg-zinc-50 dark:bg-[#09090b]">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
{/* Key Metrics */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-500/10">
|
||||
<DollarSign size={20} className="text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<span className="text-xs font-bold uppercase text-zinc-500">Budget</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(project.budget)}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-emerald-100 dark:bg-emerald-500/10">
|
||||
<Activity size={20} className="text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<span className="text-xs font-bold uppercase text-zinc-500">Completion</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">{project.completionPercentage || 0}%</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-purple-100 dark:bg-purple-500/10">
|
||||
<Calendar size={20} className="text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<span className="text-xs font-bold uppercase text-zinc-500">Timeline</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-zinc-900 dark:text-white">{project.startDate} - {project.endDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider">Project Progress</h3>
|
||||
<span className="text-lg font-bold text-blue-600 dark:text-blue-400">{project.completionPercentage || 0}%</span>
|
||||
</div>
|
||||
<div className="h-3 w-full bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-blue-400 rounded-full transition-all duration-500"
|
||||
style={{ width: `${project.completionPercentage || 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Details */}
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider mb-4">Project Information</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Project Type:</span>
|
||||
<p className="font-semibold text-zinc-900 dark:text-white capitalize">{project.projectType}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Status:</span>
|
||||
<p className="font-semibold text-zinc-900 dark:text-white capitalize">{project.status}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Start Date:</span>
|
||||
<p className="font-semibold text-zinc-900 dark:text-white">{project.startDate}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-400">End Date:</span>
|
||||
<p className="font-semibold text-zinc-900 dark:text-white">{project.endDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'milestones' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Project Milestones</h3>
|
||||
{project.milestones && project.milestones.length > 0 ? (
|
||||
project.milestones.map((milestone, idx) => (
|
||||
<div key={idx} className="p-4 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10 hover:border-blue-500/30 transition-all">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{getMilestoneIcon(milestone.status)}
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-zinc-900 dark:text-white">{milestone.name}</h4>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Due: {milestone.dueDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide ${getMilestoneColor(milestone.status)}`}>
|
||||
{milestone.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-center py-10 text-zinc-500">No milestones defined.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'financials' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Financial Summary</h3>
|
||||
|
||||
{/* Budget Overview */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<p className="text-xs font-bold uppercase text-zinc-500 mb-2">Total Budget</p>
|
||||
<p className="text-3xl font-bold text-zinc-900 dark:text-white">{formatCurrency(project.budget)}</p>
|
||||
</div>
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<p className="text-xs font-bold uppercase text-zinc-500 mb-2">Spent</p>
|
||||
<p className="text-3xl font-bold text-amber-600 dark:text-amber-400">{formatCurrency(project.spent || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Budget Utilization */}
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider">Budget Utilization</h4>
|
||||
<span className={`text-lg font-bold ${budgetUtilization > 90 ? 'text-red-500' : budgetUtilization > 70 ? 'text-amber-500' : 'text-emerald-500'}`}>
|
||||
{budgetUtilization}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3 w-full bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${budgetUtilization > 90 ? 'bg-red-500' : budgetUtilization > 70 ? 'bg-amber-500' : 'bg-emerald-500'
|
||||
}`}
|
||||
style={{ width: `${budgetUtilization}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoices */}
|
||||
{project.invoices && project.invoices.length > 0 && (
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<h4 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider mb-4">Invoices</h4>
|
||||
<div className="space-y-3">
|
||||
{project.invoices.map((invoice, idx) => (
|
||||
<div key={idx} className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5 last:border-0">
|
||||
<div>
|
||||
<p className="font-semibold text-zinc-900 dark:text-white text-sm">Invoice #{invoice.id}</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">Due: {invoice.dueDate}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-zinc-900 dark:text-white">{formatCurrency(invoice.amount)}</p>
|
||||
<span className={`text-xs font-bold uppercase ${invoice.status === 'paid' ? 'text-emerald-500' : 'text-amber-500'
|
||||
}`}>
|
||||
{invoice.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'team' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Project Team</h3>
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-500/20 flex items-center justify-center">
|
||||
<Users size={24} className="text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">Contractor ID</p>
|
||||
<p className="font-bold text-zinc-900 dark:text-white">{project.contractorId || 'Not Assigned'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{project.subcontractorIds && project.subcontractorIds.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-zinc-200 dark:border-white/10">
|
||||
<p className="text-sm font-bold text-zinc-900 dark:text-white mb-2">Subcontractors</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.subcontractorIds.map((subId, idx) => (
|
||||
<span key={idx} className="px-3 py-1 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-500/10 dark:text-purple-400 text-xs font-bold">
|
||||
{subId}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetailsModal;
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, CheckSquare, MapPin, Calendar, Camera, Clock, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
|
||||
const TaskDetailsModal = ({ isOpen, onClose, task, onUpdate }) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
|
||||
// Close on Escape key
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !task) return null;
|
||||
|
||||
const handleAction = (actionType) => {
|
||||
setIsSubmitting(true);
|
||||
// Mock API call
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
setIsSuccess(true);
|
||||
if (onUpdate) onUpdate(task.id, actionType);
|
||||
// Auto close after success
|
||||
setTimeout(() => {
|
||||
setIsSuccess(false);
|
||||
onClose();
|
||||
}, 1500);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (isSuccess) {
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<div className="relative bg-white dark:bg-[#121214] rounded-2xl p-8 flex flex-col items-center animate-in zoom-in-95 duration-200">
|
||||
<div className="w-16 h-16 bg-emerald-100 dark:bg-emerald-500/20 rounded-full flex items-center justify-center text-emerald-600 dark:text-emerald-400 mb-4">
|
||||
<CheckCircle size={32} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white">Task Updated!</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-2">Your changes have been saved successfully.</p>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400';
|
||||
case 'in_progress': return 'bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400';
|
||||
default: return 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400';
|
||||
}
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed top-0 left-0 w-screen h-[100dvh] z-[10000] flex items-end sm:items-center justify-center sm:p-6">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="relative w-full sm:max-w-lg h-[85dvh] sm:h-auto bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-0 sm:scale-95 duration-300 sm:duration-200 border-t border-x sm:border border-zinc-200 dark:border-white/10">
|
||||
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden w-full flex justify-center pt-3 pb-1 bg-zinc-50 dark:bg-white/5 border-b-0 cursor-grab active:cursor-grabbing" onClick={onClose}>
|
||||
<div className="w-12 h-1.5 rounded-full bg-zinc-300 dark:bg-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-zinc-200 dark:border-white/10 flex justify-between items-center bg-zinc-50/50 dark:bg-white/5">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<CheckSquare className="text-blue-500" size={20} />
|
||||
Task Details
|
||||
</h2>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 font-mono mt-0.5">{task.id}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-white/20 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6 overflow-y-auto flex-1 space-y-6">
|
||||
{/* Task Info Card */}
|
||||
<div className="p-5 rounded-xl bg-blue-50 dark:bg-blue-500/5 border border-blue-100 dark:border-blue-500/20">
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-3">{task.name}</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<MapPin size={16} className="mr-2 text-blue-500" />
|
||||
<span className="font-medium">{task.projectAddress}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<Calendar size={16} className="mr-2 text-blue-500" />
|
||||
<span>Due: <span className="font-bold text-zinc-900 dark:text-white">{task.dueDate}</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Status:</span>
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide ${getStatusColor(task.status)}`}>
|
||||
{task.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<h4 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||
<AlertCircle size={16} className="text-amber-500" />
|
||||
Instructions
|
||||
</h4>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">
|
||||
Complete the assigned task according to project specifications. Ensure all safety protocols are followed and document progress with photos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<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/Out</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Time Tracking (Mock) */}
|
||||
<div className="p-5 rounded-xl bg-white dark:bg-[#121214] border border-zinc-200 dark:border-white/10">
|
||||
<h4 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider mb-3">Time Tracking</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-500">Today:</span>
|
||||
<span className="font-bold text-zinc-900 dark:text-white">4.5 hrs</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-500">This Week:</span>
|
||||
<span className="font-bold text-zinc-900 dark:text-white">18 hrs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="p-6 border-t border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5 flex gap-4">
|
||||
{task.status !== 'completed' ? (
|
||||
<>
|
||||
<button
|
||||
className="flex-1 px-4 py-3 rounded-xl bg-zinc-100 dark:bg-white/10 text-zinc-700 dark:text-zinc-300 font-bold hover:bg-zinc-200 dark:hover:bg-white/20 transition-colors"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 px-4 py-3 rounded-xl bg-emerald-500 text-white font-bold hover:bg-emerald-600 transition-colors shadow-lg shadow-emerald-500/20"
|
||||
onClick={() => handleAction('complete')}
|
||||
>
|
||||
{isSubmitting ? 'Updating...' : 'Mark Complete'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="w-full px-4 py-3 rounded-xl bg-zinc-100 dark:bg-white/10 text-zinc-700 dark:text-zinc-300 font-bold hover:bg-zinc-200 dark:hover:bg-white/20 transition-colors"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetailsModal;
|
||||
Reference in New Issue
Block a user