customer login is working and customer dashboard created
This commit is contained in:
+7
-1
@@ -8,6 +8,7 @@ import Dashboard from './pages/Dashboard';
|
||||
import Maps from './pages/Maps';
|
||||
import AdminSchedule from './pages/AdminSchedule';
|
||||
import CustomerProfile from './pages/CustomerProfile';
|
||||
import CustomerDashboard from './pages/CustomerDashboard';
|
||||
import LeaderboardPage from './pages/LeaderboardPage';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import SmoothScroll from './components/SmoothScroll';
|
||||
@@ -109,7 +110,12 @@ function App() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Protected Field Agent Routes */}
|
||||
{/* Customer Portal Routes */}
|
||||
<Route path="/portal/dashboard" element={
|
||||
<ProtectedRoute allowedRoles={['CUSTOMER']}>
|
||||
<CustomerDashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/portal/profile" element={
|
||||
<ProtectedRoute allowedRoles={['CUSTOMER']}>
|
||||
<CustomerProfile />
|
||||
|
||||
@@ -219,7 +219,8 @@ const Layout = () => {
|
||||
];
|
||||
default: // Customer or Fallback
|
||||
return [
|
||||
{ to: "/customer/profile", icon: LayoutDashboard, label: "Dashboard" },
|
||||
{ to: "/portal/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
||||
{ to: "/portal/profile", icon: User, label: "My Profile" },
|
||||
...commonItems,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -986,7 +986,7 @@ function generateMockMeetings() {
|
||||
status: 'Completed',
|
||||
agentName: 'Hannah Reyes',
|
||||
dealValue: 1500,
|
||||
notes: 'Minor repairs completed. Payment received.',
|
||||
notes: 'Minor repairs completed.',
|
||||
issueDescription: 'Leak in garage.',
|
||||
customerComments: 'Please call before arriving.',
|
||||
outcome: 'Repairs Completed'
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* CustomerDashboard — the landing screen for logged-in customers (role: CUSTOMER).
|
||||
*
|
||||
* Rendered at /portal/dashboard and linked from the "Dashboard" item in the
|
||||
* customer sidebar. Gives the homeowner an at-a-glance overview of their work
|
||||
* with LynkedUp: upcoming inspections/visits, recent job history, what they've
|
||||
* invested, plus quick links into Payments and their Profile.
|
||||
*
|
||||
* Data comes from the mock store (meetings) — the same source CustomerProfile uses.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useMockStore } from '../data/mockStore';
|
||||
import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import {
|
||||
Calendar, Clock, CheckCircle, DollarSign, Sparkles, User as UserIcon,
|
||||
MessageSquare, Home, ArrowRight, Wrench,
|
||||
} from 'lucide-react';
|
||||
|
||||
const CustomerDashboard = () => {
|
||||
const { user } = useAuth();
|
||||
const { meetings } = useMockStore();
|
||||
|
||||
// My visits — match by explicit id link, fall back to name for legacy data
|
||||
const myMeetings = (meetings || []).filter(m =>
|
||||
(m.customerId === user?.id) || (m.customerName === user?.name)
|
||||
);
|
||||
|
||||
const startOfToday = () => {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
};
|
||||
|
||||
const upcomingVisits = myMeetings
|
||||
.filter(m => new Date(m.date) >= startOfToday() && m.status !== 'Completed' && m.status !== 'Cancelled')
|
||||
.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
|
||||
const pastVisits = myMeetings
|
||||
.filter(m => new Date(m.date) < startOfToday() || m.status === 'Completed')
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
const totalInvested = pastVisits.reduce((sum, m) => sum + (m.dealValue || 0), 0);
|
||||
const nextVisit = upcomingVisits[0];
|
||||
|
||||
const firstName = (user?.name || 'there').split(' ')[0];
|
||||
|
||||
const stats = [
|
||||
{ label: 'Upcoming Visits', value: upcomingVisits.length, icon: Calendar, accent: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-900/20', ring: 'border-blue-100 dark:border-blue-500/20' },
|
||||
{ label: 'Completed Jobs', value: pastVisits.length, icon: CheckCircle, accent: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-900/20', ring: 'border-emerald-100 dark:border-emerald-500/20' },
|
||||
{ label: 'Total Invested', value: `$${totalInvested.toLocaleString()}`, icon: DollarSign, accent: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20', ring: 'border-amber-100 dark:border-amber-500/20' },
|
||||
];
|
||||
|
||||
const quickActions = [
|
||||
{ to: '/estimate', label: 'Estimate Wizard', desc: 'Build a new roof estimate', icon: Sparkles, accent: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' },
|
||||
{ to: '/portal/profile', label: 'My Profile', desc: 'Update your details & property', icon: UserIcon, accent: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-900/20' },
|
||||
{ to: '/chat-assistant', label: 'Ask Assistant', desc: 'Get help anytime', icon: MessageSquare, accent: 'text-purple-600 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-900/20' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white p-4 sm:p-8 pt-20 sm:pt-24 transition-colors duration-300">
|
||||
<div className="max-w-5xl mx-auto space-y-8">
|
||||
|
||||
{/* --- Welcome header --- */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-2xl font-bold text-white shadow-lg shrink-0">
|
||||
{user?.name?.charAt(0) || 'C'}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r from-zinc-900 to-zinc-600 dark:from-white dark:to-white/60">
|
||||
Welcome back, {firstName}
|
||||
</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-sm sm:text-base">Here's what's happening with your property.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- Stat cards --- */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon;
|
||||
return (
|
||||
<SpotlightCard key={stat.label} className="p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl ${stat.bg} ${stat.ring} border flex items-center justify-center ${stat.accent} shrink-0`}>
|
||||
<Icon size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-black leading-none">{stat.value}</div>
|
||||
<div className="text-xs font-bold uppercase tracking-wider text-zinc-400 dark:text-zinc-500 mt-1.5">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* --- Next visit highlight --- */}
|
||||
<section>
|
||||
<h2 className="text-lg font-bold mb-4 flex items-center text-zinc-800 dark:text-zinc-200">
|
||||
<Calendar className="mr-2 text-blue-500" size={20} /> Your Next Visit
|
||||
</h2>
|
||||
{nextVisit ? (
|
||||
<SpotlightCard className="p-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-xl bg-blue-50 dark:bg-blue-900/20 flex flex-col items-center justify-center text-blue-600 dark:text-blue-400 border border-blue-100 dark:border-blue-500/20 shrink-0">
|
||||
<span className="text-[10px] uppercase font-bold">{new Date(nextVisit.date).toLocaleString('default', { month: 'short' })}</span>
|
||||
<span className="text-xl font-bold leading-none">{new Date(nextVisit.date).getDate()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg">{nextVisit.notes || 'Scheduled Visit'}</h3>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
<span className="flex items-center"><Clock size={14} className="mr-1" /> {nextVisit.time || 'TBD'}</span>
|
||||
<span className="flex items-center"><UserIcon size={14} className="mr-1" /> {nextVisit.agentName || 'Assigned Agent'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="self-start px-3 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 text-xs font-bold uppercase rounded-full">
|
||||
{nextVisit.status}
|
||||
</span>
|
||||
</div>
|
||||
{nextVisit.issueDescription && (
|
||||
<div className="mt-4 pt-4 border-t border-zinc-100 dark:border-white/5 flex items-start gap-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<Wrench size={15} className="mt-0.5 shrink-0 text-zinc-400" />
|
||||
<span>{nextVisit.issueDescription}</span>
|
||||
</div>
|
||||
)}
|
||||
</SpotlightCard>
|
||||
) : (
|
||||
<div className="p-8 text-center border border-dashed border-zinc-300 dark:border-zinc-700 rounded-xl text-zinc-500">
|
||||
No upcoming visits scheduled. We'll let you know when one is booked.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* --- Quick actions --- */}
|
||||
<section>
|
||||
<h2 className="text-lg font-bold mb-4 flex items-center text-zinc-800 dark:text-zinc-200">
|
||||
<Home className="mr-2 text-zinc-500" size={20} /> Quick Actions
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{quickActions.map((action) => {
|
||||
const Icon = action.icon;
|
||||
return (
|
||||
<Link key={action.to} to={action.to}>
|
||||
<SpotlightCard className="p-5 h-full group cursor-pointer hover:-translate-y-1 transition-transform duration-300">
|
||||
<div className={`w-11 h-11 rounded-xl ${action.bg} flex items-center justify-center ${action.accent} mb-4`}>
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold">{action.label}</h3>
|
||||
<ArrowRight size={16} className="text-zinc-400 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{action.desc}</p>
|
||||
</SpotlightCard>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Recent activity --- */}
|
||||
<section>
|
||||
<h2 className="text-lg font-bold mb-4 flex items-center text-zinc-800 dark:text-zinc-200">
|
||||
<CheckCircle className="mr-2 text-emerald-500" size={20} /> Recent Activity
|
||||
</h2>
|
||||
{pastVisits.length > 0 ? (
|
||||
<div className="grid gap-3">
|
||||
{pastVisits.slice(0, 3).map((visit) => (
|
||||
<SpotlightCard key={visit.id} className="p-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-zinc-500 shrink-0">
|
||||
<CheckCircle size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className="font-bold">{visit.agentName || 'Agent'}</h4>
|
||||
<span className="text-xs text-zinc-400">• {new Date(visit.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mt-0.5">{visit.notes || 'No notes available.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{visit.outcome ? (
|
||||
<span className="self-start px-3 py-1 bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300 text-xs font-bold uppercase rounded-full">
|
||||
{visit.outcome}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center border border-dashed border-zinc-300 dark:border-zinc-700 rounded-xl text-zinc-500">
|
||||
No activity yet.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerDashboard;
|
||||
+2
-2
@@ -126,7 +126,7 @@ const Login = () => {
|
||||
// Role-based Redirect
|
||||
switch (result.role) {
|
||||
case 'CUSTOMER':
|
||||
navigate('/');
|
||||
navigate('/portal/dashboard');
|
||||
break;
|
||||
case 'OWNER':
|
||||
navigate('/owner/snapshot');
|
||||
@@ -154,7 +154,7 @@ const Login = () => {
|
||||
const result = login(account.id, account.pw, account.loginType);
|
||||
if (result.success) {
|
||||
switch (result.role) {
|
||||
case 'CUSTOMER': navigate('/'); break;
|
||||
case 'CUSTOMER': navigate('/portal/dashboard'); break;
|
||||
case 'OWNER': navigate('/owner/snapshot'); break;
|
||||
case 'CONTRACTOR': navigate('/contractor/dashboard'); break;
|
||||
case 'VENDOR': navigate('/vendor/dashboard'); break;
|
||||
|
||||
Reference in New Issue
Block a user