Files
LynkedUpPro_CRM/src/components/vendor/ComplianceDetailsModal.jsx
T
Satyam 2eaac6b84a 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
2026-02-17 01:23:06 +05:30

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;