- Add subscription tiers (Basic/Pro/Business) with listing limits and dynamic commission - Add daily/monthly period filter on rentals page - Add landlord dashboard with earnings chart, stat cards, property performance - Add landlord subscription management page - Add tenant dashboard with upcoming stays - Add business model documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
4.0 KiB
TypeScript
93 lines
4.0 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Calendar, DollarSign, Star, Clock } from 'lucide-react';
|
|
import { StatCard } from '../components/ui/StatCard';
|
|
import { UpcomingStays } from '../components/rentals/UpcomingStays';
|
|
import { api } from '../api/client';
|
|
import { formatCurrency } from '../utils/format';
|
|
import type { Booking } from '../types/rental';
|
|
|
|
export function TenantDashboardPage() {
|
|
const [bookings, setBookings] = useState<Booking[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function fetchData() {
|
|
try {
|
|
const res = await api.get<Booking[]>('/bookings?role=tenant');
|
|
setBookings(res);
|
|
} catch {
|
|
// silently fail
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
fetchData();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return <div className="text-center text-gray-400 py-12">Loading dashboard...</div>;
|
|
}
|
|
|
|
const totalBookings = bookings.length;
|
|
const upcomingCount = bookings.filter(b => b.status === 'CONFIRMED' || b.status === 'ACTIVE').length;
|
|
const totalSpent = bookings
|
|
.filter(b => ['COMPLETED', 'CONFIRMED', 'ACTIVE'].includes(b.status))
|
|
.reduce((sum, b) => sum + b.totalAmount, 0);
|
|
const completedWithReview = bookings.filter(b => b.status === 'COMPLETED' && b.review);
|
|
const avgRating = completedWithReview.length > 0
|
|
? completedWithReview.reduce((sum, b) => sum + (b.review?.rating || 0), 0) / completedWithReview.length
|
|
: 0;
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
<StatCard icon={Calendar} label="Total Bookings" value={totalBookings} color="blue" />
|
|
<StatCard icon={Clock} label="Upcoming" value={upcomingCount} color="green" />
|
|
<StatCard icon={DollarSign} label="Total Spent" value={formatCurrency(totalSpent)} color="pink" />
|
|
<StatCard icon={Star} label="Avg Rating Given" value={avgRating ? avgRating.toFixed(1) : 'N/A'} color="yellow" />
|
|
</div>
|
|
|
|
<UpcomingStays bookings={bookings} />
|
|
|
|
{/* Recent Completed */}
|
|
{bookings.filter(b => b.status === 'COMPLETED').length > 0 && (
|
|
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Recent Completed Stays</h3>
|
|
<div className="space-y-3">
|
|
{bookings
|
|
.filter(b => b.status === 'COMPLETED')
|
|
.slice(0, 5)
|
|
.map((booking) => (
|
|
<div key={booking.id} className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
<div className="w-10 h-10 rounded-lg bg-gray-200 overflow-hidden flex-shrink-0">
|
|
{booking.rentalListing.images[0] ? (
|
|
<img src={booking.rentalListing.images[0].url} className="w-full h-full object-cover" alt="" />
|
|
) : (
|
|
<div className="w-full h-full bg-gray-300" />
|
|
)}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-medium text-gray-900 truncate">{booking.rentalListing.title}</p>
|
|
<p className="text-xs text-gray-500">
|
|
{new Date(booking.startDate).toLocaleDateString()} - {new Date(booking.endDate).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3 flex-shrink-0">
|
|
<span className="text-sm font-semibold text-gray-900">{formatCurrency(booking.totalAmount)}</span>
|
|
{booking.review && (
|
|
<span className="text-xs text-yellow-500">{booking.review.rating} ★</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|