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
235 lines
18 KiB
React
235 lines
18 KiB
React
import React, { useState } from 'react';
|
|
import { useMockStore } from '../../data/mockStore';
|
|
import { Search, Filter, Shield, User, Briefcase, Zap } from 'lucide-react';
|
|
import MaskedData from '../../components/MaskedData';
|
|
import ComplianceChecklist from '../../components/people/ComplianceChecklist';
|
|
import { SpotlightCard } from '../../components/SpotlightCard';
|
|
|
|
const PeopleDirectory = () => {
|
|
const { personnel, vendors } = useMockStore();
|
|
const [filter, setFilter] = useState('all'); // all, employee, contractor, subcontractor
|
|
const [search, setSearch] = useState('');
|
|
const [selectedPerson, setSelectedPerson] = useState(null);
|
|
|
|
// Combine and normalize data for display
|
|
const allPeople = [
|
|
...personnel.map(p => ({ ...p, type: 'personnel', category: 'Internal Team' })),
|
|
...vendors.map(v => ({ ...v, name: v.vendorName, type: 'vendor', category: 'Vendor/Partner' }))
|
|
];
|
|
|
|
const filteredPeople = allPeople.filter(person => {
|
|
const matchesSearch = person.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
person.email?.toLowerCase().includes(search.toLowerCase());
|
|
|
|
if (filter === 'all') return matchesSearch;
|
|
if (filter === 'employee') return matchesSearch && person.type === 'personnel';
|
|
if (filter === 'contractor') return matchesSearch && person.role === 'CONTRACTOR'; // Assuming role field exists
|
|
if (filter === 'subcontractor') return matchesSearch && person.role === 'SUBCONTRACTOR'; // Assuming role field exists
|
|
|
|
return matchesSearch;
|
|
});
|
|
|
|
return (
|
|
<div className="h-screen flex flex-col bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white transition-colors duration-300 overflow-hidden relative">
|
|
{/* Ambient Background Glows */}
|
|
<div className="fixed top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0">
|
|
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-purple-500/5 dark:bg-purple-900/10 rounded-full blur-[120px]" />
|
|
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-blue-500/5 dark:bg-blue-900/10 rounded-full blur-[120px]" />
|
|
</div>
|
|
|
|
<div className="relative z-10 flex-1 flex flex-col min-h-0 max-w-7xl mx-auto w-full p-4 md:p-6 lg:p-8">
|
|
<header className="mb-6 shrink-0">
|
|
<h1 className="text-3xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-zinc-900 to-zinc-600 dark:from-white dark:to-white/60 tracking-tight">People Directory</h1>
|
|
<p className="text-zinc-500 dark:text-zinc-400 mt-1">Manage your team, contractors, and compliance records.</p>
|
|
</header>
|
|
|
|
<div className="flex-1 flex flex-col lg:flex-row gap-6 min-h-0">
|
|
{/* Left Sidebar: List & Search */}
|
|
<SpotlightCard className="w-full lg:w-1/3 flex flex-col overflow-hidden">
|
|
<div className="p-4 border-b border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5 space-y-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
|
<input
|
|
type="text"
|
|
placeholder="Search by name or email..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="w-full bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm text-zinc-900 dark:text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar">
|
|
<button
|
|
onClick={() => setFilter('all')}
|
|
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider whitespace-nowrap transition-colors ${filter === 'all'
|
|
? 'bg-zinc-900 text-white dark:bg-white dark:text-black'
|
|
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:hover:bg-white/10'
|
|
}`}
|
|
>
|
|
All
|
|
</button>
|
|
<button
|
|
onClick={() => setFilter('employee')}
|
|
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider whitespace-nowrap transition-colors ${filter === 'employee'
|
|
? 'bg-blue-600 text-white dark:bg-blue-500 dark:text-white'
|
|
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:hover:bg-white/10'
|
|
}`}
|
|
>
|
|
Employees
|
|
</button>
|
|
<button
|
|
onClick={() => setFilter('contractor')}
|
|
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider whitespace-nowrap transition-colors ${filter === 'contractor'
|
|
? 'bg-purple-600 text-white dark:bg-purple-500 dark:text-white'
|
|
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:hover:bg-white/10'
|
|
}`}
|
|
>
|
|
Contractors
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-1">
|
|
{filteredPeople.map((person) => (
|
|
<div
|
|
key={person.id}
|
|
onClick={() => setSelectedPerson(person)}
|
|
className={`p-3 rounded-xl border transition-all cursor-pointer group ${selectedPerson?.id === person.id
|
|
? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/20 shadow-sm'
|
|
: 'bg-transparent border-transparent hover:bg-zinc-100 dark:hover:bg-white/5'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm ${person.type === 'personnel'
|
|
? 'bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400 ring-2 ring-white dark:ring-zinc-900 group-hover:scale-110 transition-transform'
|
|
: 'bg-purple-100 text-purple-600 dark:bg-purple-500/20 dark:text-purple-400 ring-2 ring-white dark:ring-zinc-900 group-hover:scale-110 transition-transform'
|
|
}`}>
|
|
{person.name.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<h3 className={`font-bold text-sm ${selectedPerson?.id === person.id ? 'text-blue-700 dark:text-blue-300' : 'text-zinc-900 dark:text-white'}`}>
|
|
{person.name}
|
|
</h3>
|
|
<p className="text-[10px] text-zinc-500 dark:text-zinc-400 uppercase tracking-wide">{person.role || person.vendorType}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</SpotlightCard>
|
|
|
|
{/* Right Panel: Details */}
|
|
<div className="w-full lg:w-2/3 flex flex-col min-h-0">
|
|
{selectedPerson ? (
|
|
<SpotlightCard className="h-full flex flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="p-8 border-b border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<h2 className="text-3xl font-bold text-zinc-900 dark:text-white">{selectedPerson.name}</h2>
|
|
<span className={`px-2.5 py-0.5 rounded-md text-[10px] font-bold border uppercase tracking-wider ${selectedPerson.status === 'active'
|
|
? 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20'
|
|
: 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20'
|
|
}`}>
|
|
{selectedPerson.status}
|
|
</span>
|
|
</div>
|
|
<p className="text-zinc-500 dark:text-zinc-400 flex items-center gap-2 text-sm font-medium">
|
|
<Briefcase size={16} />
|
|
{selectedPerson.category} • {selectedPerson.role || selectedPerson.vendorType}
|
|
</p>
|
|
</div>
|
|
<button className="px-4 py-2 bg-white dark:bg-white/5 hover:bg-zinc-50 dark:hover:bg-white/10 rounded-xl text-sm font-bold text-zinc-700 dark:text-zinc-300 transition-colors border border-zinc-200 dark:border-white/10 shadow-sm">
|
|
Edit Profile
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
{/* Sensitive Data Section */}
|
|
<div className="space-y-8">
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider border-b border-zinc-200 dark:border-white/10 pb-2 flex items-center gap-2">
|
|
<Shield size={16} className="text-blue-500" />
|
|
Sensitive Information
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
|
<span className="text-zinc-500 dark:text-zinc-400 text-sm">Email</span>
|
|
<span className="text-zinc-900 dark:text-white text-sm font-medium">{selectedPerson.email || selectedPerson.primaryContact?.email}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
|
<span className="text-zinc-500 dark:text-zinc-400 text-sm">Phone</span>
|
|
<span className="text-zinc-900 dark:text-white text-sm font-medium">{selectedPerson.phone || selectedPerson.primaryContact?.phone}</span>
|
|
</div>
|
|
|
|
{/* Masked Fields */}
|
|
{selectedPerson.sensitiveData?.ssn && (
|
|
<div className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
|
<span className="text-zinc-500 dark:text-zinc-400 text-sm">SSN</span>
|
|
<MaskedData value={selectedPerson.sensitiveData.ssn} label="SSN" />
|
|
</div>
|
|
)}
|
|
{selectedPerson.sensitiveData?.ein && (
|
|
<div className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
|
<span className="text-zinc-500 dark:text-zinc-400 text-sm">EIN</span>
|
|
<MaskedData value={selectedPerson.sensitiveData.ein} label="EIN" />
|
|
</div>
|
|
)}
|
|
{selectedPerson.sensitiveData?.bankAccount && (
|
|
<div className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
|
<span className="text-zinc-500 dark:text-zinc-400 text-sm">Bank Account</span>
|
|
<MaskedData value={selectedPerson.sensitiveData.bankAccount} label="Bank" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Compliance Section */}
|
|
<div className="space-y-8">
|
|
<div>
|
|
<h3 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider border-b border-zinc-200 dark:border-white/10 pb-2 flex items-center gap-2 mb-4">
|
|
<Zap size={16} className="text-amber-500" />
|
|
Compliance & Documents
|
|
</h3>
|
|
<ComplianceChecklist data={selectedPerson} type={selectedPerson.type} />
|
|
|
|
<div className="mt-4 p-4 rounded-xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20">
|
|
<h4 className="text-xs font-bold text-blue-600 dark:text-blue-300 mb-3 uppercase tracking-wider">Quick Actions</h4>
|
|
<div className="flex gap-3">
|
|
<button className="flex-1 px-3 py-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white rounded-lg text-xs font-bold uppercase transition-colors shadow-sm">
|
|
Request Document
|
|
</button>
|
|
<button className="flex-1 px-3 py-2 bg-white hover:bg-zinc-50 dark:bg-white/5 dark:hover:bg-white/10 rounded-lg text-xs font-bold uppercase transition-colors text-zinc-600 dark:text-zinc-300 border border-zinc-200 dark:border-white/10">
|
|
View Audit Log
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SpotlightCard>
|
|
) : (
|
|
<SpotlightCard className="h-full border-dashed border-zinc-300 dark:border-white/10 bg-zinc-50/50 dark:bg-white/5 flex items-center justify-center p-8 text-center">
|
|
<div>
|
|
<div className="w-16 h-16 bg-zinc-100 dark:bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-zinc-400">
|
|
<User size={32} />
|
|
</div>
|
|
<h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-2">Select a Person</h3>
|
|
<p className="text-zinc-500 dark:text-zinc-400 max-w-xs mx-auto text-sm">Select an employee, contractor, or vendor from the list to view their detailed profile.</p>
|
|
</div>
|
|
</SpotlightCard>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PeopleDirectory;
|