Files
LynkedUpPro_CRM/src/pages/CustomerProfile.jsx
T
Satyam 5fef584d7d feat(a11y): Implement ARIA accessibility sweep and keyboard navigation
- Added visually hidden labels, IDs, and names to form inputs across all key components and pages (modals, directories, chatbots)
- Added tabIndex and onKeyDown handlers to clickable table rows in OwnerProjectDetail for keyboard accessibility
- Validated existing ARIA roles and Escape key bindings on modal components
2026-02-22 03:15:21 +05:30

332 lines
21 KiB
React

import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import { useMockStore } from '../data/mockStore';
import { SpotlightCard } from '../components/SpotlightCard';
import { User, Calendar, Clock, DollarSign, Save, Loader2, CheckCircle, History, Home, ShieldCheck } from 'lucide-react';
import { toast } from 'sonner';
const CustomerProfile = () => {
const { user, updateProfile } = useAuth();
const { meetings, properties } = useMockStore();
const [activeTab, setActiveTab] = useState('details');
const [isLoading, setIsLoading] = useState(false);
// Form State
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
address: ''
});
useEffect(() => {
if (user) {
setFormData({
name: user.name || '',
email: user.username || '', // Assuming username is email for customers
phone: user.phone || '555-0123', // Mock default if missing
address: user.address || '123 Maple Ave, Plano, TX' // Mock default
});
}
}, [user]);
const handleSaveProfile = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 800));
updateProfile({
name: formData.name,
username: formData.email,
phone: formData.phone,
address: formData.address
});
toast.success("Profile updated successfully!");
} catch (error) {
toast.error("Failed to update profile.");
} finally {
setIsLoading(false);
}
};
// Filter Meetings
const myMeetings = meetings.filter(m =>
(m.customerId === user?.id) ||
(m.customerName === user?.name) // Fallback for legacy data
);
// Get My Property
const myProperty = properties.find(p => p.propertyData.propertyId === user?.propertyId);
const upcomingVisits = myMeetings.filter(m => {
const meetingDate = new Date(m.date);
const today = new Date();
today.setHours(0, 0, 0, 0);
return meetingDate >= today && m.status !== 'Completed' && m.status !== 'Cancelled';
}).sort((a, b) => new Date(a.date) - new Date(b.date));
const pastVisits = myMeetings.filter(m => {
const meetingDate = new Date(m.date);
const today = new Date();
today.setHours(0, 0, 0, 0);
return meetingDate < today || m.status === 'Completed';
}).sort((a, b) => new Date(b.date) - new Date(a.date));
return (
<div className="min-h-screen bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white p-8 pt-24 transition-colors duration-300">
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-center space-x-4 mb-8">
<div className="w-16 h-16 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">
{user?.name?.charAt(0) || 'C'}
</div>
<div>
<h1 className="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">
My Profile
</h1>
<p className="text-zinc-500 dark:text-zinc-400">Manage your account and view visit history.</p>
</div>
</div>
{/* Tabs */}
<div className="flex space-x-1 border-b border-zinc-200 dark:border-zinc-800 mb-6 overflow-x-auto scrollbar-hide">
<button
onClick={() => setActiveTab('details')}
className={`px-4 md:px-6 py-3 text-xs md:text-sm font-bold uppercase tracking-wider border-b-2 transition-all whitespace-nowrap ${activeTab === 'details'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
}`}
>
Personal Details
</button>
<button
onClick={() => setActiveTab('property')}
className={`px-4 md:px-6 py-3 text-xs md:text-sm font-bold uppercase tracking-wider border-b-2 transition-all whitespace-nowrap ${activeTab === 'property'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
}`}
>
My Property
</button>
<button
onClick={() => setActiveTab('visits')}
className={`px-4 md:px-6 py-3 text-xs md:text-sm font-bold uppercase tracking-wider border-b-2 transition-all whitespace-nowrap ${activeTab === 'visits'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
}`}
>
Visit Management
</button>
</div>
{/* Content */}
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
{activeTab === 'details' ? (
<SpotlightCard>
<form onSubmit={handleSaveProfile} className="p-8 space-y-6">
<FormInput id="customer-name" name="name" label="Full Name" icon={User} value={formData.name} onChange={v => setFormData({ ...formData, name: v })} required />
<FormInput id="customer-email" name="email" label="Email Address" icon={null} prefix="@" type="email" value={formData.email} onChange={v => setFormData({ ...formData, email: v })} required />
<FormInput id="customer-phone" name="phone" label="Phone Number" value={formData.phone} onChange={v => setFormData({ ...formData, phone: v })} required />
<div className="space-y-2 md:col-span-2">
<label htmlFor="customer-address" className="text-xs font-bold uppercase text-zinc-500">Address</label>
<textarea
id="customer-address"
name="address"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
className="w-full px-4 py-2 bg-zinc-100 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all h-24 resize-none"
placeholder="1234 Main St..."
/>
</div>
<div className="pt-4 border-t border-zinc-200 dark:border-zinc-800 flex justify-end">
<button
type="submit"
disabled={isLoading}
className="flex items-center px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-bold shadow-lg shadow-blue-500/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<><Loader2 className="animate-spin mr-2" size={18} /> Saving...</>
) : (
<><Save className="mr-2" size={18} /> Save Changes</>
)}
</button>
</div>
</form>
</SpotlightCard>
) : activeTab === 'property' ? (
<SpotlightCard>
<form onSubmit={(e) => { e.preventDefault(); toast.success("Property details updated!"); }} className="p-8 space-y-8">
{/* Property Basics */}
<div className="space-y-6">
<div className="flex items-center gap-2 pb-4 border-b border-zinc-200 dark:border-white/5 text-blue-600 dark:text-blue-400">
<Home size={20} /> <h3 className="text-sm font-black uppercase tracking-widest">Property Basics</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<FormInput id="prop-address" name="prop-address" label="Address" value="2612 Dunwick Dr, Plano, TX 75023" onChange={() => { }} />
</div>
<FormInput id="prop-type" name="prop-type" label="Type" value="Residential" onChange={() => { }} />
<FormInput id="prop-year" name="prop-year" label="Year Built" value="1971" onChange={() => { }} />
<FormInput id="prop-area-sqft" name="prop-area-sqft" label="Built-Up Area (sqft)" value="2450" onChange={() => { }} />
<FormInput id="prop-lot-sqft" name="prop-lot-sqft" label="Lot Size (sqft)" value="8500" onChange={() => { }} />
<div className="grid grid-cols-3 gap-4 md:col-span-2">
<FormInput id="prop-beds" name="prop-beds" label="Beds" value="4" onChange={() => { }} />
<FormInput id="prop-baths" name="prop-baths" label="Baths" value="3" onChange={() => { }} />
<FormInput id="prop-parking" name="prop-parking" label="Parking" value="2" onChange={() => { }} />
</div>
</div>
</div>
{/* Insurance Information */}
<div className="space-y-6">
<div className="flex items-center gap-2 pb-4 border-b border-zinc-200 dark:border-white/5 text-purple-600 dark:text-purple-400">
<ShieldCheck size={20} /> <h3 className="text-sm font-black uppercase tracking-widest">Insurance Information</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormInput id="ins-company" name="ins-company" label="Insurance Company" value="Farmers" onChange={() => { }} />
<FormInput id="ins-policy" name="ins-policy" label="Policy Number" value="POL-987654321" onChange={() => { }} />
<FormInput id="ins-claim-filed" name="ins-claim-filed" label="Claim Filed?" value="No" onChange={() => { }} />
<FormInput id="ins-adjuster" name="ins-adjuster" label="Adjuster Name" value="" placeholder="N/A" onChange={() => { }} />
</div>
</div>
<div className="pt-4 border-t border-zinc-200 dark:border-zinc-800 flex justify-end">
<button
type="submit"
className="flex items-center px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-bold shadow-lg shadow-blue-500/20 transition-all"
>
<Save className="mr-2" size={18} /> Save Details
</button>
</div>
</form>
</SpotlightCard>
) : (
<div className="space-y-8">
{/* Upcoming Visits */}
<section>
<h3 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} /> Upcoming Visits
</h3>
{upcomingVisits.length > 0 ? (
<div className="grid gap-4">
{upcomingVisits.map(visit => (
<SpotlightCard key={visit.id} className="p-6">
<div className="flex justify-between items-center">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 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">
<span className="text-[10px] uppercase font-bold">{new Date(visit.date).toLocaleString('default', { month: 'short' })}</span>
<span className="text-lg font-bold leading-none">{new Date(visit.date).getDate()}</span>
</div>
<div>
<h4 className="font-bold text-lg">{visit.agentName || 'Assigned Agent'}</h4>
<div className="flex items-center text-sm text-zinc-500 dark:text-zinc-400">
<Clock size={14} className="mr-1" /> {visit.time}
</div>
</div>
</div>
<div className="text-right">
<span className="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">
{visit.status}
</span>
</div>
</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 upcoming visits scheduled.
</div>
)}
</section>
{/* Visit History */}
<section>
<h3 className="text-lg font-bold mb-4 flex items-center text-zinc-800 dark:text-zinc-200">
<History className="mr-2 text-zinc-500" size={20} /> Visit History
</h3>
{pastVisits.length > 0 ? (
<div className="grid gap-4">
{pastVisits.map(visit => (
<SpotlightCard key={visit.id} className="p-6 opacity-80 hover:opacity-100 transition-opacity">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center space-x-4">
<div className="w-10 h-10 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-zinc-500">
<CheckCircle size={20} />
</div>
<div>
<div className="flex items-center gap-2">
<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-1">
{visit.notes || 'No notes available.'}
</p>
</div>
</div>
{visit.dealValue !== undefined && (
<div className="flex items-center space-x-2 bg-emerald-50 dark:bg-emerald-900/10 px-4 py-2 rounded-lg border border-emerald-100 dark:border-emerald-500/20">
<DollarSign size={16} className="text-emerald-600 dark:text-emerald-400" />
<span className="font-bold text-emerald-700 dark:text-emerald-300">
${visit.dealValue.toLocaleString()}
</span>
</div>
)}
</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 history available.
</div>
)}
</section>
</div>
)}
</div>
</div>
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
</div>
);
};
// Helper Components
const ReadOnlyField = ({ label, value }) => (
<div className="space-y-1">
<label className="text-[10px] font-bold uppercase text-zinc-500 tracking-wider">{label}</label>
<div className="text-sm font-medium text-zinc-900 dark:text-zinc-200 border-b border-zinc-200 dark:border-white/10 pb-1">
{value}
</div>
</div>
);
const FormInput = ({ id, name, label, icon: Icon, prefix, value, onChange, type = "text", required }) => (
<div className="space-y-2">
<label htmlFor={id} className="text-xs font-bold uppercase text-zinc-500">{label} {required && <span className="text-red-500">*</span>}</label>
<div className="relative">
{Icon && <Icon className="absolute left-3 top-3 text-zinc-400" size={16} />}
{prefix && <span className="absolute left-3 top-3 text-zinc-400 text-xs">{prefix}</span>}
<input
id={id}
name={name || id}
type={type}
required={required}
value={value}
onChange={(e) => onChange(e.target.value)}
className={`w-full ${Icon || prefix ? 'pl-10' : 'px-4'} pr-4 py-2 bg-zinc-100 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all`}
/>
</div>
</div>
);
export default CustomerProfile;