feat: UI/UX updates - Sidebar, ProCanvas, Dashboard, Schedule, Map

This commit is contained in:
Satyam
2026-02-20 13:32:44 +05:30
parent 36e74e9fbc
commit 7e166726da
7 changed files with 412 additions and 117 deletions
+260 -99
View File
@@ -122,6 +122,23 @@ const AdminSchedule = () => {
setOpenDropdownId(null);
};
const [viewMode, setViewMode] = useState('list'); // 'list' or 'calendar'
// Mock color mapping for agents
const agentColors = {
'a1': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800',
'a2': 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800',
'a3': 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-800',
'a4': 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-800',
'a5': 'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-300 dark:border-pink-800',
};
const getAgentColor = (agentId) => {
// Simple hash or lookup
const id = agentId?.toLowerCase() || 'a1';
return agentColors[id] || agentColors['a1'];
};
return (
<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 */}
@@ -132,12 +149,36 @@ const AdminSchedule = () => {
{/* 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 className="flex justify-between items-end">
<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 mb-2 tracking-tight">Team Schedule</h1>
<p className="text-zinc-600 dark:text-zinc-400">Manage field agent appointments</p>
</div>
{/* View Toggle */}
<div className="flex bg-zinc-100 dark:bg-zinc-800 p-1 rounded-xl border border-zinc-200 dark:border-zinc-700">
<button
onClick={() => setViewMode('list')}
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${viewMode === 'list'
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'
}`}
>
List View
</button>
<button
onClick={() => setViewMode('calendar')}
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${viewMode === 'calendar'
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'
}`}
>
Calendar View
</button>
</div>
</header>
{/* Filters Section */}
{/* Filters Section (Only show relevant filters for list view potentially, or both) */}
<div className="flex flex-wrap gap-4 items-center">
{/* Date Filter */}
<div className="relative" ref={dateDropdownRef}>
@@ -267,108 +308,112 @@ const AdminSchedule = () => {
<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
{viewMode === 'list' ? 'All Active Appointments' : 'Calendar Overview'}
</h2>
</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 ? (
{viewMode === 'list' ? (
<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>
<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>
<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>
) : (
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>
</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 flex items-center justify-center font-bold text-xs ${getAgentColor(meeting.agentId)}`}>
{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>
) : (
<CalendarView meetings={filteredMeetings} getAgentColor={getAgentColor} />
)}
</SpotlightCard>
</div>
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
@@ -376,4 +421,120 @@ const AdminSchedule = () => {
);
};
// --- Custom Month View Calendar ---
const CalendarView = ({ meetings, getAgentColor }) => {
// Current month view state
const [currentDate, setCurrentDate] = useState(new Date());
const getDaysInMonth = (date) => {
const year = date.getFullYear();
const month = date.getMonth();
const days = new Date(year, month + 1, 0).getDate();
const firstDay = new Date(year, month, 1).getDay();
return { days, firstDay };
};
const { days, firstDay } = useMemo(() => getDaysInMonth(currentDate), [currentDate]);
const changeMonth = (offset) => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + offset, 1));
};
// Group meetings by date (using local date string YYYY-MM-DD)
const meetingsByDate = useMemo(() => {
const map = {};
meetings.forEach(m => {
// Assuming m.date is YYYY-MM-DD or parseable
const d = new Date(m.date);
// We need to compare local dates
const key = d.getDate(); // 1-31
const monthMatches = d.getMonth() === currentDate.getMonth() && d.getFullYear() === currentDate.getFullYear();
if (monthMatches) {
if (!map[key]) map[key] = [];
map[key].push(m);
}
});
return map;
}, [meetings, currentDate]);
const renderCalendarCells = () => {
const cells = [];
// Empty cells for padding before the first day
for (let i = 0; i < firstDay; i++) {
cells.push(<div key={`empty-${i}`} className="h-32 bg-zinc-50/50 dark:bg-zinc-900/30 border-r border-b border-zinc-200 dark:border-zinc-800"></div>);
}
// Days
for (let d = 1; d <= days; d++) {
const dayMeetings = meetingsByDate[d] || [];
const isToday = new Date().toDateString() === new Date(currentDate.getFullYear(), currentDate.getMonth(), d).toDateString();
cells.push(
<div key={`day-${d}`} className={`group h-32 p-2 border-r border-b border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors relative overflow-y-auto custom-scrollbar ${isToday ? 'bg-blue-50/30 dark:bg-blue-500/5' : ''}`}>
<div className="flex justify-between items-start mb-1">
<span className={`text-sm font-bold w-6 h-6 flex items-center justify-center rounded-full ${isToday ? 'bg-blue-500 text-white' : 'text-zinc-500 dark:text-zinc-400'}`}>
{d}
</span>
</div>
<div className="space-y-1">
{dayMeetings.map(m => (
<div
key={m.id}
className={`text-[10px] truncate px-1.5 py-1 rounded border shadow-sm cursor-pointer hover:opacity-80 ${getAgentColor(m.agentId)}`}
title={`${m.time} - ${m.customerName} (${m.status})`}
>
<span className="font-bold mr-1">{m.time}</span>
{m.customerName}
</div>
))}
</div>
</div>
);
}
return cells;
};
return (
<div className="p-4">
{/* Calendar Header */}
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold text-zinc-900 dark:text-white capitalize">
{currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
</h3>
<div className="flex gap-2">
<button onClick={() => changeMonth(-1)} className="p-2 rounded-lg bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors">Before</button>
<button onClick={() => setCurrentDate(new Date())} className="px-4 py-2 rounded-lg bg-blue-500 text-white text-sm font-bold hover:bg-blue-600 transition-colors">Today</button>
<button onClick={() => changeMonth(1)} className="p-2 rounded-lg bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors">Next</button>
</div>
</div>
{/* Calendar Grid */}
<div className="border-t border-l border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden">
{/* Weekday Headers */}
<div className="grid grid-cols-7 bg-zinc-50 dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-800">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<div key={day} className="py-2 text-center text-xs font-bold uppercase text-zinc-500 dark:text-zinc-400 border-r border-zinc-200 dark:border-zinc-800 last:border-r-0">
{day}
</div>
))}
</div>
{/* Days Grid */}
<div className="grid grid-cols-7 bg-white dark:bg-[#09090b]">
{renderCalendarCells()}
</div>
</div>
{/* Legend */}
<div className="mt-4 flex flex-wrap gap-4 text-xs text-zinc-500 dark:text-zinc-400">
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded bg-blue-100 border border-blue-200"></div> Agent A1</div>
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded bg-emerald-100 border border-emerald-200"></div> Agent A2</div>
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded bg-purple-100 border border-purple-200"></div> Agent A3</div>
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded bg-amber-100 border border-amber-200"></div> Agent A4</div>
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded bg-pink-100 border border-pink-200"></div> Agent A5</div>
</div>
</div>
);
};
export default AdminSchedule;
+3 -3
View File
@@ -11,7 +11,7 @@ import { config } from '../config/env';
// New Widget Imports
import { RoofConditionChart } from '../components/dashboard/RoofConditionChart';
import { OwnerIntentFunnel } from '../components/dashboard/OwnerIntentFunnel';
import { TopSalesLeaderboard } from '../components/dashboard/TopSalesLeaderboard';
import { RevenueByNeighborhood } from '../components/dashboard/RevenueByNeighborhood';
import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter';
import { ActionCenterWidget } from '../components/dashboard/ActionCenterWidget';
@@ -220,9 +220,9 @@ const Dashboard = () => {
<RevenueByNeighborhood properties={properties} />
</div>
{/* Owner Intent Funnel */}
{/* Top 6 Sales Reps Leaderboard */}
<div className="lg:col-span-1 h-80">
<OwnerIntentFunnel properties={properties} />
<TopSalesLeaderboard />
</div>
</div>
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import { Construction } from 'lucide-react';
import { SpotlightCard } from '../components/SpotlightCard';
const ProCanvas = () => {
return (
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white p-8 flex items-center justify-center">
<SpotlightCard className="max-w-2xl w-full p-12 text-center">
<div className="flex justify-center mb-6">
<Construction size={64} className="text-[#fda913] animate-pulse" />
</div>
<h1 className="text-4xl font-extrabold mb-4">
Pro<span className="text-[#fda913]">Canvas</span>
</h1>
<p className="text-xl text-zinc-500 dark:text-zinc-400 mb-8 font-light">
This module is currently under construction.
</p>
<div className="p-4 bg-zinc-100 dark:bg-white/5 rounded-xl border border-dashed border-zinc-300 dark:border-white/10">
<p className="text-sm font-mono text-[#fda913]">STATUS: DEVELOPMENT_IN_PROGRESS</p>
</div>
</SpotlightCard>
</div>
);
};
export default ProCanvas;