mobile: Optimizations for Maps Drawer, Legend, and Customer Profile Tabs
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
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 label="Full Name" icon={User} value={formData.name} onChange={v => setFormData({ ...formData, name: v })} required />
|
||||
<FormInput label="Email Address" icon={null} prefix="@" type="email" value={formData.email} onChange={v => setFormData({ ...formData, email: v })} required />
|
||||
<FormInput label="Phone Number" value={formData.phone} onChange={v => setFormData({ ...formData, phone: v })} required />
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<label className="text-xs font-bold uppercase text-zinc-500">Address</label>
|
||||
<textarea
|
||||
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 label="Address" value="2612 Dunwick Dr, Plano, TX 75023" onChange={() => { }} />
|
||||
</div>
|
||||
<FormInput label="Type" value="Residential" onChange={() => { }} />
|
||||
<FormInput label="Year Built" value="1971" onChange={() => { }} />
|
||||
<FormInput label="Built-Up Area (sqft)" value="2450" onChange={() => { }} />
|
||||
<FormInput label="Lot Size (sqft)" value="8500" onChange={() => { }} />
|
||||
<div className="grid grid-cols-3 gap-4 md:col-span-2">
|
||||
<FormInput label="Beds" value="4" onChange={() => { }} />
|
||||
<FormInput label="Baths" value="3" onChange={() => { }} />
|
||||
<FormInput 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 label="Insurance Company" value="Farmers" onChange={() => { }} />
|
||||
<FormInput label="Policy Number" value="POL-987654321" onChange={() => { }} />
|
||||
<FormInput label="Claim Filed?" value="No" onChange={() => { }} />
|
||||
<FormInput 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>
|
||||
</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 = ({ label, icon: Icon, prefix, value, onChange, type = "text", required }) => (
|
||||
<div className="space-y-2">
|
||||
<label 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
|
||||
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;
|
||||
Reference in New Issue
Block a user