feat: Multi-role platform expansion, mobile nav redesign, and UI polish

- Add Owner, Contractor, Vendor, Subcontractor dashboards and role-based routing
- Owner now has superuser access to all Admin pages (dashboard, schedule, leaderboard)
- Redesign landing page mobile menu as slide-in sidebar (replaces broken Framer Motion overlay)
- Add body scroll lock to app sidebar for mobile consistency
- Fix Team Schedule to match Admin Dashboard design language (ambient glows, gradient header, SpotlightCard, zinc palette)
- Fix login page tab overflow — use abbreviated labels and grid layout for 6 role tabs
- Fix Recharts ResponsiveContainer warnings — replace height="100%" with fixed pixel heights
- Fix lightbox image viewer — unified nav bar, touch swipe support, no more overlapping text
- Add AI Assistant page, People/Vendor/Document management for Owner role
- Expand mock data store with contractor, vendor, and subcontractor data
This commit is contained in:
Satyam
2026-02-17 01:19:41 +05:30
parent 35cbdeb33c
commit 2eaac6b84a
44 changed files with 6857 additions and 662 deletions
+241 -231
View File
@@ -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>
);