Admin Dashboard Frontend (Phases 1-4) #3
323
src/App.jsx
323
src/App.jsx
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { 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 ProtectedRoute from './components/ProtectedRoute';
|
||||
import Login from './pages/Login';
|
||||
@ -9,7 +9,7 @@ import Login from './pages/Login';
|
||||
const ApartmentDashboard = () => {
|
||||
// Router
|
||||
const location = useLocation();
|
||||
const { user, logout } = useAuth();
|
||||
const { user, loading: authLoading, logout } = useAuth();
|
||||
|
||||
// Basic state
|
||||
const [selectedUnit, setSelectedUnit] = useState(null);
|
||||
@ -27,6 +27,21 @@ const ApartmentDashboard = () => {
|
||||
const [sortDirection, setSortDirection] = useState('asc');
|
||||
const [watchedUnits, setWatchedUnits] = useState(new Set());
|
||||
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
|
||||
const [dailySummary, setDailySummary] = useState(null);
|
||||
@ -142,16 +157,19 @@ const ApartmentDashboard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initial data fetch
|
||||
// Initial data fetch - only when authenticated
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
if (user) {
|
||||
fetchData();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// Auto-refresh every 5 minutes
|
||||
// Auto-refresh every 5 minutes - only when authenticated
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const interval = setInterval(fetchData, 5 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
}, [user]);
|
||||
|
||||
// Helper functions
|
||||
const formatPrice = (price) => `$${(price || 0).toLocaleString()}`;
|
||||
@ -402,6 +420,9 @@ const ApartmentDashboard = () => {
|
||||
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 '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 || '');
|
||||
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
|
||||
if (error && !dailySummary) {
|
||||
return (
|
||||
@ -762,6 +792,51 @@ const ApartmentDashboard = () => {
|
||||
</button>
|
||||
</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>
|
||||
|
||||
{/* Unit Cards - Grid View */}
|
||||
@ -816,79 +891,130 @@ const ApartmentDashboard = () => {
|
||||
<table className="w-full min-w-max">
|
||||
<thead className="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th
|
||||
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
|
||||
{sortBy === 'unitCode' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</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')}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
Plan
|
||||
{sortBy === 'planName' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('bedCount')}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
Bed/Bath
|
||||
{sortBy === 'bedCount' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('area')}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
Area
|
||||
{sortBy === 'area' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
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-end">
|
||||
Price
|
||||
{sortBy === 'price' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('pricePerSqft')}
|
||||
>
|
||||
<span className="flex items-center justify-end">
|
||||
$/sqft
|
||||
{sortBy === 'pricePerSqft' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('daysAvailable')}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
Days
|
||||
{sortBy === 'daysAvailable' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
<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>
|
||||
<th className="text-center py-3 px-4 font-semibold text-gray-700">Actions</th>
|
||||
{visibleColumns.unit && (
|
||||
<th
|
||||
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
|
||||
{sortBy === 'unitCode' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.plan && (
|
||||
<th
|
||||
className="text-left py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('planName')}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
Plan
|
||||
{sortBy === 'planName' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.bedBath && (
|
||||
<th
|
||||
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('bedCount')}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
Bed/Bath
|
||||
{sortBy === 'bedCount' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.area && (
|
||||
<th
|
||||
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('area')}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
Area
|
||||
{sortBy === 'area' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.price && (
|
||||
<th
|
||||
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-end">
|
||||
Price
|
||||
{sortBy === 'price' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.pricePerSqft && (
|
||||
<th
|
||||
className="text-right py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('pricePerSqft')}
|
||||
>
|
||||
<span className="flex items-center justify-end">
|
||||
$/sqft
|
||||
{sortBy === 'pricePerSqft' && (sortDirection === 'asc' ? <ArrowUp className="w-4 h-4 ml-1" /> : <ArrowDown className="w-4 h-4 ml-1" />)}
|
||||
</span>
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.days && (
|
||||
<th
|
||||
className="text-center py-3 px-4 font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('daysAvailable')}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
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>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -897,22 +1023,27 @@ const ApartmentDashboard = () => {
|
||||
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`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<button
|
||||
onClick={() => openUnitDetail(unit)}
|
||||
className="bg-blue-600 text-white py-1 px-3 rounded-md hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
</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>}
|
||||
{visibleColumns.plan && <td className="py-3 px-4 text-gray-600">{unit.planName}</td>}
|
||||
{visibleColumns.bedBath && <td className="py-3 px-4 text-center">{unit.bedCount}/{unit.bathCount}</td>}
|
||||
{visibleColumns.area && <td className="py-3 px-4 text-center">{unit.area} sqft</td>}
|
||||
{visibleColumns.price && <td className="py-3 px-4 text-right font-bold text-green-600">{formatPrice(unit.currentPrice)}</td>}
|
||||
{visibleColumns.pricePerSqft && <td className="py-3 px-4 text-right text-gray-500">${((unit.currentPrice || 0) / (unit.area || 1)).toFixed(2)}</td>}
|
||||
{visibleColumns.days && <td className="py-3 px-4 text-center">{unit.daysAvailable || 0}</td>}
|
||||
{visibleColumns.high && <td className="py-3 px-4 text-right text-gray-600">{formatPrice(unit.maxPrice)}</td>}
|
||||
{visibleColumns.low && <td className="py-3 px-4 text-right text-gray-600">{formatPrice(unit.minPrice)}</td>}
|
||||
{visibleColumns.delta && <td className="py-3 px-4 text-right text-gray-600">{formatPrice((unit.maxPrice || 0) - (unit.minPrice || 0))}</td>}
|
||||
{visibleColumns.soonest && <td className="py-3 px-4 text-center">{unit.soonest ? (new Date(unit.soonest) <= new Date() ? 'Now' : formatDate(unit.soonest)) : '-'}</td>}
|
||||
{visibleColumns.actions && (
|
||||
<td className="py-3 px-4 text-center">
|
||||
<button
|
||||
onClick={() => openUnitDetail(unit)}
|
||||
className="bg-blue-600 text-white py-1 px-3 rounded-md hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user