"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { useSite } from "@/contexts/site-context"; import { Users, UserPlus, Clock, RefreshCw, MapPin, X, Search, } from "lucide-react"; // --- Types --- interface Player { id: string; firstName?: string; lastName?: string; walkInName?: string; checkedInAt: string; sessionId: string; } interface Court { id: string; name: string; type: "INDOOR" | "OUTDOOR"; isOpenPlay: boolean; status: "available" | "active" | "booked" | "open_play"; players: Player[]; upcomingBooking?: { startTime: string; clientName: string; }; } interface ClientResult { id: string; firstName: string; lastName: string; } // --- Helpers --- function getStatusConfig(court: Court) { if (court.status === "active") { return { dotColor: "bg-primary-500", text: `Active (${court.players.length} player${court.players.length !== 1 ? "s" : ""})`, bgColor: "bg-primary-50", borderColor: "border-primary-200", textColor: "text-primary-500", }; } if (court.status === "open_play" && court.players.length === 0) { return { dotColor: "bg-amber-500", text: "Open Play", bgColor: "bg-amber-50", borderColor: "border-amber-200", textColor: "text-amber-500", }; } if (court.status === "open_play" && court.players.length > 0) { return { dotColor: "bg-amber-500", text: `Open Play (${court.players.length} player${court.players.length !== 1 ? "s" : ""})`, bgColor: "bg-amber-50", borderColor: "border-amber-200", textColor: "text-amber-500", }; } if (court.status === "booked") { return { dotColor: "bg-purple-500", text: "Booked", bgColor: "bg-purple-50", borderColor: "border-purple-200", textColor: "text-purple-500", }; } return { dotColor: "bg-green-500", text: "Available", bgColor: "bg-green-50", borderColor: "border-green-200", textColor: "text-green-500", }; } function formatTime(dateStr: string) { return new Date(dateStr).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); } function playerName(player: Player) { if (player.walkInName) return player.walkInName; return [player.firstName, player.lastName].filter(Boolean).join(" ") || "Unknown"; } function playerInitials(player: Player) { const name = playerName(player); const parts = name.split(" "); if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); return name.slice(0, 2).toUpperCase(); } // --- Main Page --- export default function LiveCourtsPage() { const { selectedSiteId } = useSite(); const [courts, setCourts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const [checkInCourtId, setCheckInCourtId] = useState(null); const [endSessionCourtId, setEndSessionCourtId] = useState(null); const intervalRef = useRef(null); // --- Data fetching --- const fetchCourts = useCallback(async () => { try { setError(null); const url = selectedSiteId ? `/api/live?siteId=${selectedSiteId}` : "/api/live"; const response = await fetch(url); if (!response.ok) throw new Error("Failed to load court data"); const data = await response.json(); setCourts(data.courts ?? data.data ?? []); setLastUpdated(new Date()); } catch (err) { console.error("Live courts fetch error:", err); setError(err instanceof Error ? err.message : "Unknown error"); } finally { setIsLoading(false); } }, [selectedSiteId]); useEffect(() => { fetchCourts(); intervalRef.current = setInterval(fetchCourts, 30000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [fetchCourts]); // --- End session handler --- const handleEndSession = async (court: Court) => { try { await Promise.all( court.players.map((p) => fetch(`/api/court-sessions/${p.sessionId}`, { method: "PUT" }) ) ); setEndSessionCourtId(null); fetchCourts(); } catch (err) { console.error("End session error:", err); } }; // --- Render --- const checkInCourt = courts.find((c) => c.id === checkInCourtId) ?? null; const endSessionCourt = courts.find((c) => c.id === endSessionCourtId) ?? null; return (
{/* Header */}

Live Courts

{lastUpdated ? `Last updated: ${lastUpdated.toLocaleTimeString()}` : "Loading..."}

{/* Error */} {error && (
{error}
)} {/* Loading skeleton */} {isLoading && courts.length === 0 && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)} {/* Court grid */} {!isLoading && courts.length === 0 && !error && (

No courts found

Courts will appear here once configured.

)}
{courts.map((court) => { const cfg = getStatusConfig(court); const earliestCheckIn = court.players.length > 0 ? court.players.reduce((earliest, p) => new Date(p.checkedInAt) < new Date(earliest.checkedInAt) ? p : earliest ) : null; return (
{court.name}
{court.type} {court.isOpenPlay && ( Open Play )}
{/* Status indicator */}
{cfg.text}
{/* Upcoming booking info for booked courts */} {court.status === "booked" && court.upcomingBooking && (
{court.upcomingBooking.clientName} at{" "} {formatTime(court.upcomingBooking.startTime)}
)} {/* Player list */} {court.players.length > 0 && (
{court.players.map((player) => (
{playerInitials(player)}
{playerName(player)}
))}
)} {/* Time since first check-in */} {earliestCheckIn && (
Since {formatTime(earliestCheckIn.checkedInAt)}
)} {/* Actions */}
{(court.status === "available" || court.status === "active" || court.status === "open_play") && ( )} {(court.status === "active" || (court.status === "open_play" && court.players.length > 0)) && ( )} {court.isOpenPlay && ( )}
); })}
{/* Check In Modal */} {checkInCourt && ( setCheckInCourtId(null)} onSuccess={() => { setCheckInCourtId(null); fetchCourts(); }} /> )} {/* End Session Confirm Dialog */} {endSessionCourt && (

End Session

End session on {endSessionCourt.name}? This will check out all players.

)}
); } // --- Check In Modal Component --- function CheckInModal({ court, onClose, onSuccess, }: { court: Court; onClose: () => void; onSuccess: () => void; }) { const [mode, setMode] = useState<"search" | "walkin">("search"); const [searchQuery, setSearchQuery] = useState(""); const [walkInName, setWalkInName] = useState(""); const [clients, setClients] = useState([]); const [selectedClient, setSelectedClient] = useState(null); const [isSearching, setIsSearching] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const debounceRef = useRef(null); // Debounced client search useEffect(() => { if (searchQuery.length < 2) { setClients([]); return; } if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(async () => { setIsSearching(true); try { const res = await fetch( `/api/clients?search=${encodeURIComponent(searchQuery)}` ); if (res.ok) { const data = await res.json(); setClients(data.data ?? data ?? []); } } catch { console.error("Client search failed"); } finally { setIsSearching(false); } }, 300); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [searchQuery]); const handleSubmit = async () => { setIsSubmitting(true); try { const body: Record = { courtId: court.id }; if (mode === "search" && selectedClient) { body.clientId = selectedClient.id; } else if (mode === "walkin" && walkInName.trim()) { body.walkInName = walkInName.trim(); } else { return; } const res = await fetch("/api/court-sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) throw new Error("Check-in failed"); onSuccess(); } catch (err) { console.error("Check-in error:", err); } finally { setIsSubmitting(false); } }; const canSubmit = (mode === "search" && selectedClient !== null) || (mode === "walkin" && walkInName.trim().length > 0); return (
{/* Modal header */}

Check In — {court.name}

{/* Mode toggle */}
{/* Search mode */} {mode === "search" && (
{ setSearchQuery(e.target.value); setSelectedClient(null); }} autoFocus /> {isSearching && (

Searching...

)} {clients.length > 0 && (
{clients.map((client) => ( ))}
)} {searchQuery.length >= 2 && !isSearching && clients.length === 0 && (

No clients found.

)} {selectedClient && (
Selected: {selectedClient.firstName} {selectedClient.lastName}
)}
)} {/* Walk-in mode */} {mode === "walkin" && ( setWalkInName(e.target.value)} autoFocus /> )} {/* Submit */}
); }