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:
+241
-231
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { useMockStore } from '../data/mockStore';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import { Calendar, User, Clock, MapPin, MoreHorizontal, Filter, X, ChevronDown } from 'lucide-react';
|
||||
import { SpotlightCard } from '../components/SpotlightCard';
|
||||
|
||||
const AdminSchedule = () => {
|
||||
const { meetings } = useMockStore();
|
||||
@@ -122,244 +123,253 @@ const AdminSchedule = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white mb-2">Team Schedule</h1>
|
||||
<p className="text-zinc-600 dark:text-slate-400">Manage field agent appointments</p>
|
||||
</header>
|
||||
|
||||
{/* Filters Section */}
|
||||
<div className="mb-6 flex flex-wrap gap-4 items-center">
|
||||
{/* Date Filter */}
|
||||
<div className="relative" ref={dateDropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowDateDropdown(!showDateDropdown)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg text-sm font-medium text-zinc-700 dark:text-slate-200 hover:bg-zinc-50 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
<Calendar size={16} />
|
||||
{dateFilter === 'all' ? 'All Dates' :
|
||||
dateFilter === 'today' ? 'Today' :
|
||||
dateFilter === 'week' ? 'This Week' :
|
||||
dateFilter === 'month' ? 'This Month' :
|
||||
dateFilter === 'quarter' ? 'This Quarter' :
|
||||
'Custom Range'}
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
|
||||
{showDateDropdown && (
|
||||
<div className="absolute top-full mt-2 left-0 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg shadow-xl z-50 min-w-[200px]">
|
||||
<button
|
||||
onClick={() => { setDateFilter('all'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
All Dates
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('today'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('week'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
This Week
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('month'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
This Month
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('quarter'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
This Quarter
|
||||
</button>
|
||||
<div className="border-t border-zinc-200 dark:border-slate-700 p-3">
|
||||
<p className="text-xs font-semibold text-zinc-500 dark:text-slate-400 mb-2">Custom Range</p>
|
||||
<input
|
||||
type="date"
|
||||
value={customDateRange.start}
|
||||
onChange={(e) => setCustomDateRange(prev => ({ ...prev, start: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-xs bg-white dark:bg-slate-900 border border-zinc-200 dark:border-slate-600 rounded mb-2 text-zinc-900 dark:text-white"
|
||||
placeholder="Start date"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={customDateRange.end}
|
||||
onChange={(e) => setCustomDateRange(prev => ({ ...prev, end: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-xs bg-white dark:bg-slate-900 border border-zinc-200 dark:border-slate-600 rounded mb-2 text-zinc-900 dark:text-white"
|
||||
placeholder="End date"
|
||||
/>
|
||||
<button
|
||||
onClick={() => { setDateFilter('custom'); setShowDateDropdown(false); }}
|
||||
disabled={!customDateRange.start || !customDateRange.end}
|
||||
className="w-full px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="relative" ref={statusDropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowStatusDropdown(!showStatusDropdown)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg text-sm font-medium text-zinc-700 dark:text-slate-200 hover:bg-zinc-50 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
<Filter size={16} />
|
||||
Status {statusFilter.length > 0 && `(${statusFilter.length})`}
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
|
||||
{showStatusDropdown && (
|
||||
<div className="absolute top-full mt-2 left-0 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg shadow-xl z-50 min-w-[200px]">
|
||||
{availableStatuses.map(status => (
|
||||
<label
|
||||
key={status}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 cursor-pointer transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={statusFilter.includes(status)}
|
||||
onChange={() => toggleStatusFilter(status)}
|
||||
className="rounded border-zinc-300 dark:border-slate-600"
|
||||
/>
|
||||
{status}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Clear Filters */}
|
||||
{(dateFilter !== 'all' || statusFilter.length > 0) && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
Clear Filters
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Results Count */}
|
||||
<div className="ml-auto text-sm text-zinc-600 dark:text-slate-400">
|
||||
Showing {filteredMeetings.length} of {meetings.length} appointments
|
||||
</div>
|
||||
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white selection:bg-blue-500/30 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="bg-white dark:bg-slate-900 border border-zinc-200 dark:border-slate-800 rounded-3xl shadow-xl overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-200 dark:border-slate-800">
|
||||
<h2 className="text-xl font-bold text-zinc-900 dark:text-white flex items-center">
|
||||
<Calendar size={20} className="text-blue-500 mr-2" />
|
||||
All Active Appointments
|
||||
</h2>
|
||||
{/* Content */}
|
||||
<div className="relative z-10 p-8 max-w-7xl mx-auto space-y-8">
|
||||
<header>
|
||||
<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 mb-2 tracking-tight">Team Schedule</h1>
|
||||
<p className="text-zinc-600 dark:text-zinc-400">Manage field agent appointments</p>
|
||||
</header>
|
||||
|
||||
{/* Filters Section */}
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
{/* Date Filter */}
|
||||
<div className="relative" ref={dateDropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowDateDropdown(!showDateDropdown)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/[0.04] border border-zinc-200 dark:border-white/10 rounded-lg text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/[0.07] transition-colors"
|
||||
>
|
||||
<Calendar size={16} />
|
||||
{dateFilter === 'all' ? 'All Dates' :
|
||||
dateFilter === 'today' ? 'Today' :
|
||||
dateFilter === 'week' ? 'This Week' :
|
||||
dateFilter === 'month' ? 'This Month' :
|
||||
dateFilter === 'quarter' ? 'This Quarter' :
|
||||
'Custom Range'}
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
|
||||
{showDateDropdown && (
|
||||
<div className="absolute top-full mt-2 left-0 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700/50 rounded-lg backdrop-blur-xl shadow-2xl z-50 min-w-[200px]">
|
||||
<button
|
||||
onClick={() => { setDateFilter('all'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
All Dates
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('today'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('week'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
This Week
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('month'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
This Month
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setDateFilter('quarter'); setShowDateDropdown(false); }}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
This Quarter
|
||||
</button>
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-700/50 p-3">
|
||||
<p className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 mb-2">Custom Range</p>
|
||||
<input
|
||||
type="date"
|
||||
value={customDateRange.start}
|
||||
onChange={(e) => setCustomDateRange(prev => ({ ...prev, start: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-xs bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded mb-2 text-zinc-900 dark:text-white"
|
||||
placeholder="Start date"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={customDateRange.end}
|
||||
onChange={(e) => setCustomDateRange(prev => ({ ...prev, end: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-xs bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded mb-2 text-zinc-900 dark:text-white"
|
||||
placeholder="End date"
|
||||
/>
|
||||
<button
|
||||
onClick={() => { setDateFilter('custom'); setShowDateDropdown(false); }}
|
||||
disabled={!customDateRange.start || !customDateRange.end}
|
||||
className="w-full px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="relative" ref={statusDropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowStatusDropdown(!showStatusDropdown)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/[0.04] border border-zinc-200 dark:border-white/10 rounded-lg text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/[0.07] transition-colors"
|
||||
>
|
||||
<Filter size={16} />
|
||||
Status {statusFilter.length > 0 && `(${statusFilter.length})`}
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
|
||||
{showStatusDropdown && (
|
||||
<div className="absolute top-full mt-2 left-0 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700/50 rounded-lg backdrop-blur-xl shadow-2xl z-50 min-w-[200px]">
|
||||
{availableStatuses.map(status => (
|
||||
<label
|
||||
key={status}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={statusFilter.includes(status)}
|
||||
onChange={() => toggleStatusFilter(status)}
|
||||
className="rounded border-zinc-300 dark:border-zinc-600"
|
||||
/>
|
||||
{status}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Clear Filters */}
|
||||
{(dateFilter !== 'all' || statusFilter.length > 0) && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
Clear Filters
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Results Count */}
|
||||
<div className="ml-auto text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Showing {filteredMeetings.length} of {meetings.length} appointments
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm text-zinc-600 dark:text-slate-400">
|
||||
<thead className="bg-zinc-100 dark:bg-slate-950 text-zinc-700 dark:text-slate-200 uppercase font-bold text-xs">
|
||||
<tr>
|
||||
<th className="px-6 py-4 min-w-[150px]">Agent</th>
|
||||
<th className="px-6 py-4 min-w-[180px]">Customer</th>
|
||||
<th className="px-6 py-4 min-w-[120px]">Property</th>
|
||||
<th className="px-6 py-4 min-w-[160px]">Date & Time</th>
|
||||
<th className="px-6 py-4 min-w-[120px]">Status</th>
|
||||
<th className="px-6 py-4 text-right min-w-[150px]">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-200 dark:divide-slate-800">
|
||||
{filteredMeetings.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="6" className="px-6 py-12 text-center text-zinc-500 dark:text-slate-400">
|
||||
No appointments found matching the selected filters.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredMeetings.map((meeting) => (
|
||||
<tr key={meeting.id} className="hover:bg-zinc-50 dark:hover:bg-slate-800/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-200 dark:bg-slate-700 flex items-center justify-center text-zinc-700 dark:text-slate-300 font-bold text-xs">
|
||||
{meeting.agentId}
|
||||
</div>
|
||||
<span className="font-medium text-zinc-900 dark:text-slate-200 whitespace-normal">Agent {meeting.agentId}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-zinc-900 dark:text-slate-300 whitespace-normal">{meeting.customerName}</td>
|
||||
<td className="px-6 py-4 text-zinc-700 dark:text-slate-400">{meeting.propertyId}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-zinc-900 dark:text-slate-200">{meeting.date}</span>
|
||||
<span className="text-xs text-zinc-600 dark:text-slate-400">{meeting.time}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-1 text-xs font-semibold rounded whitespace-nowrap ${meeting.status === 'Completed' ? 'bg-green-500/20 text-green-600 dark:text-green-400' :
|
||||
meeting.status === 'Scheduled' ? 'bg-blue-500/20 text-blue-600 dark:text-blue-400' :
|
||||
meeting.status === 'Converted' ? 'bg-purple-500/20 text-purple-600 dark:text-purple-400' :
|
||||
meeting.status === 'Cancelled' ? 'bg-red-500/20 text-red-600 dark:text-red-400' :
|
||||
'bg-yellow-500/20 text-yellow-600 dark:text-yellow-400'
|
||||
}`}>
|
||||
{meeting.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<button className="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 text-xs font-bold">
|
||||
Reschedule
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setOpenDropdownId(openDropdownId === meeting.id ? null : meeting.id)}
|
||||
className="text-zinc-600 dark:text-slate-400 hover:text-zinc-900 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</button>
|
||||
<SpotlightCard className="overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<h2 className="text-xl font-bold text-zinc-900 dark:text-white flex items-center">
|
||||
<Calendar size={20} className="text-blue-500 mr-2" />
|
||||
All Active Appointments
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{openDropdownId === meeting.id && (
|
||||
<div className="absolute right-0 top-full mt-1 bg-white dark:bg-slate-800 border border-zinc-200 dark:border-slate-700 rounded-lg shadow-xl z-50 min-w-[160px]">
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'view')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'reschedule')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
Reschedule
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'complete')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-slate-200 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors"
|
||||
>
|
||||
Mark Complete
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'cancel')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<thead className="bg-zinc-100 dark:bg-zinc-950 text-zinc-700 dark:text-zinc-200 uppercase font-bold text-xs">
|
||||
<tr>
|
||||
<th className="px-6 py-4 min-w-[150px]">Agent</th>
|
||||
<th className="px-6 py-4 min-w-[180px]">Customer</th>
|
||||
<th className="px-6 py-4 min-w-[120px]">Property</th>
|
||||
<th className="px-6 py-4 min-w-[160px]">Date & Time</th>
|
||||
<th className="px-6 py-4 min-w-[120px]">Status</th>
|
||||
<th className="px-6 py-4 text-right min-w-[150px]">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
{filteredMeetings.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="6" className="px-6 py-12 text-center text-zinc-500 dark:text-zinc-400">
|
||||
No appointments found matching the selected filters.
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
filteredMeetings.map((meeting) => (
|
||||
<tr key={meeting.id} className="hover:bg-zinc-50 dark:hover:bg-white/[0.03] transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-200 dark:bg-zinc-700 flex items-center justify-center text-zinc-700 dark:text-zinc-300 font-bold text-xs">
|
||||
{meeting.agentId}
|
||||
</div>
|
||||
<span className="font-medium text-zinc-900 dark:text-zinc-200 whitespace-normal">Agent {meeting.agentId}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-zinc-900 dark:text-zinc-300 whitespace-normal">{meeting.customerName}</td>
|
||||
<td className="px-6 py-4 text-zinc-700 dark:text-zinc-400">{meeting.propertyId}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-zinc-900 dark:text-zinc-200">{meeting.date}</span>
|
||||
<span className="text-xs text-zinc-600 dark:text-zinc-400">{meeting.time}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-1 text-xs font-semibold rounded whitespace-nowrap ${meeting.status === 'Completed' ? 'bg-green-500/20 text-green-600 dark:text-green-400' :
|
||||
meeting.status === 'Scheduled' ? 'bg-blue-500/20 text-blue-600 dark:text-blue-400' :
|
||||
meeting.status === 'Converted' ? 'bg-purple-500/20 text-purple-600 dark:text-purple-400' :
|
||||
meeting.status === 'Cancelled' ? 'bg-red-500/20 text-red-600 dark:text-red-400' :
|
||||
'bg-yellow-500/20 text-yellow-600 dark:text-yellow-400'
|
||||
}`}>
|
||||
{meeting.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<button className="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 text-xs font-bold">
|
||||
Reschedule
|
||||
</button>
|
||||
<div className="relative action-dropdown-container">
|
||||
<button
|
||||
onClick={() => setOpenDropdownId(openDropdownId === meeting.id ? null : meeting.id)}
|
||||
className="text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 transition-colors"
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</button>
|
||||
|
||||
{openDropdownId === meeting.id && (
|
||||
<div className="absolute right-0 top-full mt-1 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700/50 rounded-lg backdrop-blur-xl shadow-2xl z-50 min-w-[160px]">
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'view')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'reschedule')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
Reschedule
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'complete')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
Mark Complete
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleActionClick(meeting.id, 'cancel')}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import Chatbot from '../components/Chatbot';
|
||||
|
||||
const AiAssistantPage = () => {
|
||||
return (
|
||||
<div className="h-full bg-zinc-50 dark:bg-black flex flex-col">
|
||||
<div className="h-full flex flex-col">
|
||||
<header className="p-4 md:p-6 border-b border-zinc-200 dark:border-zinc-800 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm z-10">
|
||||
<h1 className="text-2xl font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<span className="w-2 h-6 bg-blue-500 rounded-full"></span>
|
||||
AI Assistant
|
||||
</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-sm ml-4">Your dedicated AI concierge for detailed insights and assistance.</p>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
<Chatbot inline={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiAssistantPage;
|
||||
+141
-113
@@ -2,14 +2,13 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import Chatbot from '../components/Chatbot';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import { ArrowRight, CheckCircle2, Home, Star, ShieldCheck, Clock, Award, TrendingDown, Users, MapPin, Phone, Mail, Instagram, Twitter, Youtube, Sun, Moon, LogOut, LayoutDashboard, User, Menu, X, ChevronDown } from 'lucide-react';
|
||||
import { ArrowRight, CheckCircle2, Home, Star, ShieldCheck, Clock, Award, TrendingDown, Users, MapPin, Phone, Mail, Instagram, Twitter, Youtube, Sun, Moon, LogOut, LayoutDashboard, User, Menu, X, ChevronDown, Zap, Briefcase, Map, Tag } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import { ComparisonSlider } from '../components/ComparisonSlider';
|
||||
import Services from '../components/Services';
|
||||
import IntelligenceMap from '../components/IntelligenceMap';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
||||
@@ -132,6 +131,20 @@ const Landing = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to determine dashboard path based on role
|
||||
const getDashboardPath = (role) => {
|
||||
switch (role) {
|
||||
case 'OWNER': return '/owner/snapshot';
|
||||
case 'ADMIN': return '/admin/dashboard';
|
||||
case 'CONTRACTOR': return '/contractor/dashboard';
|
||||
case 'SUBCONTRACTOR': return '/subcontractor/dashboard';
|
||||
case 'VENDOR': return '/vendor/dashboard';
|
||||
case 'FIELD_AGENT': return '/emp/fa/dashboard';
|
||||
case 'CUSTOMER': return '/portal/profile';
|
||||
default: return '/login';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 dark:bg-[#050505] text-zinc-900 dark:text-white transition-colors duration-300 overflow-x-hidden selection:bg-emerald-500/30">
|
||||
|
||||
@@ -207,7 +220,7 @@ const Landing = () => {
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
to={user.role === 'CUSTOMER' ? '/portal/profile' : '/emp/fa/dashboard'}
|
||||
to={getDashboardPath(user.role)}
|
||||
className="px-5 py-2 rounded-lg text-sm font-bold bg-blue-500 text-white hover:bg-blue-600 transition-colors shadow-sm shadow-blue-500/25"
|
||||
>
|
||||
{user.role === 'CUSTOMER' ? 'My Profile' : 'Dashboard'}
|
||||
@@ -226,16 +239,10 @@ const Landing = () => {
|
||||
{/* Mobile Menu Toggle */}
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors text-zinc-500 dark:text-zinc-400"
|
||||
aria-label="Toggle Theme"
|
||||
>
|
||||
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
<button
|
||||
className="p-2 text-zinc-600 dark:text-zinc-400"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors text-zinc-600 dark:text-zinc-400"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
>
|
||||
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
@@ -243,109 +250,128 @@ const Landing = () => {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* ── Mobile Full-Screen Overlay ── */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-50 bg-white dark:bg-zinc-950 flex flex-col"
|
||||
{/* ── Mobile Backdrop ── */}
|
||||
{isMobileMenuOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[100] md:hidden"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Mobile Sidebar — slides from left, mirrors Layout.jsx pattern ── */}
|
||||
<aside
|
||||
className={`
|
||||
fixed inset-y-0 left-0 z-[101] w-72 md:hidden
|
||||
bg-white dark:bg-[#09090b] border-r border-zinc-200 dark:border-white/5
|
||||
transition-transform duration-300 ease-in-out flex flex-col shadow-2xl
|
||||
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="h-16 flex items-center justify-between px-4 border-b border-zinc-100 dark:border-white/5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={Logo} alt="LynkedUp Pro" className="h-10 w-auto object-contain" />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-500 dark:text-zinc-400 transition-colors"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
{/* Overlay Header */}
|
||||
<div className="flex items-center justify-between px-6 h-16">
|
||||
<img src={Logo} alt="LynkedUp Pro" className="h-12 w-auto object-contain" />
|
||||
<button
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav Links */}
|
||||
<nav className="flex-1 px-3 py-6 space-y-1 overflow-y-auto">
|
||||
{[
|
||||
{ label: 'How It Works', id: 'how-it-works', icon: Zap },
|
||||
{ label: 'Services', id: 'services', icon: Briefcase },
|
||||
{ label: 'Intelligence', id: 'intelligence', icon: Map },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/5 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<item.icon size={18} className="shrink-0 text-zinc-400 dark:text-zinc-500" />
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/5 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<Tag size={18} className="shrink-0 text-zinc-400 dark:text-zinc-500" />
|
||||
Pricing
|
||||
</Link>
|
||||
|
||||
{/* Sign Up / Login nav item — visible only when logged out */}
|
||||
{!user && (
|
||||
<>
|
||||
<div className="my-3 border-t border-zinc-100 dark:border-white/5" />
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="p-2 text-zinc-600 dark:text-zinc-400"
|
||||
aria-label="Close menu"
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<User size={18} className="shrink-0" />
|
||||
Sign Up / Login
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Overlay Links */}
|
||||
<motion.div
|
||||
initial="closed"
|
||||
animate="open"
|
||||
variants={{ open: { transition: { staggerChildren: 0.06 } }, closed: {} }}
|
||||
className="flex-1 flex flex-col items-center justify-center gap-2 px-6"
|
||||
>
|
||||
{[
|
||||
{ label: 'How It Works', id: 'how-it-works' },
|
||||
{ label: 'Services', id: 'services' },
|
||||
{ label: 'Intelligence', id: 'intelligence' },
|
||||
].map((item) => (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
variants={{ closed: { opacity: 0, y: 20 }, open: { opacity: 1, y: 0 } }}
|
||||
transition={{ duration: 0.3 }}
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className="text-2xl font-bold text-zinc-900 dark:text-white py-3 hover:text-blue-500 transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</motion.button>
|
||||
))}
|
||||
<motion.div
|
||||
variants={{ closed: { opacity: 0, y: 20 }, open: { opacity: 1, y: 0 } }}
|
||||
transition={{ duration: 0.3 }}
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-zinc-100 dark:border-white/5 space-y-3 shrink-0">
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-white/5 hover:text-amber-500 transition-colors"
|
||||
>
|
||||
<span className="text-xs font-medium">Dark Mode</span>
|
||||
{theme === 'dark' ? <Moon size={16} /> : <Sun size={16} />}
|
||||
</button>
|
||||
|
||||
{/* CTAs */}
|
||||
{!user ? (
|
||||
<>
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full text-center py-3 rounded-xl text-sm font-bold bg-blue-500 text-white hover:bg-blue-600 transition-colors shadow-sm shadow-blue-500/25"
|
||||
>
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="text-2xl font-bold text-zinc-900 dark:text-white py-3 hover:text-blue-500 transition-colors"
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Overlay Bottom Actions */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.3 }}
|
||||
className="px-6 pb-10 space-y-3"
|
||||
>
|
||||
{!user ? (
|
||||
<>
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full text-center py-4 rounded-xl text-base font-bold bg-blue-500 text-white hover:bg-blue-600 transition-colors shadow-sm"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full text-center py-4 rounded-xl text-base font-bold border border-zinc-200 dark:border-white/10 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to={user.role === 'CUSTOMER' ? '/portal/profile' : '/emp/fa/dashboard'}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full text-center py-4 rounded-xl text-base font-bold bg-blue-500 text-white hover:bg-blue-600 transition-colors shadow-sm"
|
||||
>
|
||||
{user.role === 'CUSTOMER' ? 'My Profile' : 'Dashboard'}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => { useAuth().logout(); setIsMobileMenuOpen(false); }}
|
||||
className="block w-full text-center py-4 rounded-xl text-base font-bold border border-red-200 dark:border-red-500/20 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/10 transition-colors"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
Get Started
|
||||
</Link>
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full text-center py-3 rounded-xl text-sm font-bold border border-zinc-200 dark:border-white/10 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to={getDashboardPath(user.role)}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full text-center py-3 rounded-xl text-sm font-bold bg-blue-500 text-white hover:bg-blue-600 transition-colors shadow-sm shadow-blue-500/25"
|
||||
>
|
||||
{user.role === 'CUSTOMER' ? 'My Profile' : 'Dashboard'}
|
||||
</Link>
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full text-center py-3 rounded-xl text-sm font-bold border border-red-200 dark:border-red-500/20 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/10 transition-colors"
|
||||
>
|
||||
Sign Out
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* HERO SECTION — "The Overhead View" */}
|
||||
@@ -409,13 +435,15 @@ const Landing = () => {
|
||||
<ArrowRight size={18} className="ml-2 transition-transform group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
{/* B2B: Contractors */}
|
||||
<Link
|
||||
to="/login"
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group px-8 py-4 rounded-full bg-zinc-100 dark:bg-white/[0.06] hover:bg-zinc-200 dark:hover:bg-white/[0.10] border border-zinc-300 dark:border-white/[0.10] hover:border-zinc-400 dark:hover:border-white/[0.15] text-zinc-700 dark:text-white/90 font-bold text-sm transition-all flex items-center hover:scale-[1.02]"
|
||||
>
|
||||
Start Free Trial
|
||||
See The Process
|
||||
<ArrowRight size={18} className="ml-2 transition-transform group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Discount banner — Veterans, Seniors, Active Duty */}
|
||||
|
||||
+132
-59
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { User, Briefcase, Lock, ArrowRight, AlertCircle, Home } from 'lucide-react';
|
||||
import { User, Briefcase, Lock, ArrowRight, AlertCircle, Eye, EyeOff } from 'lucide-react';
|
||||
import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
||||
|
||||
@@ -53,9 +53,10 @@ const RainbowButton = ({ children, onClick, type = "button", className = "" }) =
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const [isEmployee, setIsEmployee] = useState(false);
|
||||
const [loginType, setLoginType] = useState('customer'); // 'customer', 'employee', 'owner', 'contractor', 'subcontractor'
|
||||
const [identifier, setIdentifier] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login } = useAuth();
|
||||
@@ -70,15 +71,28 @@ const Login = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = isEmployee ? 'employee' : 'customer';
|
||||
const result = login(identifier, password, type);
|
||||
const result = login(identifier, password, loginType);
|
||||
|
||||
if (result.success) {
|
||||
// Role-based Redirect
|
||||
if (result.role === 'CUSTOMER') {
|
||||
navigate('/');
|
||||
} else {
|
||||
navigate('/emp/fa/dashboard');
|
||||
switch (result.role) {
|
||||
case 'CUSTOMER':
|
||||
navigate('/');
|
||||
break;
|
||||
case 'OWNER':
|
||||
navigate('/owner/snapshot');
|
||||
break;
|
||||
case 'CONTRACTOR':
|
||||
navigate('/contractor/dashboard');
|
||||
break;
|
||||
case 'VENDOR':
|
||||
navigate('/vendor/dashboard');
|
||||
break;
|
||||
case 'SUBCONTRACTOR':
|
||||
navigate('/subcontractor/dashboard');
|
||||
break;
|
||||
default:
|
||||
navigate('/emp/fa/dashboard'); // Default for Employee/Admin
|
||||
}
|
||||
} else {
|
||||
setError(result.message);
|
||||
@@ -87,17 +101,33 @@ const Login = () => {
|
||||
|
||||
const fillDemo = (role) => {
|
||||
if (role === 'customer') {
|
||||
setIsEmployee(false);
|
||||
setLoginType('customer');
|
||||
setIdentifier('alice');
|
||||
setPassword('password');
|
||||
} else if (role === 'agent') {
|
||||
setIsEmployee(true);
|
||||
setLoginType('employee');
|
||||
setIdentifier('FA001');
|
||||
setPassword('password');
|
||||
} else if (role === 'admin') {
|
||||
setIsEmployee(true);
|
||||
setLoginType('employee');
|
||||
setIdentifier('ADM01');
|
||||
setPassword('password');
|
||||
} else if (role === 'owner') {
|
||||
setLoginType('owner');
|
||||
setIdentifier('justin');
|
||||
setPassword('password');
|
||||
} else if (role === 'contractor') {
|
||||
setLoginType('contractor');
|
||||
setIdentifier('mike');
|
||||
setPassword('password');
|
||||
} else if (role === 'vendor') {
|
||||
setLoginType('vendor');
|
||||
setIdentifier('abc_supply');
|
||||
setPassword('password');
|
||||
} else if (role === 'subcontractor') {
|
||||
setLoginType('subcontractor');
|
||||
setIdentifier('carlos');
|
||||
setPassword('password');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,96 +140,139 @@ const Login = () => {
|
||||
<div className="absolute bottom-[-20%] right-[-20%] w-[70%] h-[70%] bg-blue-500/5 dark:bg-blue-900/10 blur-[150px] rounded-full opacity-50"></div>
|
||||
</div>
|
||||
|
||||
{/* Autofill CSS Override */}
|
||||
<style>{`
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px #f4f4f5 inset !important;
|
||||
-webkit-text-fill-color: #18181b !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px #18181b inset !important; /* zinc-900 matches dark mode inputs */
|
||||
-webkit-text-fill-color: white !important;
|
||||
}
|
||||
}
|
||||
.dark input:-webkit-autofill,
|
||||
.dark input:-webkit-autofill:hover,
|
||||
.dark input:-webkit-autofill:focus,
|
||||
.dark input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px #1c1917 inset !important; /* Slightly lighter than pure black for input bg */
|
||||
-webkit-text-fill-color: white !important;
|
||||
caret-color: white !important;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
|
||||
<div className="w-full max-w-md z-10 relative">
|
||||
<div className="w-full max-w-lg z-10 relative">
|
||||
<SpotlightCard className="bg-white/60 dark:bg-black/30 backdrop-blur-3xl border-0 ring-1 ring-black/5 dark:ring-white/5 shadow-2xl dark:shadow-2xl">
|
||||
<div className="p-8">
|
||||
<div className="text-center mb-10 pt-2">
|
||||
<img src={Logo} alt="LynkedUp Pro" className="w-20 h-20 mx-auto mb-6 object-contain drop-shadow-2xl" />
|
||||
<h1 className="text-3xl font-black text-zinc-900 dark:text-white mb-2 tracking-tighter">
|
||||
<div className="p-6 md:p-10"> {/* ADJUSTED PADDING FOR MOBILE */}
|
||||
<div className="text-center mb-8 md:mb-10 pt-2">
|
||||
<img src={Logo} alt="LynkedUp Pro" className="w-20 md:w-24 h-20 md:h-24 mx-auto mb-4 md:mb-6 object-contain drop-shadow-2xl" />
|
||||
<h1 className="text-2xl md:text-3xl font-black text-zinc-900 dark:text-white mb-2 tracking-tighter">
|
||||
Welcome Back
|
||||
</h1>
|
||||
<p className="text-zinc-500 font-medium">Sign in to your LynkedUp Pro account</p>
|
||||
<p className="text-zinc-500 font-medium text-base md:text-lg">Sign in to your LynkedUp Pro account</p>
|
||||
</div>
|
||||
|
||||
{/* Neo-Toggle */}
|
||||
<div className="flex p-1.5 bg-zinc-100 dark:bg-black/40 rounded-xl mb-8 border border-zinc-200 dark:border-white/5 mx-1">
|
||||
<button
|
||||
onClick={() => setIsEmployee(false)}
|
||||
className={`flex-1 flex items-center justify-center py-2.5 text-xs font-bold uppercase tracking-wider rounded-lg transition-all duration-300 ${!isEmployee
|
||||
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm border border-black/5 dark:border-white/5'
|
||||
: 'text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'}`}
|
||||
>
|
||||
<User size={14} className="mr-2" />
|
||||
Customer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsEmployee(true)}
|
||||
className={`flex-1 flex items-center justify-center py-2.5 text-xs font-bold uppercase tracking-wider rounded-lg transition-all duration-300 ${isEmployee
|
||||
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm border border-black/5 dark:border-white/5'
|
||||
: 'text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'}`}
|
||||
>
|
||||
<Briefcase size={14} className="mr-2" />
|
||||
Employee
|
||||
</button>
|
||||
{/* Role Tabs — 3-col grid on mobile, 6-col on desktop */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-1.5 p-1.5 bg-zinc-100 dark:bg-black/40 rounded-xl mb-8 md:mb-10 border border-zinc-200 dark:border-white/5">
|
||||
{[
|
||||
{ key: 'customer', label: 'Customer' },
|
||||
{ key: 'employee', label: 'Employee' },
|
||||
{ key: 'owner', label: 'Owner' },
|
||||
{ key: 'contractor', label: 'Contract.' },
|
||||
{ key: 'vendor', label: 'Vendor' },
|
||||
{ key: 'subcontractor', label: 'Sub-Con' },
|
||||
].map(({ key, label }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setLoginType(key)}
|
||||
className={`flex items-center justify-center py-2.5 px-1 text-[10px] font-bold uppercase rounded-lg transition-all duration-300 whitespace-nowrap ${loginType === key
|
||||
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm border border-black/5 dark:border-white/5'
|
||||
: 'text-zinc-400 dark:text-zinc-600 hover:text-zinc-600 dark:hover:text-zinc-400'}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<form onSubmit={handleLogin} className="space-y-6 md:space-y-8">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold text-zinc-500 ml-1 uppercase tracking-widest">
|
||||
{isEmployee ? 'Employee ID' : 'Username'}
|
||||
<label className="text-xs font-bold text-zinc-500 ml-1 uppercase tracking-widest">
|
||||
{loginType === 'employee' ? 'Employee ID' : 'Username'}
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors">
|
||||
{isEmployee ? <Briefcase size={18} /> : <User size={18} />}
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors z-10">
|
||||
{loginType === 'customer' ? <User size={20} /> : <Briefcase size={20} />}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
className="w-full bg-zinc-100 dark:bg-black/20 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-4 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-black/40 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium"
|
||||
placeholder={isEmployee ? "e.g., FA001" : "e.g., alice"}
|
||||
className="w-full bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-4 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-zinc-900 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium relative z-0"
|
||||
placeholder={loginType === 'employee' ? "e.g., FA001" : "e.g., username"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold text-zinc-500 ml-1 uppercase tracking-widest">Password</label>
|
||||
<div className="flex justify-between items-center ml-1">
|
||||
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest">Password</label>
|
||||
<button type="button" className="text-xs font-semibold text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 transition-colors">
|
||||
Forgot Password?
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors">
|
||||
<Lock size={18} />
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-zinc-900 dark:group-focus-within:text-white transition-colors z-10">
|
||||
<Lock size={20} />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-zinc-100 dark:bg-black/20 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-4 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-black/40 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium"
|
||||
className="w-full bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-900 dark:text-white rounded-xl py-4 pl-12 pr-12 focus:outline-none focus:ring-1 focus:ring-black/10 dark:focus:ring-white/20 focus:bg-white dark:focus:bg-zinc-900 transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-700 font-medium relative z-0"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors z-10"
|
||||
>
|
||||
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center space-x-2 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 p-3 rounded-xl text-xs font-bold uppercase tracking-wide">
|
||||
<AlertCircle size={16} />
|
||||
<div className="flex items-center space-x-2 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 p-4 rounded-xl text-sm font-bold uppercase tracking-wide">
|
||||
<AlertCircle size={18} />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RainbowButton type="submit" className="mt-2 text-white">
|
||||
<RainbowButton type="submit" className="mt-4 text-white py-4 md:py-5 text-base md:text-lg">
|
||||
<span>Sign In</span>
|
||||
<ArrowRight size={18} />
|
||||
<ArrowRight size={20} />
|
||||
</RainbowButton>
|
||||
</form>
|
||||
|
||||
{/* Demo Quick Links */}
|
||||
<div className="mt-8 pt-6 border-t border-zinc-200 dark:border-white/5">
|
||||
<div className="mt-8 md:mt-10 pt-6 md:pt-8 border-t border-zinc-200 dark:border-white/5">
|
||||
<p className="text-[10px] text-center text-zinc-500 uppercase tracking-widest font-bold mb-4">Quick Demo Access</p>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<button onClick={() => fillDemo('customer')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-3 py-2 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Customer</button>
|
||||
<button onClick={() => fillDemo('agent')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-3 py-2 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Agent</button>
|
||||
<button onClick={() => fillDemo('admin')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-3 py-2 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Admin</button>
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-2.5">
|
||||
<button onClick={() => fillDemo('customer')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-2 py-2.5 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Customer</button>
|
||||
<button onClick={() => fillDemo('agent')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-2 py-2.5 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Agent</button>
|
||||
<button onClick={() => fillDemo('admin')} className="text-[10px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/5 text-zinc-500 px-2 py-2.5 rounded-lg hover:border-zinc-300 dark:hover:border-white/20 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors">Admin</button>
|
||||
<button onClick={() => fillDemo('owner')} className="text-[10px] font-bold uppercase tracking-wider bg-amber-100 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 px-2 py-2.5 rounded-lg hover:bg-amber-200 dark:hover:bg-amber-900/30 transition-colors">Owner</button>
|
||||
<button onClick={() => fillDemo('contractor')} className="text-[10px] font-bold uppercase tracking-wider bg-blue-100 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-500/20 text-blue-600 dark:text-blue-400 px-2 py-2.5 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-900/30 transition-colors">Contractor</button>
|
||||
<button onClick={() => fillDemo('vendor')} className="text-[10px] font-bold uppercase tracking-wider bg-cyan-100 dark:bg-cyan-900/20 border border-cyan-200 dark:border-cyan-500/20 text-cyan-600 dark:text-cyan-400 px-2 py-2.5 rounded-lg hover:bg-cyan-200 dark:hover:bg-cyan-900/30 transition-colors">Vendor</button>
|
||||
<button onClick={() => fillDemo('subcontractor')} className="text-[10px] font-bold uppercase tracking-wider bg-purple-100 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-500/20 text-purple-600 dark:text-purple-400 px-2 py-2.5 rounded-lg hover:bg-purple-200 dark:hover:bg-purple-900/30 transition-colors">Sub-Con</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,25 @@ const MapLegend = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Map Resizer Component
|
||||
const MapResizer = () => {
|
||||
const map = useMapEvents({});
|
||||
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
map.invalidateSize();
|
||||
});
|
||||
const container = map.getContainer();
|
||||
resizeObserver.observe(container);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Map View
|
||||
const MapView = ({ data, onSelect, onMapCreate }) => {
|
||||
const mapStart = [33.0708, -96.7455];
|
||||
@@ -110,6 +129,9 @@ const MapView = ({ data, onSelect, onMapCreate }) => {
|
||||
|
||||
return (
|
||||
<MapContainer center={mapStart} zoom={18} scrollWheelZoom={true} className="w-full h-full z-0 relative outline-none" style={{ height: "100%", width: "100%" }}>
|
||||
|
||||
<MapResizer />
|
||||
|
||||
{/*
|
||||
- Light Mode: Standard vibrant OSM tiles (no filters)
|
||||
- Dark Mode: Inverted, Grayscale, High Contrast for dark map
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useMockStore } from '../data/mockStore';
|
||||
import { filterProjectsForUser, canViewSensitiveData } from '../utils/permissions';
|
||||
import MaskedData from '../components/MaskedData';
|
||||
|
||||
const PlaceholderDashboard = ({ title, role }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const { personnel, vendors, documents, projects } = useMockStore();
|
||||
|
||||
// Filter data based on permissions
|
||||
const visibleProjects = filterProjectsForUser(user, projects);
|
||||
const canSeeSensitive = canViewSensitiveData(user);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<header className="flex justify-between items-center mb-8 pb-4 border-b border-white/10">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-emerald-400">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-slate-400 mt-2">Welcome, {user?.name} ({role})</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-4 py-2 bg-red-500/10 text-red-400 hover:bg-red-500/20 rounded-lg transition-colors border border-red-500/20"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6">
|
||||
<div className="p-6 rounded-xl bg-slate-900/50 border border-white/10 backdrop-blur-md">
|
||||
<h2 className="text-xl font-semibold mb-4 text-emerald-400">Security & Permissions Check 🔒</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="text-slate-300 font-medium mb-3">Data Visibility</h3>
|
||||
<ul className="space-y-2 text-sm text-slate-400">
|
||||
<li className="flex justify-between">
|
||||
<span>Total Projects:</span>
|
||||
<span className="font-mono text-white">{projects.length}</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span>Visible Projects (Role filtered):</span>
|
||||
<span className="font-mono text-blue-400 font-bold">{visibleProjects.length}</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span>Can View Sensitive Data:</span>
|
||||
<span className={`font-mono font-bold ${canSeeSensitive ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{canSeeSensitive ? 'YES' : 'NO'}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-slate-300 font-medium mb-3">Sensitive Data Test</h3>
|
||||
<div className="bg-black/40 p-4 rounded-lg border border-white/5 space-y-3">
|
||||
{personnel.length > 0 && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-slate-400">Sample SSN ({personnel[0].name}):</span>
|
||||
<MaskedData value={personnel[0].sensitiveData?.ssn} label="SSN" />
|
||||
</div>
|
||||
)}
|
||||
{vendors.length > 0 && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-slate-400">Sample Bank ({vendors[0].vendorName}):</span>
|
||||
<MaskedData value={vendors[0].sensitiveData?.bankAccount} label="Bank" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div className="p-5 rounded-lg bg-slate-900/30 border border-white/5">
|
||||
<div className="text-slate-400 text-sm mb-1">My Projects</div>
|
||||
<div className="text-3xl font-bold text-blue-400">{visibleProjects.length || 0}</div>
|
||||
</div>
|
||||
<div className="p-5 rounded-lg bg-slate-900/30 border border-white/5">
|
||||
<div className="text-slate-400 text-sm mb-1">Vendors</div>
|
||||
<div className="text-3xl font-bold text-purple-400">{vendors?.length || 0}</div>
|
||||
</div>
|
||||
<div className="p-5 rounded-lg bg-slate-900/30 border border-white/5">
|
||||
<div className="text-slate-400 text-sm mb-1">Personnel</div>
|
||||
<div className="text-3xl font-bold text-amber-400">{personnel?.length || 0}</div>
|
||||
</div>
|
||||
<div className="p-5 rounded-lg bg-slate-900/30 border border-white/5">
|
||||
<div className="text-slate-400 text-sm mb-1">Documents</div>
|
||||
<div className="text-3xl font-bold text-emerald-400">{documents?.length || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-lg bg-slate-900/30 border border-white/5 mt-4">
|
||||
<div className="text-slate-400 text-sm mb-1">Session ID</div>
|
||||
<div className="text-white/50 font-mono text-xs">{user?.id || 'Unknown'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceholderDashboard;
|
||||
@@ -0,0 +1,240 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useMockStore } from '../../data/mockStore';
|
||||
import { StatCard } from '../../components/StatCard';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
import { Hammer, CheckSquare, Clock, AlertTriangle, Calendar, DollarSign } from 'lucide-react';
|
||||
import ProjectDetailsModal from '../../components/contractor/ProjectDetailsModal';
|
||||
import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal';
|
||||
|
||||
const ContractorDashboard = () => {
|
||||
const { user } = useAuth();
|
||||
const { projects } = useMockStore();
|
||||
|
||||
// 1. Identify current contractor
|
||||
// Filter projects where this contractor is the Lead
|
||||
const myProjects = projects.filter(p => p.contractorId === user?.id || p.contractorId === 'con_001'); // Default to con_001 for mock
|
||||
|
||||
// 2. Aggregate Data
|
||||
const activeProjects = myProjects.filter(p => p.status === 'active').length;
|
||||
const upcomingProjects = myProjects.filter(p => p.status === 'scheduled').length;
|
||||
|
||||
// Calculate total budget managed
|
||||
const totalBudget = myProjects.reduce((sum, p) => sum + (p.budget || 0), 0);
|
||||
const totalSpent = myProjects.reduce((sum, p) => sum + (p.spent || 0), 0);
|
||||
const budgetUtilization = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0;
|
||||
|
||||
// Upcoming Milestones (next 7 days)
|
||||
const upcomingMilestones = myProjects.flatMap(p =>
|
||||
p.milestones.filter(m => {
|
||||
const dueDate = new Date(m.dueDate);
|
||||
const today = new Date('2026-02-05'); // Fixed context date
|
||||
const diffTime = Math.abs(dueDate - today);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays <= 7 && m.status !== 'completed';
|
||||
}).map(m => ({ ...m, projectAddress: p.address }))
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Modal state
|
||||
const [selectedProject, setSelectedProject] = useState(null);
|
||||
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
||||
const [isFinancialModalOpen, setIsFinancialModalOpen] = useState(false);
|
||||
|
||||
// Financial data for modal
|
||||
const financialData = {
|
||||
total: totalBudget,
|
||||
spent: totalSpent,
|
||||
remaining: totalBudget - totalSpent,
|
||||
paid: totalSpent,
|
||||
pending: 0,
|
||||
items: myProjects.map(p => ({
|
||||
date: p.startDate,
|
||||
description: p.address,
|
||||
project: p.projectType,
|
||||
status: p.status === 'completed' ? 'paid' : 'pending',
|
||||
amount: p.budget
|
||||
}))
|
||||
};
|
||||
|
||||
const handleProjectClick = (project) => {
|
||||
setSelectedProject(project);
|
||||
setIsProjectModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">Contractor Command Center</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Managing {activeProjects} active builds with <span className="text-blue-500 font-semibold">${totalBudget.toLocaleString()}</span> in volume.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => navigate('/contractor/projects')}
|
||||
className="bg-zinc-100 dark:bg-white/5 hover:bg-zinc-200 dark:hover:bg-white/10 text-zinc-900 dark:text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors border border-zinc-200 dark:border-white/10"
|
||||
>
|
||||
View Schedule
|
||||
</button>
|
||||
<button className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors shadow-lg shadow-blue-500/20">
|
||||
+ Log Daily Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
label="Active Projects"
|
||||
value={activeProjects}
|
||||
icon={Hammer}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard
|
||||
label="Upcoming Starts"
|
||||
value={upcomingProjects}
|
||||
icon={Calendar}
|
||||
color="purple"
|
||||
/>
|
||||
<div onClick={() => setIsFinancialModalOpen(true)} className="cursor-pointer">
|
||||
<StatCard
|
||||
label="Budget Utilization"
|
||||
value={budgetUtilization}
|
||||
icon={DollarSign}
|
||||
color={budgetUtilization > 90 ? 'red' : 'emerald'}
|
||||
suffix="%"
|
||||
/>
|
||||
</div>
|
||||
<StatCard
|
||||
label="Pending Actions"
|
||||
value={upcomingMilestones.length}
|
||||
icon={AlertTriangle}
|
||||
color="amber"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
{/* Active Projects List */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<SpotlightCard className="p-6 h-full">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white">Active Build Status</h2>
|
||||
<button onClick={() => navigate('/contractor/projects')} className="text-sm text-blue-500 hover:text-blue-400">View All</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{myProjects.filter(p => p.status === 'active').map(proj => (
|
||||
<div
|
||||
key={proj.id}
|
||||
onClick={() => handleProjectClick(proj)}
|
||||
className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5 hover:border-blue-500/30 transition-all cursor-pointer group"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h4 className="font-bold text-zinc-900 dark:text-white">{proj.address}</h4>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">{proj.projectType} • Started {new Date(proj.startDate).toLocaleDateString()}</p>
|
||||
</div>
|
||||
<span className="px-2 py-1 bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-xs font-bold rounded uppercase">
|
||||
On Track
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs font-medium text-zinc-500">
|
||||
<span>Completion</span>
|
||||
<span>{proj.completionPercentage}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full"
|
||||
style={{ width: `${proj.completionPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Milestones Preview */}
|
||||
<div className="mt-4 flex gap-2 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{proj.milestones.map(ms => (
|
||||
<span
|
||||
key={ms.id}
|
||||
className={`whitespace-nowrap px-2 py-1 text-[10px] font-bold uppercase border rounded ${ms.status === 'completed'
|
||||
? 'border-emerald-200 text-emerald-600 dark:border-emerald-500/30 dark:text-emerald-400'
|
||||
: ms.status === 'in_progress'
|
||||
? 'border-blue-200 text-blue-600 dark:border-blue-500/30 dark:text-blue-400'
|
||||
: 'border-zinc-200 text-zinc-400 dark:border-zinc-700 dark:text-zinc-500'
|
||||
}`}
|
||||
>
|
||||
{ms.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Tasks & Alerts */}
|
||||
<div className="space-y-6">
|
||||
<SpotlightCard className="p-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Critical Tasks (Next 7 Days)</h2>
|
||||
<div className="space-y-3">
|
||||
{upcomingMilestones.length > 0 ? upcomingMilestones.map((ms, idx) => (
|
||||
<div key={idx} className="flex items-start gap-3 p-3 rounded-lg bg-red-50 dark:bg-red-500/5 hover:bg-red-100 dark:hover:bg-red-500/10 transition-colors cursor-pointer group">
|
||||
<div className="mt-1 w-2 h-2 rounded-full bg-red-500 shrink-0 group-hover:scale-125 transition-transform" />
|
||||
<div>
|
||||
<h5 className="text-sm font-bold text-zinc-900 dark:text-white">{ms.name}</h5>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">{ms.projectAddress}</p>
|
||||
<p className="text-xs font-mono font-medium text-red-500 mt-1">Due: {ms.dueDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-sm text-zinc-500 italic">No critical tasks due.</p>
|
||||
)}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
|
||||
<SpotlightCard className="p-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Crew Availability</h2>
|
||||
<div className="space-y-3">
|
||||
{/* Mock Crew Data */}
|
||||
{['Crew Alpha (Roofing)', 'Crew Beta (Gutters)'].map((crew, i) => (
|
||||
<div key={i} className="flex justify-between items-center text-sm py-2 border-b border-zinc-100 dark:border-white/5 last:border-0">
|
||||
<span className="text-zinc-900 dark:text-white">{crew}</span>
|
||||
<span className="flex items-center text-emerald-500 text-xs font-bold uppercase">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2" />
|
||||
Available
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<ProjectDetailsModal
|
||||
isOpen={isProjectModalOpen}
|
||||
onClose={() => setIsProjectModalOpen(false)}
|
||||
project={selectedProject}
|
||||
/>
|
||||
<FinancialSummaryModal
|
||||
isOpen={isFinancialModalOpen}
|
||||
onClose={() => setIsFinancialModalOpen(false)}
|
||||
role="CONTRACTOR"
|
||||
data={financialData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractorDashboard;
|
||||
@@ -0,0 +1,143 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useMockStore } from '../../data/mockStore';
|
||||
import { Briefcase, Calendar, CheckCircle, Clock, AlertTriangle, ChevronRight } from 'lucide-react';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
import ProjectDetailsModal from '../../components/contractor/ProjectDetailsModal';
|
||||
|
||||
const ProjectList = () => {
|
||||
const { user } = useAuth();
|
||||
const { projects, vendors } = useMockStore();
|
||||
|
||||
// Filter projects for this contractor/subcontractor
|
||||
const myProjects = projects.filter(p =>
|
||||
p.contractorId === user.id || p.subcontractorIds?.includes(user.id)
|
||||
);
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return 'text-emerald-700 bg-emerald-100 border-emerald-200 dark:text-emerald-300 dark:bg-emerald-500/10 dark:border-emerald-500/20';
|
||||
case 'completed': return 'text-blue-700 bg-blue-100 border-blue-200 dark:text-blue-300 dark:bg-blue-500/10 dark:border-blue-500/20';
|
||||
case 'pending': return 'text-amber-700 bg-amber-100 border-amber-200 dark:text-amber-300 dark:bg-amber-500/10 dark:border-amber-500/20';
|
||||
default: return 'text-zinc-600 bg-zinc-100 border-zinc-200 dark:text-zinc-400 dark:bg-zinc-500/10 dark:border-zinc-500/20';
|
||||
}
|
||||
};
|
||||
|
||||
// Modal state
|
||||
const [selectedProject, setSelectedProject] = useState(null);
|
||||
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
||||
|
||||
const handleProjectClick = (project) => {
|
||||
setSelectedProject(project);
|
||||
setIsProjectModalOpen(true);
|
||||
};
|
||||
|
||||
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">
|
||||
<header className="mb-8 border-b border-zinc-200 dark:border-white/5 pb-6">
|
||||
<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 mb-2 tracking-tight">My Projects</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 font-light">Manage your active assignments and track progress.</p>
|
||||
</header>
|
||||
|
||||
{myProjects.length === 0 ? (
|
||||
<SpotlightCard className="text-center py-20 border-dashed border-zinc-300 dark:border-white/10 bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="p-4 bg-zinc-100 dark:bg-white/5 rounded-full text-zinc-400 mb-4">
|
||||
<Briefcase size={48} className="opacity-50" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-2">No Active Projects</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400">You currently have no projects assigned.</p>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{myProjects.map(project => (
|
||||
<SpotlightCard key={project.id} className="p-0 group overflow-hidden cursor-pointer" onClick={() => handleProjectClick(project)}>
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between md:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h2 className="text-xl font-bold text-zinc-900 dark:text-white">Project #{project.id}</h2>
|
||||
<span className={`px-2.5 py-0.5 rounded-md text-[10px] font-bold border uppercase tracking-wider ${getStatusColor(project.status)}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-sm flex items-center font-medium">
|
||||
<Briefcase size={14} className="mr-2" />
|
||||
{project.projectType.charAt(0).toUpperCase() + project.projectType.slice(1)} Project
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] text-zinc-400 uppercase font-bold tracking-wider">Budget</div>
|
||||
<div className="text-lg font-bold text-zinc-900 dark:text-white">${project.budget.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="text-right pl-6 border-l border-zinc-200 dark:border-white/10">
|
||||
<div className="text-[10px] text-zinc-400 uppercase font-bold tracking-wider">Deadline</div>
|
||||
<div className="text-lg font-bold text-zinc-900 dark:text-white flex items-center justify-end gap-2">
|
||||
{project.endDate}
|
||||
<Calendar size={16} className="text-zinc-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-6 p-4 bg-zinc-50 dark:bg-white/5 rounded-xl border border-zinc-100 dark:border-white/5">
|
||||
<div className="flex justify-between text-xs mb-2 font-bold uppercase tracking-wider">
|
||||
<span className="text-zinc-500">Completion Status</span>
|
||||
<span className="text-emerald-600 dark:text-emerald-400">
|
||||
{project.status === 'completed' ? '100%' : 'In Progress'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-zinc-200 dark:bg-zinc-700/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-emerald-500 to-emerald-400 rounded-full transition-all duration-1000 ease-out"
|
||||
style={{ width: project.status === 'completed' ? '100%' : '45%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="flex -space-x-2">
|
||||
{/* Avatar Placeholders */}
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-[10px] font-bold text-blue-600 dark:text-blue-300 border-2 border-white dark:border-zinc-900 shadow-sm">PM</div>
|
||||
<div className="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-[10px] font-bold text-purple-600 dark:text-purple-300 border-2 border-white dark:border-zinc-900 shadow-sm">GC</div>
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center text-sm font-bold text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors bg-blue-50 dark:bg-blue-500/10 px-4 py-2 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-500/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleProjectClick(project);
|
||||
}}
|
||||
>
|
||||
View Details <ChevronRight size={16} className="ml-1 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Decorative Bottom Glow */}
|
||||
<div className="absolute bottom-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-zinc-200 dark:via-white/10 to-transparent opacity-50" />
|
||||
</SpotlightCard>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
<ProjectDetailsModal
|
||||
isOpen={isProjectModalOpen}
|
||||
onClose={() => setIsProjectModalOpen(false)}
|
||||
project={selectedProject}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectList;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,240 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useMockStore } from '../../data/mockStore';
|
||||
import { StatCard } from '../../components/StatCard';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
import { CheckSquare, Clock, Wallet, MapPin, Camera } from 'lucide-react';
|
||||
import TaskDetailsModal from '../../components/contractor/TaskDetailsModal';
|
||||
import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal';
|
||||
|
||||
const SubContractorDashboard = () => {
|
||||
const { user } = useAuth();
|
||||
const { projects } = useMockStore();
|
||||
|
||||
// 1. Identify current subcontractor (e.g. 'sub_001')
|
||||
const subId = user?.id || 'sub_001';
|
||||
|
||||
// 2. Aggregate Tasks (Milestones assigned to me)
|
||||
const myTasks = projects.flatMap(p =>
|
||||
p.milestones.filter(m => m.assignedTo === subId)
|
||||
.map(m => ({
|
||||
...m,
|
||||
projectAddress: p.address,
|
||||
projectId: p.id,
|
||||
projectStatus: p.status
|
||||
}))
|
||||
);
|
||||
|
||||
const activeTasks = myTasks.filter(t => t.status === 'in_progress').length;
|
||||
const pendingTasks = myTasks.filter(t => t.status === 'pending').length;
|
||||
const completedTasks = myTasks.filter(t => t.status === 'completed').length;
|
||||
|
||||
// 3. Financials (Invoices submitted by me)
|
||||
const myInvoices = projects.flatMap(p =>
|
||||
p.invoices.filter(i => i.submittedBy === subId)
|
||||
);
|
||||
const pendingPay = myInvoices.filter(i => i.status === 'pending').reduce((sum, i) => sum + i.amount, 0);
|
||||
const totalEarnings = myInvoices.filter(i => i.status === 'paid').reduce((sum, i) => sum + i.amount, 0);
|
||||
|
||||
// Modal state
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
||||
const [isFinancialModalOpen, setIsFinancialModalOpen] = useState(false);
|
||||
|
||||
// Financial data for modal
|
||||
const financialData = {
|
||||
total: totalEarnings + pendingPay,
|
||||
paid: totalEarnings,
|
||||
pending: pendingPay,
|
||||
spent: 0,
|
||||
remaining: 0,
|
||||
items: myInvoices.map(inv => ({
|
||||
date: inv.dueDate || 'N/A',
|
||||
description: `Invoice #${inv.id}`,
|
||||
project: myTasks.find(t => t.projectId === inv.projectId)?.projectAddress || 'Unknown',
|
||||
status: inv.status,
|
||||
amount: inv.amount
|
||||
}))
|
||||
};
|
||||
|
||||
const handleTaskClick = (task) => {
|
||||
setSelectedTask(task);
|
||||
setIsTaskModalOpen(true);
|
||||
};
|
||||
|
||||
const handleTaskUpdate = (taskId, actionType) => {
|
||||
console.log(`Task ${taskId} updated with action: ${actionType}`);
|
||||
// In a real app, this would update the backend
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">Field Task Center</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
You have <span className="text-blue-500 font-bold">{activeTasks} active tasks</span> and <span className="text-emerald-500 font-bold">${pendingPay.toLocaleString()}</span> in pending payouts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions (Mobile First) */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<button className="flex flex-col items-center justify-center p-4 rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-500/30 hover:bg-blue-500 transition-all active:scale-95">
|
||||
<Camera size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Upload Photo</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center justify-center p-4 rounded-xl bg-zinc-100 dark:bg-white/10 text-zinc-900 dark:text-white hover:bg-zinc-200 dark:hover:bg-white/20 transition-all active:scale-95">
|
||||
<Clock size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Clock In</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center justify-center p-4 rounded-xl bg-zinc-100 dark:bg-white/10 text-zinc-900 dark:text-white hover:bg-zinc-200 dark:hover:bg-white/20 transition-all active:scale-95">
|
||||
<MapPin size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Check Location</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center justify-center p-4 rounded-xl bg-emerald-600 text-white shadow-lg shadow-emerald-500/30 hover:bg-emerald-500 transition-all active:scale-95">
|
||||
<CheckSquare size={24} className="mb-2" />
|
||||
<span className="text-sm font-bold">Complete Task</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
label="Tasks In Progress"
|
||||
value={activeTasks}
|
||||
icon={Clock}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard
|
||||
label="Pending Tasks"
|
||||
value={pendingTasks}
|
||||
icon={CheckSquare}
|
||||
color="amber"
|
||||
/>
|
||||
<div onClick={() => setIsFinancialModalOpen(true)} className="cursor-pointer">
|
||||
<StatCard
|
||||
label="Pending Payouts"
|
||||
value={`$${pendingPay.toLocaleString()}`}
|
||||
icon={Wallet}
|
||||
color="emerald"
|
||||
/>
|
||||
</div>
|
||||
<StatCard
|
||||
label="Jobs Completed"
|
||||
value={completedTasks}
|
||||
icon={CheckSquare}
|
||||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Task List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<SpotlightCard className="p-6 h-full">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white">My Assignments</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{myTasks.length > 0 ? myTasks.map((task, idx) => (
|
||||
<div key={idx} className="group p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5 hover:border-blue-500/30 transition-all cursor-pointer" onClick={() => handleTaskClick(task)}>
|
||||
<div className="flex flex-col md:flex-row justify-between md:items-center gap-4">
|
||||
|
||||
{/* Left: Task Info */}
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-3 rounded-lg ${task.status === 'completed' ? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400' :
|
||||
task.status === 'in_progress' ? 'bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400' :
|
||||
'bg-zinc-100 text-zinc-600 dark:bg-white/10 dark:text-zinc-400'
|
||||
}`}>
|
||||
<CheckSquare size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-zinc-900 dark:text-white">{task.name}</h4>
|
||||
<div className="flex items-center text-xs text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
<MapPin size={12} className="mr-1" />
|
||||
{task.projectAddress}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Status & Action */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-xs font-mono font-medium text-zinc-500 mb-1">Due: {task.dueDate}</p>
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${task.status === 'completed' ? 'bg-emerald-100 text-emerald-600' :
|
||||
task.status === 'in_progress' ? 'bg-blue-100 text-blue-600' :
|
||||
'bg-amber-100 text-amber-600'
|
||||
}`}>
|
||||
{task.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
{task.status !== 'completed' && (
|
||||
<button
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTaskClick(task);
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="text-center py-10 text-zinc-500">
|
||||
<p>No tasks assigned.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Mobile Earnings Widget */}
|
||||
<div>
|
||||
<SpotlightCard className="p-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Earnings Tracker</h2>
|
||||
<div className="relative pt-4 pb-8">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-1">Total Earned (YTD)</p>
|
||||
<h3 className="text-4xl font-mono font-bold text-emerald-500">${totalEarnings.toLocaleString()}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-500">Paid Invoices</span>
|
||||
<span className="font-bold text-zinc-900 dark:text-white">{myInvoices.filter(i => i.status === 'paid').length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-500">Pending Invoices</span>
|
||||
<span className="font-bold text-amber-500">{myInvoices.filter(i => i.status === 'pending').length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full mt-8 py-3 rounded-xl bg-zinc-100 dark:bg-white/10 hover:bg-zinc-200 dark:hover:bg-white/20 text-zinc-900 dark:text-white font-bold text-sm transition-colors">
|
||||
View Payment History
|
||||
</button>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<TaskDetailsModal
|
||||
isOpen={isTaskModalOpen}
|
||||
onClose={() => setIsTaskModalOpen(false)}
|
||||
task={selectedTask}
|
||||
onUpdate={handleTaskUpdate}
|
||||
/>
|
||||
<FinancialSummaryModal
|
||||
isOpen={isFinancialModalOpen}
|
||||
onClose={() => setIsFinancialModalOpen(false)}
|
||||
role="SUBCONTRACTOR"
|
||||
data={financialData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubContractorDashboard;
|
||||
Vendored
+258
@@ -0,0 +1,258 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useMockStore } from '../../data/mockStore';
|
||||
import { StatCard } from '../../components/StatCard';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
import { DollarSign, ClipboardCheck, TrendingUp, AlertCircle, Calendar, FileText, CheckCircle2 } from 'lucide-react';
|
||||
import OrderDetailsModal from '../../components/vendor/OrderDetailsModal';
|
||||
import VendorFinancialSummaryModal from '../../components/vendor/VendorFinancialSummaryModal';
|
||||
import ComplianceDetailsModal from '../../components/vendor/ComplianceDetailsModal';
|
||||
import PerformanceMetricsModal from '../../components/vendor/PerformanceMetricsModal';
|
||||
|
||||
const VendorDashboard = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { vendors, projects, orders, vendorInvoices } = useMockStore();
|
||||
|
||||
// Modal state
|
||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||
const [isOrderModalOpen, setIsOrderModalOpen] = useState(false);
|
||||
const [isFinancialModalOpen, setIsFinancialModalOpen] = useState(false);
|
||||
const [isComplianceModalOpen, setIsComplianceModalOpen] = useState(false);
|
||||
const [isPerformanceModalOpen, setIsPerformanceModalOpen] = useState(false);
|
||||
|
||||
// 1. Identify current vendor
|
||||
// For mock purposes, if logged in as 'abc_supply', we map to 'v3'
|
||||
// In real app, user.vendorId would track this.
|
||||
const vendorId = user.id === 'ven_001' ? 'v3' : 'v1';
|
||||
const vendorData = vendors.find(v => v.id === vendorId) || vendors[0];
|
||||
|
||||
// 2. Aggregate Data
|
||||
// Find projects where this vendor is assigned to milestones or has invoices
|
||||
// A. Milestones
|
||||
const activeDeliveries = projects.flatMap(p => p.milestones)
|
||||
.filter(m => m.assignedTo === vendorId && m.status === 'pending').length;
|
||||
|
||||
const completedDeliveries = projects.flatMap(p => p.milestones)
|
||||
.filter(m => m.assignedTo === vendorId && m.status === 'completed').length;
|
||||
|
||||
// B. Financials
|
||||
const pendingInvoices = vendorData.spend?.pendingInvoices || 0;
|
||||
const totalEarnings = vendorData.spend?.totalSpend || 0;
|
||||
|
||||
// C. Compliance
|
||||
const complianceStatus = vendorData.compliance?.coi?.status === 'compliant' ? 'Verified' : 'Action Required';
|
||||
|
||||
// D. Financial Data for Modal
|
||||
const vendorOrders = orders.filter(o => o.vendorId === vendorId);
|
||||
const vendorInvoicesList = vendorInvoices.filter(inv => inv.vendorId === vendorId);
|
||||
const paidInvoices = vendorInvoicesList.filter(inv => inv.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0);
|
||||
const pendingPayments = vendorInvoicesList.filter(inv => inv.status === 'pending').reduce((sum, inv) => sum + inv.amount, 0);
|
||||
const financialData = {
|
||||
totalEarnings: vendorData.spend?.ytdSpend || 0,
|
||||
paidInvoices,
|
||||
pendingPayments,
|
||||
invoices: vendorInvoicesList
|
||||
};
|
||||
|
||||
// E. Click Handlers
|
||||
const handleOrderClick = (order) => {
|
||||
setSelectedOrder(order);
|
||||
setIsOrderModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">Vendor Portal</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Welcome back, <span className="text-blue-500 font-semibold">{vendorData.vendorName}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
onClick={() => setIsComplianceModalOpen(true)}
|
||||
className={`px-3 py-1 rounded-full text-xs font-bold uppercase cursor-pointer hover:opacity-80 transition-opacity ${complianceStatus === 'Verified'
|
||||
? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400'
|
||||
: 'bg-red-100 text-red-600 dark:bg-red-500/10 dark:text-red-400'
|
||||
}`}>
|
||||
Compliance: {complianceStatus}
|
||||
</span>
|
||||
<button className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
|
||||
Submit Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div onClick={() => setIsFinancialModalOpen(true)} className="cursor-pointer">
|
||||
<StatCard
|
||||
label="Pending Invoices"
|
||||
value={`$${pendingInvoices.toLocaleString()}`}
|
||||
icon={DollarSign}
|
||||
color="amber"
|
||||
/>
|
||||
</div>
|
||||
<StatCard
|
||||
label="Active Orders"
|
||||
value={activeDeliveries}
|
||||
icon={ClipboardCheck}
|
||||
color="blue"
|
||||
onClick={() => navigate('/vendor/orders')}
|
||||
className="cursor-pointer hover:border-blue-500/50 transition-colors"
|
||||
/>
|
||||
<div onClick={() => setIsFinancialModalOpen(true)} className="cursor-pointer">
|
||||
<StatCard
|
||||
label="Total Earnings (YTD)"
|
||||
value={`$${vendorData.spend?.ytdSpend?.toLocaleString() || '0'}`}
|
||||
icon={TrendingUp}
|
||||
color="emerald"
|
||||
trend={12}
|
||||
/>
|
||||
</div>
|
||||
<div onClick={() => setIsPerformanceModalOpen(true)} className="cursor-pointer">
|
||||
<StatCard
|
||||
label="Performance Rating"
|
||||
value={vendorData.performance?.rating || 0}
|
||||
icon={CheckCircle2}
|
||||
color="purple"
|
||||
suffix="/5.0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
{/* Active Projects / Deliveries */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<SpotlightCard className="p-6 h-full">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Active Orders & Deliveries</h2>
|
||||
<div className="space-y-3">
|
||||
{vendorOrders.map(order => (
|
||||
<div
|
||||
key={order.id}
|
||||
onClick={() => handleOrderClick(order)}
|
||||
className="flex items-center justify-between p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5 hover:border-blue-500/30 transition-colors cursor-pointer group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 rounded-lg bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400">
|
||||
<FileText size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{order.id}</h4>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||
{order.projectAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-mono font-medium text-zinc-700 dark:text-zinc-300">
|
||||
Due: {order.dueDate}
|
||||
</p>
|
||||
<span className={`text-xs font-bold uppercase ${order.status === 'delivered' ? 'text-emerald-500' :
|
||||
order.status === 'shipped' ? 'text-blue-500' : 'text-amber-500'
|
||||
}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{vendorOrders.length === 0 && (
|
||||
<div className="text-center py-10 text-zinc-500 dark:text-zinc-400">
|
||||
<p>No active orders found.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Updates & Alerts */}
|
||||
<div className="space-y-6">
|
||||
<SpotlightCard className="p-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Action Center</h2>
|
||||
<div className="space-y-4">
|
||||
{/* Invoices */}
|
||||
<div className="p-4 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-100 dark:border-amber-500/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle size={18} className="text-amber-600 dark:text-amber-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-amber-900 dark:text-amber-100">Review Rejected Invoice</h4>
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
|
||||
Invoice #INV-2024-001 was flagged for missing PO number.
|
||||
</p>
|
||||
<button className="mt-2 text-xs font-bold text-amber-600 dark:text-amber-400 hover:underline">
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compliance */}
|
||||
<div className="p-4 rounded-xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<Calendar size={18} className="text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-blue-900 dark:text-blue-100">COI Renewal Upcoming</h4>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||
Your General Liability policy expires in 45 days.
|
||||
</p>
|
||||
<button className="mt-2 text-xs font-bold text-blue-600 dark:text-blue-400 hover:underline">
|
||||
Upload Renewal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
|
||||
<SpotlightCard className="p-6">
|
||||
<h2 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Quick Stats</h2>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
||||
<span className="text-zinc-500">On-Time Delivery Rate</span>
|
||||
<span className="font-bold text-emerald-500">{vendorData.performance?.onTimeRate * 100}%</span>
|
||||
</li>
|
||||
<li className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
||||
<span className="text-zinc-500">Average Payment Window</span>
|
||||
<span className="font-bold text-zinc-900 dark:text-white">Net 15</span>
|
||||
</li>
|
||||
<li className="flex justify-between items-center py-2 border-b border-zinc-100 dark:border-white/5">
|
||||
<span className="text-zinc-500">Active Contracts</span>
|
||||
<span className="font-bold text-zinc-900 dark:text-white">3</span>
|
||||
</li>
|
||||
</ul>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<OrderDetailsModal
|
||||
isOpen={isOrderModalOpen}
|
||||
onClose={() => setIsOrderModalOpen(false)}
|
||||
order={selectedOrder}
|
||||
/>
|
||||
<VendorFinancialSummaryModal
|
||||
isOpen={isFinancialModalOpen}
|
||||
onClose={() => setIsFinancialModalOpen(false)}
|
||||
data={financialData}
|
||||
/>
|
||||
<ComplianceDetailsModal
|
||||
isOpen={isComplianceModalOpen}
|
||||
onClose={() => setIsComplianceModalOpen(false)}
|
||||
vendorData={vendorData}
|
||||
/>
|
||||
<PerformanceMetricsModal
|
||||
isOpen={isPerformanceModalOpen}
|
||||
onClose={() => setIsPerformanceModalOpen(false)}
|
||||
vendorData={vendorData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VendorDashboard;
|
||||
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useMockStore } from '../../data/mockStore';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { SpotlightCard } from '../../components/SpotlightCard';
|
||||
import { Package, FileText, Filter, Search, ChevronRight, CheckCircle, Clock, Truck } from 'lucide-react';
|
||||
import OrderDetailsModal from '../../components/vendor/OrderDetailsModal';
|
||||
|
||||
const VendorOrders = () => {
|
||||
const { user } = useAuth();
|
||||
const { projects, orders } = useMockStore();
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||
const [isOrderModalOpen, setIsOrderModalOpen] = useState(false);
|
||||
|
||||
// Mock ID logic
|
||||
const vendorId = user.id === 'ven_001' ? 'v3' : 'v1';
|
||||
|
||||
// Get vendor orders from MOCK_ORDERS
|
||||
const vendorOrders = orders.filter(o => o.vendorId === vendorId);
|
||||
const filteredOrders = filter === 'all' ? vendorOrders : vendorOrders.filter(o => o.status === filter);
|
||||
|
||||
const handleOrderClick = (order) => {
|
||||
setSelectedOrder(order);
|
||||
setIsOrderModalOpen(true);
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'delivered': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400';
|
||||
case 'shipped': return 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400';
|
||||
default: return 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-400';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||
<header className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">My Orders</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mt-1">Manage purchase orders and deliveries.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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 orders..."
|
||||
className="pl-9 pr-4 py-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-3 py-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-sm font-medium hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors">
|
||||
<Filter size={16} /> Filter
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Status Tabs */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
{['all', 'pending', 'confirmed', 'shipped', 'delivered'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setFilter(tab)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-bold uppercase tracking-wide whitespace-nowrap transition-colors ${filter === tab
|
||||
? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900'
|
||||
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SpotlightCard className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-white/10 text-xs font-bold uppercase tracking-wider text-zinc-500 bg-zinc-50/50 dark:bg-white/5">
|
||||
<th className="p-4">Order ID</th>
|
||||
<th className="p-4">Project / Location</th>
|
||||
<th className="p-4">Items / Service</th>
|
||||
<th className="p-4">Due Date</th>
|
||||
<th className="p-4">Amount</th>
|
||||
<th className="p-4">Status</th>
|
||||
<th className="p-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
|
||||
{filteredOrders.map(order => (
|
||||
<tr
|
||||
key={order.id}
|
||||
onClick={() => handleOrderClick(order)}
|
||||
className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors group cursor-pointer"
|
||||
>
|
||||
<td className="p-4 font-mono text-sm text-zinc-500">{order.id}</td>
|
||||
<td className="p-4 text-sm font-medium text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||||
{order.projectAddress || order.project}
|
||||
</td>
|
||||
<td className="p-4 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package size={16} className="text-zinc-400" />
|
||||
{order.items ? `${order.items.length} items` : order.item}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-zinc-500">{order.dueDate}</td>
|
||||
<td className="p-4 text-sm font-medium text-zinc-900 dark:text-white">${(order.total || order.amount).toLocaleString()}</td>
|
||||
<td className="p-4">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide ${getStatusColor(order.status)}`}>
|
||||
{order.status === 'delivered' && <CheckCircle size={12} className="mr-1.5" />}
|
||||
{order.status === 'shipped' && <Truck size={12} className="mr-1.5" />}
|
||||
{(order.status === 'pending' || order.status === 'confirmed') && <Clock size={12} className="mr-1.5" />}
|
||||
{order.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOrderClick(order);
|
||||
}}
|
||||
className="p-2 rounded-lg hover:bg-zinc-200 dark:hover:bg-white/10 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{filteredOrders.length === 0 && (
|
||||
<div className="p-12 text-center text-zinc-500">
|
||||
<Package size={48} className="mx-auto mb-4 opacity-20" />
|
||||
<p>No orders found matching this filter.</p>
|
||||
</div>
|
||||
)}
|
||||
</SpotlightCard>
|
||||
|
||||
{/* Order Details Modal */}
|
||||
<OrderDetailsModal
|
||||
isOpen={isOrderModalOpen}
|
||||
onClose={() => setIsOrderModalOpen(false)}
|
||||
order={selectedOrder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VendorOrders;
|
||||
Reference in New Issue
Block a user