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,93 @@
|
||||
import React from 'react';
|
||||
import DocumentReviewQueue from '../../components/documents/DocumentReviewQueue';
|
||||
import { FileText, Shield, AlertTriangle, CheckCircle, Clock } from 'lucide-react';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
|
||||
const DocumentManagement = () => {
|
||||
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 flex flex-col md:flex-row justify-between items-start md:items-end shrink-0">
|
||||
<div>
|
||||
<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">Document Control</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-1">Review, approve, and manage critical business documents.</p>
|
||||
</div>
|
||||
<div className="mt-4 md:mt-0 flex space-x-3">
|
||||
<SpotlightCard className="cursor-pointer hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors" rounded="rounded-xl">
|
||||
<div className="px-4 py-2 flex items-center gap-2">
|
||||
<Shield size={16} className="text-emerald-500" />
|
||||
<span className="text-sm font-bold text-zinc-700 dark:text-zinc-300">Audit Log</span>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-sm font-bold transition-colors shadow-lg shadow-blue-600/20">
|
||||
<FileText size={16} />
|
||||
<span>Upload New</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 flex flex-col lg:flex-row gap-6 min-h-0">
|
||||
{/* Main Content: Review Queue */}
|
||||
<div className="w-full lg:w-2/3 flex flex-col min-h-0">
|
||||
<SpotlightCard className="h-full flex flex-col overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5 flex items-center gap-2">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-500/10 rounded-lg text-amber-600 dark:text-amber-400">
|
||||
<AlertTriangle size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white">Review Queue</h2>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">Documents requiring your attention</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden p-6">
|
||||
<DocumentReviewQueue />
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Sidebar: Compliance Overview */}
|
||||
<div className="w-full lg:w-1/3 flex flex-col gap-6">
|
||||
<SpotlightCard className="p-6">
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider mb-4 flex items-center gap-2">
|
||||
<Clock size={16} className="text-blue-500" />
|
||||
Quick Stats
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center py-3 border-b border-zinc-100 dark:border-white/5">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 text-sm">Pending Approval</span>
|
||||
<span className="text-xl font-bold text-amber-500">3</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-3 border-b border-zinc-100 dark:border-white/5">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 text-sm">Expiring Soon (30d)</span>
|
||||
<span className="text-xl font-bold text-red-500">1</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-3 border-b border-zinc-100 dark:border-white/5">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 text-sm">Total Documents</span>
|
||||
<span className="text-xl font-bold text-zinc-900 dark:text-white">124</span>
|
||||
</div>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
|
||||
<SpotlightCard className="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/10 dark:to-indigo-900/10 border-blue-200 dark:border-blue-500/20">
|
||||
<h3 className="text-sm font-bold text-blue-700 dark:text-blue-300 mb-2 flex items-center gap-2">
|
||||
<CheckCircle size={16} />
|
||||
Compliance Tip
|
||||
</h3>
|
||||
<p className="text-sm text-blue-600/80 dark:text-blue-200/80 leading-relaxed">
|
||||
Ensure all 1099 vendors have an updated W-9 on file before processing Q1 payments to avoid compliance flags.
|
||||
</p>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentManagement;
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import FinancialKPICards from '../../components/owner/FinancialKPICards';
|
||||
import UrgentItemsPanel from '../../components/owner/UrgentItemsPanel';
|
||||
import FinancialDetailsModal from '../../components/owner/FinancialDetailsModal';
|
||||
import ActionCenterModal from '../../components/owner/ActionCenterModal';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
|
||||
const OwnerSnapshot = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Modal States
|
||||
const [financialModal, setFinancialModal] = useState({ isOpen: false, type: 'revenue' });
|
||||
const [actionModal, setActionModal] = useState({ isOpen: false, filter: 'all' });
|
||||
|
||||
// Handlers
|
||||
const handleFinancialClick = (type) => {
|
||||
setFinancialModal({ isOpen: true, type });
|
||||
};
|
||||
|
||||
const handleActionClick = (id) => {
|
||||
// Map IDs to filter types
|
||||
const filterMap = {
|
||||
'docs': 'docs',
|
||||
'vendors': 'vendors',
|
||||
'invoices': 'invoices',
|
||||
'all': 'all'
|
||||
};
|
||||
setActionModal({ isOpen: true, filter: filterMap[id] || 'all' });
|
||||
};
|
||||
|
||||
// Get time of day for greeting
|
||||
const hour = new Date().getHours();
|
||||
const greeting = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white relative pb-20 transition-colors duration-300">
|
||||
{/* 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 p-8 max-w-7xl mx-auto space-y-8">
|
||||
|
||||
{/* Header Section */}
|
||||
<header className="flex flex-col md:flex-row justify-between items-start md:items-center border-b border-zinc-200 dark:border-white/5 pb-6">
|
||||
<div>
|
||||
<h1 className="text-4xl 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">
|
||||
{greeting}, {user?.name?.split(' ')[0] || 'Owner'}
|
||||
</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-2 font-light">Here's your business snapshot for today.</p>
|
||||
</div>
|
||||
<div className="mt-4 md:mt-0 flex space-x-3">
|
||||
<button className="px-4 py-2 bg-zinc-100 dark:bg-white/5 hover:bg-zinc-200 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/5 shadow-sm">
|
||||
Download Report
|
||||
</button>
|
||||
<button className="px-4 py-2 bg-amber-500 hover:bg-amber-600 text-black font-bold rounded-xl text-sm transition-colors shadow-lg shadow-amber-500/20">
|
||||
Quick Action
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Financial KPIs */}
|
||||
<section>
|
||||
<h2 className="text-xl font-bold text-zinc-900 dark:text-white/90 mb-4 flex items-center">
|
||||
<span className="w-2 h-8 bg-emerald-500 rounded-full mr-3"></span>
|
||||
Financial Overview
|
||||
</h2>
|
||||
<FinancialKPICards onCardClick={handleFinancialClick} />
|
||||
</section>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Urgent Items Panel (Takes up 2 columns on large screens) */}
|
||||
<section className="lg:col-span-2">
|
||||
<UrgentItemsPanel onActionClick={handleActionClick} />
|
||||
</section>
|
||||
|
||||
{/* Activity Feed / Notifications Placeholder (Right Column) */}
|
||||
<SpotlightCard className="h-full">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500 mr-2 animate-pulse"></div>
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Recent Activity</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-start space-x-3 p-3 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<div className="w-2 h-2 mt-2 rounded-full bg-blue-400 shrink-0"></div>
|
||||
<div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">New invoice submitted by <span className="text-zinc-900 dark:text-white font-bold">Texas Builders LLC</span></p>
|
||||
<span className="text-[10px] text-zinc-400 uppercase font-bold tracking-wider">2 hours ago</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-start space-x-3 p-3 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<div className="w-2 h-2 mt-2 rounded-full bg-emerald-400 shrink-0"></div>
|
||||
<div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">Payment received for <span className="text-zinc-900 dark:text-white font-bold">Project P-2602</span></p>
|
||||
<span className="text-[10px] text-zinc-400 uppercase font-bold tracking-wider">5 hours ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/owner/vendors')}
|
||||
className="w-full mt-6 py-2 text-xs font-bold uppercase tracking-wider text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white border border-zinc-200 dark:border-white/10 hover:bg-zinc-100 dark:hover:bg-white/5 rounded-xl transition-all"
|
||||
>
|
||||
View All Activity
|
||||
</button>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive Modals */}
|
||||
<FinancialDetailsModal
|
||||
isOpen={financialModal.isOpen}
|
||||
onClose={() => setFinancialModal({ ...financialModal, isOpen: false })}
|
||||
type={financialModal.type}
|
||||
/>
|
||||
<ActionCenterModal
|
||||
isOpen={actionModal.isOpen}
|
||||
onClose={() => setActionModal({ ...actionModal, isOpen: false })}
|
||||
defaultFilter={actionModal.filter}
|
||||
// Force re-render on filter change if needed, generally okay as props update
|
||||
key={actionModal.filter}
|
||||
/>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default OwnerSnapshot;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,218 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useMockStore } from '../../data/mockStore';
|
||||
import { Search, Filter, Briefcase, DollarSign, FileText, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||
import MaskedData from '../../components/MaskedData';
|
||||
import ComplianceChecklist from '../../components/people/ComplianceChecklist';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
|
||||
const VendorDirectory = () => {
|
||||
const { vendors } = useMockStore();
|
||||
const [filterType, setFilterType] = useState('all'); // all, roofing, electrical, plumbing, hvac
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedVendor, setSelectedVendor] = useState(null);
|
||||
|
||||
const filteredVendors = vendors.filter(vendor => {
|
||||
const matchesSearch = vendor.vendorName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
vendor.primaryContact.name.toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
const matchesType = filterType === 'all' || vendor.vendorType === filterType;
|
||||
|
||||
return matchesSearch && matchesType;
|
||||
});
|
||||
|
||||
// Calculate total spend for filtered view
|
||||
const totalSpend = filteredVendors.reduce((acc, v) => acc + (v.spend?.totalSpend || 0), 0);
|
||||
|
||||
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 flex flex-col md:flex-row justify-between items-start md:items-end shrink-0">
|
||||
<div>
|
||||
<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">Vendor Management</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-1">Track vendor compliance, spend, and performance.</p>
|
||||
</div>
|
||||
<SpotlightCard className="mt-4 md:mt-0 px-4 py-2 flex items-center gap-3">
|
||||
<span className="text-[10px] text-zinc-500 dark:text-zinc-400 uppercase font-bold tracking-wider">Total Spend (Visible)</span>
|
||||
<div className="h-4 w-px bg-zinc-200 dark:bg-white/10"></div>
|
||||
<p className="text-xl font-bold text-zinc-900 dark:text-white">${totalSpend.toLocaleString()}</p>
|
||||
</SpotlightCard>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 flex flex-col lg:flex-row gap-6 min-h-0">
|
||||
{/* Left Sidebar: Vendor List */}
|
||||
<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 space-y-4 bg-zinc-50/50 dark:bg-white/5">
|
||||
<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 vendors..."
|
||||
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 flex-wrap gap-2">
|
||||
{['all', 'roofing_crew', 'electrical', 'plumbing', 'hvac'].map(type => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setFilterType(type)}
|
||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-colors ${filterType === type
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
{type.replace('_', ' ')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-1">
|
||||
{filteredVendors.map((vendor) => (
|
||||
<div
|
||||
key={vendor.id}
|
||||
onClick={() => setSelectedVendor(vendor)}
|
||||
className={`p-3 rounded-xl border transition-all cursor-pointer group ${selectedVendor?.id === vendor.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 justify-between items-start">
|
||||
<div>
|
||||
<h3 className={`font-bold text-sm ${selectedVendor?.id === vendor.id ? 'text-blue-700 dark:text-blue-300' : 'text-zinc-900 dark:text-white'}`}>
|
||||
{vendor.vendorName}
|
||||
</h3>
|
||||
<p className="text-[10px] text-zinc-500 dark:text-zinc-400 uppercase tracking-wide mb-1.5">{vendor.vendorType?.replace('_', ' ')}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{vendor.status === 'active' ?
|
||||
<span className="flex items-center gap-1 text-[10px] font-bold text-emerald-600 dark:text-emerald-400"><CheckCircle size={10} /> Active</span> :
|
||||
<span className="flex items-center gap-1 text-[10px] font-bold text-red-600 dark:text-red-400"><AlertTriangle size={10} /> {vendor.status}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[10px] text-zinc-400 uppercase font-bold">YTD Spend</p>
|
||||
<p className="font-mono text-xs font-bold text-zinc-700 dark:text-zinc-300">${(vendor.spend?.totalSpend / 1000).toFixed(1)}k</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
|
||||
{/* Right Panel: Vendor Details */}
|
||||
<div className="w-full lg:w-2/3 flex flex-col min-h-0">
|
||||
{selectedVendor ? (
|
||||
<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">{selectedVendor.vendorName}</h2>
|
||||
<span className={`px-2.5 py-0.5 rounded-md text-[10px] font-bold border uppercase tracking-wider ${selectedVendor.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-red-100 text-red-700 border-red-200 dark:bg-red-500/10 dark:text-red-400 dark:border-red-500/20'
|
||||
}`}>
|
||||
{selectedVendor.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 flex items-center gap-2 text-sm font-medium">
|
||||
<Briefcase size={16} />
|
||||
{selectedVendor.vendorType?.replace('_', ' ')} • {selectedVendor.primaryContact?.name}
|
||||
</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 Vendor
|
||||
</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">
|
||||
{/* Financials & Compliance */}
|
||||
<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">
|
||||
<DollarSign size={16} className="text-emerald-500" />
|
||||
Financials & Banking
|
||||
</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">EIN / Tax ID</span>
|
||||
<MaskedData value={selectedVendor.sensitiveData?.ein} label="EIN" />
|
||||
</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">Bank Account</span>
|
||||
<MaskedData value={selectedVendor.sensitiveData?.bankAccount} label="Bank" />
|
||||
</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">Last Payment</span>
|
||||
<div className="text-right">
|
||||
<div className="text-zinc-900 dark:text-white font-bold text-sm">${selectedVendor.spend?.lastPayment?.amount.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-zinc-400">{new Date(selectedVendor.spend?.lastPayment?.date).toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-zinc-900 dark:text-white mb-4 uppercase tracking-wider">Compliance Status</h4>
|
||||
<ComplianceChecklist data={selectedVendor} type="vendor" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents & Actions */}
|
||||
<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">
|
||||
<FileText size={16} className="text-blue-500" />
|
||||
Actions
|
||||
</h3>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<button className="w-full p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:border-amber-400/50 hover:bg-zinc-100 dark:hover:bg-white/10 text-left transition-all group">
|
||||
<span className="block text-sm font-bold text-zinc-700 dark:text-zinc-300 group-hover:text-amber-600 dark:group-hover:text-amber-400 transition-colors">Request COI Update</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">Send automated email request</span>
|
||||
</button>
|
||||
<button className="w-full p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:border-blue-400/50 hover:bg-zinc-100 dark:hover:bg-white/10 text-left transition-all group">
|
||||
<span className="block text-sm font-bold text-zinc-700 dark:text-zinc-300 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">View Invoices</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">See payment history</span>
|
||||
</button>
|
||||
<button className="w-full p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:border-red-400/50 hover:bg-zinc-100 dark:hover:bg-white/10 text-left transition-all group">
|
||||
<span className="block text-sm font-bold text-zinc-700 dark:text-zinc-300 group-hover:text-red-600 dark:group-hover:text-red-400 transition-colors">Suspend Vendor</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">Restrict access to new projects</span>
|
||||
</button>
|
||||
</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">
|
||||
<Briefcase size={32} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-2">Select a Vendor</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-xs mx-auto text-sm">Choose a vendor from the list to view their compliance status, spend history, and manage their account.</p>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VendorDirectory;
|
||||
Reference in New Issue
Block a user