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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user