Admin Dashboard Frontend (Phases 1-4) #3

Merged
stephen merged 7 commits from admin into main 2026-01-22 14:12:59 -07:00
7 changed files with 519 additions and 117 deletions
Showing only changes of commit d285429e25 - Show all commits

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Routes, Route, Link, useLocation, Navigate } from 'react-router-dom'; import { Routes, Route, Link, useLocation, Navigate } from 'react-router-dom';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Area, AreaChart, ScatterChart, Scatter, ComposedChart, ReferenceLine } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Area, AreaChart, ScatterChart, Scatter, ComposedChart, ReferenceLine } from 'recharts';
import { TrendingUp, TrendingDown, Home, DollarSign, Calendar, MapPin, Activity, AlertCircle, Star, RefreshCw, Wifi, WifiOff, Bell, Target, Filter, Search, ArrowUp, ArrowDown, Zap, Clock, Eye, LayoutGrid, List, LogOut, User } from 'lucide-react'; import { TrendingUp, TrendingDown, Home, DollarSign, Calendar, MapPin, Activity, AlertCircle, Star, RefreshCw, Wifi, WifiOff, Bell, Target, Filter, Search, ArrowUp, ArrowDown, Zap, Clock, Eye, LayoutGrid, List, LogOut, User, ChevronDown, ChevronUp, Settings } from 'lucide-react';
import useAuth from './hooks/useAuth'; import useAuth from './hooks/useAuth';
import ProtectedRoute from './components/ProtectedRoute'; import ProtectedRoute from './components/ProtectedRoute';
import Login from './pages/Login'; import Login from './pages/Login';
@ -9,7 +9,7 @@ import Login from './pages/Login';
const ApartmentDashboard = () => { const ApartmentDashboard = () => {
// Router // Router
const location = useLocation(); const location = useLocation();
const { user, logout } = useAuth(); const { user, loading: authLoading, logout } = useAuth();
// Basic state // Basic state
const [selectedUnit, setSelectedUnit] = useState(null); const [selectedUnit, setSelectedUnit] = useState(null);
@ -27,6 +27,21 @@ const ApartmentDashboard = () => {
const [sortDirection, setSortDirection] = useState('asc'); const [sortDirection, setSortDirection] = useState('asc');
const [watchedUnits, setWatchedUnits] = useState(new Set()); const [watchedUnits, setWatchedUnits] = useState(new Set());
const [unitsViewMode, setUnitsViewMode] = useState('list'); const [unitsViewMode, setUnitsViewMode] = useState('list');
const [columnSettingsOpen, setColumnSettingsOpen] = useState(false);
const [visibleColumns, setVisibleColumns] = useState({
unit: true,
plan: true,
bedBath: true,
area: true,
price: true,
pricePerSqft: true,
days: true,
high: true,
low: true,
delta: true,
soonest: true,
actions: true
});
// API Data state - using your working endpoints // API Data state - using your working endpoints
const [dailySummary, setDailySummary] = useState(null); const [dailySummary, setDailySummary] = useState(null);
@ -142,16 +157,19 @@ const ApartmentDashboard = () => {
} }
}; };
// Initial data fetch // Initial data fetch - only when authenticated
useEffect(() => { useEffect(() => {
fetchData(); if (user) {
}, []); fetchData();
}
}, [user]);
// Auto-refresh every 5 minutes // Auto-refresh every 5 minutes - only when authenticated
useEffect(() => { useEffect(() => {
if (!user) return;
const interval = setInterval(fetchData, 5 * 60 * 1000); const interval = setInterval(fetchData, 5 * 60 * 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, [user]);
// Helper functions // Helper functions
const formatPrice = (price) => `$${(price || 0).toLocaleString()}`; const formatPrice = (price) => `$${(price || 0).toLocaleString()}`;
@ -402,6 +420,9 @@ const ApartmentDashboard = () => {
case 'price': return dir * ((a.currentPrice || 0) - (b.currentPrice || 0)); case 'price': return dir * ((a.currentPrice || 0) - (b.currentPrice || 0));
case 'pricePerSqft': return dir * (((a.currentPrice || 0) / (a.area || 1)) - ((b.currentPrice || 0) / (b.area || 1))); case 'pricePerSqft': return dir * (((a.currentPrice || 0) / (a.area || 1)) - ((b.currentPrice || 0) / (b.area || 1)));
case 'daysAvailable': return dir * ((a.daysAvailable || 0) - (b.daysAvailable || 0)); case 'daysAvailable': return dir * ((a.daysAvailable || 0) - (b.daysAvailable || 0));
case 'maxPrice': return dir * ((a.maxPrice || 0) - (b.maxPrice || 0));
case 'minPrice': return dir * ((a.minPrice || 0) - (b.minPrice || 0));
case 'delta': return dir * (((a.maxPrice || 0) - (a.minPrice || 0)) - ((b.maxPrice || 0) - (b.minPrice || 0)));
case 'soonest': return dir * (a.soonest || '').localeCompare(b.soonest || ''); case 'soonest': return dir * (a.soonest || '').localeCompare(b.soonest || '');
default: return 0; default: return 0;
} }
@ -444,6 +465,15 @@ const ApartmentDashboard = () => {
); );
} }
// Auth loading state
if (authLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
// Error state // Error state
if (error && !dailySummary) { if (error && !dailySummary) {
return ( return (
@ -452,7 +482,7 @@ const ApartmentDashboard = () => {
<WifiOff className="w-12 h-12 text-red-600 mx-auto mb-4" /> <WifiOff className="w-12 h-12 text-red-600 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2">Connection Error</h2> <h2 className="text-xl font-semibold text-gray-900 mb-2">Connection Error</h2>
<p className="text-gray-600 mb-4">Unable to fetch apartment data: {error}</p> <p className="text-gray-600 mb-4">Unable to fetch apartment data: {error}</p>
<button <button
onClick={fetchData} onClick={fetchData}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors" className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
> >
@ -762,6 +792,51 @@ const ApartmentDashboard = () => {
</button> </button>
</div> </div>
</div> </div>
{/* Column Settings Toggle */}
<div className="mt-4 border-t pt-4">
<button
onClick={() => setColumnSettingsOpen(!columnSettingsOpen)}
className="flex items-center text-sm text-gray-600 hover:text-gray-900"
>
<Settings className="w-4 h-4 mr-2" />
<span>Column Settings</span>
{columnSettingsOpen ? (
<ChevronUp className="w-4 h-4 ml-1" />
) : (
<ChevronDown className="w-4 h-4 ml-1" />
)}
</button>
{columnSettingsOpen && (
<div className="mt-3 flex flex-wrap gap-4">
{[
{ key: 'unit', label: 'Unit' },
{ key: 'plan', label: 'Plan' },
{ key: 'bedBath', label: 'Bed/Bath' },
{ key: 'area', label: 'Area' },
{ key: 'price', label: 'Price' },
{ key: 'pricePerSqft', label: '$/sqft' },
{ key: 'days', label: 'Days' },
{ key: 'high', label: 'High' },
{ key: 'low', label: 'Low' },
{ key: 'delta', label: 'Delta' },
{ key: 'soonest', label: 'Soonest' },
{ key: 'actions', label: 'Actions' }
].map(({ key, label }) => (
<label key={key} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={visibleColumns[key]}
onChange={(e) => setVisibleColumns({...visibleColumns, [key]: e.target.checked})}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm">{label}</span>
</label>
))}
</div>
)}
</div>
</div> </div>
{/* Unit Cards - Grid View */} {/* Unit Cards - Grid View */}
@ -816,79 +891,130 @@ const ApartmentDashboard = () => {
<table className="w-full min-w-max"> <table className="w-full min-w-max">
<thead className="bg-gray-50 border-b"> <thead className="bg-gray-50 border-b">
<tr> <tr>
<th {visibleColumns.unit && (
className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none sticky left-0 z-10 bg-gray-50 shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)]" <th
onClick={() => handleSort('unitCode')} className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none sticky left-0 z-10 bg-gray-50 shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)]"
> onClick={() => handleSort('unitCode')}
<span className="flex items-center"> >
Unit <span className="flex items-center">
{sortBy === 'unitCode' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} Unit
</span> {sortBy === 'unitCode' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
</th> </span>
<th </th>
className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none" )}
onClick={() => handleSort('planName')} {visibleColumns.plan && (
> <th
<span className="flex items-center"> className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
Plan onClick={() => handleSort('planName')}
{sortBy === 'planName' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} >
</span> <span className="flex items-center">
</th> Plan
<th {sortBy === 'planName' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none" </span>
onClick={() => handleSort('bedCount')} </th>
> )}
<span className="flex items-center justify-center"> {visibleColumns.bedBath && (
Bed/Bath <th
{sortBy === 'bedCount' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
</span> onClick={() => handleSort('bedCount')}
</th> >
<th <span className="flex items-center justify-center">
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none" Bed/Bath
onClick={() => handleSort('area')} {sortBy === 'bedCount' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
> </span>
<span className="flex items-center justify-center"> </th>
Area )}
{sortBy === 'area' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} {visibleColumns.area && (
</span> <th
</th> className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
<th onClick={() => handleSort('area')}
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none" >
onClick={() => handleSort('price')} <span className="flex items-center justify-center">
> Area
<span className="flex items-center justify-end"> {sortBy === 'area' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
Price </span>
{sortBy === 'price' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} </th>
</span> )}
</th> {visibleColumns.price && (
<th <th
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none" className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
onClick={() => handleSort('pricePerSqft')} onClick={() => handleSort('price')}
> >
<span className="flex items-center justify-end"> <span className="flex items-center justify-end">
$/sqft Price
{sortBy === 'pricePerSqft' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} {sortBy === 'price' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
</span> </span>
</th> </th>
<th )}
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none" {visibleColumns.pricePerSqft && (
onClick={() => handleSort('daysAvailable')} <th
> className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
<span className="flex items-center justify-center"> onClick={() => handleSort('pricePerSqft')}
Days >
{sortBy === 'daysAvailable' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} <span className="flex items-center justify-end">
</span> $/sqft
</th> {sortBy === 'pricePerSqft' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
<th </span>
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none" </th>
onClick={() => handleSort('soonest')} )}
> {visibleColumns.days && (
<span className="flex items-center justify-center"> <th
Soonest className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
{sortBy === 'soonest' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)} onClick={() => handleSort('daysAvailable')}
</span> >
</th> <span className="flex items-center justify-center">
<th className="text-center py-3 px-4 font-semibold text-gray-700">Actions</th> Days
{sortBy === 'daysAvailable' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
</span>
</th>
)}
{visibleColumns.high && (
<th
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
onClick={() => handleSort('maxPrice')}
>
<span className="flex items-center justify-end">
High
{sortBy === 'maxPrice' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
</span>
</th>
)}
{visibleColumns.low && (
<th
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
onClick={() => handleSort('minPrice')}
>
<span className="flex items-center justify-end">
Low
{sortBy === 'minPrice' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
</span>
</th>
)}
{visibleColumns.delta && (
<th
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
onClick={() => handleSort('delta')}
>
<span className="flex items-center justify-end">
Delta
{sortBy === 'delta' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
</span>
</th>
)}
{visibleColumns.soonest && (
<th
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
onClick={() => handleSort('soonest')}
>
<span className="flex items-center justify-center">
Soonest
{sortBy === 'soonest' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
</span>
</th>
)}
{visibleColumns.actions && (
<th className="text-center py-3 px-4 font-semibold text-gray-700">Actions</th>
)}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -897,22 +1023,27 @@ const ApartmentDashboard = () => {
key={unit.unitCode} key={unit.unitCode}
className={`border-b ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'} ${watchedUnits.has(unit.unitCode) ? 'ring-2 ring-inset ring-blue-400' : ''} hover:bg-blue-50`} className={`border-b ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'} ${watchedUnits.has(unit.unitCode) ? 'ring-2 ring-inset ring-blue-400' : ''} hover:bg-blue-50`}
> >
<td className={`py-3 px-4 font-bold sticky left-0 z-10 shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}>{unit.unitCode}</td> {visibleColumns.unit && <td className={`py-3 px-4 font-bold sticky left-0 z-10 shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}>{unit.unitCode}</td>}
<td className="py-3 px-4 text-gray-600">{unit.planName}</td> {visibleColumns.plan && <td className="py-3 px-4 text-gray-600">{unit.planName}</td>}
<td className="py-3 px-4 text-center">{unit.bedCount}/{unit.bathCount}</td> {visibleColumns.bedBath && <td className="py-3 px-4 text-center">{unit.bedCount}/{unit.bathCount}</td>}
<td className="py-3 px-4 text-center">{unit.area} sqft</td> {visibleColumns.area && <td className="py-3 px-4 text-center">{unit.area} sqft</td>}
<td className="py-3 px-4 text-right font-bold text-green-600">{formatPrice(unit.currentPrice)}</td> {visibleColumns.price && <td className="py-3 px-4 text-right font-bold text-green-600">{formatPrice(unit.currentPrice)}</td>}
<td className="py-3 px-4 text-right text-gray-500">${((unit.currentPrice || 0) / (unit.area || 1)).toFixed(2)}</td> {visibleColumns.pricePerSqft && <td className="py-3 px-4 text-right text-gray-500">${((unit.currentPrice || 0) / (unit.area || 1)).toFixed(2)}</td>}
<td className="py-3 px-4 text-center">{unit.daysAvailable || 0}</td> {visibleColumns.days && <td className="py-3 px-4 text-center">{unit.daysAvailable || 0}</td>}
<td className="py-3 px-4 text-center">{unit.soonest ? (new Date(unit.soonest) <= new Date() ? 'Now' : formatDate(unit.soonest)) : '-'}</td> {visibleColumns.high && <td className="py-3 px-4 text-right text-gray-600">{formatPrice(unit.maxPrice)}</td>}
<td className="py-3 px-4 text-center"> {visibleColumns.low && <td className="py-3 px-4 text-right text-gray-600">{formatPrice(unit.minPrice)}</td>}
<button {visibleColumns.delta && <td className="py-3 px-4 text-right text-gray-600">{formatPrice((unit.maxPrice || 0) - (unit.minPrice || 0))}</td>}
onClick={() => openUnitDetail(unit)} {visibleColumns.soonest && <td className="py-3 px-4 text-center">{unit.soonest ? (new Date(unit.soonest) <= new Date() ? 'Now' : formatDate(unit.soonest)) : '-'}</td>}
className="bg-blue-600 text-white py-1 px-3 rounded-md hover:bg-blue-700 transition-colors text-sm" {visibleColumns.actions && (
> <td className="py-3 px-4 text-center">
Details <button
</button> onClick={() => openUnitDetail(unit)}
</td> className="bg-blue-600 text-white py-1 px-3 rounded-md hover:bg-blue-700 transition-colors text-sm"
>
Details
</button>
</td>
)}
</tr> </tr>
))} ))}
</tbody> </tbody>