customer login is working and customer dashboard created

This commit is contained in:
Mayur Shinde
2026-06-12 20:58:46 +05:30
parent 5cb59b8a73
commit 233af7fb3d
5 changed files with 217 additions and 5 deletions
+7 -1
View File
@@ -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 />
+2 -1
View File
@@ -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,
]; ];
} }
+1 -1
View File
@@ -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'
+205
View File
@@ -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
View File
@@ -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;