2eaac6b84a
- 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
223 lines
12 KiB
React
223 lines
12 KiB
React
import React from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { X, Shield, FileText, CheckCircle, AlertCircle, Clock, Upload, Calendar } from 'lucide-react';
|
|
|
|
const ComplianceDetailsModal = ({ isOpen, onClose, vendorData }) => {
|
|
// 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 || !vendorData) return null;
|
|
|
|
const compliance = vendorData.compliance || {};
|
|
|
|
const getDocumentStatus = (doc) => {
|
|
if (!doc || !doc.uploaded) return 'missing';
|
|
if (doc.status === 'expired') return 'expired';
|
|
if (doc.status === 'approved' || doc.status === 'compliant') return 'compliant';
|
|
return 'pending';
|
|
};
|
|
|
|
const getStatusColor = (status) => {
|
|
switch (status) {
|
|
case 'compliant':
|
|
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20';
|
|
case 'pending':
|
|
return 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400 border-amber-200 dark:border-amber-500/20';
|
|
case 'expired':
|
|
case 'missing':
|
|
return 'bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400 border-red-200 dark:border-red-500/20';
|
|
default:
|
|
return 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-400 border-zinc-200 dark:border-white/10';
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status) => {
|
|
switch (status) {
|
|
case 'compliant':
|
|
return <CheckCircle size={20} className="text-emerald-500" />;
|
|
case 'pending':
|
|
return <Clock size={20} className="text-amber-500" />;
|
|
case 'expired':
|
|
case 'missing':
|
|
return <AlertCircle size={20} className="text-red-500" />;
|
|
default:
|
|
return <FileText size={20} className="text-zinc-400" />;
|
|
}
|
|
};
|
|
|
|
const documents = [
|
|
{
|
|
id: 'w9',
|
|
name: 'W-9 Form',
|
|
description: 'Tax identification form',
|
|
data: compliance.w9,
|
|
required: true
|
|
},
|
|
{
|
|
id: 'coi',
|
|
name: 'Certificate of Insurance',
|
|
description: 'General liability and workers comp',
|
|
data: compliance.coi,
|
|
required: true,
|
|
expirationDate: compliance.coi?.expirationDate
|
|
},
|
|
{
|
|
id: 'agreement',
|
|
name: 'Subcontractor Agreement',
|
|
description: 'Signed service agreement',
|
|
data: compliance.subcontractorAgreement,
|
|
required: true
|
|
}
|
|
];
|
|
|
|
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-3xl 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 bg-zinc-50/50 dark:bg-white/5">
|
|
<div className="flex justify-between items-start gap-4">
|
|
<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">
|
|
<Shield className="text-blue-500 shrink-0" size={24} />
|
|
<span className="truncate">Compliance Status</span>
|
|
</h2>
|
|
<p className="text-zinc-500 dark:text-zinc-400 text-xs sm:text-sm mt-1">
|
|
Document verification and compliance tracking
|
|
</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={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar bg-white dark:bg-[#121214]">
|
|
<div className="p-4 sm:p-6 space-y-6">
|
|
|
|
{/* Overall Status */}
|
|
<div className={`p-4 rounded-xl border ${vendorData.status === 'active'
|
|
? 'bg-emerald-50 dark:bg-emerald-500/5 border-emerald-200 dark:border-emerald-500/20'
|
|
: 'bg-red-50 dark:bg-red-500/5 border-red-200 dark:border-red-500/20'
|
|
}`}>
|
|
<div className="flex items-center gap-3">
|
|
{vendorData.status === 'active' ? (
|
|
<CheckCircle size={24} className="text-emerald-600 dark:text-emerald-400" />
|
|
) : (
|
|
<AlertCircle size={24} className="text-red-600 dark:text-red-400" />
|
|
)}
|
|
<div>
|
|
<h3 className="font-bold text-zinc-900 dark:text-white">
|
|
{vendorData.status === 'active' ? 'Compliance Verified' : 'Action Required'}
|
|
</h3>
|
|
<p className="text-sm text-zinc-600 dark:text-zinc-300 mt-1">
|
|
{vendorData.status === 'active'
|
|
? 'All required documents are up to date'
|
|
: 'Some documents need attention'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Documents List */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Required Documents</h3>
|
|
|
|
{documents.map((doc) => {
|
|
const status = getDocumentStatus(doc.data);
|
|
return (
|
|
<div key={doc.id} className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
|
<div className="flex items-start gap-4">
|
|
<div className="p-3 rounded-lg bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/10">
|
|
{getStatusIcon(status)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-4 mb-2">
|
|
<div className="flex-1">
|
|
<h4 className="font-semibold text-zinc-900 dark:text-white">{doc.name}</h4>
|
|
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">{doc.description}</p>
|
|
</div>
|
|
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide border whitespace-nowrap ${getStatusColor(status)}`}>
|
|
{status}
|
|
</span>
|
|
</div>
|
|
|
|
{doc.expirationDate && (
|
|
<div className="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-300 mt-2">
|
|
<Calendar size={14} />
|
|
<span>Expires: {doc.expirationDate}</span>
|
|
</div>
|
|
)}
|
|
|
|
{status === 'missing' && (
|
|
<button className="mt-3 flex items-center gap-2 px-3 py-1.5 rounded-lg bg-blue-600 text-white text-xs font-medium hover:bg-blue-500 transition-colors">
|
|
<Upload size={14} />
|
|
Upload Document
|
|
</button>
|
|
)}
|
|
|
|
{status === 'expired' && (
|
|
<button className="mt-3 flex items-center gap-2 px-3 py-1.5 rounded-lg bg-amber-600 text-white text-xs font-medium hover:bg-amber-500 transition-colors">
|
|
<Upload size={14} />
|
|
Upload Renewal
|
|
</button>
|
|
)}
|
|
|
|
{status === 'compliant' && doc.data?.uploaded && (
|
|
<div className="mt-3 flex items-center gap-2 text-xs text-emerald-600 dark:text-emerald-400">
|
|
<CheckCircle size={14} />
|
|
<span>Verified and approved</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Upload Center */}
|
|
<div className="p-6 rounded-xl border-2 border-dashed border-zinc-200 dark:border-white/10 bg-zinc-50/50 dark:bg-white/5 text-center">
|
|
<Upload size={32} className="mx-auto text-zinc-400 mb-3" />
|
|
<h4 className="font-semibold text-zinc-900 dark:text-white mb-1">Upload Documents</h4>
|
|
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-4">
|
|
Drag and drop files here or click to browse
|
|
</p>
|
|
<button className="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-500 transition-colors">
|
|
Choose Files
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
);
|
|
};
|
|
|
|
export default ComplianceDetailsModal;
|