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:
Satyam
2026-02-17 01:19:41 +05:30
parent 35cbdeb33c
commit 2eaac6b84a
44 changed files with 6857 additions and 662 deletions
+234
View File
@@ -0,0 +1,234 @@
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;