import PlayzerIcon from '@/assets/playzer-favicon-512-transparent.png'; import { Button } from '@/components/ui/button'; import { DatePicker } from '@/components/ui/date-picker'; import { Input } from '@/components/ui/input'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { UserAvatar } from '@/components/user-avatar'; import { apiClient } from '@/lib/api-client'; import { useAuth } from '@/lib/auth'; import { useCurrentComplexStore } from '@/lib/stores/current-complex-store'; import { useTheme } from '@/lib/theme'; import { cn } from '@/lib/utils'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { CalendarDays, Check, ChevronLeft, ChevronRight, CircleDot, Download, Dumbbell, Grid2X2, Laptop, ListChecks, LogOut, Moon, MoreHorizontal, Plus, Search, Settings, Sun, User, Users, } from 'lucide-react'; import type React from 'react'; import { useMemo, useState } from 'react'; import { useBooking } from '../booking-provider'; import type { BookingCourtSchedule, BookingTimelineSegment } from '../booking.types'; import { fromIsoDateLocal, toIsoDateLocal } from '../lib/booking-time'; type MobileTab = 'panel' | 'status' | 'bookings' | 'more'; export function BookingMobile() { const { schedules, isLoading, isError, errorMessage } = useBooking(); const [selectedCourtId, setSelectedCourtId] = useState(null); const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState('panel'); const selectedSchedule = useMemo(() => { if (!selectedCourtId) return null; return schedules.find((schedule) => schedule.court.id === selectedCourtId) ?? null; }, [schedules, selectedCourtId]); const filteredSchedules = useMemo(() => { const query = search.trim().toLowerCase(); if (!query) return schedules; return schedules.filter((schedule) => { return ( schedule.court.name.toLowerCase().includes(query) || schedule.court.sport.name.toLowerCase().includes(query) ); }); }, [schedules, search]); if (selectedSchedule) { return setSelectedCourtId(null)} />; } return (
{activeTab === 'panel' && ( <>

Canchas

setSearch(event.target.value)} />
{isLoading && (
Cargando reservas...
)} {isError && !isLoading && (
{errorMessage}
)} {!isLoading && !isError && filteredSchedules.length === 0 && (
No hay canchas para los filtros seleccionados.
)} {!isLoading && !isError && filteredSchedules.map((schedule) => ( setSelectedCourtId(schedule.court.id)} /> ))}
)} {activeTab === 'status' && } {activeTab === 'bookings' && } {activeTab === 'more' && }
); } function MobileTopBar() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { user, isAuthenticated, displayName, avatarUrl, signOut } = useAuth(); const { currentComplexSlug, setCurrentComplex } = useCurrentComplexStore(); const { theme, setTheme } = useTheme(); const { openCreateBooking } = useBooking(); const [open, setOpen] = useState(false); const myComplexesQuery = useQuery({ queryKey: ['my-complexes'], enabled: isAuthenticated, queryFn: () => apiClient.complexes.listMine(), }); const complexSlug = currentComplexSlug || myComplexesQuery.data?.[0]?.complexSlug; const currentComplexName = useMemo(() => { const complexes = myComplexesQuery.data ?? []; if (complexes.length === 0) return 'Mi complejo'; const selected = complexes.find((complex) => complex.complexSlug === complexSlug); return selected?.complexName ?? complexes[0]?.complexName ?? 'Mi complejo'; }, [complexSlug, myComplexesQuery.data]); const handleSelectComplex = async (complexId: string, nextSlug: string) => { await apiClient.complexes.select({ complexId }); setCurrentComplex(nextSlug); await queryClient.invalidateQueries({ queryKey: ['current-complex'] }); await queryClient.invalidateQueries({ queryKey: ['admin-bookings'] }); }; const handleSignOut = async () => { setOpen(false); await signOut(); await navigate({ to: '/login' }); }; return (
Playzer Playzer

{displayName}

{user?.email}

{currentComplexName}

Cuenta

); } function MobilePanelHeader() { const { complex, openCreateBooking } = useBooking(); return (

Panel de Reservas

Gestiona las canchas de {complex.complexName}

); } function MobileFilters() { const { selectedDate, setSelectedDate, moveSelectedDate } = useBooking(); return (
setSelectedDate(date ? toIsoDateLocal(date) : toIsoDateLocal(new Date())) } className="h-12 rounded-lg border-border/70 bg-card/75 text-sm" />
); } function MobileSummary() { const { summary } = useBooking(); return (

Resumen del día

); } function MobileStatusTab() { const { courts, selectedSportId, selectedStatus, visibleTimeRange, setSelectedSportId, setSelectedStatus, setVisibleTimeRange, } = useBooking(); const sports = [...new Map(courts.map((court) => [court.sport.id, court.sport])).values()]; const activeRangeValue = `${visibleTimeRange.start}-${visibleTimeRange.end}`; const statusOptions = [ { value: 'all', label: 'Todos' }, { value: 'free', label: 'Libres' }, { value: 'reserved', label: 'Reservados' }, ] as const; const timeRangeOptions = [ { label: '08:00 - 22:00', start: '08:00', end: '22:00' }, { label: '06:00 - 00:00', start: '06:00', end: '23:59' }, { label: '08:00 - 14:00', start: '08:00', end: '14:00' }, { label: '14:00 - 22:00', start: '14:00', end: '22:00' }, ]; return ( <>

Estado

Filtra disponibilidad por cancha.

Estado

{statusOptions.map((option) => ( setSelectedStatus(option.value)} /> ))}

Deporte

setSelectedSportId('all')} /> {sports.map((sport) => ( setSelectedSportId(sport.id)} /> ))}

Horario visible

{timeRangeOptions.map((option) => ( setVisibleTimeRange({ start: option.start, end: option.end })} /> ))}
); } function MobileFilterChip({ active, label, onClick, }: { active: boolean; label: string; onClick: () => void; }) { return ( ); } function MobileBookingsTab() { const { selectedDate, schedules, openBookingTools, openCreateBooking } = useBooking(); const reservedSegments = schedules .flatMap((schedule) => schedule.segments .filter((segment) => segment.booking) .map((segment) => ({ schedule, segment })) ) .sort((a, b) => a.segment.startMinutes - b.segment.startMinutes); return ( <>

Reservas

{formatMobileDate(selectedDate)}

{reservedSegments.length === 0 ? (
No hay reservas para este día.
) : ( reservedSegments.map(({ schedule, segment }) => ( )) )}
); } function MobileMoreTab() { const navigate = useNavigate(); const { currentComplexSlug } = useCurrentComplexStore(); const { openCreateBooking, exportDayReport } = useBooking(); return ( <>

Más

Accesos rápidos del panel.

} label="Nueva reserva" onClick={() => openCreateBooking()} /> } label="Exportar reservas del día" onClick={exportDayReport} /> } label="Mi perfil" onClick={() => { void navigate({ to: '/profile' }); }} /> {currentComplexSlug && ( } label="Configuración del complejo" onClick={() => { void navigate({ to: '/complex/$slug/edit', params: { slug: currentComplexSlug }, }); }} /> )}
); } function MobileMenuAction({ icon, label, onClick, }: { icon: React.ReactNode; label: string; onClick: () => void; }) { return ( ); } function formatMobileDate(date: string) { return new Intl.DateTimeFormat('es-AR', { day: 'numeric', month: 'long', year: 'numeric', }).format(new Date(`${date}T00:00:00`)); } function SummaryPill({ value, label, className, }: { value: number; label: string; className: string; }) { return (

{value}

{label}

); } function MobileCourtCard({ schedule, onOpen, }: { schedule: BookingCourtSchedule; onOpen: () => void; }) { const nextSegments = schedule.segments.slice(0, 5); return (

{schedule.court.name}

{schedule.court.sport.name}

Próximos horarios

{nextSegments.map((segment) => ( ))}
); } function MobileCourtDay({ schedule, onBack, }: { schedule: BookingCourtSchedule; onBack: () => void }) { const { selectedDate, moveSelectedDate, setSelectedDate, openCreateBooking } = useBooking(); return (

{schedule.court.name}

{schedule.court.sport.name}

setSelectedDate(date ? toIsoDateLocal(date) : toIsoDateLocal(new Date())) } className="h-12 rounded-lg border-border/70 bg-card/75 text-sm" />
{schedule.segments.map((segment) => ( ))}
); } function MobileSegmentRow({ schedule, segment, }: { schedule: BookingCourtSchedule; segment: BookingTimelineSegment; }) { const { openCreateBooking, openBookingTools } = useBooking(); const isFree = segment.status === 'free'; return ( ); } function SegmentStatusLine({ segment }: { segment: BookingTimelineSegment }) { const status = segment.status; const label = status === 'free' ? 'Libre' : 'Reservado'; return (

{label}

); } function StatusLegend() { return (
); } function LegendItem({ label, className }: { label: string; className: string }) { return ( {label} ); } function MiniSlot({ schedule, segment, }: { schedule: BookingCourtSchedule; segment: BookingTimelineSegment; }) { const { openCreateBooking, openBookingTools } = useBooking(); const label = segment.status === 'free' ? 'Libre' : segment.status === 'reserved' ? 'Reservado' : 'Manten.'; const isActionable = segment.status === 'free' || Boolean(segment.booking); return ( ); } function SportIcon({ sportName }: { sportName: string }) { const isPadel = sportName.toLowerCase().includes('padel'); return (
{isPadel ? : }
); } function MetricDots({ schedule }: { schedule: BookingCourtSchedule }) { return (
{schedule.metrics.free} {schedule.metrics.reserved}
); } function MobileBottomNav({ active, onSelect, }: { active: MobileTab; onSelect: (tab: MobileTab) => void; }) { return ( ); } function BottomNavItem({ active, icon, label, onClick, }: { active: boolean; icon: React.ReactNode; label: string; onClick: () => void }) { return ( ); }