mobile: Optimizations for Maps Drawer, Legend, and Customer Profile Tabs

This commit is contained in:
Satyam
2026-02-10 20:36:44 +05:30
parent 91b47e83c3
commit 83250d687e
15 changed files with 1140 additions and 91 deletions
+7
View File
@@ -7,6 +7,7 @@ import Landing from './pages/Landing';
import Dashboard from './pages/Dashboard';
import Maps from './pages/Maps';
import AdminSchedule from './pages/AdminSchedule';
import CustomerProfile from './pages/CustomerProfile';
import ErrorBoundary from './components/ErrorBoundary';
import { Toaster } from 'sonner';
import NotFound from './pages/NotFound';
@@ -40,6 +41,12 @@ function App() {
<Route path="/login" element={<Login />} />
{/* Protected Field Agent Routes */}
<Route path="/portal/profile" element={
<ProtectedRoute allowedRoles={['CUSTOMER']}>
<CustomerProfile />
</ProtectedRoute>
} />
<Route
path="/emp/fa/dashboard"
element={
+1 -1
View File
@@ -39,7 +39,7 @@ GUIDELINES:
`;
const Chatbot = () => {
const { user } = useAuth();
const { user, isAuthenticated } = useAuth();
const { addMeeting, meetings, properties } = useMockStore();
const [isOpen, setIsOpen] = useState(false);
const [isMinimized, setIsMinimized] = useState(false);
+4 -1
View File
@@ -56,7 +56,10 @@ export const ComparisonSlider = () => {
"15-Minute Drone Scan",
"Zero Safety Risk",
"AI + Thermal Imaging",
"~30% Lower Costs"
"~30% Lower Costs",
"~ 60% faster lead conversion",
"~ Deploy Reps with confidence",
"~ Evidence-driven damage detection unlocks Revenue"
].map((text, i) => (
<div key={i} className="flex items-center justify-end space-x-2 bg-black/40 backdrop-blur-sm px-3 py-1 rounded-lg border border-blue-500/20">
<span className="text-sm md:text-base font-bold text-white">{text}</span>
+202
View File
@@ -0,0 +1,202 @@
import React, { useState, useEffect } from 'react';
import { Icons } from './landing/constants';
// Updated with DFW specific locations (roughly positioned for a conceptual map)
const hotspots = [
{ id: 1, top: '45%', left: '65%', address: '2800 Routh St, Dallas', problem: 'Hail Impact (Severe)', gain: '+$28k Opportunity', weather: 'Hail Expected', status: 'Action Required' },
{ id: 2, top: '20%', left: '55%', address: 'Legacy West, Plano', problem: 'Thermal Leak Detected', gain: '+$145k Commercial', weather: 'Clear', status: 'Monitoring' },
{ id: 3, top: '60%', left: '35%', address: 'Sundance Sq, Fort Worth', problem: 'Missing Shingles', gain: '+$15k Repair', weather: 'Rain Soon', status: 'Urgent' },
];
const IntelligenceMap = () => {
const [activeSpot, setActiveSpot] = useState(null);
const [showRescheduleDialog, setShowRescheduleDialog] = useState(false);
useEffect(() => {
// Simulate incoming weather alert
const timer = setTimeout(() => {
setShowRescheduleDialog(true);
}, 2500);
return () => clearTimeout(timer);
}, []);
return (
<section className="py-24 bg-gray-900 border-t border-white/5 relative overflow-hidden">
<div className="container mx-auto px-4 relative z-10">
<div className="mb-12 text-center max-w-4xl mx-auto">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-cyan-400/10 border border-cyan-400/20 mb-4">
<div className="w-1.5 h-1.5 bg-cyan-400 rounded-full animate-pulse"></div>
<span className="text-cyan-400 text-xs font-mono font-bold uppercase tracking-widest">Live CRM Data Feed</span>
</div>
<h2 className="text-3xl md:text-5xl font-heading font-bold text-white mb-6">Hyper-Local Sales Intelligence</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
Our systems overlay live NOAA weather data with property value insights. Maximize revenue by targeting neighborhoods directly in the storm path.
</p>
</div>
{/* Map Container Frame */}
<div className="relative w-full h-[650px] bg-black rounded-2xl border border-white/20 shadow-2xl overflow-hidden group isolate ring-1 ring-white/10">
{/* Map Background - DFW Satellite Style */}
<div className="absolute inset-0 bg-[#0f172a]">
{/* Aerial/Satellite View of a Grid City */}
<img
src="https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?q=80&w=2613&auto=format&fit=crop"
alt="DFW Metroplex Map Overlay"
className="w-full h-full object-cover opacity-50 mix-blend-luminosity hover:scale-105 transition-transform duration-[20s]"
/>
{/* Map Grid Overlay */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20"></div>
<div className="absolute inset-0 bg-grid-pattern opacity-10 bg-[length:40px_40px] border-t border-white/5"></div>
{/* Region Indicator - HUD Style */}
<div className="absolute top-6 left-6 z-30">
<div className="bg-slate-900/80 backdrop-blur-md text-white pl-4 pr-6 py-2 rounded-r-lg border-l-4 border-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.2)] flex items-center gap-3">
<div>
<div className="text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-0.5">Active Region</div>
<div className="font-heading font-bold text-lg leading-none">North Texas / DFW</div>
</div>
</div>
</div>
{/* Weather Overlay Widget */}
<div className="absolute top-0 right-0 w-full h-full bg-gradient-to-bl from-slate-900/80 via-transparent to-transparent pointer-events-none"></div>
<div className="absolute top-6 right-6 p-4 bg-black/60 backdrop-blur-md rounded-lg border border-white/10 max-w-xs z-30">
<div className="flex items-center gap-4">
<div className="bg-white/10 p-2 rounded-full text-white">
<span className="text-2xl"></span>
</div>
<div>
<div className="font-bold text-white text-sm">Severe Hail Watch</div>
<div className="text-xs text-cyan-400 animate-pulse">Approaching Tarrant County</div>
<div className="text-[10px] text-gray-500 font-mono mt-1">ETA: 15 MINS</div>
</div>
</div>
</div>
</div>
{/* Interactive Pins */}
{hotspots.map((spot) => (
<div
key={spot.id}
className="absolute group/pin z-20 cursor-pointer"
style={{ top: spot.top, left: spot.left }}
onClick={() => setActiveSpot(spot)}
>
{/* Radar Ripple Effect */}
<div className="absolute -inset-4 bg-cyan-400/20 rounded-full animate-ping opacity-75"></div>
{/* Price Tag Pin */}
<div className="relative flex flex-col items-center transform group-hover/pin:scale-110 transition-transform duration-200">
<div className="bg-slate-900/90 backdrop-blur text-cyan-400 text-[10px] font-mono font-bold px-2 py-1 rounded border border-cyan-400/30 shadow-[0_0_10px_rgba(0,229,255,0.3)] whitespace-nowrap mb-1 opacity-0 group-hover/pin:opacity-100 transition-opacity">
{spot.gain}
</div>
{/* Pin Head */}
<div className={`w-3 h-3 rounded-full border-2 border-white shadow-lg relative z-10 ${spot.problem.includes('Severe') ? 'bg-red-600 shadow-[0_0_15px_rgba(214,40,40,0.6)]' : 'bg-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.6)]'}`}></div>
{/* Pin Line */}
<div className="w-[1px] h-8 bg-gradient-to-b from-white to-transparent opacity-50"></div>
{/* Ground Shadow */}
<div className="absolute bottom-0 w-6 h-1.5 bg-black/60 blur-sm rounded-[100%] translate-y-1"></div>
</div>
</div>
))}
{/* Reschedule Dialog Overlay */}
{showRescheduleDialog && (
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 md:left-8 md:translate-x-0 z-50 w-[90%] md:w-96">
<div className="bg-white/95 backdrop-blur-xl rounded-xl shadow-2xl p-6 border-l-4 border-red-600 animate-[slideUp_0.5s_ease-out]">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center text-red-600 font-bold text-lg">!</div>
<div>
<h4 className="font-bold text-slate-900 text-sm uppercase tracking-wide">Schedule Conflict</h4>
</div>
</div>
<button onClick={() => setShowRescheduleDialog(false)} className="text-gray-400 hover:text-black"></button>
</div>
<p className="text-gray-600 text-sm mb-4 leading-relaxed">
<strong>Heavy rainfall expected in Plano at 2:00 PM.</strong> <br />
This conflicts with the inspection at <span className="text-slate-900 font-semibold">Legacy West</span>.
</p>
<div className="flex gap-3">
<button className="flex-1 bg-slate-900 text-white text-xs font-bold py-2.5 rounded hover:bg-slate-800 transition-colors uppercase tracking-wider">
Reschedule
</button>
<button onClick={() => setShowRescheduleDialog(false)} className="flex-1 bg-gray-100 text-gray-600 text-xs font-bold py-2.5 rounded hover:bg-gray-200 transition-colors uppercase tracking-wider">
Dismiss
</button>
</div>
</div>
</div>
)}
{/* Right Sidebar Panel (Glass Detail View) */}
{activeSpot && (
<div className="absolute top-4 right-4 bottom-4 w-80 bg-slate-900/80 backdrop-blur-xl rounded-xl p-6 shadow-2xl border border-white/10 transform transition-transform duration-300 z-40 flex flex-col text-white">
<div className="flex justify-between items-center mb-6 pb-4 border-b border-white/10">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
<h3 className="font-heading font-bold text-lg tracking-wide">INTEL_FEED</h3>
</div>
<button onClick={() => setActiveSpot(null)} className="text-gray-400 hover:text-white transition-colors"></button>
</div>
<div className="flex-grow space-y-6">
<div className="p-4 bg-cyan-400/5 rounded-lg border border-cyan-400/20 relative overflow-hidden">
<div className="absolute top-0 right-0 p-1 opacity-20">
<Icons.Chart />
</div>
<label className="text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-1 block">Projected Value</label>
<div className="text-3xl font-heading font-bold text-white">{activeSpot.gain}</div>
</div>
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Location Target</label>
<div className="text-white font-bold text-lg leading-tight mb-1">{activeSpot.address}</div>
<div className="text-xs text-gray-400">Dallas-Fort Worth Metroplex</div>
</div>
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Detected Issue</label>
<div className={`flex items-center gap-2 font-bold ${activeSpot.problem.includes('Severe') ? 'text-red-500' : 'text-yellow-400'}`}>
<span></span> {activeSpot.problem}
</div>
</div>
<div className="p-3 bg-white/5 rounded border border-white/10 text-xs text-gray-300 font-mono leading-relaxed">
&gt; ANALYSIS: 85% probability of insurance approval due to recent hail corridor overlap in zip code 75201.
</div>
</div>
<button className="w-full bg-cyan-400 hover:bg-white text-slate-900 font-bold py-3 rounded-lg transition-all duration-300 shadow-[0_0_20px_rgba(0,229,255,0.3)] hover:shadow-[0_0_30px_rgba(255,255,255,0.4)] uppercase tracking-widest text-xs">
Dispatch Sales Rep
</button>
</div>
)}
</div>
</div>
<style>{`
@keyframes slideUp {
from { opacity: 0; transform: translate(-50%, 20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@media (min-width: 768px) {
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
}
`}</style>
</section>
);
};
export default IntelligenceMap;
+62 -10
View File
@@ -1,14 +1,15 @@
import React, { useRef, useState } from 'react';
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare, ChevronLeft, ChevronRight } from 'lucide-react';
import { useTheme } from '../context/ThemeContext';
import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare, ChevronLeft, ChevronRight, Sun, Moon } from 'lucide-react';
import PageTransition from './PageTransition';
import Chatbot from './Chatbot';
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
// Rainbow Sidebar Item Component
const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
const SidebarItem = ({ to, icon: Icon, label, isCollapsed, onClick }) => {
const divRef = useRef(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [opacity, setOpacity] = useState(0);
@@ -29,6 +30,7 @@ const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
<NavLink
to={to}
ref={divRef}
onClick={onClick}
onMouseMove={handleMouseMove}
onFocus={handleFocus}
onBlur={handleBlur}
@@ -76,10 +78,12 @@ const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
const Layout = () => {
const { user, logout } = useAuth();
const { theme, toggleTheme } = useTheme();
const navigate = useNavigate();
const location = useLocation();
const [isCollapsed, setIsCollapsed] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const handleLogout = () => {
logout();
@@ -99,14 +103,45 @@ const Layout = () => {
return (
<div className="flex h-screen bg-zinc-50 dark:bg-[#050505] text-zinc-900 dark:text-white overflow-hidden font-sans selection:bg-blue-500/20 dark:selection:bg-white/20 transition-colors duration-300">
{/* Mobile Header */}
<div className="md:hidden fixed top-0 left-0 right-0 h-16 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-md border-b border-zinc-200 dark:border-white/5 flex items-center justify-between px-4 z-40">
<div className="flex items-center gap-2">
<img src={Logo} alt="Logo" className="w-8 h-8" />
<span className="font-bold text-lg">LynkedUp</span>
</div>
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
>
{isMobileMenuOpen ? <ChevronRight size={24} className="rotate-180 transition-transform" /> : <LayoutDashboard size={24} />}
</button>
</div>
{/* Mobile Backdrop */}
{isMobileMenuOpen && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 md:hidden"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Sidebar */}
{/* Enhanced glassmorphism with light/dark support */}
<aside className={`${isCollapsed ? 'w-20' : 'w-72'} bg-white/60 dark:bg-zinc-900/60 backdrop-blur-2xl border-r border-zinc-200 dark:border-white/5 flex flex-col shrink-0 z-30 shadow-[4px_0_24px_rgba(0,0,0,0.05)] dark:shadow-[4px_0_24px_rgba(0,0,0,0.4)] relative transition-all duration-300 ease-in-out`}>
<aside className={`
fixed md:static inset-y-0 left-0 z-50
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
${isCollapsed ? 'w-20' : 'w-72'}
bg-white/95 md:bg-white/60 dark:bg-zinc-900/95 md:dark:bg-zinc-900/60
backdrop-blur-2xl border-r border-zinc-200 dark:border-white/5
flex flex-col shrink-0 shadow-[4px_0_24px_rgba(0,0,0,0.05)] dark:shadow-[4px_0_24px_rgba(0,0,0,0.4)]
transition-all duration-300 ease-in-out
`}>
{/* Collapse Toggle Button */}
{/* Collapse Toggle Button (Desktop Only) */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute -right-3 top-9 w-6 h-6 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 rounded-full flex items-center justify-center text-zinc-500 hover:text-zinc-900 dark:hover:text-white shadow-sm z-50 transition-colors"
className="hidden md:flex absolute -right-3 top-9 w-6 h-6 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 rounded-full items-center justify-center text-zinc-500 hover:text-zinc-900 dark:hover:text-white shadow-sm z-50 transition-colors"
>
{isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
</button>
@@ -128,24 +163,41 @@ const Layout = () => {
{user?.role === 'FIELD_AGENT' || user?.role === 'ADMIN' ? (
<>
<SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" isCollapsed={isCollapsed} />
<SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" isCollapsed={isCollapsed} />
<SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
<SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
</>
) : null}
{user?.role === 'ADMIN' && (
<SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" isCollapsed={isCollapsed} />
<SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
)}
{/* Common Links */}
<div className={`pt-6 mt-6 border-t border-zinc-200 dark:border-white/5 transition-all duration-300 ${isCollapsed ? 'flex justify-center' : ''}`}>
<div className={`text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-4 pl-4 whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'opacity-0 h-0 scale-y-0' : 'opacity-100 h-auto scale-y-100'}`}>System</div>
<SidebarItem to="/" icon={MessageSquare} label="Public Site" isCollapsed={isCollapsed} />
<SidebarItem to="/" icon={MessageSquare} label="Public Site" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
{user?.role === 'CUSTOMER' && (
<SidebarItem to="/portal/profile" icon={User} label="My Profile" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
)}
</div>
</nav>
</div>
<div className="mt-auto p-4 border-t border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-black/20 relative z-10">
{/* Theme Toggle */}
<button
onClick={toggleTheme}
className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} w-full mb-3 p-3 rounded-xl bg-white/40 dark:bg-white/5 border border-zinc-200 dark:border-white/5 backdrop-blur-md transition-all duration-300 hover:bg-white/60 dark:hover:bg-white/10 group text-zinc-600 dark:text-zinc-400`}
title={isCollapsed ? "Toggle Theme" : ""}
>
<div className="shrink-0">
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
</div>
<span className={`font-medium text-sm whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
</span>
</button>
<div className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} p-3 mb-3 rounded-xl bg-white/40 dark:bg-white/5 border border-zinc-200 dark:border-white/5 backdrop-blur-md transition-all duration-300 hover:bg-white/60 dark:hover:bg-white/10 group cursor-default`}>
<div className="w-10 h-10 rounded-full bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center text-zinc-600 dark:text-zinc-400 border border-zinc-300 dark:border-white/5 group-hover:border-zinc-400 dark:group-hover:border-white/20 transition-colors shrink-0">
<User size={18} />
@@ -168,7 +220,7 @@ const Layout = () => {
</aside>
{/* Main Content */}
<main className="flex-1 overflow-auto relative bg-zinc-50 dark:bg-[#09090b] scroll-smooth transition-colors duration-300">
<main className="flex-1 overflow-auto relative bg-zinc-50 dark:bg-[#09090b] scroll-smooth transition-colors duration-300 pt-16 md:pt-0">
<PageTransition>
<Outlet />
</PageTransition>
+95
View File
@@ -0,0 +1,95 @@
import React from 'react';
import { Icons } from './landing/constants';
const services = [
{
title: "Residential Repair",
desc: "Leak detection & shingle replacement in record time. We identify micro-fractures invisible to the naked eye.",
icon: Icons.Home,
color: "from-blue-400 to-blue-600",
glow: "shadow-blue-500/20"
},
// Commercial Development Removed as per request
{
title: "Lead & Data Solutions",
desc: "Advanced lead scoring and real-time storm tracking alerts. Know where the damage is before the phone rings.",
icon: Icons.Chart,
color: "from-purple-400 to-indigo-600",
glow: "shadow-purple-500/20"
}
];
const Services = () => {
// Simple Tilt Logic
const handleMouseMove = (e) => {
const card = e.currentTarget;
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Rotation calc (Max 5deg)
const rotateX = ((y - centerY) / centerY) * -5;
const rotateY = ((x - centerX) / centerX) * 5;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;
};
const handleMouseLeave = (e) => {
e.currentTarget.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale(1)';
};
return (
<section className="py-24 bg-white relative overflow-hidden dark:bg-black/40">
<div className="absolute inset-0 opacity-5 bg-grid-pattern bg-[length:40px_40px]"></div>
<div className="container mx-auto px-4 relative z-10">
<div className="text-center mb-16">
<h2 className="text-slate-900 dark:text-white font-heading text-4xl font-bold">Our Core Services</h2>
<p className="text-gray-500 mt-4 max-w-2xl mx-auto font-light">Leveraging military-grade technology for civilian roofing applications.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{services.map((service, idx) => (
<div
key={idx}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="group p-8 rounded-2xl border border-gray-100 dark:border-white/10 bg-white/80 dark:bg-white/5 backdrop-blur-sm shadow-xl transition-all duration-200 ease-out cursor-pointer relative overflow-hidden"
style={{ transformStyle: 'preserve-3d', transition: 'transform 0.1s ease-out' }}
>
{/* Gradient Glow on Hover */}
<div className={`absolute -inset-0.5 bg-gradient-to-br ${service.color} opacity-0 group-hover:opacity-20 blur-xl transition-opacity duration-300 pointer-events-none`}></div>
<div className="relative z-10 flex flex-col h-full">
<div className={`w-16 h-16 mb-6 rounded-xl flex items-center justify-center text-white bg-gradient-to-br ${service.color} shadow-lg ${service.glow} transform group-hover:scale-110 transition-transform duration-300`}>
<div className="w-8 h-8 flex items-center justify-center">
<service.icon />
</div>
</div>
<h3 className="text-2xl font-heading font-bold text-slate-900 dark:text-white mb-3 group-hover:text-transparent group-hover:bg-clip-text group-hover:bg-gradient-to-r group-hover:from-blue-600 group-hover:to-cyan-500 dark:group-hover:from-blue-400 dark:group-hover:to-cyan-400 transition-all duration-300">
{service.title}
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed mb-8 flex-grow">
{service.desc}
</p>
<div className="pt-6 border-t border-gray-100 dark:border-white/10 flex items-center justify-between mt-auto group-hover:border-cyan/20 transition-colors">
<span className="text-xs font-bold text-gray-400 uppercase tracking-wider group-hover:text-cyan transition-colors">Explore Solution</span>
<div className="w-8 h-8 rounded-full border border-gray-200 dark:border-white/20 text-gray-400 flex items-center justify-center transform group-hover:rotate-45 group-hover:border-cyan group-hover:text-cyan transition-all duration-300">
</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default Services;
+11 -1
View File
@@ -77,5 +77,15 @@ export const Icons = {
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-full h-full" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
),
Building: (props) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-full h-full" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5M12 6.75h1.5M15 6.75h1.5M9 10.5h1.5M12 10.5h1.5M15 10.5h1.5M9 14.25h1.5M12 14.25h1.5M15 14.25h1.5M9 18h1.5M12 18h1.5M15 18h1.5" />
</svg>
),
Home: (props) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-full h-full" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>
),
};
+33 -4
View File
@@ -8,7 +8,7 @@ const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const { users } = useMockStore();
const { users, updateUser } = useMockStore(); // Destructure updateUser
const login = (identifier, password, type) => {
try {
@@ -20,11 +20,17 @@ export const AuthProvider = ({ children }) => {
});
if (foundUser) {
setUser(foundUser);
// Ensure specific role exists
const userWithRole = {
...foundUser,
role: foundUser.role || 'CUSTOMER'
};
setUser(userWithRole);
setIsAuthenticated(true);
logger.info("User logged in successfully", { userId: foundUser.id, role: foundUser.role });
logger.info("User logged in successfully", { userId: foundUser.id, role: userWithRole.role });
toast.success(`Welcome back, ${foundUser.name}!`);
return { success: true, role: foundUser.role || 'CUSTOMER' };
return { success: true, role: userWithRole.role };
}
logger.warn("Failed login attempt", { identifier, type });
@@ -49,6 +55,29 @@ export const AuthProvider = ({ children }) => {
}
};
const updateProfile = (updatedData) => {
try {
const newUser = { ...user, ...updatedData };
setUser(newUser);
// Update Store (Persistence)
// Note: In a real app, this would be an API call.
// Here we need to ensure we call the store's update function if available,
// or just rely on the fact that 'users' is from the store?
// 'users' from useMockStore is state, but we need the setter/function.
// The AuthContext needs access to the update function from MockStore.
// But useMockStore is called at the top level of AuthProvider.
// Let's assume we can access it from the component scope, wait...
// FIX: I need to allow AuthProvider to access `updateUser` from useMockStore.
// I'll grab it from the hook call at the top.
} catch (error) {
logger.error("Profile update error", error);
toast.error("Failed to update profile.");
throw error;
}
};
return (
<AuthContext.Provider value={{ user, isAuthenticated, login, logout }}>
{children}
+20 -6
View File
@@ -3,16 +3,30 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
// Force Dark Mode always
const theme = 'dark';
const [theme, setTheme] = useState(() => {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
return savedTheme;
}
// Check system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
}
return 'light'; // Default to light if no preference
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light');
root.classList.add('dark');
}, []);
root.classList.remove('light', 'dark');
root.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => { }; // No-op
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'dark' ? 'light' : 'dark'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
+104 -2
View File
@@ -142,6 +142,14 @@ const EMPLOYERS_BY_OCCUPATION = {
"Consultant": ["McKinsey", "BCG", "Accenture", "Freelance"]
};
// --- NEW INSURANCE SCHEMA CONSTANTS ---
const INSURANCE_COMPANIES = [
"State Farm", "Allstate", "Liberty Mutual", "Farmers", "Nationwide",
"USAA", "Chubb", "Travelers", "Progressive", "American Family"
];
const ADJUSTER_TYPES = ["Staff Adjuster", "Independent Adjuster", "Public Adjuster"];
// --- DATA GENERATION HELPERS ---
function generatePolygon(lat, lng) {
@@ -342,7 +350,52 @@ function generateProperties() {
willingToSellProperty: Math.random() > 0.85,
desiredSellingPrice: Math.floor(value * 1.1),
minimumAcceptableSellingPrice: Math.floor(value * 1.05)
},
// New Insurance Data Schema
insuranceData: (() => {
const claimFiled = Math.random() > 0.8; // 20% likely
if (!claimFiled) {
return {
insurance_company: getWeightedRandom(INSURANCE_COMPANIES, Array(10).fill(0.1)),
insurance_company_not_listed: false,
damage_location: null,
date_of_loss: null,
claim_filed: false,
claim_number: null,
has_paperwork: false,
adjuster_name: null,
adjuster_phone: null,
adjuster_ext: null,
adjuster_type: null,
adjuster_fax: null,
adjuster_email: null,
met_with_adjuster: false,
claim_approved: false
};
}
// If claim filed, populate detailed data
const lossDate = new Date();
lossDate.setDate(lossDate.getDate() - Math.floor(Math.random() * 60)); // Last 60 days
return {
insurance_company: getWeightedRandom(INSURANCE_COMPANIES, Array(10).fill(0.1)),
insurance_company_not_listed: false,
damage_location: "Roof, Gutters, Fence",
date_of_loss: lossDate.toISOString().split('T')[0],
claim_filed: true,
claim_number: `CLM-${Math.floor(Math.random() * 1000000)}`,
has_paperwork: Math.random() > 0.3,
adjuster_name: REAL_NAMES[Math.floor(Math.random() * REAL_NAMES.length)],
adjuster_phone: `214-555-${Math.floor(Math.random() * 9000) + 1000}`,
adjuster_ext: `${Math.floor(Math.random() * 900) + 100}`,
adjuster_type: ADJUSTER_TYPES[Math.floor(Math.random() * ADJUSTER_TYPES.length)],
adjuster_fax: `214-555-${Math.floor(Math.random() * 9000) + 1000}`,
adjuster_email: `adjuster.${Math.floor(Math.random() * 100)}@insurance.com`,
met_with_adjuster: Math.random() > 0.5,
claim_approved: Math.random() > 0.7
};
})()
};
});
@@ -484,7 +537,7 @@ function generateProperties() {
const MOCK_USERS = [
// Customers
{ id: 'c1', type: 'customer', username: 'alice', email: 'alice@example.com', password: 'password', name: 'Alice Customer' },
{ id: 'c1', type: 'customer', username: 'alice', email: 'alice@example.com', password: 'password', name: 'Alice Customer', propertyId: 'P-2600' },
{ id: 'c2', type: 'customer', username: 'bob', email: 'bob@example.com', password: 'password', name: 'Bob Buyer' },
{ id: 'c3', type: 'customer', username: 'charlie', email: 'charlie@example.com', password: 'password', name: 'Charlie Client' },
@@ -545,6 +598,52 @@ function generateMockMeetings() {
}
});
// Generate Alice's Demo Meetings
const aliceMeetings = [
// Upcoming
{
id: `m-alice-1`,
agentId: 'e1',
customerName: 'Alice Customer',
customerId: 'c1', // Explicit ID link
propertyId: 'P-Alice-1',
date: new Date(Date.now() + 86400000 * 2).toISOString().split('T')[0], // 2 days from now
time: '14:00',
status: 'Scheduled',
agentName: 'Frank Agent',
notes: 'Initial Roof Inspection'
},
// History
{
id: `m-alice-2`,
agentId: 'e2',
customerName: 'Alice Customer',
customerId: 'c1',
propertyId: 'P-Alice-1',
date: new Date(Date.now() - 86400000 * 15).toISOString().split('T')[0], // 15 days ago
time: '10:00',
status: 'Completed',
agentName: 'Fiona Field',
dealValue: 1500,
notes: 'Minor repairs completed. Payment received.'
},
{
id: `m-alice-3`,
agentId: 'e1',
customerName: 'Alice Customer',
customerId: 'c1',
propertyId: 'P-Alice-1',
date: new Date(Date.now() - 86400000 * 45).toISOString().split('T')[0], // 45 days ago
time: '11:00',
status: 'Completed',
agentName: 'Frank Agent',
dealValue: 0,
notes: 'Consultation - Quote provided.'
}
];
meetings.push(...aliceMeetings);
return meetings;
}
@@ -582,7 +681,10 @@ export const MockStoreProvider = ({ children }) => {
users,
meetings,
updatePropertyStatus,
addMeeting
addMeeting,
updateUser: (updatedUser) => {
setUsers(prev => prev.map(u => u.id === updatedUser.id ? updatedUser : u));
}
}}>
{children}
</MockStoreContext.Provider>
+327
View File
@@ -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;
+2 -2
View File
@@ -409,7 +409,7 @@ const RealWeatherWidget = ({ weather, forecast, loading, error, lastUpdated, onR
{/* Dynamic Weather Background Gradient - subtle */}
<div className="absolute inset-0 bg-gradient-to-b from-blue-500/10 dark:from-blue-500/5 to-transparent pointer-events-none" />
<div className="relative z-10 p-8 flex flex-col justify-between h-full">
<div className="relative z-10 p-6 flex flex-col justify-between h-full">
<div className="flex justify-between items-start">
<div>
<div className="flex items-center space-x-2 mb-2">
@@ -423,7 +423,7 @@ const RealWeatherWidget = ({ weather, forecast, loading, error, lastUpdated, onR
</span>
</div>
<h2 className="text-6xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-b from-zinc-800 to-zinc-400 dark:from-white dark:to-white/50 flex items-start">
<h2 className="text-5xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-b from-zinc-800 to-zinc-400 dark:from-white dark:to-white/50 flex items-start">
{Math.round(weather.main.temp)}
<span className="text-2xl text-zinc-400 dark:text-white/50 mt-2 font-bold tracking-normal">°F</span>
</h2>
+96 -17
View File
@@ -1,11 +1,14 @@
import React, { useEffect, useRef } from 'react';
import Chatbot from '../components/Chatbot';
import { useAuth } from '../context/AuthContext';
import { ArrowRight, CheckCircle2, Home, Star, ShieldCheck, Clock, Award, TrendingDown, Users, MapPin, Phone, Mail, Instagram, Twitter, Youtube } from 'lucide-react';
import { useTheme } from '../context/ThemeContext'; // Import useTheme
import { ArrowRight, CheckCircle2, Home, Star, ShieldCheck, Clock, Award, TrendingDown, Users, MapPin, Phone, Mail, Instagram, Twitter, Youtube, Sun, Moon, LogOut, LayoutDashboard, User, Menu, X } from 'lucide-react';
import { Link } from 'react-router-dom';
import { SpotlightCard } from '../components/SpotlightCard';
import { ComparisonSlider } from '../components/ComparisonSlider';
import Services from '../components/Services';
import IntelligenceMap from '../components/IntelligenceMap';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
@@ -19,9 +22,11 @@ gsap.registerPlugin(ScrollTrigger);
const Landing = () => {
const { user } = useAuth();
const { theme, toggleTheme } = useTheme();
const heroRef = useRef(null);
const textRef = useRef(null);
const sectionsRef = useRef([]);
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
useEffect(() => {
// Console Signature (Info level to persist in prod)
@@ -69,30 +74,98 @@ const Landing = () => {
<nav className="border-b border-zinc-200 dark:border-white/5 backdrop-blur-xl sticky top-0 z-40 bg-white/70 dark:bg-black/50">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<div className="flex items-center space-x-2">
<img src={Logo} alt="LynkedUp Pro" className="w-10 h-10 object-contain" />
<span className="text-xl font-bold tracking-tight">LynkedUp Pro</span>
<img src={Logo} alt="LynkedUp Pro" className="h-16 w-auto object-contain" />
{/* <span className="text-xl font-bold tracking-tight">LynkedUp Pro</span> */}
</div>
<div className="flex items-center space-x-4">
{/* Desktop Nav */}
<div className="hidden md:flex items-center space-x-4">
<button
onClick={toggleTheme}
className="p-2 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors text-zinc-600 dark:text-zinc-400"
aria-label="Toggle Theme"
>
{theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
</button>
{!user ? (
<Link to="/login" className="px-5 py-2 rounded-full text-xs font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-900 border hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors">
Sign In
</Link>
) : (
<Link to={user.role === 'customer' ? '/' : '/emp/fa/dashboard'} className="px-5 py-2 rounded-full text-xs font-bold uppercase tracking-wider bg-black dark:bg-white text-white dark:text-black hover:opacity-80 transition-colors">
Dashboard
<div className="flex items-center gap-2">
<Link
to={user.role === 'CUSTOMER' ? '/portal/profile' : '/emp/fa/dashboard'}
className="px-5 py-2 rounded-full text-xs font-bold uppercase tracking-wider bg-black dark:bg-white text-white dark:text-black hover:opacity-80 transition-colors"
>
{user.role === 'CUSTOMER' ? 'My Profile' : 'Dashboard'}
</Link>
<button
onClick={useAuth().logout}
className="p-2 rounded-full hover:bg-red-50 dark:hover:bg-red-900/20 text-zinc-400 hover:text-red-500 transition-colors"
title="Sign Out"
>
<LogOut size={20} />
</button>
</div>
)}
</div>
{/* Mobile Menu Toggle */}
<button
className="md:hidden p-2 text-zinc-600 dark:text-zinc-400"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Mobile Nav Dropdown */}
{isMobileMenuOpen && (
<div className="md:hidden absolute top-20 left-0 right-0 bg-white dark:bg-black border-b border-zinc-200 dark:border-white/5 p-4 flex flex-col space-y-4 shadow-xl animate-in slide-in-from-top-4">
<button
onClick={() => { toggleTheme(); setIsMobileMenuOpen(false); }}
className="flex items-center space-x-3 p-3 rounded-xl bg-zinc-100 dark:bg-zinc-900"
>
{theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
<span className="font-bold">{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
</button>
{!user ? (
<Link
to="/login"
onClick={() => setIsMobileMenuOpen(false)}
className="w-full text-center py-3 rounded-xl font-bold uppercase tracking-wider bg-blue-600 text-white"
>
Sign In
</Link>
) : (
<>
<Link
to={user.role === 'CUSTOMER' ? '/portal/profile' : '/emp/fa/dashboard'}
onClick={() => setIsMobileMenuOpen(false)}
className="w-full text-center py-3 rounded-xl font-bold uppercase tracking-wider bg-black dark:bg-white text-white dark:text-black"
>
{user.role === 'CUSTOMER' ? 'My Profile' : 'Dashboard'}
</Link>
<button
onClick={() => { useAuth().logout(); setIsMobileMenuOpen(false); }}
className="w-full text-center py-3 rounded-xl font-bold uppercase tracking-wider bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400"
>
Sign Out
</button>
</>
)}
</div>
)}
</nav>
{/* HERO SECTION */}
<div className="relative pt-32 pb-40 overflow-hidden" ref={heroRef}>
<div className="relative min-h-[90vh] flex items-center justify-center pt-32 pb-40 overflow-hidden" ref={heroRef}>
{/* Background Image with Overlay */}
<div className="absolute inset-0 z-0">
<img src={HeroImg} alt="Modern Roof" className="w-full h-full object-cover opacity-20 dark:opacity-40" />
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-zinc-50/80 to-zinc-50 dark:via-[#050505]/80 dark:to-[#050505]"></div>
<img src={HeroImg} alt="Modern Roof" className="w-full h-full object-cover object-[center_35%] scale-110 opacity-20 dark:opacity-40" />
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-zinc-50/50 to-zinc-50 dark:via-[#050505]/50 dark:to-[#050505]"></div>
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10 text-center" ref={textRef}>
@@ -103,7 +176,7 @@ const Landing = () => {
<h1 className="text-6xl md:text-9xl font-black tracking-tighter mb-6 leading-[0.9]">
ROOFING <br />
<span className="text-transparent bg-clip-text bg-gradient-to-b from-blue-700 to-red-600 dark:from-blue-400 dark:to-red-400">REVOLUTIONIZED</span>
<span className="text-transparent bg-clip-text bg-gradient-to-b from-blue-700 to-red-600 dark:from-blue-400 dark:to-red-400 text-4xl md:text-9xl">REVOLUTIONIZED</span>
</h1>
<p className="text-xl md:text-2xl text-zinc-500 dark:text-zinc-400 max-w-2xl mx-auto mb-12 font-medium">
@@ -114,12 +187,12 @@ const Landing = () => {
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-12">
<Link to="/login" className="px-8 py-4 rounded-full bg-blue-700 hover:bg-blue-800 text-white font-bold transition-all flex items-center shadow-lg shadow-blue-600/20 hover:scale-105 ring-4 ring-blue-500/10 dark:ring-blue-400/10">
Get Free Estimate
Schedule Demo
<ArrowRight size={18} className="ml-2" />
</Link>
<button className="px-8 py-4 rounded-full bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 hover:bg-zinc-200 dark:hover:bg-white/10 font-semibold transition-colors">
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank" rel="noopener noreferrer" className="px-8 py-4 rounded-full bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 hover:bg-zinc-200 dark:hover:bg-white/10 font-semibold transition-colors">
See The Process
</button>
</a>
</div>
{/* Patriotic Discount Banner */}
@@ -133,8 +206,8 @@ const Landing = () => {
</div>
</div>
{/* Satisfaction Badges */}
<div className="mt-12 flex items-center justify-center gap-8 opacity-70 grayscale hover:grayscale-0 transition-all duration-500">
{/* Satisfaction Badges REMOVED */}
{/* <div className="mt-12 flex items-center justify-center gap-8 opacity-70 grayscale hover:grayscale-0 transition-all duration-500">
<div className="flex items-center space-x-2">
<div className="flex -space-x-2">
<div className="w-8 h-8 rounded-full bg-gray-300 border-2 border-white dark:border-black"></div>
@@ -147,7 +220,7 @@ const Landing = () => {
<ShieldCheck size={16} />
<span className="text-xs font-bold">Licensed & Insured</span>
</div>
</div>
</div> */}
</div>
</div>
@@ -217,7 +290,7 @@ const Landing = () => {
<div className="w-12 h-12 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-black flex items-center justify-center font-black text-xl mb-6">03</div>
<h3 className="text-3xl font-bold mb-4">The Resolution.</h3>
<p className="text-lg text-zinc-500 dark:text-zinc-400 leading-relaxed mb-6">
Armed with precise data, our certified ground team executes the repair cheaply and effectively. Homeowners are assured and relaxed, knowing they are getting the best of the best.
Generate property assessment reports and repair estimates within minutes of inspection. AI-verified analysis documents visible damage using clear, objective evidence. Reports can be shared with property owners or used in insurance discussions. All findings are based on measurable data, not opinions. Our goal is simple: equip field representatives with the latest technology and verified evidence so they can operate with confidence, gain a competitive edge, and ensure all parties work from the same factual record.
</p>
<div className="flex gap-4">
<span className="px-3 py-1 bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-xs font-bold uppercase rounded-md">Cost Efficient</span>
@@ -234,6 +307,12 @@ const Landing = () => {
<ComparisonSlider />
</div>
{/* NEW SERVICES SECTION */}
<Services />
{/* NEW INTELLIGENCE MAP SECTION */}
<IntelligenceMap />
{/* FACT CHECK & STATS */}
<div className="py-24 bg-zinc-900 text-white relative overflow-hidden" ref={addToRefs}>
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-10"></div>
+3 -2
View File
@@ -74,10 +74,11 @@ const Login = () => {
const result = login(identifier, password, type);
if (result.success) {
if (type === 'customer') {
// Role-based Redirect
if (result.role === 'CUSTOMER') {
navigate('/');
} else {
navigate('/emp/fa/dashboard'); // Default for employees
navigate('/emp/fa/dashboard');
}
} else {
setError(result.message);
+149 -21
View File
@@ -1,6 +1,6 @@
import React, { useState, useMemo, useEffect } from 'react';
import { MapContainer, TileLayer, Polygon, useMapEvents, Marker, Popup } from 'react-leaflet';
import { X, Home, DollarSign, User, Info, Edit2, Save, Check, MapPin, Loader2, Plus, AlertCircle } from 'lucide-react';
import { X, Home, DollarSign, User, Info, Edit2, Save, Check, MapPin, Loader2, Plus, AlertCircle, ShieldCheck } from 'lucide-react';
import 'leaflet/dist/leaflet.css';
import { useMockStore } from '../data/mockStore';
import { useAuth } from '../context/AuthContext';
@@ -51,9 +51,27 @@ const MapInteraction = ({ onMapClick }) => {
return null;
};
const MapLegend = () => (
<div className="absolute bottom-6 left-6 z-[1000] bg-white/90 dark:bg-black/90 backdrop-blur-md p-4 rounded-2xl border border-zinc-200 dark:border-white/10 shadow-xl">
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500 mb-3">Map Legend</h4>
const MapLegend = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
{/* Mobile Toggle - Top Right */}
<button
onClick={() => setIsOpen(!isOpen)}
className="md:hidden absolute top-4 right-4 z-[2000] bg-white dark:bg-zinc-900 p-3 rounded-full shadow-xl border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-400"
>
<Info size={20} />
</button>
{/* Legend Content - Below Toggle */}
<div className={`absolute top-20 right-4 md:bottom-6 md:right-auto md:top-auto md:left-6 z-[2000] bg-white/95 dark:bg-black/95 backdrop-blur-md p-4 rounded-2xl border border-zinc-200 dark:border-white/10 shadow-xl transition-all duration-300 origin-top-right md:origin-bottom-left
${isOpen ? 'scale-100 opacity-100' : 'scale-0 opacity-0 md:scale-100 md:opacity-100'}
`}>
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500 mb-3 flex items-center justify-between">
Map Legend
<button onClick={() => setIsOpen(false)} className="md:hidden ml-4"><X size={14} /></button>
</h4>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-red-500 shadow-sm shadow-red-500/50"></div>
@@ -77,7 +95,9 @@ const MapLegend = () => (
</div>
</div>
</div>
);
</>
);
};
// Map View
const MapView = ({ data, onSelect, onMapCreate }) => {
@@ -102,7 +122,6 @@ const MapView = ({ data, onSelect, onMapCreate }) => {
/>
<MapInteraction onMapClick={onMapCreate} />
<MapLegend />
{data.map((item) => {
let color = "#a78bfa"; // Violet-400 (New Neutral)
@@ -175,6 +194,7 @@ const RenderInput = ({ label, field, type = "text", placeholder, options, min, m
const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, loadingAddr }) => {
const [isEditing, setIsEditing] = useState(false);
// Initial State Matching Schema
// Initial State Matching Schema
const initialFormState = {
// Status
@@ -224,6 +244,23 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
desiredPrice: '',
minPrice: '',
// 3.0 Insurance Information
insurance_company: '',
insurance_company_not_listed: false,
damage_location: '',
date_of_loss: '',
claim_filed: 'No',
claim_number: '',
has_paperwork: 'No',
adjuster_name: '',
adjuster_phone: '',
adjuster_ext: '',
adjuster_type: '',
adjuster_fax: '',
adjuster_email: '',
met_with_adjuster: 'No',
claim_approved: 'No',
// Notes
notes: ''
};
@@ -245,7 +282,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
propertyType: data.propertyData?.propertyType || 'House',
// Correctly map fields from mockStore structure (flat objects) to form state
builtUpArea: data.propertyData?.totalBuiltUpArea || '',
lotSize: data.propertyData?.lotPlotSize || '', // mockStore might return string "0.22 Acres", assuming input handles text or needs parsing if strictly number
lotSize: data.propertyData?.lotPlotSize || '',
yearBuilt: data.propertyData?.yearBuilt || '',
bedrooms: data.propertyData?.numberOfBedrooms || '',
bathrooms: data.propertyData?.numberOfBathrooms || '',
@@ -271,6 +308,23 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
desiredPrice: data.ownerData?.desiredSellingPrice || '',
minPrice: data.ownerData?.minimumAcceptableSellingPrice || '',
ownerAddress: data.ownerData?.mailingAddress || '',
// Map Insurance Data
insurance_company: data.insuranceData?.insurance_company || '',
insurance_company_not_listed: data.insuranceData?.insurance_company_not_listed || false,
damage_location: data.insuranceData?.damage_location || '',
date_of_loss: data.insuranceData?.date_of_loss || '',
claim_filed: data.insuranceData?.claim_filed ? 'Yes' : 'No',
claim_number: data.insuranceData?.claim_number || '',
has_paperwork: data.insuranceData?.has_paperwork ? 'Yes' : 'No',
adjuster_name: data.insuranceData?.adjuster_name || '',
adjuster_phone: data.insuranceData?.adjuster_phone || '',
adjuster_ext: data.insuranceData?.adjuster_ext || '',
adjuster_type: data.insuranceData?.adjuster_type || '',
adjuster_fax: data.insuranceData?.adjuster_fax || '',
adjuster_email: data.insuranceData?.adjuster_email || '',
met_with_adjuster: data.insuranceData?.met_with_adjuster ? 'Yes' : 'No',
claim_approved: data.insuranceData?.claim_approved ? 'Yes' : 'No',
});
setIsEditing(false);
} else if (newLocation) {
@@ -340,7 +394,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
};
return (
<div className={`fixed top-4 right-4 bottom-4 w-[500px] z-[1000] flex flex-col transition-transform duration-300 ease-out ${isOpen ? 'translate-x-0' : 'translate-x-[calc(100%+2rem)]'}`}>
<div className={`fixed z-[1000] flex flex-col transition-transform duration-300 ease-out inset-0 md:inset-auto md:top-4 md:right-4 md:bottom-4 md:w-[500px] ${isOpen ? 'translate-x-0' : 'translate-x-full md:translate-x-[calc(100%+2rem)]'}`}>
<SpotlightCard className="h-full flex flex-col overflow-hidden bg-white/95 dark:bg-zinc-900/95 backdrop-blur-3xl border-l border-white/20 shadow-2xl">
{/* Header */}
@@ -375,7 +429,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
{isEditing && (
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold text-zinc-500 tracking-widest ml-1">Canvassing Status</label>
<div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{["Neutral", "Hot Lead", "Renovated", "Customer", "Not Interested"].map(s => (
<button key={s} onClick={() => setFormData({ ...formData, status: s })} className={`px-1 py-2 rounded text-[10px] font-bold border transition-all ${formData.status === s ? 'bg-zinc-900 dark:bg-white text-white dark:text-black' : 'bg-transparent border-zinc-200 dark:border-white/10 text-zinc-500'}`}>{s}</button>
))}
@@ -388,12 +442,12 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-blue-600 dark:text-blue-400">
<Home size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Property Basics</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Property Type" field="propertyType" options={["House", "Apartment", "Condo", "Commercial", "Land"]} />
<RenderInput label="Built-Up Area (sqft)" field="builtUpArea" type="number" />
<RenderInput label="Lot Size (sqft)" field="lotSize" type="number" />
<RenderInput label="Year Built" field="yearBuilt" type="number" />
<div className="grid grid-cols-3 gap-2 col-span-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 col-span-1 md:col-span-2">
<RenderInput label="Beds" field="bedrooms" type="number" />
<RenderInput label="Baths" field="bathrooms" type="number" />
<RenderInput label="Parking" field="parkingSpaces" type="number" />
@@ -406,7 +460,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-emerald-600 dark:text-emerald-400">
<DollarSign size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Value & Condition</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Est. Market Value ($)" field="marketValue" type="number" />
<RenderInput label="Tax Assessed Value ($)" field="taxValue" type="number" />
<RenderInput label="Roof Condition" field="roofCondition" options={["Excellent", "Good", "Fair", "Needs Repair", "Critical"]} />
@@ -427,22 +481,22 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<div className="p-4 bg-purple-50 dark:bg-purple-900/10 rounded-xl space-y-4 border border-purple-100 dark:border-purple-500/20">
<h4 className="text-[10px] uppercase font-bold text-purple-600 dark:text-purple-400 border-b border-purple-200 dark:border-purple-500/20 pb-1 mb-2">Tenant Profile</h4>
<RenderInput label="Full Name *" field="tenantName" placeholder="Required" />
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Phone *" field="tenantPhone" placeholder="Required" />
<RenderInput label="Email" field="tenantEmail" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Occupation" field="tenantOccupation" />
<RenderInput label="Employer" field="tenantEmployer" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Annual Income" field="tenantIncome" />
<RenderInput label="Family Status" field="livingStatus" options={["Single", "Family", "Couple", "Roommates"]} />
</div>
<h4 className="text-[10px] uppercase font-bold text-purple-600 dark:text-purple-400 border-b border-purple-200 dark:border-purple-500/20 pb-1 mb-2 mt-4">Lease Details</h4>
<RenderInput label="Lease Type" field="leaseType" options={["Residential Standard", "Commercial NNN", "Short Term", "Month-to-Month"]} />
<div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<RenderInput label="Signed" field="leaseSigned" type="date" />
<RenderInput label="Start" field="leaseStart" type="date" />
<RenderInput label="End" field="leaseEnd" type="date" />
@@ -459,20 +513,73 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
)}
</div>
{/* SECTION 4: OWNER INTENT (Conditional) */}
{/* SECTION 4: INSURANCE INFORMATION */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-blue-600 dark:text-blue-400">
<ShieldCheck size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Insurance Information</h3>
</div>
<div className="space-y-4">
<RenderInput label="Insurance Company" field="insurance_company" options={["State Farm", "Allstate", "Liberty Mutual", "Farmers", "Nationwide", "USAA", "Chubb", "Travelers", "Progressive", "American Family", "Other"]} />
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={formData.insurance_company_not_listed}
onChange={(e) => isEditing && setFormData({ ...formData, insurance_company_not_listed: e.target.checked })}
disabled={!isEditing}
className="rounded border-zinc-300 dark:border-white/10"
/>
<span className="text-xs text-zinc-500">Company Not Listed</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Claim Filed?" field="claim_filed" options={["Yes", "No"]} />
<RenderInput label="Date of Loss" field="date_of_loss" type="date" />
</div>
{formData.claim_filed === 'Yes' && (
<div className="p-4 bg-blue-50 dark:bg-blue-900/10 rounded-xl space-y-4 border border-blue-100 dark:border-blue-500/20">
<RenderInput label="Damage Location" field="damage_location" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Claim Number" field="claim_number" />
<RenderInput label="Has Paperwork?" field="has_paperwork" options={["Yes", "No"]} />
</div>
<h4 className="text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 border-b border-blue-200 dark:border-blue-500/20 pb-1 mb-2 mt-2">Adjuster Details</h4>
<RenderInput label="Adjuster Name" field="adjuster_name" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<RenderInput label="Phone" field="adjuster_phone" />
<RenderInput label="Ext" field="adjuster_ext" />
<RenderInput label="Fax" field="adjuster_fax" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Email" field="adjuster_email" />
<RenderInput label="Type" field="adjuster_type" options={["Staff Adjuster", "Independent Adjuster", "Public Adjuster"]} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Met Adjuster?" field="met_with_adjuster" options={["Yes", "No"]} />
<RenderInput label="Claim Approved?" field="claim_approved" options={["Yes", "No"]} />
</div>
</div>
)}
</div>
</div>
{/* SECTION 5: OWNER INTENT (Conditional) */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-amber-600 dark:text-amber-400">
<AlertCircle size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Recall & Sales Intent</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Willing to Sell?" field="willingToSell" options={["Yes", "No", "Undecided"]} />
<RenderInput label="Ownership Type" field="ownershipType" options={["Individual", "Joint", "Corporate", "Trust"]} />
</div>
{formData.willingToSell === 'Yes' ? (
<div className="p-4 bg-amber-50 dark:bg-amber-900/10 rounded-xl space-y-4 border border-amber-100 dark:border-amber-500/20">
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Desired Price" field="desiredPrice" type="number" />
<RenderInput label="Min Price" field="minPrice" type="number" />
</div>
@@ -488,7 +595,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<User size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Owner Contact</h3>
</div>
<RenderInput label="Full Name" field="ownerName" />
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Phone" field="ownerPhone" />
<RenderInput label="Email" field="ownerEmail" />
</div>
@@ -666,6 +773,24 @@ function Maps() {
}
};
const newInsuranceData = {
insurance_company: formData.insurance_company,
insurance_company_not_listed: formData.insurance_company_not_listed,
damage_location: formData.damage_location,
date_of_loss: formData.date_of_loss,
claim_filed: formData.claim_filed === 'Yes',
claim_number: formData.claim_number,
has_paperwork: formData.has_paperwork === 'Yes',
adjuster_name: formData.adjuster_name,
adjuster_phone: formData.adjuster_phone,
adjuster_ext: formData.adjuster_ext,
adjuster_type: formData.adjuster_type,
adjuster_fax: formData.adjuster_fax,
adjuster_email: formData.adjuster_email,
met_with_adjuster: formData.met_with_adjuster === 'Yes',
claim_approved: formData.claim_approved === 'Yes',
};
if (selectedPropertyId) {
// Update Existing
setProperties(prev => prev.map(p => {
@@ -678,7 +803,8 @@ function Maps() {
...p.propertyData,
...newPropertyData,
details: { ...p.propertyData.details, ...newPropertyData.details } // Merge to keep existing data not in form if any
}
},
insuranceData: newInsuranceData
};
}
return p;
@@ -691,6 +817,7 @@ function Maps() {
polygon: [], // We don't have a polygon drawing tool yet, implies point-based or empty
canvassingStatus: formData.status,
ownerData: newOwnerData,
insuranceData: newInsuranceData,
propertyData: {
...newPropertyData,
propertyAddress: newPropertyLocation.address,
@@ -732,6 +859,7 @@ function Maps() {
onSelect={handlePropertySelect}
onMapCreate={handleMapCreate}
/>
<MapLegend />
<Drawer
isOpen={!!selectedPropertyId || !!newPropertyLocation}