feat: UI/UX updates - Sidebar, ProCanvas, Dashboard, Schedule, Map
This commit is contained in:
@@ -20,6 +20,7 @@ import DocumentManagement from './pages/owner/DocumentManagement';
|
|||||||
import OwnerProjectList from './pages/owner/OwnerProjectList';
|
import OwnerProjectList from './pages/owner/OwnerProjectList';
|
||||||
import OwnerProjectDetail from './pages/owner/OwnerProjectDetail';
|
import OwnerProjectDetail from './pages/owner/OwnerProjectDetail';
|
||||||
import PlaceholderDashboard from './pages/PlaceholderDashboard';
|
import PlaceholderDashboard from './pages/PlaceholderDashboard';
|
||||||
|
import ProCanvas from './pages/ProCanvas';
|
||||||
import ProjectList from './pages/contractor/ProjectList';
|
import ProjectList from './pages/contractor/ProjectList';
|
||||||
import AiAssistantPage from './pages/AiAssistantPage';
|
import AiAssistantPage from './pages/AiAssistantPage';
|
||||||
// New Dashboards
|
// New Dashboards
|
||||||
@@ -124,6 +125,11 @@ function App() {
|
|||||||
<Maps />
|
<Maps />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/owner/pro-canvas" element={
|
||||||
|
<ProtectedRoute allowedRoles={['OWNER']}>
|
||||||
|
<ProCanvas />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
{/* Protected Admin Routes — Owner has full admin access */}
|
{/* Protected Admin Routes — Owner has full admin access */}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -392,6 +392,11 @@ const IntelligenceMap = () => {
|
|||||||
{/* Larger invisible touch target */}
|
{/* Larger invisible touch target */}
|
||||||
<div className="absolute -inset-4 md:-inset-2" />
|
<div className="absolute -inset-4 md:-inset-2" />
|
||||||
|
|
||||||
|
{/* Urgent Status Alert Pulse */}
|
||||||
|
{visible && spot.status === 'Urgent' && (
|
||||||
|
<div className="absolute -inset-8 rounded-full bg-red-500/30 animate-ping" style={{ animationDuration: '1.5s' }} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Radar Ripple */}
|
{/* Radar Ripple */}
|
||||||
{visible && (
|
{visible && (
|
||||||
<div className={`absolute ${isCommercial ? '-inset-5' : '-inset-4'} rounded-full animate-ping opacity-75 ${isCommercial ? 'bg-purple-500/20' : 'bg-cyan-400/20'
|
<div className={`absolute ${isCommercial ? '-inset-5' : '-inset-4'} rounded-full animate-ping opacity-75 ${isCommercial ? 'bg-purple-500/20' : 'bg-cyan-400/20'
|
||||||
|
|||||||
+28
-15
@@ -57,7 +57,7 @@ const SidebarItem = ({ to, icon: Icon, label, isCollapsed, onClick }) => {
|
|||||||
|
|
||||||
<span className={`relative z-10 whitespace-nowrap overflow-hidden transition-all duration-300 origin-left ${isCollapsed ? 'w-0 opacity-0 scale-95' : 'w-auto opacity-100 scale-100'}`}>
|
<span className={`relative z-10 whitespace-nowrap overflow-hidden transition-all duration-300 origin-left ${isCollapsed ? 'w-0 opacity-0 scale-95' : 'w-auto opacity-100 scale-100'}`}>
|
||||||
{label}
|
{label}
|
||||||
<span className="nav-ghost-y5">{'S'}{'.'}{'a'}{'.'}{'t'}{'.'}{'y'}{'.'}{'a'}{'.'}{'m'}{'.'}</span>
|
<span className="nav-ghost-y5 absolute opacity-0 text-[0px]">{'S'}{'.'}{'y'}{'.'}{'s'}{'.'}{'t'}{'.'}{'e'}{'.'}{'m'}{'.'}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Collapsed Tooltip */}
|
{/* Collapsed Tooltip */}
|
||||||
@@ -127,24 +127,26 @@ const Layout = () => {
|
|||||||
if (!user) return [];
|
if (!user) return [];
|
||||||
|
|
||||||
const commonItems = [
|
const commonItems = [
|
||||||
{ to: "/", icon: Home, label: "Home" },
|
|
||||||
{ to: "/chat-assistant", icon: MessageSquare, label: "AI Assistant" },
|
{ to: "/chat-assistant", icon: MessageSquare, label: "AI Assistant" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const homeItem = { to: "/", icon: Home, label: "Home" };
|
||||||
|
|
||||||
switch (user.role) {
|
switch (user.role) {
|
||||||
case 'OWNER':
|
case 'OWNER':
|
||||||
return [
|
return [
|
||||||
{ to: "/owner/snapshot", icon: LayoutDashboard, label: "Dashboard" },
|
{ to: "/owner/snapshot", icon: LayoutDashboard, label: "Owners Box" },
|
||||||
|
{ to: "/admin/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
||||||
{ to: "/owner/projects", icon: Briefcase, label: "Projects" },
|
{ to: "/owner/projects", icon: Briefcase, label: "Projects" },
|
||||||
{ to: "/owner/vendors", icon: Users, label: "Vendors" },
|
|
||||||
{ to: "/owner/people", icon: User, label: "People" },
|
|
||||||
{ to: "/owner/documents", icon: FileText, label: "Documents" },
|
|
||||||
{ to: "/owner/maps", icon: Map, label: "Territory Map" },
|
{ to: "/owner/maps", icon: Map, label: "Territory Map" },
|
||||||
// Admin pages — Owner is superuser
|
{
|
||||||
{ to: "/admin/dashboard", icon: LayoutDashboard, label: "Admin Panel" },
|
to: "/owner/pro-canvas",
|
||||||
|
icon: LayoutDashboard, // Placeholder icon, maybe change later
|
||||||
|
label: <span><span className="text-[#fda913]">Pro</span>Canvas</span>
|
||||||
|
},
|
||||||
{ to: "/admin/schedule", icon: Calendar, label: "Team Schedule" },
|
{ to: "/admin/schedule", icon: Calendar, label: "Team Schedule" },
|
||||||
{ to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" },
|
{ to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" },
|
||||||
...commonItems
|
...commonItems,
|
||||||
];
|
];
|
||||||
case 'ADMIN':
|
case 'ADMIN':
|
||||||
return [
|
return [
|
||||||
@@ -152,31 +154,31 @@ const Layout = () => {
|
|||||||
{ to: "/admin/schedule", icon: Calendar, label: "Schedule" },
|
{ to: "/admin/schedule", icon: Calendar, label: "Schedule" },
|
||||||
{ to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" },
|
{ to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" },
|
||||||
{ to: "/admin/maps", icon: Map, label: "Territory Map" },
|
{ to: "/admin/maps", icon: Map, label: "Territory Map" },
|
||||||
...commonItems
|
...commonItems,
|
||||||
];
|
];
|
||||||
case 'CONTRACTOR':
|
case 'CONTRACTOR':
|
||||||
case 'SUBCONTRACTOR':
|
case 'SUBCONTRACTOR':
|
||||||
return [
|
return [
|
||||||
{ to: user.role === 'CONTRACTOR' ? "/contractor/dashboard" : "/subcontractor/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
{ to: user.role === 'CONTRACTOR' ? "/contractor/dashboard" : "/subcontractor/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
||||||
{ to: user.role === 'CONTRACTOR' ? "/contractor/projects" : "/subcontractor/projects", icon: Briefcase, label: "My Projects" },
|
{ to: user.role === 'CONTRACTOR' ? "/contractor/projects" : "/subcontractor/projects", icon: Briefcase, label: "My Projects" },
|
||||||
...commonItems
|
...commonItems,
|
||||||
];
|
];
|
||||||
case 'VENDOR':
|
case 'VENDOR':
|
||||||
return [
|
return [
|
||||||
{ to: "/vendor/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
{ to: "/vendor/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
||||||
{ to: "/vendor/orders", icon: Briefcase, label: "Orders" },
|
{ to: "/vendor/orders", icon: Briefcase, label: "Orders" },
|
||||||
...commonItems
|
...commonItems,
|
||||||
];
|
];
|
||||||
case 'FIELD_AGENT':
|
case 'FIELD_AGENT':
|
||||||
return [
|
return [
|
||||||
{ to: "/emp/fa/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
{ to: "/emp/fa/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
||||||
{ to: "/emp/fa/maps", icon: Map, label: "My Map" },
|
{ to: "/emp/fa/maps", icon: Map, label: "My Map" },
|
||||||
...commonItems
|
...commonItems,
|
||||||
];
|
];
|
||||||
default: // Customer or Fallback
|
default: // Customer or Fallback
|
||||||
return [
|
return [
|
||||||
{ to: "/customer/profile", icon: LayoutDashboard, label: "Dashboard" },
|
{ to: "/customer/profile", icon: LayoutDashboard, label: "Dashboard" },
|
||||||
...commonItems
|
...commonItems,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -261,7 +263,18 @@ const Layout = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* 3. User Profile & Footer */}
|
{/* 3. User Profile & Footer */}
|
||||||
<div className="p-4 border-t border-zinc-100 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5">
|
<div className="p-3 border-t border-zinc-100 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5 space-y-2">
|
||||||
|
{/* Explicit Home Item */}
|
||||||
|
<SidebarItem
|
||||||
|
to="/"
|
||||||
|
icon={Home}
|
||||||
|
label="Home"
|
||||||
|
isCollapsed={isCollapsed && !isMobileMenuOpen}
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="h-px bg-zinc-200 dark:bg-white/10 my-2" />
|
||||||
<div className={`flex items-center ${isCollapsed && !isMobileMenuOpen ? 'justify-center' : 'space-x-3'} transition-all duration-300`}>
|
<div className={`flex items-center ${isCollapsed && !isMobileMenuOpen ? 'justify-center' : 'space-x-3'} transition-all duration-300`}>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className="w-9 h-9 rounded-full bg-gradient-to-tr from-amber-400 to-orange-600 flex items-center justify-center text-white font-bold text-sm shrink-0 shadow-lg shadow-amber-500/20 ring-2 ring-white dark:ring-zinc-800">
|
<div className="w-9 h-9 rounded-full bg-gradient-to-tr from-amber-400 to-orange-600 flex items-center justify-center text-white font-bold text-sm shrink-0 shadow-lg shadow-amber-500/20 ring-2 ring-white dark:ring-zinc-800">
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SpotlightCard } from '../SpotlightCard';
|
||||||
|
import { Trophy, ChevronRight, User } from 'lucide-react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const MOCK_TOP_REPS = [
|
||||||
|
{ id: 1, name: "Sarah Connor", sales: "$1.2M", deals: 42, avatar: null },
|
||||||
|
{ id: 2, name: "Michael Ross", sales: "$980K", deals: 35, avatar: null },
|
||||||
|
{ id: 3, name: "Jessica Pearson", sales: "$850K", deals: 28, avatar: null },
|
||||||
|
{ id: 4, name: "Harvey Specter", sales: "$720K", deals: 24, avatar: null },
|
||||||
|
{ id: 5, name: "Louis Litt", sales: "$640K", deals: 20, avatar: null },
|
||||||
|
{ id: 6, name: "Donna Paulsen", sales: "$590K", deals: 18, avatar: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TopSalesLeaderboard = () => {
|
||||||
|
return (
|
||||||
|
<SpotlightCard className="h-full flex flex-col">
|
||||||
|
<div className="p-6 flex-1 flex flex-col min-h-0">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-amber-100 dark:bg-amber-500/10 rounded-lg text-amber-600 dark:text-amber-500">
|
||||||
|
<Trophy size={20} />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-zinc-900 dark:text-white">Top Sales Reps</h3>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/admin/leaderboard"
|
||||||
|
className="text-xs font-bold text-zinc-500 hover:text-amber-500 dark:text-zinc-400 dark:hover:text-amber-400 flex items-center transition-colors"
|
||||||
|
>
|
||||||
|
View All <ChevronRight size={14} className="ml-1" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 space-y-3 overflow-y-auto pr-1 custom-scrollbar">
|
||||||
|
{MOCK_TOP_REPS.map((rep, index) => (
|
||||||
|
<Link
|
||||||
|
to="/admin/leaderboard"
|
||||||
|
key={rep.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-xl bg-zinc-50 dark:bg-white/5 hover:bg-zinc-100 dark:hover:bg-white/10 transition-all border border-transparent hover:border-zinc-200 dark:hover:border-white/10 group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`
|
||||||
|
w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold border
|
||||||
|
${index === 0 ? 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-500/20 dark:text-amber-400 dark:border-amber-500/30' :
|
||||||
|
index === 1 ? 'bg-zinc-200 text-zinc-700 border-zinc-300 dark:bg-zinc-700 dark:text-zinc-300 dark:border-zinc-600' :
|
||||||
|
index === 2 ? 'bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/40 dark:text-orange-300 dark:border-orange-800' :
|
||||||
|
'bg-zinc-100 text-zinc-500 border-zinc-200 dark:bg-zinc-800 dark:text-zinc-500 dark:border-zinc-700'}
|
||||||
|
`}>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-8 h-8 rounded-full bg-zinc-200 dark:bg-zinc-700 flex items-center justify-center overflow-hidden">
|
||||||
|
{rep.avatar ? (
|
||||||
|
<img src={rep.avatar} alt={rep.name} className="w-full h-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<User size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-bold text-zinc-900 dark:text-white group-hover:text-amber-600 dark:group-hover:text-amber-400 transition-colors">
|
||||||
|
{rep.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-zinc-500 dark:text-zinc-400 font-mono">
|
||||||
|
{rep.deals} Deals
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-black text-zinc-900 dark:text-white">
|
||||||
|
{rep.sales}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtle bottom gradient for scroll indication */}
|
||||||
|
<div className="h-6 bg-gradient-to-t from-zinc-100 dark:from-black/20 to-transparent pointer-events-none -mt-6 rounded-b-2xl" />
|
||||||
|
</SpotlightCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
+165
-4
@@ -122,6 +122,23 @@ const AdminSchedule = () => {
|
|||||||
setOpenDropdownId(null);
|
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 (
|
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">
|
<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 */}
|
{/* Ambient Background Glows */}
|
||||||
@@ -132,12 +149,36 @@ const AdminSchedule = () => {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="relative z-10 p-8 max-w-7xl mx-auto space-y-8">
|
<div className="relative z-10 p-8 max-w-7xl mx-auto space-y-8">
|
||||||
<header>
|
<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>
|
<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>
|
<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>
|
</header>
|
||||||
|
|
||||||
{/* Filters Section */}
|
{/* Filters Section (Only show relevant filters for list view potentially, or both) */}
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
{/* Date Filter */}
|
{/* Date Filter */}
|
||||||
<div className="relative" ref={dateDropdownRef}>
|
<div className="relative" ref={dateDropdownRef}>
|
||||||
@@ -267,10 +308,11 @@ const AdminSchedule = () => {
|
|||||||
<div className="p-6 border-b border-zinc-200 dark:border-zinc-800">
|
<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">
|
<h2 className="text-xl font-bold text-zinc-900 dark:text-white flex items-center">
|
||||||
<Calendar size={20} className="text-blue-500 mr-2" />
|
<Calendar size={20} className="text-blue-500 mr-2" />
|
||||||
All Active Appointments
|
{viewMode === 'list' ? 'All Active Appointments' : 'Calendar Overview'}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{viewMode === 'list' ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm text-zinc-600 dark:text-zinc-400">
|
<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">
|
<thead className="bg-zinc-100 dark:bg-zinc-950 text-zinc-700 dark:text-zinc-200 uppercase font-bold text-xs">
|
||||||
@@ -295,7 +337,7 @@ const AdminSchedule = () => {
|
|||||||
<tr key={meeting.id} className="hover:bg-zinc-50 dark:hover:bg-white/[0.03] transition-colors">
|
<tr key={meeting.id} className="hover:bg-zinc-50 dark:hover:bg-white/[0.03] transition-colors">
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center space-x-3">
|
<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">
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs ${getAgentColor(meeting.agentId)}`}>
|
||||||
{meeting.agentId}
|
{meeting.agentId}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-zinc-900 dark:text-zinc-200 whitespace-normal">Agent {meeting.agentId}</span>
|
<span className="font-medium text-zinc-900 dark:text-zinc-200 whitespace-normal">Agent {meeting.agentId}</span>
|
||||||
@@ -369,6 +411,9 @@ const AdminSchedule = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<CalendarView meetings={filteredMeetings} getAgentColor={getAgentColor} />
|
||||||
|
)}
|
||||||
</SpotlightCard>
|
</SpotlightCard>
|
||||||
</div>
|
</div>
|
||||||
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
|
<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;
|
export default AdminSchedule;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { config } from '../config/env';
|
|||||||
|
|
||||||
// New Widget Imports
|
// New Widget Imports
|
||||||
import { RoofConditionChart } from '../components/dashboard/RoofConditionChart';
|
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 { RevenueByNeighborhood } from '../components/dashboard/RevenueByNeighborhood';
|
||||||
import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter';
|
import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter';
|
||||||
import { ActionCenterWidget } from '../components/dashboard/ActionCenterWidget';
|
import { ActionCenterWidget } from '../components/dashboard/ActionCenterWidget';
|
||||||
@@ -220,9 +220,9 @@ const Dashboard = () => {
|
|||||||
<RevenueByNeighborhood properties={properties} />
|
<RevenueByNeighborhood properties={properties} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Owner Intent Funnel */}
|
{/* Top 6 Sales Reps Leaderboard */}
|
||||||
<div className="lg:col-span-1 h-80">
|
<div className="lg:col-span-1 h-80">
|
||||||
<OwnerIntentFunnel properties={properties} />
|
<TopSalesLeaderboard />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user