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 Maps from './pages/Maps';
|
||||||
import AdminSchedule from './pages/AdminSchedule';
|
import AdminSchedule from './pages/AdminSchedule';
|
||||||
import CustomerProfile from './pages/CustomerProfile';
|
import CustomerProfile from './pages/CustomerProfile';
|
||||||
|
import CustomerDashboard from './pages/CustomerDashboard';
|
||||||
import LeaderboardPage from './pages/LeaderboardPage';
|
import LeaderboardPage from './pages/LeaderboardPage';
|
||||||
import ErrorBoundary from './components/ErrorBoundary';
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import SmoothScroll from './components/SmoothScroll';
|
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={
|
<Route path="/portal/profile" element={
|
||||||
<ProtectedRoute allowedRoles={['CUSTOMER']}>
|
<ProtectedRoute allowedRoles={['CUSTOMER']}>
|
||||||
<CustomerProfile />
|
<CustomerProfile />
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ const Layout = () => {
|
|||||||
];
|
];
|
||||||
default: // Customer or Fallback
|
default: // Customer or Fallback
|
||||||
return [
|
return [
|
||||||
{ to: "/customer/profile", icon: LayoutDashboard, label: "Dashboard" },
|
{ to: "/portal/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
||||||
|
{ to: "/portal/profile", icon: User, label: "My Profile" },
|
||||||
...commonItems,
|
...commonItems,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -986,7 +986,7 @@ function generateMockMeetings() {
|
|||||||
status: 'Completed',
|
status: 'Completed',
|
||||||
agentName: 'Hannah Reyes',
|
agentName: 'Hannah Reyes',
|
||||||
dealValue: 1500,
|
dealValue: 1500,
|
||||||
notes: 'Minor repairs completed. Payment received.',
|
notes: 'Minor repairs completed.',
|
||||||
issueDescription: 'Leak in garage.',
|
issueDescription: 'Leak in garage.',
|
||||||
customerComments: 'Please call before arriving.',
|
customerComments: 'Please call before arriving.',
|
||||||
outcome: 'Repairs Completed'
|
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
|
// Role-based Redirect
|
||||||
switch (result.role) {
|
switch (result.role) {
|
||||||
case 'CUSTOMER':
|
case 'CUSTOMER':
|
||||||
navigate('/');
|
navigate('/portal/dashboard');
|
||||||
break;
|
break;
|
||||||
case 'OWNER':
|
case 'OWNER':
|
||||||
navigate('/owner/snapshot');
|
navigate('/owner/snapshot');
|
||||||
@@ -154,7 +154,7 @@ const Login = () => {
|
|||||||
const result = login(account.id, account.pw, account.loginType);
|
const result = login(account.id, account.pw, account.loginType);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
switch (result.role) {
|
switch (result.role) {
|
||||||
case 'CUSTOMER': navigate('/'); break;
|
case 'CUSTOMER': navigate('/portal/dashboard'); break;
|
||||||
case 'OWNER': navigate('/owner/snapshot'); break;
|
case 'OWNER': navigate('/owner/snapshot'); break;
|
||||||
case 'CONTRACTOR': navigate('/contractor/dashboard'); break;
|
case 'CONTRACTOR': navigate('/contractor/dashboard'); break;
|
||||||
case 'VENDOR': navigate('/vendor/dashboard'); break;
|
case 'VENDOR': navigate('/vendor/dashboard'); break;
|
||||||
|
|||||||
Reference in New Issue
Block a user