import { useEffect, useState } from "react"; import { addDays, format, parseISO } from "date-fns"; import { Area, AreaChart, CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Spinner } from "@/components/ui/spinner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { apiGet } from "@/lib/api"; import { isLikelyApiHotelBooking, isLikelyApiHotelRoom, mapApiBookingToBooking, mapApiRoomToRoom, } from "@/lib/hotel-adapters"; import { formatDate, formatMoney } from "@/lib/format"; import type { BookingTrendDay, RatingSnapshot } from "@/lib/hotel-staff-types"; import { roomDisplayName } from "@/lib/room-utils"; import type { Booking, Room } from "@/lib/types"; import { useAuthStore } from "@/store/authStore"; const tooltipStyle = { backgroundColor: "var(--navy)", border: "none", borderRadius: "8px", color: "#fff", }; type VisitSeriesResponse = { series: Array<{ date: string; count: number }>; }; type ExtraRevenues = { roomService: string; laundry: string; gymSpa: string; total: string; }; type DashboardState = { recentBookings: Booking[]; visitsSeries: { date: string; views: number; sessions: number }[]; heatmap: { roomId: string; state: "vacant" | "not_ready" | "occupied" | "unavailable" }[]; bookingTrends: BookingTrendDay[]; rating: RatingSnapshot | null; extraRevenues: ExtraRevenues | null; pointsSummary: { issued: number; redeemed: number } | null; upcoming: { id: string; title: string; date: string }[]; }; function chartDayLabel(isoDate: string) { try { return format(parseISO(isoDate), "MMM d"); } catch { return isoDate; } } function ratingLabel(score: number): string { if (score >= 4.5) return "Impressive"; if (score >= 3.5) return "Solid"; if (score >= 2.5) return "Fair"; return "Growing"; } function mapRoomState(room: Room): "vacant" | "not_ready" | "occupied" | "unavailable" { switch (room.status) { case "occupied": return "occupied"; case "maintenance": return "not_ready"; case "out_of_order": return "unavailable"; default: return "vacant"; } } function shortRoomLabel(roomId: string, index: number) { const digits = roomId.match(/\d+/g)?.join("").slice(-2); if (digits) return digits.padStart(2, "0"); return String(index + 1).padStart(2, "0"); } export function DashboardPage() { const selectedPropertyId = useAuthStore((s) => s.selectedPropertyId); const [data, setData] = useState(null); const [err, setErr] = useState(null); useEffect(() => { let active = true; async function load() { setErr(null); setData(null); const to = new Date(); const from = addDays(to, -14); const from30 = addDays(to, -30); const qTrends = `from=${from.toISOString()}&to=${to.toISOString()}`; const qExtras = `from=${from30.toISOString()}&to=${to.toISOString()}`; const [ bookingsRes, visitsRes, roomsRes, trendsRes, ratingRes, extrasRes, pointsRes, ] = await Promise.allSettled([ apiGet<{ data: unknown[] }>("/bookings"), apiGet(`/analytics/visits?${qTrends}`), apiGet<{ data: unknown[] }>("/rooms"), apiGet(`/analytics/booking-trends?${qTrends}`), apiGet("/analytics/rating"), apiGet(`/analytics/extra-revenues?${qExtras}`), apiGet<{ issued: number; redeemed: number }>( `/analytics/points-summary?${qExtras}` ), ]); if (!active) return; const next: DashboardState = { recentBookings: [], visitsSeries: [], heatmap: [], bookingTrends: [], rating: null, extraRevenues: null, pointsSummary: null, upcoming: [], }; const failures: string[] = []; if (bookingsRes.status === "fulfilled") { const allBookings = bookingsRes.value.data .map((row) => isLikelyApiHotelBooking(row) ? mapApiBookingToBooking(row) : (row as Booking) ) .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)); next.recentBookings = allBookings.slice(0, 5); const soon = addDays(new Date(), 14); next.upcoming = allBookings .filter( (b) => b.status !== "cancelled" && Date.parse(b.checkIn) >= Date.now() && Date.parse(b.checkIn) <= soon.getTime() ) .slice(0, 6) .map((b) => ({ id: b.id, title: `${b.guest.firstName} ${b.guest.lastName} — check-in`, date: b.checkIn, })); } else { failures.push("bookings"); } if (visitsRes.status === "fulfilled") { next.visitsSeries = visitsRes.value.series.slice(-14).map((item) => ({ date: item.date, views: item.count || 0, sessions: 0, })); } else { failures.push("visits"); } if (roomsRes.status === "fulfilled") { next.heatmap = roomsRes.value.data .map((row) => (isLikelyApiHotelRoom(row) ? mapApiRoomToRoom(row) : (row as Room))) .map((room) => ({ roomId: room.name || room.id, state: mapRoomState(room), })); } else { failures.push("rooms"); } if (trendsRes.status === "fulfilled") { next.bookingTrends = trendsRes.value; } if (ratingRes.status === "fulfilled") { next.rating = ratingRes.value; } if (extrasRes.status === "fulfilled") { next.extraRevenues = extrasRes.value; } if (pointsRes.status === "fulfilled") { next.pointsSummary = pointsRes.value; } if (failures.length === 3) { setErr("Unable to load dashboard data."); return; } setData(next); } void load(); return () => { active = false; }; }, [selectedPropertyId]); if (err) return

{err}

; if (!data) { return (
); } const bookingChartData = data.bookingTrends.length > 0 ? data.bookingTrends.map((d) => ({ date: chartDayLabel(d.date), total: d.total, online: d.online, cancelled: d.cancelled, })) : [{ date: "—", total: 0, online: 0, cancelled: 0 }]; let extraBars: { label: string; value: number; pct: number }[] = []; if (data.extraRevenues) { const room = Number(data.extraRevenues.roomService) || 0; const laundry = Number(data.extraRevenues.laundry) || 0; const gym = Number(data.extraRevenues.gymSpa) || 0; const maxVal = Math.max(room, laundry, gym, 1); extraBars = [ { label: "Room service", value: room, pct: (room / maxVal) * 100 }, { label: "Laundry", value: laundry, pct: (laundry / maxVal) * 100 }, { label: "Spa / gym", value: gym, pct: (gym / maxVal) * 100 }, ]; } const ratingScore = data.rating?.averageRating ?? 0; const ratingDisplay = data.rating && data.rating.reviewCount > 0 ? ratingScore.toFixed(1) : "—"; const ratingSub = data.rating && data.rating.reviewCount > 0 ? ratingLabel(ratingScore) : "No reviews yet"; return (

Dashboard

Bookings, visits, and property snapshot

Booking trends
Site visits
Room status
{data.heatmap.map((h, index) => (
{shortRoomLabel(h.roomId, index)}
))}
Vacant Occupied Not ready
Guest rating

{ratingDisplay}

{ratingSub}

Hotel preview

Discount uses

{data.rating?.discountUses ?? "—"}

Referral bookings

{data.rating?.referralBookings ?? "—"}

{data.pointsSummary && (

Points (30d)

+{data.pointsSummary.issued} / −{data.pointsSummary.redeemed}

)}
Extra revenue {extraBars.length === 0 && (

No extra revenue in range yet.

)} {extraBars.map((r) => (
{r.label} {formatMoney(r.value, "ETB")} (30d)
))}
Upcoming {data.upcoming.length === 0 && (

No check-ins in the next two weeks.

)} {data.upcoming.map((e) => (

{e.title}

{formatDate(e.date)}

))}
Recent bookings Guest Stay Room type Status Total {data.recentBookings.map((b) => ( {b.guest.firstName} {b.guest.lastName} {formatDate(b.checkIn)} to {formatDate(b.checkOut)} {b.roomDisplayLabel ?? roomDisplayName(b.roomId)} {b.status} {formatMoney(b.pricing.total)} ))}
{data.recentBookings.map((b) => (

{b.guest.firstName} {b.guest.lastName}

{formatDate(b.checkIn)} - {b.status}

{formatMoney(b.pricing.total)}

))}
); }