Update to real api
All checks were successful
Deploy Apartment Dashboard / deploy (push) Successful in 9m59s
All checks were successful
Deploy Apartment Dashboard / deploy (push) Successful in 9m59s
This commit is contained in:
352
src/App.jsx
352
src/App.jsx
@ -1,134 +1,103 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Area, AreaChart } from 'recharts';
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Area, AreaChart } from 'recharts';
|
||||||
import { TrendingUp, TrendingDown, Home, DollarSign, Calendar, MapPin, Activity, AlertCircle, Star } from 'lucide-react';
|
import { TrendingUp, TrendingDown, Home, DollarSign, Calendar, MapPin, Activity, AlertCircle, Star, RefreshCw, Wifi, WifiOff } from 'lucide-react';
|
||||||
|
|
||||||
const ApartmentDashboard = () => {
|
const ApartmentDashboard = () => {
|
||||||
const [selectedUnit, setSelectedUnit] = useState(null);
|
const [selectedUnit, setSelectedUnit] = useState(null);
|
||||||
const [timeRange, setTimeRange] = useState('30d');
|
|
||||||
const [viewMode, setViewMode] = useState('overview');
|
const [viewMode, setViewMode] = useState('overview');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [lastUpdated, setLastUpdated] = useState(null);
|
||||||
|
|
||||||
// Mock data based on your MongoDB structure
|
// Real data state
|
||||||
const [mockData] = useState({
|
const [dailySummary, setDailySummary] = useState(null);
|
||||||
dailySummary: {
|
const [priceHistory, setPriceHistory] = useState([]);
|
||||||
date: '2025-01-15',
|
const [availableUnits, setAvailableUnits] = useState([]);
|
||||||
newUnits: 3,
|
const [recentActivity, setRecentActivity] = useState([]);
|
||||||
rentedUnits: 5,
|
const [planStats, setPlanStats] = useState([]);
|
||||||
staleUnits: 1,
|
|
||||||
netChange: -3,
|
// API base URL
|
||||||
turnoverRate: 8.2,
|
const API_BASE = '/api';
|
||||||
totalAvailable: 47
|
|
||||||
},
|
// Fetch data from API
|
||||||
priceHistory: [
|
const fetchData = async () => {
|
||||||
{ date: '2025-01-01', avgPrice: 3850, unitCount: 52 },
|
setLoading(true);
|
||||||
{ date: '2025-01-02', avgPrice: 3845, unitCount: 48 },
|
setError(null);
|
||||||
{ date: '2025-01-03', avgPrice: 3860, unitCount: 51 },
|
|
||||||
{ date: '2025-01-04', avgPrice: 3875, unitCount: 49 },
|
try {
|
||||||
{ date: '2025-01-05', avgPrice: 3820, unitCount: 53 },
|
console.log('🔄 Fetching apartment data...');
|
||||||
{ date: '2025-01-06', avgPrice: 3835, unitCount: 50 },
|
|
||||||
{ date: '2025-01-07', avgPrice: 3890, unitCount: 47 },
|
// Fetch all endpoints in parallel
|
||||||
{ date: '2025-01-08', avgPrice: 3865, unitCount: 52 },
|
const [
|
||||||
{ date: '2025-01-09', avgPrice: 3880, unitCount: 48 },
|
dailySummaryRes,
|
||||||
{ date: '2025-01-10', avgPrice: 3855, unitCount: 51 },
|
priceHistoryRes,
|
||||||
{ date: '2025-01-11', avgPrice: 3870, unitCount: 49 },
|
availableUnitsRes,
|
||||||
{ date: '2025-01-12', avgPrice: 3825, unitCount: 54 },
|
recentActivityRes,
|
||||||
{ date: '2025-01-13', avgPrice: 3845, unitCount: 50 },
|
planStatsRes
|
||||||
{ date: '2025-01-14', avgPrice: 3885, unitCount: 46 },
|
] = await Promise.all([
|
||||||
{ date: '2025-01-15', avgPrice: 3875, unitCount: 47 }
|
fetch(`${API_BASE}/daily-summary`),
|
||||||
],
|
fetch(`${API_BASE}/price-history`),
|
||||||
availableUnits: [
|
fetch(`${API_BASE}/available-units`),
|
||||||
{
|
fetch(`${API_BASE}/recent-activity`),
|
||||||
unitCode: 'W2506',
|
fetch(`${API_BASE}/plan-stats`)
|
||||||
planName: 'Maroon Peak',
|
]);
|
||||||
bedCount: 2,
|
|
||||||
bathCount: 2,
|
// Check if all requests were successful
|
||||||
area: 1089,
|
if (!dailySummaryRes.ok || !priceHistoryRes.ok || !availableUnitsRes.ok ||
|
||||||
currentPrice: 3750,
|
!recentActivityRes.ok || !planStatsRes.ok) {
|
||||||
minPrice: 3650,
|
throw new Error('One or more API requests failed');
|
||||||
maxPrice: 3850,
|
|
||||||
daysAvailable: 12,
|
|
||||||
community: 'Country Club Towers',
|
|
||||||
priceHistory: [
|
|
||||||
{ date: '2025-01-04', price: 3850 },
|
|
||||||
{ date: '2025-01-05', price: 3825 },
|
|
||||||
{ date: '2025-01-06', price: 3800 },
|
|
||||||
{ date: '2025-01-07', price: 3775 },
|
|
||||||
{ date: '2025-01-08', price: 3750 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unitCode: 'T1205',
|
|
||||||
planName: 'North Maroon Peak',
|
|
||||||
bedCount: 2,
|
|
||||||
bathCount: 2,
|
|
||||||
area: 1150,
|
|
||||||
currentPrice: 3950,
|
|
||||||
minPrice: 3875,
|
|
||||||
maxPrice: 3995,
|
|
||||||
daysAvailable: 8,
|
|
||||||
community: 'Country Club Towers',
|
|
||||||
priceHistory: [
|
|
||||||
{ date: '2025-01-08', price: 3995 },
|
|
||||||
{ date: '2025-01-09', price: 3975 },
|
|
||||||
{ date: '2025-01-10', price: 3950 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
unitCode: 'G0304',
|
|
||||||
planName: 'Snowmass',
|
|
||||||
bedCount: 1,
|
|
||||||
bathCount: 1,
|
|
||||||
area: 750,
|
|
||||||
currentPrice: 2850,
|
|
||||||
minPrice: 2800,
|
|
||||||
maxPrice: 2950,
|
|
||||||
daysAvailable: 22,
|
|
||||||
community: 'Country Club Gardens',
|
|
||||||
priceHistory: [
|
|
||||||
{ date: '2024-12-24', price: 2950 },
|
|
||||||
{ date: '2024-12-30', price: 2925 },
|
|
||||||
{ date: '2025-01-05', price: 2900 },
|
|
||||||
{ date: '2025-01-10', price: 2875 },
|
|
||||||
{ date: '2025-01-15', price: 2850 }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
recentActivity: [
|
// Parse JSON responses
|
||||||
{
|
const [
|
||||||
type: 'new',
|
dailySummaryData,
|
||||||
unitCode: 'W1408',
|
priceHistoryData,
|
||||||
planName: 'Maroon Peak',
|
availableUnitsData,
|
||||||
price: 3825,
|
recentActivityData,
|
||||||
date: '2025-01-15',
|
planStatsData
|
||||||
bedCount: 2,
|
] = await Promise.all([
|
||||||
bathCount: 2
|
dailySummaryRes.json(),
|
||||||
},
|
priceHistoryRes.json(),
|
||||||
{
|
availableUnitsRes.json(),
|
||||||
type: 'rented',
|
recentActivityRes.json(),
|
||||||
unitCode: 'T0912',
|
planStatsRes.json()
|
||||||
planName: 'North Maroon Peak',
|
]);
|
||||||
price: 3950,
|
|
||||||
date: '2025-01-15',
|
console.log('✅ Data fetched successfully:', {
|
||||||
bedCount: 2,
|
dailySummary: dailySummaryData,
|
||||||
bathCount: 2
|
priceHistory: priceHistoryData.length,
|
||||||
},
|
availableUnits: availableUnitsData.length,
|
||||||
{
|
recentActivity: recentActivityData.length,
|
||||||
type: 'price_drop',
|
planStats: planStatsData.length
|
||||||
unitCode: 'W2506',
|
|
||||||
oldPrice: 3775,
|
|
||||||
newPrice: 3750,
|
|
||||||
date: '2025-01-15',
|
|
||||||
planName: 'Maroon Peak'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
planStats: [
|
|
||||||
{ name: 'Maroon Peak', count: 15, avgPrice: 3780, avgArea: 1089 },
|
|
||||||
{ name: 'North Maroon Peak', count: 8, avgPrice: 3920, avgArea: 1150 },
|
|
||||||
{ name: 'Snowmass', count: 12, avgPrice: 2850, avgArea: 750 },
|
|
||||||
{ name: 'Aspen', count: 7, avgPrice: 3200, avgArea: 950 },
|
|
||||||
{ name: 'Capitol Peak', count: 5, avgPrice: 4200, avgArea: 1350 }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update state with real data
|
||||||
|
setDailySummary(dailySummaryData);
|
||||||
|
setPriceHistory(priceHistoryData);
|
||||||
|
setAvailableUnits(availableUnitsData);
|
||||||
|
setRecentActivity(recentActivityData);
|
||||||
|
setPlanStats(planStatsData);
|
||||||
|
setLastUpdated(new Date());
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Error fetching data:', err);
|
||||||
|
setError(err.message);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial data fetch
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-refresh every 5 minutes
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(fetchData, 5 * 60 * 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const MetricCard = ({ title, value, subtitle, icon: Icon, trend, color = "blue" }) => (
|
const MetricCard = ({ title, value, subtitle, icon: Icon, trend, color = "blue" }) => (
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 border-l-4" style={{borderLeftColor: color === 'blue' ? '#3B82F6' : color === 'green' ? '#10B981' : color === 'red' ? '#EF4444' : '#F59E0B'}}>
|
<div className="bg-white rounded-lg shadow-md p-6 border-l-4" style={{borderLeftColor: color === 'blue' ? '#3B82F6' : color === 'green' ? '#10B981' : color === 'red' ? '#EF4444' : '#F59E0B'}}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -155,13 +124,64 @@ const ApartmentDashboard = () => {
|
|||||||
|
|
||||||
const COLORS = ['#8B0000', '#DC143C', '#B22222', '#CD5C5C', '#F08080'];
|
const COLORS = ['#8B0000', '#DC143C', '#B22222', '#CD5C5C', '#F08080'];
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (loading && !dailySummary) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<RefreshCw className="w-12 h-12 animate-spin text-blue-600 mx-auto mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">Loading Apartment Data</h2>
|
||||||
|
<p className="text-gray-600">Fetching real-time market information...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error && !dailySummary) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<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>
|
||||||
|
<p className="text-gray-600 mb-4">Unable to fetch apartment data: {error}</p>
|
||||||
|
<button
|
||||||
|
onClick={fetchData}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-6">
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Country Club Towers & Gardens</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Country Club Towers & Gardens</h1>
|
||||||
<p className="text-gray-600">Real-time apartment market dashboard</p>
|
<p className="text-gray-600">Real-time apartment market dashboard</p>
|
||||||
|
{lastUpdated && (
|
||||||
|
<div className="flex items-center mt-2 text-sm text-gray-500">
|
||||||
|
<Wifi className="w-4 h-4 mr-1" />
|
||||||
|
Last updated: {lastUpdated.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={fetchData}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
<span>Refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 mt-4">
|
<div className="flex items-center space-x-4 mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('overview')}
|
onClick={() => setViewMode('overview')}
|
||||||
@ -173,7 +193,7 @@ const ApartmentDashboard = () => {
|
|||||||
onClick={() => setViewMode('units')}
|
onClick={() => setViewMode('units')}
|
||||||
className={`px-4 py-2 rounded-md ${viewMode === 'units' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 border'}`}
|
className={`px-4 py-2 rounded-md ${viewMode === 'units' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 border'}`}
|
||||||
>
|
>
|
||||||
Available Units
|
Available Units ({availableUnits.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('analytics')}
|
onClick={() => setViewMode('analytics')}
|
||||||
@ -184,33 +204,34 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'overview' && (
|
{/* Overview Tab */}
|
||||||
|
{viewMode === 'overview' && dailySummary && (
|
||||||
<>
|
<>
|
||||||
{/* Daily Metrics */}
|
{/* Daily Metrics */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="Available Units"
|
title="Available Units"
|
||||||
value={mockData.dailySummary.totalAvailable}
|
value={dailySummary.totalAvailable || availableUnits.length}
|
||||||
icon={Home}
|
icon={Home}
|
||||||
color="blue"
|
color="blue"
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="New Today"
|
title="New Today"
|
||||||
value={mockData.dailySummary.newUnits}
|
value={dailySummary.newUnits || dailySummary.new_units_count || 0}
|
||||||
subtitle="Units available"
|
subtitle="Units available"
|
||||||
icon={TrendingUp}
|
icon={TrendingUp}
|
||||||
color="green"
|
color="green"
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="Rented Today"
|
title="Rented Today"
|
||||||
value={mockData.dailySummary.rentedUnits}
|
value={dailySummary.rentedUnits || dailySummary.rented_units_count || 0}
|
||||||
subtitle="Units leased"
|
subtitle="Units leased"
|
||||||
icon={TrendingDown}
|
icon={TrendingDown}
|
||||||
color="red"
|
color="red"
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="Turnover Rate"
|
title="Turnover Rate"
|
||||||
value={`${mockData.dailySummary.turnoverRate}%`}
|
value={`${(dailySummary.turnoverRate || dailySummary.turnover_rate || 0).toFixed(1)}%`}
|
||||||
subtitle="Daily activity"
|
subtitle="Daily activity"
|
||||||
icon={Activity}
|
icon={Activity}
|
||||||
color="orange"
|
color="orange"
|
||||||
@ -220,9 +241,9 @@ const ApartmentDashboard = () => {
|
|||||||
{/* Price Trends */}
|
{/* Price Trends */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Average Price Trend (15 days)</h3>
|
<h3 className="text-lg font-semibold mb-4">Average Price Trend ({priceHistory.length} days)</h3>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<AreaChart data={mockData.priceHistory}>
|
<AreaChart data={priceHistory.filter(d => d.avgPrice !== null)}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
@ -241,7 +262,7 @@ const ApartmentDashboard = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Unit Availability</h3>
|
<h3 className="text-lg font-semibold mb-4">Unit Availability</h3>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<AreaChart data={mockData.priceHistory}>
|
<AreaChart data={priceHistory}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
@ -259,13 +280,14 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plan Distribution */}
|
{/* Plan Distribution */}
|
||||||
|
{planStats.length > 0 && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Units by Floor Plan</h3>
|
<h3 className="text-lg font-semibold mb-4">Units by Floor Plan</h3>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
data={mockData.planStats}
|
data={planStats}
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
labelLine={false}
|
labelLine={false}
|
||||||
@ -274,7 +296,7 @@ const ApartmentDashboard = () => {
|
|||||||
fill="#8884d8"
|
fill="#8884d8"
|
||||||
dataKey="count"
|
dataKey="count"
|
||||||
>
|
>
|
||||||
{mockData.planStats.map((entry, index) => (
|
{planStats.map((entry, index) => (
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
))}
|
))}
|
||||||
</Pie>
|
</Pie>
|
||||||
@ -286,7 +308,7 @@ const ApartmentDashboard = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Average Price by Plan</h3>
|
<h3 className="text-lg font-semibold mb-4">Average Price by Plan</h3>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={mockData.planStats}>
|
<BarChart data={planStats}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} />
|
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} />
|
||||||
<YAxis tickFormatter={(value) => `$${value}`} />
|
<YAxis tickFormatter={(value) => `$${value}`} />
|
||||||
@ -296,12 +318,14 @@ const ApartmentDashboard = () => {
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Recent Activity */}
|
{/* Recent Activity */}
|
||||||
|
{recentActivity.length > 0 && (
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
|
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{mockData.recentActivity.map((activity, index) => (
|
{recentActivity.map((activity, index) => (
|
||||||
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{activity.type === 'new' && <div className="w-3 h-3 bg-green-500 rounded-full"></div>}
|
{activity.type === 'new' && <div className="w-3 h-3 bg-green-500 rounded-full"></div>}
|
||||||
@ -317,7 +341,7 @@ const ApartmentDashboard = () => {
|
|||||||
{activity.planName} • {activity.bedCount}BR/{activity.bathCount}BA
|
{activity.planName} • {activity.bedCount}BR/{activity.bathCount}BA
|
||||||
{activity.type === 'price_drop'
|
{activity.type === 'price_drop'
|
||||||
? ` • ${formatPrice(activity.oldPrice)} → ${formatPrice(activity.newPrice)}`
|
? ` • ${formatPrice(activity.oldPrice)} → ${formatPrice(activity.newPrice)}`
|
||||||
: ` • ${formatPrice(activity.price)}`
|
: activity.price ? ` • ${formatPrice(activity.price)}` : ''
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -327,38 +351,40 @@ const ApartmentDashboard = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Units Tab */}
|
||||||
{viewMode === 'units' && (
|
{viewMode === 'units' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-xl font-semibold">Available Units ({mockData.availableUnits.length})</h2>
|
<h2 className="text-xl font-semibold">Available Units ({availableUnits.length})</h2>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<select className="border rounded-md px-3 py-2">
|
<select className="border rounded-md px-3 py-2">
|
||||||
<option>All Plans</option>
|
<option>All Plans</option>
|
||||||
<option>Maroon Peak</option>
|
{[...new Set(availableUnits.map(u => u.planName))].map(plan => (
|
||||||
<option>North Maroon Peak</option>
|
<option key={plan}>{plan}</option>
|
||||||
<option>Snowmass</option>
|
))}
|
||||||
</select>
|
</select>
|
||||||
<select className="border rounded-md px-3 py-2">
|
<select className="border rounded-md px-3 py-2">
|
||||||
<option>All Bedrooms</option>
|
<option>All Bedrooms</option>
|
||||||
<option>1 BR</option>
|
{[...new Set(availableUnits.map(u => u.bedCount))].sort().map(beds => (
|
||||||
<option>2 BR</option>
|
<option key={beds}>{beds} BR</option>
|
||||||
<option>3 BR</option>
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
{mockData.availableUnits.map((unit) => (
|
{availableUnits.map((unit) => (
|
||||||
<div key={unit.unitCode} className="bg-white rounded-lg shadow-md overflow-hidden">
|
<div key={unit.unitCode} className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold">{unit.unitCode}</h3>
|
<h3 className="text-lg font-bold">{unit.unitCode}</h3>
|
||||||
<p className="text-gray-600">{unit.planName}</p>
|
<p className="text-gray-600">{unit.planName}</p>
|
||||||
{unit.planName.toLowerCase().includes('maroon peak') &&
|
{unit.planName && unit.planName.toLowerCase().includes('maroon peak') && unit.currentPrice < 3800 &&
|
||||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 mt-1">
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 mt-1">
|
||||||
<Star className="w-3 h-3 mr-1" />
|
<Star className="w-3 h-3 mr-1" />
|
||||||
Priority
|
Priority
|
||||||
@ -386,7 +412,7 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-center p-3 bg-gray-50 rounded">
|
<div className="text-center p-3 bg-gray-50 rounded">
|
||||||
<p className="text-sm text-gray-600">Available</p>
|
<p className="text-sm text-gray-600">Available</p>
|
||||||
<p className="font-semibold">{unit.daysAvailable} days</p>
|
<p className="font-semibold">{unit.daysAvailable || 0} days</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -399,7 +425,9 @@ const ApartmentDashboard = () => {
|
|||||||
<div
|
<div
|
||||||
className="bg-blue-600 h-2 rounded-full"
|
className="bg-blue-600 h-2 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
width: `${((unit.currentPrice - unit.minPrice) / (unit.maxPrice - unit.minPrice)) * 100}%`
|
width: unit.maxPrice > unit.minPrice
|
||||||
|
? `${((unit.currentPrice - unit.minPrice) / (unit.maxPrice - unit.minPrice)) * 100}%`
|
||||||
|
: '50%'
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@ -415,9 +443,18 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{availableUnits.length === 0 && !loading && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Home className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">No Available Units</h3>
|
||||||
|
<p className="text-gray-600">No apartment units are currently available.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Analytics Tab */}
|
||||||
{viewMode === 'analytics' && (
|
{viewMode === 'analytics' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-xl font-semibold">Market Analytics</h2>
|
<h2 className="text-xl font-semibold">Market Analytics</h2>
|
||||||
@ -426,7 +463,7 @@ const ApartmentDashboard = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Price Distribution by Plan</h3>
|
<h3 className="text-lg font-semibold mb-4">Price Distribution by Plan</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{mockData.planStats.map((plan, index) => (
|
{planStats.map((plan, index) => (
|
||||||
<div key={plan.name} className="flex items-center justify-between">
|
<div key={plan.name} className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div
|
<div
|
||||||
@ -453,18 +490,29 @@ const ApartmentDashboard = () => {
|
|||||||
Maroon Peak units under $3,800 are highlighted as priority opportunities based on your preferences.
|
Maroon Peak units under $3,800 are highlighted as priority opportunities based on your preferences.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{dailySummary && (
|
||||||
<div className="p-4 bg-blue-50 rounded-lg border-l-4 border-blue-500">
|
<div className="p-4 bg-blue-50 rounded-lg border-l-4 border-blue-500">
|
||||||
<h4 className="font-semibold text-blue-800">Market Activity</h4>
|
<h4 className="font-semibold text-blue-800">Market Activity</h4>
|
||||||
<p className="text-sm text-blue-700">
|
<p className="text-sm text-blue-700">
|
||||||
Daily turnover rate of {mockData.dailySummary.turnoverRate}% indicates moderate market activity.
|
Daily turnover rate of {(dailySummary.turnoverRate || dailySummary.turnover_rate || 0).toFixed(1)}% indicates {
|
||||||
|
(dailySummary.turnoverRate || dailySummary.turnover_rate || 0) > 10 ? 'high' :
|
||||||
|
(dailySummary.turnoverRate || dailySummary.turnover_rate || 0) > 5 ? 'moderate' : 'low'
|
||||||
|
} market activity.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{priceHistory.length > 0 && (
|
||||||
<div className="p-4 bg-green-50 rounded-lg border-l-4 border-green-500">
|
<div className="p-4 bg-green-50 rounded-lg border-l-4 border-green-500">
|
||||||
<h4 className="font-semibold text-green-800">Price Trends</h4>
|
<h4 className="font-semibold text-green-800">Price Trends</h4>
|
||||||
<p className="text-sm text-green-700">
|
<p className="text-sm text-green-700">
|
||||||
Average prices have stabilized around $3,850 with minimal daily fluctuation.
|
{priceHistory.length > 1 ? (
|
||||||
|
`Average prices ${priceHistory[priceHistory.length - 1].avgPrice > priceHistory[priceHistory.length - 2].avgPrice ? 'increased' : 'decreased'} in recent days.`
|
||||||
|
) : (
|
||||||
|
'Price trend data is being collected.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -483,7 +531,7 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedUnit(null)}
|
onClick={() => setSelectedUnit(null)}
|
||||||
className="text-gray-500 hover:text-gray-700"
|
className="text-gray-500 hover:text-gray-700 text-2xl"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -515,10 +563,11 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-gray-600">Days Available</p>
|
<p className="text-sm text-gray-600">Days Available</p>
|
||||||
<p className="text-xl font-semibold">{selectedUnit.daysAvailable}</p>
|
<p className="text-xl font-semibold">{selectedUnit.daysAvailable || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedUnit.priceHistory && selectedUnit.priceHistory.length > 0 && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="font-semibold mb-4">Price History</h3>
|
<h3 className="font-semibold mb-4">Price History</h3>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
@ -537,6 +586,7 @@ const ApartmentDashboard = () => {
|
|||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user