You are iteration 4 of a 10-step Ballbox accessibility loop.

Read these files first:
- /home/sebas/runtime/ralph-loops/ballbox-a11y-20260517-120638/TASKS.md
- /home/sebas/runtime/ralph-loops/ballbox-a11y-20260517-120638/STATE.md

Then do exactly this:
1. Pick the highest-value pending task from TASKS.md.
2. Mark it in_progress in TASKS.md and briefly note it in STATE.md.
3. Implement the work in /home/sebas/work/projects/ballbox.
4. Run appropriate validation commands for your change.
5. Update TASKS.md with done/pending state changes.
6. Update STATE.md with a concise result, validation outcome, and next suggested task.
7. Do not use chat-style handoff; communicate only by editing those files.
8. Do not push, merge, or ask the user anything.
9. If blocked, write the blocker clearly in STATE.md and TASKS.md, then stop.

Be decisive. Prefer semantic HTML-first accessibility improvements.


bash

cd /home/sebas/work/projects/ballbox && git status --short --branch


# Ballbox accessibility tasks

Status legend: pending | in_progress | done

1. done - coach-settings-page-client: package format and active state controls to semantic grouped controls.
2. done - coach-settings-page-client: students table contact save/error/status semantics and grouping.
3. done - coach-settings-page-client: availability section audit for html-first labels/help/status.
4. pending - club-coaches-page-client: calendar grid action text/status cleanup and explicit helper copy.
5. pending - payments-admin-client: remaining CRUD session area form semantics audit.
6. pending - authenticated/public nav final keyboard/a11y polish audit.
7. pending - cross-flow pass: improve error/status live messaging where still weak.
8. pending - add or adjust tests for changed semantics where useful, especially keyboard interaction for weekly availability grid.
9. done - validation run: `pnpm exec eslint components/coach-settings-page-client.tsx components/weekly-availability-grid.tsx` and `pnpm typecheck` passed.
10. pending - write final summary and remaining backlog.


# Ralph loop state

Status: running
Iteration: 4/10
Rule: each agent must read TASKS.md and STATE.md first, then update both before exit.
Rule: communicate only through files in this directory.
Rule: work only in /home/sebas/work/projects/ballbox.
Rule: do not push or merge.

Current owner: agent-4
Current focus: iteration 4
Last result: task 3 done in `components/coach-settings-page-client.tsx` and `components/weekly-availability-grid.tsx`; availability now exposes a live autosave status, semantic grid fieldset/help/error wiring, and keyboard-toggleable half-hour buttons with explicit day/time aria labels. Validation: `pnpm exec eslint components/coach-settings-page-client.tsx components/weekly-availability-grid.tsx` and `pnpm typecheck` passed. Next suggested task: task 4, club coaches calendar grid action text/status cleanup and explicit helper copy.


## main...origin/main
 M components/coach-settings-page-client.tsx
 M components/weekly-availability-grid.tsx


**Inspecting relevant files**

I need to check the relevant files, particularly the tasks that mention the coaches page for the client. I’m thinking about how to locate the specific information I need. It's important to ensure I have everything organized so I can accurately address whatever requests or requirements come up. I wonder if there’s a specific format or layout I should be looking for while I inspect these files. Let’s make sure I cover all bases!

cd /home/sebas/work/projects/ballbox && rg -n "club-coaches-page-client|calendar grid|weekly availability|coach calendar|helper copy" components app docs -g '!node_modules'

cd /home/sebas/work/projects/ballbox && ls components | sed -n '1,200p'


docs/coach-availability-rfc.md:7:- Coaches define weekly availability windows (day-of-week + time ranges).
docs/coach-availability-rfc.md:196:- Current UI opens on a dashboard with per-club metrics plus a strip of days with booked classes, then shows affiliation, assigned sede when present, editable weekly availability inside allowed venue/club hours, reusable sales packages, and a Mercado Pago placeholder field inside `Mi Perfil`. A third `Alumnos` section now handles the student roster, contacts, and manually occupied slots.
docs/coach-availability-rfc.md:267:  - Expand weekly availability to concrete 90m slots in the visible range.
app/clubs/coaches/page.tsx:3:import { ClubCoachesPageClient } from "@/components/club-coaches-page-client";


admin-product-page.tsx
admin-shell-frame.tsx
admin-shell.tsx
advertising.tsx
authenticated-workspace-navbar.tsx
benefits.tsx
classes-by-date-page-client.tsx
classes-offers-page-client.tsx
classes-request-form.tsx
class-offer-detail-client.tsx
club-coaches-page-client.tsx
club-services-home-client.tsx
club-venue-page-client.tsx
coach-access-page-client.tsx
coach-settings-page-client.tsx
cta.tsx
faq.tsx
footer.tsx
hero.tsx
how-it-works.tsx
navbar.tsx
network-admin-client-config.ts
network-admin-client.tsx
network-admin-club-card.tsx
network-admin-club-list.tsx
network-admin-create-club-form.tsx
network-admin-create-club-section.tsx
network-admin-create-machine-form.tsx
network-admin-create-venue-form.tsx
network-admin-mutation-helpers.ts
network-admin-section-header.tsx
network-admin-shell-styles.ts
network-admin-shell.tsx
network-admin-stats.tsx
network-admin-venue-card.tsx
network-admin-venue-header.tsx
offline-shell-register.tsx
payments-admin-client.tsx
players-admin-client-config.ts
players-admin-client.tsx
players-admin-create-player-section.tsx
players-admin-player-card.tsx
players-admin-player-list.tsx
players-admin-shell-styles.ts
players-admin-shell.tsx
players-admin-stats.tsx
product-overview-card.tsx
route-feedback.tsx
screen-player.tsx
screens-runtime-grid.tsx
sports-admin-client-config.ts
sports-admin-client.tsx
sports-admin-coaches-section.tsx
sports-admin-events-section.tsx
sports-admin-shell.tsx
stats.tsx
theme-provider.tsx
tvs-admin-client.tsx
ui
use-network-admin-client-forms.ts
use-network-admin-client-state.ts
use-players-admin-client-state.ts
use-sports-admin-client-state.ts
value-proposition.tsx
weekly-availability-grid.tsx


"use client";

import Link from "next/link";
import { useEffect, useEffectEvent, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import type { z } from "zod";
import { AuthenticatedWorkspaceNavbar } from "@/components/authenticated-workspace-navbar";
import { Button } from "@/components/ui/button";
import { clubCoachesResponseSchema, clubInviteMutationResponseSchema, clubVenueMutationResponseSchema } from "@/lib/contracts/club-accounts";
import { coachStudentContactMutationResponseSchema } from "@/lib/contracts/coach-students";

type ClubCoachesResponse = z.infer<typeof clubCoachesResponseSchema>;
type ClubProfile = ClubCoachesResponse["club"];
type ClubDashboard = ClubCoachesResponse["dashboard"];
type ClubVenue = ClubCoachesResponse["configuration"]["venues"][number];
type ClubCoach = ClubCoachesResponse["coaches"][number];
type ClubInvite = ClubCoachesResponse["invites"][number];
type ClubManualClassOptions = ClubDashboard["manualClassOptions"];
type ClubPageView = "calendar" | "analytics" | "venues" | "coaches";
type ClubCalendarMode = "availability" | "reservation";
type ManualClassDraft = {
  venueId: string;
  courtId: string;
  coachId: string;
  packageId: string;
  playerId: string;
  playerWhatsappPhone: string;
  date: string;
  time: string;
  durationMinutes: number;
  repeatWeeklyCount: number;
  courtPriceMinor: number;
  coachPriceMinor: number;
  note: string;
};

type ClubStudentCreateDraft = {
  name: string;
  whatsappPhone: string;
  telegramHandle: string;
  gender: "" | "female" | "male" | "other";
  birthYear: string;
  coachCategory: string;
};

const calendarCourtThemes = [
  {
    accent: "bg-[#c4d600]",
    pill: "border-[#c4d600]/35 bg-[#c4d600]/12 text-[#f4ff95] hover:border-[#d7f53b]/60 hover:bg-[#c4d600]/18",
    pillActive: "border-[#d7f53b]/70 bg-[#c4d600]/22 text-white shadow-[0_0_0_1px_rgba(196,214,0,0.22)]",
    cell: "border-[#c4d600]/30 bg-[#c4d600]/12 hover:border-[#d7f53b]/60",
    badge: "border-[#c4d600]/30 bg-[#c4d600]/12 text-[#f4ff95]",
  },
  {
    accent: "bg-sky-400",
    pill: "border-sky-400/30 bg-sky-400/12 text-sky-100 hover:border-sky-300/55 hover:bg-sky-400/18",
    pillActive: "border-sky-300/70 bg-sky-400/20 text-white shadow-[0_0_0_1px_rgba(56,189,248,0.18)]",
    cell: "border-sky-400/30 bg-sky-400/12 hover:border-sky-300/55",
    badge: "border-sky-400/30 bg-sky-400/12 text-sky-100",
  },
  {
    accent: "bg-fuchsia-400",
    pill: "border-fuchsia-400/30 bg-fuchsia-400/12 text-fuchsia-100 hover:border-fuchsia-300/55 hover:bg-fuchsia-400/18",
    pillActive: "border-fuchsia-300/70 bg-fuchsia-400/20 text-white shadow-[0_0_0_1px_rgba(232,121,249,0.18)]",
    cell: "border-fuchsia-400/30 bg-fuchsia-400/12 hover:border-fuchsia-300/55",
    badge: "border-fuchsia-400/30 bg-fuchsia-400/12 text-fuchsia-100",
  },
  {
    accent: "bg-amber-300",
    pill: "border-amber-300/30 bg-amber-300/12 text-amber-100 hover:border-amber-200/55 hover:bg-amber-300/18",
    pillActive: "border-amber-200/70 bg-amber-300/20 text-white shadow-[0_0_0_1px_rgba(252,211,77,0.18)]",
    cell: "border-amber-300/30 bg-amber-300/12 hover:border-amber-200/55",
    badge: "border-amber-300/30 bg-amber-300/12 text-amber-100",
  },
  {
    accent: "bg-emerald-400",
    pill: "border-emerald-400/30 bg-emerald-400/12 text-emerald-100 hover:border-emerald-300/55 hover:bg-emerald-400/18",
    pillActive: "border-emerald-300/70 bg-emerald-400/20 text-white shadow-[0_0_0_1px_rgba(52,211,153,0.18)]",
    cell: "border-emerald-400/30 bg-emerald-400/12 hover:border-emerald-300/55",
    badge: "border-emerald-400/30 bg-emerald-400/12 text-emerald-100",
  },
  {
    accent: "bg-rose-400",
    pill: "border-rose-400/30 bg-rose-400/12 text-rose-100 hover:border-rose-300/55 hover:bg-rose-400/18",
    pillActive: "border-rose-300/70 bg-rose-400/20 text-white shadow-[0_0_0_1px_rgba(251,113,133,0.18)]",
    cell: "border-rose-400/30 bg-rose-400/12 hover:border-rose-300/55",
    badge: "border-rose-400/30 bg-rose-400/12 text-rose-100",
  },
] as const;

const inviteDateFormatter = new Intl.DateTimeFormat("es-UY", {
  day: "2-digit",
  month: "short",
  hour: "2-digit",
  minute: "2-digit",
});

function formatInviteDate(value: string) {
  return inviteDateFormatter.format(new Date(value));
}

function formatRequestDate(value: string | null) {
  if (!value) return "Pendiente";
  return inviteDateFormatter.format(new Date(value));
}

function formatAverageReview(value: number | null) {
  return value === null ? "Sin resenas" : `${value.toFixed(1)} / 5`;
}

function formatOpeningWindow(openingTime: string, closingTime: string) {
  return `${openingTime} a ${closingTime}`;
}

function formatMoney(value: number) {
  return `$ ${value.toLocaleString("es-UY")}`;
}

function getDateDayOfWeek(value: string) {
  return new Date(`${value}T12:00:00`).getDay();
}

function getVenueSlotPrice(
  venue: Pick<ClubVenue, "coachReservationPrice" | "coachPriceOverrides"> | null,
  dayOfWeek: number,
  startTime: string
) {
  if (!venue) return 0;

  const slotStart = parseTimeToMinutes(startTime);
  if (slotStart === null) return venue.coachReservationPrice;

  const override = venue.coachPriceOverrides.find((item) => {
    if (item.dayOfWeek !== dayOfWeek) return false;
    const overrideStart = parseTimeToMinutes(item.startTime);
    const overrideEnd = parseTimeToMinutes(item.endTime);
    if (overrideStart === null || overrideEnd === null) return false;
    return slotStart >= overrideStart && slotStart < overrideEnd;
  });

  return override?.price ?? venue.coachReservationPrice;
}

type ManualCoachOption = ClubManualClassOptions["coaches"][number];
type ManualCoachPackage = ManualCoachOption["packages"][number];

function getActiveCoachPackages(coach: ManualCoachOption | null | undefined) {
  return coach?.packages ?? [];
}

function applyCoachPackageToDraft(draft: ManualClassDraft, salesPackage: ManualCoachPackage | null): ManualClassDraft {
  return {
    ...draft,
    packageId: salesPackage?.id ?? "",
    durationMinutes: salesPackage?.durationMinutes ?? draft.durationMinutes,
    coachPriceMinor: salesPackage?.price ?? 0,
  };
}

function formatWeekDate(value: string) {
  return new Intl.DateTimeFormat("es-UY", {
    weekday: "short",
    day: "2-digit",
    month: "short",
  }).format(new Date(`${value}T12:00:00`));
}

function formatWeekRange(start: string, end: string) {
  return `${formatWeekDate(start)} - ${formatWeekDate(end)}`;
}

function formatCalendarDayLabel(value: string) {
  return new Intl.DateTimeFormat("es-UY", {
    weekday: "long",
    day: "numeric",
    month: "short",
  }).format(new Date(`${value}T12:00:00`));
}

function getCurrentWeekDays(weekStart: string) {
  const start = new Date(`${weekStart}T12:00:00`);

  return Array.from({ length: 7 }, (_, index) => {
    const date = new Date(start);
    date.setDate(start.getDate() + index);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
  });
}

function parseTimeToMinutes(value: string) {
  const [hour, minute] = value.split(":").map((part) => Number(part));
  if (!Number.isInteger(hour) || !Number.isInteger(minute)) return null;
  return hour * 60 + minute;
}

function formatSlotLabel(value: string) {
  return value.slice(0, 5);
}

function listHalfHourSlots(startTime: string, endTime: string) {
  const startMinutes = parseTimeToMinutes(startTime);
  const endMinutes = parseTimeToMinutes(endTime);
  if (startMinutes === null || endMinutes === null || endMinutes <= startMinutes) return [];

  const slots: string[] = [];
  for (let minutes = startMinutes; minutes < endMinutes; minutes += 30) {
    const hour = String(Math.floor(minutes / 60)).padStart(2, "0");
    const minute = String(minutes % 60).padStart(2, "0");
    slots.push(`${hour}:${minute}`);
  }

  return slots;
}

function buildDaySlots(ranges: Array<{ dayOfWeek: number; startTime: string; endTime: string }>, dayOfWeek: number) {
  const slotSet = new Set<string>();

  for (const range of ranges) {
    if (range.dayOfWeek !== dayOfWeek) continue;
    for (const slot of listHalfHourSlots(range.startTime, range.endTime)) {
      slotSet.add(slot);
    }
  }

  return Array.from(slotSet).sort((left, right) => (parseTimeToMinutes(left) ?? 0) - (parseTimeToMinutes(right) ?? 0));
}

function blockOverlapsSlot(block: { startTime: string; endTime: string }, slot: string) {
  const blockStart = parseTimeToMinutes(block.startTime);
  const blockEnd = parseTimeToMinutes(block.endTime);
  const slotStart = parseTimeToMinutes(slot);
  if (blockStart === null || blockEnd === null || slotStart === null) return false;
  return slotStart >= blockStart && slotStart < blockEnd;
}

function todayDateValue() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function createManualClassDraft(options: ClubManualClassOptions, venues: ClubVenue[]): ManualClassDraft {
  const firstCoach = options.coaches.find((coach) => coach.venueId) ?? options.coaches[0] ?? null;
  const firstCoachWithPackage =
    options.coaches.find((coach) => coach.venueId && getActiveCoachPackages(coach).length > 0) ??
    firstCoach;
  const firstVenue =
    venues.find((venue) => venue.id === firstCoachWithPackage?.venueId) ??
    venues[0] ??
    null;
  const firstCourt = firstVenue?.courts.find((court) => court.coachesEnabled) ?? firstVenue?.courts[0] ?? null;
  const firstPlayer = options.players[0] ?? null;
  const firstPackage = getActiveCoachPackages(firstCoachWithPackage)[0] ?? null;

  return {
    venueId: firstVenue?.id ?? "",
    courtId: firstCourt?.id ?? "",
    coachId: firstCoachWithPackage?.id ?? "",
    packageId: firstPackage?.id ?? "",
    playerId: firstPlayer?.id ?? "",
    playerWhatsappPhone: firstPlayer?.whatsappPhone ?? "",
    date: todayDateValue(),
    time: "09:00",
    durationMinutes: firstPackage?.durationMinutes ?? 60,
    repeatWeeklyCount: 1,
    courtPriceMinor: getVenueSlotPrice(firstVenue, getDateDayOfWeek(todayDateValue()), "09:00"),
    coachPriceMinor: firstPackage?.price ?? 0,
    note: "",
  };
}

function createClubStudentDraft(): ClubStudentCreateDraft {
  return {
    name: "",
    whatsappPhone: "",
    telegramHandle: "",
    gender: "",
    birthYear: "",
    coachCategory: "",
  };
}

export function ClubCoachesPageClient() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [view, setView] = useState<ClubPageView>(() => {
    const nextView = searchParams.get("view");
    if (nextView === "analytics" || nextView === "venues" || nextView === "coaches") return nextView;
    return "calendar";
  });
  const [club, setClub] = useState<ClubProfile | null>(null);
  const [dashboard, setDashboard] = useState<ClubDashboard | null>(null);
  const [venues, setVenues] = useState<ClubVenue[]>([]);
  const [coaches, setCoaches] = useState<ClubCoach[]>([]);
  const [invites, setInvites] = useState<ClubInvite[]>([]);
  const [newVenueName, setNewVenueName] = useState("");
  const [coveredCourtCount, setCoveredCourtCount] = useState(1);
  const [uncoveredCourtCount, setUncoveredCourtCount] = useState(0);
  const [loading, setLoading] = useState(true);
  const [inviteSaving, setInviteSaving] = useState(false);
  const [creatingVenue, setCreatingVenue] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [authRequired, setAuthRequired] = useState(false);
  const [copiedInviteId, setCopiedInviteId] = useState<string | null>(null);
  const [coachVenueFilter, setCoachVenueFilter] = useState("all");
  const [calendarVenueId, setCalendarVenueId] = useState("");
  const [calendarDate, setCalendarDate] = useState("");
  const [calendarCoachFilter, setCalendarCoachFilter] = useState("all");
  const [calendarMode, setCalendarMode] = useState<ClubCalendarMode>("availability");
  const [showManualClassForm, setShowManualClassForm] = useState(false);
  const [showRequestsPanel, setShowRequestsPanel] = useState(false);
  const [manualClassDraft, setManualClassDraft] = useState<ManualClassDraft>({
    venueId: "",
    courtId: "",
    coachId: "",
    packageId: "",
    playerId: "",
    playerWhatsappPhone: "",
    date: todayDateValue(),
    time: "09:00",
    durationMinutes: 60,
    repeatWeeklyCount: 1,
    courtPriceMinor: 0,
    coachPriceMinor: 0,
    note: "",
  });
  const [studentCreateDraft, setStudentCreateDraft] = useState<ClubStudentCreateDraft>(() => createClubStudentDraft());
  const [savingManualClass, setSavingManualClass] = useState(false);
  const [manualClassError, setManualClassError] = useState<string | null>(null);
  const [creatingStudent, setCreatingStudent] = useState(false);
  const [studentCreateError, setStudentCreateError] = useState<string | null>(null);

  useEffect(() => {
    const nextView = searchParams.get("view");
    if (nextView === "analytics" || nextView === "venues" || nextView === "coaches") {
      setView(nextView);
      return;
    }

    setView("calendar");
  }, [searchParams]);

  function handleViewChange(nextView: ClubPageView) {
    setView(nextView);

    const params = new URLSearchParams(searchParams.toString());
    if (nextView === "calendar") {
      params.delete("view");
    } else {
      params.set("view", nextView);
    }

    const nextQuery = params.toString();
    router.replace(nextQuery ? `/clubs/coaches?${nextQuery}` : "/clubs/coaches", { scroll: false });
  }

  async function loadClubPanel() {
    try {
      const response = await fetch("/api/clubs/coaches", { cache: "no-store" });
      const payload = await response.json().catch(() => null);

      if (!response.ok) {
        setClub(null);
        setDashboard(null);
        setVenues([]);
        setCoaches([]);
        setInvites([]);
        setAuthRequired(response.status === 401);
        setError(response.status === 401 ? null : payload?.error === "INVALID_PAYLOAD" ? "El panel del club devolvio datos invalidos." : "No pudimos cargar el panel del club.");
        return;
      }

      setAuthRequired(false);

      const parsed = clubCoachesResponseSchema.parse(payload);
      setClub(parsed.club);
      setDashboard(parsed.dashboard);
      setVenues(parsed.configuration.venues);
      setCalendarVenueId((current) => current || parsed.dashboard.calendar.venues[0]?.id || parsed.configuration.venues[0]?.id || "");
      setShowManualClassForm(false);
      setShowRequestsPanel(false);
      setManualClassDraft(createManualClassDraft(parsed.dashboard.manualClassOptions, parsed.configuration.venues));
      setCoaches(parsed.coaches);
      setInvites(parsed.invites);
      setError(null);
    } catch {
      setClub(null);
      setDashboard(null);
      setVenues([]);
      setShowManualClassForm(false);
      setShowRequestsPanel(false);
      setManualClassDraft({
        venueId: "",
        courtId: "",
        coachId: "",
        packageId: "",
        playerId: "",
        playerWhatsappPhone: "",
        date: todayDateValue(),
        time: "09:00",
        durationMinutes: 60,
        repeatWeeklyCount: 1,
        courtPriceMinor: 0,
        coachPriceMinor: 0,
        note: "",
      });
      setCoaches([]);
      setInvites([]);
      setAuthRequired(false);
      setError("No pudimos cargar el panel del club.");
    } finally {
      setLoading(false);
    }
  }

  const loadClubPanelEffect = useEffectEvent(loadClubPanel);

  useEffect(() => {
    void loadClubPanelEffect();
  }, []);

  useEffect(
    () => () => {
    },
    []
  );

  useEffect(() => {
    if (!dashboard) return;

    const days = getCurrentWeekDays(dashboard.calendar.weekStart);
    setCalendarDate((current) => (days.includes(current) ? current : days[0] ?? ""));
  }, [dashboard]);

  async function handleCreateInvite() {
    setInviteSaving(true);
    setError(null);

    try {
      const response = await fetch("/api/clubs/invites", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({}),
      });

      const payload = await response.json().catch(() => null);
      if (!response.ok) {
        setError(payload?.details?.message ?? "No pudimos generar el link.");
        return;
      }

      const parsed = clubInviteMutationResponseSchema.parse(payload);
      setInvites((current) => [parsed.item, ...current.filter((invite) => invite.id !== parsed.item.id)]);
      setCopiedInviteId(null);
    } catch {
      setError("No pudimos generar el link.");
    } finally {
      setInviteSaving(false);
    }
  }

  async function handleCreateVenue() {
    setCreatingVenue(true);
    setError(null);

    try {
      const response = await fetch("/api/clubs/venues", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: newVenueName,
          coveredCourtCount,
          uncoveredCourtCount,
        }),
      });

      const payload = await response.json().catch(() => null);
      if (!response.ok) {
        setError(payload?.details?.message ?? "No pudimos crear la sede.");
        return;
      }

      const parsed = clubVenueMutationResponseSchema.parse(payload);
      window.location.href = `/clubs/venues/${parsed.item.id}`;
    } catch {
      setError("No pudimos crear la sede.");
    } finally {
      setCreatingVenue(false);
    }
  }

  async function copyInvite(invite: ClubInvite) {
    try {
      await navigator.clipboard.writeText(invite.inviteUrl);
      setCopiedInviteId(invite.id);
      window.setTimeout(() => {
        setCopiedInviteId((current) => (current === invite.id ? null : current));
      }, 1500);
    } catch {
      setError("No pudimos copiar el link.");
    }
  }

  async function handleCreateManualClass() {
    if (!dashboard) return;

    setSavingManualClass(true);
    setManualClassError(null);

    try {
      const startsAt = new Date(`${manualClassDraft.date}T${manualClassDraft.time}:00`);
      const response = await fetch("/api/clubs/manual-classes", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          venueId: manualClassDraft.venueId,
          courtId: manualClassDraft.courtId || null,
          coachId: manualClassDraft.coachId,
          packageId: manualClassDraft.packageId,
          playerId: manualClassDraft.playerId,
          playerWhatsappPhone: manualClassDraft.playerWhatsappPhone || null,
          startsAt: startsAt.toISOString(),
          durationMinutes: manualClassDraft.durationMinutes,
          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? "America/Montevideo",
          courtPriceMinor: manualClassDraft.courtPriceMinor,
          coachPriceMinor: manualClassDraft.coachPriceMinor,
          recurrence:
            manualClassDraft.repeatWeeklyCount > 1
              ? { frequency: "weekly", occurrences: manualClassDraft.repeatWeeklyCount }
              : undefined,
          note: manualClassDraft.note || null,
        }),
      });

      const payload = await response.json().catch(() => null);
      if (!response.ok) {
        setManualClassError(payload?.details?.message ?? "No pudimos cargar la clase manual.");
        return;
      }

      await loadClubPanel();
      setShowManualClassForm(false);
    } catch {
      setManualClassError("No pudimos cargar la clase manual.");
    } finally {
      setSavingManualClass(false);
    }
  }

  async function handleCreateStudent() {
    if (!studentCreateDraft.name.trim()) {
      setStudentCreateError("Ponle al menos un nombre al alumno.");
      return;
    }

    setCreatingStudent(true);
    setStudentCreateError(null);

    try {
      const response = await fetch("/api/clubs/players", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: studentCreateDraft.name.trim(),
          whatsappPhone: studentCreateDraft.whatsappPhone.trim() || null,
          telegramHandle: studentCreateDraft.telegramHandle.trim() || null,
          gender: studentCreateDraft.gender || null,
          birthYear: studentCreateDraft.birthYear ? Number(studentCreateDraft.birthYear) : null,
          coachCategory: studentCreateDraft.coachCategory.trim() || null,
        }),
      });
      const payload = await response.json().catch(() => null);

      if (!response.ok) {
        setStudentCreateError(payload?.details?.message ?? "No pudimos crear el alumno.");
        return;
      }

      const parsed = coachStudentContactMutationResponseSchema.parse(payload);
      await loadClubPanel();
      setManualClassDraft((current) => ({
        ...current,
        playerId: parsed.item.id,
        playerWhatsappPhone: parsed.item.whatsappPhone ?? "",
      }));
      setStudentCreateDraft(createClubStudentDraft());
    } catch {
      setStudentCreateError("No pudimos crear el alumno.");
    } finally {
      setCreatingStudent(false);
    }
  }

  if (loading) {
    return <p className="mt-8 text-sm text-white/70">Cargando panel del club...</p>;
  }

  if (!club || !dashboard) {
    if (authRequired) {
      return (
        <div className="mt-8 max-w-xl rounded-[1.75rem] border border-white/10 bg-[#0f140f]/80 p-6 text-white">
          <h2 className="text-2xl font-semibold">Acceso club</h2>
          <p className="mt-2 text-sm text-white/65">
            Entra desde <span className="text-white">/coaches</span> y usa sign in o sign up para clubs.
          </p>
          <Link
            href="/coaches"
            className="mt-5 inline-flex h-11 items-center justify-center rounded-xl bg-[#c4d600] px-5 text-sm font-medium text-[#10150d] transition hover:bg-[#d2e63e]"
          >
            Ir a sign in
          </Link>
        </div>
      );
    }

    return (
      <div className="mt-8 max-w-xl rounded-[1.75rem] border border-red-500/30 bg-[#0f140f]/80 p-6 text-white">
        <h2 className="text-2xl font-semibold">No pudimos abrir Entrenadores</h2>
        <p className="mt-2 text-sm text-white/65">{error ?? "El panel del club fallo al cargar."}</p>
        <button
          type="button"
          onClick={() => {
            setLoading(true);
            void loadClubPanel();
          }}
          className="mt-5 inline-flex h-11 items-center justify-center rounded-xl bg-[#c4d600] px-5 text-sm font-medium text-[#10150d] transition hover:bg-[#d2e63e]"
        >
          Reintentar
        </button>
      </div>
    );
  }

  const totalCourts = venues.reduce((total, venue) => total + venue.courtSummary.total, 0);
  const totalCoachHours = venues.reduce((total, venue) => total + venue.coachHours.length, 0);
  const latestInvite = invites[0] ?? null;
  const manualClassOptions = dashboard.manualClassOptions;
  const venueFilterOptions = [
    { id: "all", label: "Todas las sedes" },
    ...venues.map((venue) => ({ id: venue.id, label: venue.name })),
    { id: "unassigned", label: "Sin sede" },
  ];
  const coachVenueById = new Map(dashboard.coachTable.map((coach) => [coach.id, coach.venue]));
  const filteredProfessorRows = dashboard.analytics.professors.filter((professor) => {
    const venue = coachVenueById.get(professor.coachId) ?? null;
    if (coachVenueFilter === "all") return true;
    if (coachVenueFilter === "unassigned") return venue === null;
    return venue?.id === coachVenueFilter;
  });
  const weekDays = getCurrentWeekDays(dashboard.calendar.weekStart);
  const selectedCalendarDate = weekDays.includes(calendarDate) ? calendarDate : weekDays[0] ?? "";
  const selectedCalendarVenue =
    dashboard.calendar.venues.find((venue) => venue.id === calendarVenueId) ?? dashboard.calendar.venues[0] ?? null;
  const selectedCalendarVenueConfig = venues.find((venue) => venue.id === selectedCalendarVenue?.id) ?? null;
  const calendarCoachTagOptions = coaches
    .filter((coach) => coach.affiliation === "club")
    .sort((left, right) => left.name.localeCompare(right.name));
  const effectiveCalendarCoachFilter =
    calendarCoachFilter === "all" || calendarCoachTagOptions.some((coach) => coach.id === calendarCoachFilter)
      ? calendarCoachFilter
      : "all";
  const selectedCalendarCourts = (selectedCalendarVenue?.courts ?? []).filter((court) => court.coachesEnabled);
  const calendarCourtThemesById = new Map(
    selectedCalendarCourts.map((court, index) => [court.id, calendarCourtThemes[index % calendarCourtThemes.length]])
  );
  const filteredCalendarBlocks = dashboard.calendar.blocks.filter((block) => {
    if (!selectedCalendarVenue || block.venueId !== selectedCalendarVenue.id) return false;
    if (block.type !== calendarMode) return false;
    if (effectiveCalendarCoachFilter !== "all" && block.coachId !== effectiveCalendarCoachFilter) return false;
    return true;
  });
  const calendarDays = (() => {
    const blockEntriesByDate = new Map<string, typeof filteredCalendarBlocks>();
    for (const block of filteredCalendarBlocks) {
      const current = blockEntriesByDate.get(block.date) ?? [];
      current.push(block);
      blockEntriesByDate.set(block.date, current);
    }

    return weekDays.map((date, index) => {
      const dayOfWeek = index === 6 ? 0 : index + 1;
      const daySlots = selectedCalendarVenueConfig ? buildDaySlots(selectedCalendarVenueConfig.coachHours, dayOfWeek) : [];
      const dayBlocks = (blockEntriesByDate.get(date) ?? []).sort((left, right) => {
        const leftStart = parseTimeToMinutes(left.startTime) ?? 0;
        const rightStart = parseTimeToMinutes(right.startTime) ?? 0;
        if (leftStart !== rightStart) return leftStart - rightStart;
        return (left.coachName ?? "").localeCompare(right.coachName ?? "");
      });
      const occupancy = new Map<string, (typeof dayBlocks)[number]>();

      const canPlaceBlockOnCourt = (courtId: string, block: (typeof dayBlocks)[number]) =>
        daySlots.filter((slot) => blockOverlapsSlot(block, slot)).every((slot) => !occupancy.has(`${courtId}|${slot}`));

      for (const block of dayBlocks) {
        const preferredCourtId =
          block.courtId && selectedCalendarCourts.some((court) => court.id === block.courtId) ? block.courtId : null;
        const targetCourtId =
          (preferredCourtId && canPlaceBlockOnCourt(preferredCourtId, block) ? preferredCourtId : null) ??
          selectedCalendarCourts.find((court) => canPlaceBlockOnCourt(court.id, block))?.id ??
          null;

        if (!targetCourtId) continue;

        for (const slot of daySlots) {
          if (blockOverlapsSlot(block, slot)) {
            occupancy.set(`${targetCourtId}|${slot}`, block);
          }
        }
      }

      let coveredCells = 0;
      for (const court of selectedCalendarCourts) {
        for (const slot of daySlots) {
          if (occupancy.has(`${court.id}|${slot}`)) coveredCells += 1;
        }
      }

      return {
        date,
        slots: daySlots,
        occupancy,
        blockCount: dayBlocks.length,
        coveredCells,
        totalCells: daySlots.length * selectedCalendarCourts.length,
      };
    });
  })();
  const selectedCalendarDay = calendarDays.find((day) => day.date === selectedCalendarDate) ?? calendarDays[0] ?? null;
  const totalCalendarCells = selectedCalendarDay?.totalCells ?? 0;
  const totalCoveredCalendarCells = selectedCalendarDay?.coveredCells ?? 0;
  const totalBlankCalendarCells = Math.max(0, totalCalendarCells - totalCoveredCalendarCells);
  const calendarWeekSlots = selectedCalendarDay?.slots ?? [];
  const availableManualCoaches = manualClassOptions.coaches.filter((coach) =>
    manualClassDraft.venueId ? coach.venueId === manualClassDraft.venueId : Boolean(coach.venueId)
  );
  const selectedManualCoach = availableManualCoaches.find((coach) => coach.id === manualClassDraft.coachId) ?? null;
  const availableManualPackages = getActiveCoachPackages(selectedManualCoach);
  const selectedManualVenue = venues.find((venue) => venue.id === manualClassDraft.venueId) ?? null;
  const availableManualCourts = selectedManualVenue?.courts.filter((court) => court.coachesEnabled) ?? [];
  const analyticsReservations = dashboard.analytics.reservations;
  const resolveManualCourtPrice = (venueId: string, coachId: string, date: string, time: string) => {
    const venue = venues.find((item) => item.id === venueId) ?? null;
    const slotPrice = getVenueSlotPrice(venue, getDateDayOfWeek(date), time);
    const coach = coaches.find((item) => item.id === coachId) ?? null;

    if (venue && coach?.affiliation === "club" && coach.venue?.id === venue.id) {
      return Math.max(0, slotPrice - venue.clubCoachReservationDiscount);
    }

    return slotPrice;
  };

  return (
    <section className="space-y-6 text-white">
      <AuthenticatedWorkspaceNavbar
        email={club.contactEmail}
        sections={[
          {
            label: "Calendario",
            active: view === "calendar",
            onClick: () => handleViewChange("calendar"),
          },
          {
            label: "Analiticas",
            active: view === "analytics",
            onClick: () => handleViewChange("analytics"),
          },
          {
            label: "Sedes",
            active: view === "venues",
            onClick: () => handleViewChange("venues"),
          },
          {
            label: "Profesores",
            active: view === "coaches",
            onClick: () => handleViewChange("coaches"),
          },
        ]}
        onSignOut={async () => {
          await fetch("/api/clubs/logout", { method: "POST" });
          window.location.href = "/coaches";
        }}
      />

      {view === "calendar" ? (
        <>
          <article className="rounded-[1.75rem] border border-white/10 bg-[#0f140f]/80 p-6">
            <div className="grid gap-4 md:grid-cols-2">
              <details className="rounded-[1.35rem] border border-white/10 bg-black/20 open:border-[#c4d600]/45 open:bg-[#c4d600]/12 open:shadow-[0_0_0_1px_rgba(196,214,0,0.22)]" open={showManualClassForm}>
                <summary
                  onClick={() => {
                    if (showRequestsPanel) {
                      setShowRequestsPanel(false);
                    }
                    setShowManualClassForm((current) => !current);
                  }}
                  className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
                >
                  <p className="text-xs uppercase tracking-[0.2em] text-[#c4d600]">Operacion diaria</p>
                  <div className="mt-3 flex items-center justify-between gap-4">
                    <div>
                      <h3 className="text-2xl font-semibold text-white">Agregar clase</h3>
                      <p className="mt-2 text-sm text-white/60">
                        Carga una reserva manual para una sede, profesor, cancha y alumno.
                      </p>
                    </div>
                    <span className="rounded-full border border-[#c4d600]/30 px-3 py-1 text-xs text-[#ebff7b]">
                      {showManualClassForm ? "Ocultar" : "Abrir"}
                    </span>
                  </div>
                </summary>
              </details>

              <details className="rounded-[1.35rem] border border-white/10 bg-black/20 open:border-sky-400/45 open:bg-sky-400/10 open:shadow-[0_0_0_1px_rgba(56,189,248,0.18)]" open={showRequestsPanel}>
                <summary
                  onClick={() => {
                    if (showManualClassForm) {
                      setShowManualClassForm(false);
                    }
                    setShowRequestsPanel((current) => !current);
                  }}
                  className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
                >
                  <p className="text-xs uppercase tracking-[0.2em] text-sky-200">Seguimiento</p>
                  <div className="mt-3 flex items-center justify-between gap-4">
                    <div>
                      <h3 className="text-2xl font-semibold text-white">Ver solicitudes</h3>
                      <p className="mt-2 text-sm text-white/60">
                        Revisa pedidos, cambios o problemas pendientes de alumnos y profesores.
                      </p>
                    </div>
                    <span className="rounded-full border border-sky-400/30 px-3 py-1 text-xs text-sky-100">
                      {dashboard.requests.length} items
                    </span>
                  </div>
                </summary>
              </details>
            </div>
          </article>

          {showManualClassForm ? (
            <article className="rounded-[1.75rem] border border-white/10 bg-[#0f140f]/80 p-6" aria-labelledby="manual-class-heading">
              <div className="flex flex-wrap items-center justify-between gap-3">
                <div>
                  <p className="text-xs uppercase tracking-[0.2em] text-[#c4d600]">Agregar clase</p>
                  <h3 id="manual-class-heading" className="mt-2 text-2xl font-semibold">Carga manual para tu operacion diaria</h3>
                </div>
              </div>

              <form
                className="mt-5"
                onSubmit={(event) => {
                  event.preventDefault();
                  void handleCreateManualClass();
                }}
              >
                <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
                <div className="rounded-[1rem] border border-white/10 bg-black/20 p-4 md:col-span-2 xl:col-span-4">
                  <div className="flex flex-wrap items-center justify-between gap-3">
                    <div>
                      <p className="text-sm font-medium text-white">Nuevo alumno</p>
                      <p className="mt-1 text-xs text-white/55">Lo creas para el club y despues se lo asignas al profesor que corresponda.</p>
                    </div>
                    <Button
                      type="button"
                      className="h-10 rounded-xl px-4 text-sm font-medium"
                      disabled={creatingStudent}
                      onClick={() => void handleCreateStudent()}
                    >
                      {creatingStudent ? "Creando..." : "Crear alumno"}
                    </Button>
                  </div>

                  <div className="mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
                    <label className="block text-sm text-white/70">
                      Nombre
                      <input
                        value={studentCreateDraft.name}
                        onChange={(event) => setStudentCreateDraft((current) => ({ ...current, name: event.target.value }))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />
                    </label>
                    <label className="block text-sm text-white/70">
                      WhatsApp
                      <input
                        value={studentCreateDraft.whatsappPhone}
                        onChange={(event) => setStudentCreateDraft((current) => ({ ...current, whatsappPhone: event.target.value }))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />
                    </label>
                    <label className="block text-sm text-white/70">
                      Telegram
                      <input
                        value={studentCreateDraft.telegramHandle}
                        onChange={(event) => setStudentCreateDraft((current) => ({ ...current, telegramHandle: event.target.value }))}
                        placeholder="@usuario"
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />
                    </label>
                    <label className="block text-sm text-white/70">
                      Sexo
                      <select
                        value={studentCreateDraft.gender}
                        onChange={(event) =>
                          setStudentCreateDraft((current) => ({
                            ...current,
                            gender: event.target.value as ClubStudentCreateDraft["gender"],
                          }))
                        }
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      >
                        <option value="">Sin dato</option>
                        <option value="female">Mujer</option>
                        <option value="male">Hombre</option>
                        <option value="other">Otro</option>
                      </select>
                    </label>
                    <label className="block text-sm text-white/70">
                      Ano de nacimiento
                      <input
                        type="number"
                        min={1900}
                        max={2100}
                        value={studentCreateDraft.birthYear}
                        onChange={(event) => setStudentCreateDraft((current) => ({ ...current, birthYear: event.target.value }))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />
                    </label>
                    <label className="block text-sm text-white/70">
                      Categoria
                      <select
                        value={studentCreateDraft.coachCategory}
                        onChange={(event) => setStudentCreateDraft((current) => ({ ...current, coachCategory: event.target.value }))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      >
                        <option value="">Sin categoria</option>
                        {Array.from({ length: 10 }, (_, index) => String(index + 1)).map((value) => (
                          <option key={value} value={value}>
                            {value}
                          </option>
                        ))}
                      </select>
                    </label>
                  </div>

                  {studentCreateError ? (
                    <p className="mt-4 rounded-xl border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
                      {studentCreateError}
                    </p>
                  ) : null}
                </div>

                <label className="block text-sm text-white/70">
                  Sede
                  <select
                    value={manualClassDraft.venueId}
                    onChange={(event) => {
                      const nextVenueId = event.target.value;
                      const nextVenue = venues.find((venue) => venue.id === nextVenueId) ?? null;
                      const nextCourt = nextVenue?.courts.find((court) => court.coachesEnabled) ?? nextVenue?.courts[0] ?? null;
                      const nextCoach =
                        manualClassOptions.coaches.find((coach) => coach.venueId === nextVenueId && getActiveCoachPackages(coach).length > 0) ??
                        manualClassOptions.coaches.find((coach) => coach.venueId === nextVenueId) ??
                        null;
                      const nextPackage = getActiveCoachPackages(nextCoach)[0] ?? null;
                      setManualClassDraft((current) => ({
                        ...applyCoachPackageToDraft(
                          {
                            ...current,
                            venueId: nextVenueId,
                            courtId: nextCourt?.id ?? "",
                            coachId: nextCoach?.id ?? "",
                            courtPriceMinor: resolveManualCourtPrice(
                              nextVenueId,
                              nextCoach?.id ?? "",
                              current.date,
                              current.time
                            ),
                          },
                          nextPackage
                        ),
                      }));
                    }}
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  >
                    <option value="">Selecciona sede</option>
                    {manualClassOptions.venues.map((venue) => (
                      <option key={venue.id} value={venue.id}>
                        {venue.name}
                      </option>
                    ))}
                  </select>
                </label>

                <label className="block text-sm text-white/70">
                  Profesor
                  <select
                    value={manualClassDraft.coachId}
                    onChange={(event) => {
                      const nextCoachId = event.target.value;
                      const nextCoach = availableManualCoaches.find((coach) => coach.id === nextCoachId) ?? null;
                      const nextPackage = getActiveCoachPackages(nextCoach)[0] ?? null;
                      setManualClassDraft((current) =>
                        applyCoachPackageToDraft(
                          {
                            ...current,
                            coachId: nextCoachId,
                            courtPriceMinor: resolveManualCourtPrice(current.venueId, nextCoachId, current.date, current.time),
                          },
                          nextPackage
                        )
                      );
                    }}
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  >
                    <option value="">Selecciona profesor</option>
                    {availableManualCoaches.map((coach) => (
                      <option key={coach.id} value={coach.id}>
                        {coach.name}{getActiveCoachPackages(coach).length === 0 ? " (sin paquetes)" : ""}
                      </option>
                    ))}
                  </select>
                </label>

                <label className="block text-sm text-white/70">
                  Paquete del profesor
                  <select
                    value={manualClassDraft.packageId}
                    disabled={!manualClassDraft.coachId || availableManualPackages.length === 0}
                    onChange={(event) => {
                      const nextPackage = availableManualPackages.find((salesPackage) => salesPackage.id === event.target.value) ?? null;
                      setManualClassDraft((current) => applyCoachPackageToDraft(current, nextPackage));
                    }}
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white disabled:cursor-not-allowed disabled:text-white/40"
                  >
                    <option value="">
                      {manualClassDraft.coachId
                        ? availableManualPackages.length === 0
                          ? "Este profesor no tiene paquetes"
                          : "Selecciona paquete"
                        : "Primero selecciona profesor"}
                    </option>
                    {availableManualPackages.map((salesPackage) => (
                      <option key={salesPackage.id} value={salesPackage.id}>
                        {salesPackage.title} · {salesPackage.durationMinutes} min · {formatMoney(salesPackage.price)}
                      </option>
                    ))}
                  </select>
                </label>

                <label className="block text-sm text-white/70">
                  Cancha
                  <select
                    value={manualClassDraft.courtId}
                    onChange={(event) => setManualClassDraft((current) => ({ ...current, courtId: event.target.value }))}
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  >
                    <option value="">Sin asignar</option>
                    {availableManualCourts.map((court) => (
                      <option key={court.id} value={court.id}>
                        {court.name}
                      </option>
                    ))}
                  </select>
                </label>

                <label className="block text-sm text-white/70">
                  Alumno
                  <select
                    value={manualClassDraft.playerId}
                    onChange={(event) => {
                      const nextPlayer = manualClassOptions.players.find((player) => player.id === event.target.value) ?? null;
                      setManualClassDraft((current) => ({
                        ...current,
                        playerId: event.target.value,
                        playerWhatsappPhone: nextPlayer?.whatsappPhone ?? "",
                      }));
                    }}
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  >
                    <option value="">Selecciona alumno</option>
                    {manualClassOptions.players.map((player) => (
                      <option key={player.id} value={player.id}>
                        {player.name}
                      </option>
                    ))}
                  </select>
                </label>

                <label className="block text-sm text-white/70">
                  WhatsApp
                  <input
                    value={manualClassDraft.playerWhatsappPhone}
                    onChange={(event) => setManualClassDraft((current) => ({ ...current, playerWhatsappPhone: event.target.value }))}
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  />
                </label>

                <label className="block text-sm text-white/70">
                  Fecha
                  <input
                    type="date"
                    value={manualClassDraft.date}
                    onChange={(event) =>
                      setManualClassDraft((current) => ({
                        ...current,
                        date: event.target.value,
                        courtPriceMinor: resolveManualCourtPrice(current.venueId, current.coachId, event.target.value, current.time),
                      }))
                    }
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  />
                </label>

                <label className="block text-sm text-white/70">
                  Hora
                  <input
                    type="time"
                    step={1800}
                    value={manualClassDraft.time}
                    onChange={(event) =>
                      setManualClassDraft((current) => ({
                        ...current,
                        time: event.target.value,
                        courtPriceMinor: resolveManualCourtPrice(current.venueId, current.coachId, current.date, event.target.value),
                      }))
                    }
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  />
                </label>

                <label className="block text-sm text-white/70">
                  Duracion
                  <select
                    value={String(manualClassDraft.durationMinutes)}
                    onChange={(event) =>
                      setManualClassDraft((current) => ({ ...current, durationMinutes: Number(event.target.value || 60) }))
                    }
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  >
                    {[60, 90, 120].map((minutes) => (
                      <option key={minutes} value={minutes}>
                        {minutes} min
                      </option>
                    ))}
                  </select>
                </label>

                <label className="block text-sm text-white/70">
                  Repetir semanalmente
                  <select
                    value={String(manualClassDraft.repeatWeeklyCount)}
                    onChange={(event) =>
                      setManualClassDraft((current) => ({ ...current, repeatWeeklyCount: Number(event.target.value || 1) }))
                    }
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  >
                    <option value="1">Solo esta clase</option>
                    <option value="4">4 semanas</option>
                    <option value="8">8 semanas</option>
                    <option value="12">12 semanas</option>
                  </select>
                </label>

                <label className="block text-sm text-white/70">
                  Precio cancha
                  <input
                    type="number"
                    min={0}
                    value={manualClassDraft.courtPriceMinor}
                    onChange={(event) =>
                      setManualClassDraft((current) => ({ ...current, courtPriceMinor: Math.max(0, Number(event.target.value || 0)) }))
                    }
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  />
                </label>

                <label className="block text-sm text-white/70">
                  Precio coach
                  <input
                    type="number"
                    min={0}
                    readOnly
                    value={manualClassDraft.coachPriceMinor}
                    className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/20 px-3 text-sm text-white/75"
                  />
                </label>
              </div>

              <label className="mt-4 block text-sm text-white/70">
                Nota interna
                <textarea
                  value={manualClassDraft.note}
                  onChange={(event) => setManualClassDraft((current) => ({ ...current, note: event.target.value }))}
                  className="mt-2 min-h-[110px] w-full rounded-[1rem] border border-white/15 bg-black/30 px-3 py-3 text-sm text-white"
                />
              </label>

              {selectedManualVenue ? (
                <p className="mt-4 text-sm text-white/55">

[Showing lines 1-1205 of 2075 (50.0KB limit). Use offset=1206 to continue.]


cd /home/sebas/work/projects/ballbox && rg -n "Calendario|calendar|Solicitud|solicitudes|availability|reservation|status|helper|aria|grid|table" components/club-coaches-page-client.tsx


19:type ClubPageView = "calendar" | "analytics" | "venues" | "coaches";
20:type ClubCalendarMode = "availability" | "reservation";
46:const calendarCourtThemes = [
294:    return "calendar";
311:  const [calendarVenueId, setCalendarVenueId] = useState("");
312:  const [calendarDate, setCalendarDate] = useState("");
313:  const [calendarCoachFilter, setCalendarCoachFilter] = useState("all");
314:  const [calendarMode, setCalendarMode] = useState<ClubCalendarMode>("availability");
345:    setView("calendar");
352:    if (nextView === "calendar") {
373:        setAuthRequired(response.status === 401);
374:        setError(response.status === 401 ? null : payload?.error === "INVALID_PAYLOAD" ? "El panel del club devolvio datos invalidos." : "No pudimos cargar el panel del club.");
384:      setCalendarVenueId((current) => current || parsed.dashboard.calendar.venues[0]?.id || parsed.configuration.venues[0]?.id || "");
436:    const days = getCurrentWeekDays(dashboard.calendar.weekStart);
655:  const weekDays = getCurrentWeekDays(dashboard.calendar.weekStart);
656:  const selectedCalendarDate = weekDays.includes(calendarDate) ? calendarDate : weekDays[0] ?? "";
658:    dashboard.calendar.venues.find((venue) => venue.id === calendarVenueId) ?? dashboard.calendar.venues[0] ?? null;
660:  const calendarCoachTagOptions = coaches
664:    calendarCoachFilter === "all" || calendarCoachTagOptions.some((coach) => coach.id === calendarCoachFilter)
665:      ? calendarCoachFilter
668:  const calendarCourtThemesById = new Map(
669:    selectedCalendarCourts.map((court, index) => [court.id, calendarCourtThemes[index % calendarCourtThemes.length]])
671:  const filteredCalendarBlocks = dashboard.calendar.blocks.filter((block) => {
673:    if (block.type !== calendarMode) return false;
677:  const calendarDays = (() => {
733:  const selectedCalendarDay = calendarDays.find((day) => day.date === selectedCalendarDate) ?? calendarDays[0] ?? null;
737:  const calendarWeekSlots = selectedCalendarDay?.slots ?? [];
745:  const analyticsReservations = dashboard.analytics.reservations;
764:            label: "Calendario",
765:            active: view === "calendar",
766:            onClick: () => handleViewChange("calendar"),
790:      {view === "calendar" ? (
793:

804:
Operacion diaria

832:
Ver solicitudes

847:

851:
Carga manual para tu operacion diaria

862:

879:

1256:

1259:
Solicitudes

1282:                        {request.statusLabel}
1286:

1307:
Calendario

1309:
{formatWeekRange(dashboard.calendar.weekStart, dashboard.calendar.weekEnd)}

1312:                <legend className="sr-only">Modo de calendario</legend>
1315:                    calendarMode === "availability" ? "bg-[#c4d600]/14 text-white" : "text-white/55 hover:text-white"
1319:                      name="calendar-mode"
1320:                      value="availability"
1321:                      checked={calendarMode === "availability"}
1322:                      onChange={() => setCalendarMode("availability")}
1328:                    calendarMode === "reservation" ? "bg-sky-400/14 text-white" : "text-white/55 hover:text-white"
1332:                      name="calendar-mode"
1333:                      value="reservation"
1334:                      checked={calendarMode === "reservation"}
1335:                      onChange={() => setCalendarMode("reservation")}
1348:                  {dashboard.calendar.venues.map((venue) => {
1362:                          name="calendar-venue"
1387:                      name="calendar-coach"
1395:                  {calendarCoachTagOptions.map((coach) => {
1409:                          name="calendar-coach"
1423:

1426:                  {calendarMode === "availability" ? "Cobertura del dia" : "Reservas del dia"}
1448:                No hay sedes disponibles para mostrar en el calendario.
1467:                  <fieldset className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7">
1468:                    <legend className="sr-only">Dia del calendario</legend>
1469:                    {calendarDays.map((day) => {
1483:                            name="calendar-day"
1499:                {calendarWeekSlots.length === 0 ? (
1509:                          {calendarMode === "availability" ? "Cobertura de coaches por cancha" : "Reservas tomadas por cancha"}
1519:                        className="grid min-w-[1120px] gap-2"
1520:                        style={{ gridTemplateColumns: `96px repeat(${selectedCalendarCourts.length}, minmax(160px, 1fr))` }}
1526:                          const theme = calendarCourtThemesById.get(court.id) ?? calendarCourtThemes[0];
1537:                        {calendarWeekSlots.map((slot) => (
1545:                              const theme = calendarCourtThemesById.get(court.id) ?? calendarCourtThemes[0];
1551:                                calendarMode === "reservation" ? (block?.courtPriceMinor ?? slotPriceMinor) : slotPriceMinor;
1558:                                  aria-label={`${court.name}, ${formatSlotLabel(slot)}, ${
1562:                                        ? calendarMode === "availability"
1565:                                        : calendarMode === "availability"
1624:                                        {calendarMode === "availability" ? "Cubierta por profesor" : "Reserva cargada"}
1626:                                      {calendarMode === "reservation" && block.playerName ? (
1638:                                        {calendarMode === "availability" ? "Sin coach cubriendo" : "Sin reserva cargada"}
1682:

1685:
{dashboard.analytics.summary.reservationCount}

1702:

1730:                      {professor.reservationCount}
1738:

1753:

1773:                  {analyticsReservations.map((reservation) => (
1774:

1775:                      {formatRequestDate(reservation.startsAt)}
1776:                      {reservation.coachName}
1777:                      {reservation.playerName}
1779:                        {reservation.venueName}
1780:                        {reservation.courtName ? ` · ${reservation.courtName}` : ""}
1782:                      {formatMoney(reservation.courtPriceMinor ?? 0)}
1783:                      {formatMoney(reservation.coachPriceMinor ?? 0)}
1787:

1807:

1845:

1889:

1912:

1937:                                  {venue.status === "active" ? "Activa" : "Inactiva"}
1972:

1979:

2008:                      variant="outline"
2033:

2039:                      Solicitudes
2065:


Precio actual del horario: {formatMoney(resolveManualCourtPrice(
                    selectedManualVenue.id,
                    manualClassDraft.coachId,
                    manualClassDraft.date,
                    manualClassDraft.time
                  ))}
                  . Base de la sede: {formatMoney(selectedManualVenue.coachReservationPrice)}. Descuento coach del club:{" "}
                  {formatMoney(selectedManualVenue.clubCoachReservationDiscount)}.

              ) : null}

              {manualClassDraft.coachId && availableManualPackages.length === 0 ? (

                  Este profesor todavia no tiene paquetes activos. Primero tiene que cargar uno para poder armar una clase.

              ) : null}

              {manualClassError ? (

{manualClassError}

              ) : null}

                <Button
                  type="submit"
                  className="h-11 rounded-xl px-5 text-sm font-medium"
                  disabled={
                    savingManualClass ||
                    !manualClassDraft.venueId ||
                    !manualClassDraft.coachId ||
                    !manualClassDraft.packageId ||
                    !manualClassDraft.playerId ||
                    !manualClassDraft.date ||
                    !manualClassDraft.time
                  }
                >
                  {savingManualClass ? "Guardando..." : "Guardar clase"}
                </Button>
                <button
                  type="button"
                  onClick={() => setShowManualClassForm(false)}
                  className="text-sm text-white/60 transition hover:text-white"
                >
                  Cancelar
                </button>

              </form>

          ) : null}

          {showRequestsPanel ? (

Solicitudes

Pendientes del club

                  {dashboard.requests.length} items

                {dashboard.requests.length === 0 ? (

                    No hay pedidos ni problemas pendientes para revisar ahora.

                ) : null}

                {dashboard.requests.map((request) => (

{request.title}

{request.subtitle}

                        {request.statusLabel}

{request.venue ? `Sede ${request.venue.name}` : "Sin sede"}

{request.coach ? `Coach ${request.coach.name}` : "Sin coach"}

{formatRequestDate(request.createdAt)}

                    {request.student ? (

                        {request.student.email}
                        {request.student.whatsappPhone ? ` · ${request.student.whatsappPhone}` : ""}

                    ) : null}

                ))}

          ) : null}

Calendario

Semana actual

{formatWeekRange(dashboard.calendar.weekStart, dashboard.calendar.weekEnd)}

              <fieldset className="flex flex-wrap items-center gap-3">
                <legend className="sr-only">Modo de calendario</legend>

                    <input
                      type="radio"
                      name="calendar-mode"
                      value="availability"
                      checked={calendarMode === "availability"}
                      onChange={() => setCalendarMode("availability")}
                      className="sr-only"
                    />
                    Disponibilidad

                    <input
                      type="radio"
                      name="calendar-mode"
                      value="reservation"
                      checked={calendarMode === "reservation"}
                      onChange={() => setCalendarMode("reservation")}
                      className="sr-only"
                    />
                    Reservas

              </fieldset>

              <fieldset>
                <legend className="text-xs uppercase tracking-[0.16em] text-white/45">Sede</legend>

                  {dashboard.calendar.venues.map((venue) => {
                    const isActive = venue.id === selectedCalendarVenue?.id;

                    return (

                        <input
                          type="radio"
                          name="calendar-venue"
                          value={venue.id}
                          checked={isActive}
                          onChange={() => setCalendarVenueId(venue.id)}
                          className="sr-only"
                        />
                        {venue.name}

                    );
                  })}

              </fieldset>

              <fieldset>
                <legend className="text-xs uppercase tracking-[0.16em] text-white/45">Profesores del club</legend>

                    <input
                      type="radio"
                      name="calendar-coach"
                      value="all"
                      checked={effectiveCalendarCoachFilter === "all"}
                      onChange={() => setCalendarCoachFilter("all")}
                      className="sr-only"
                    />
                    Todos

                  {calendarCoachTagOptions.map((coach) => {
                    const isActive = effectiveCalendarCoachFilter === coach.id;

                    return (

                        <input
                          type="radio"
                          name="calendar-coach"
                          value={coach.id}
                          checked={isActive}
                          onChange={() => setCalendarCoachFilter(coach.id)}
                          className="sr-only"
                        />
                        {coach.name}

                    );
                  })}

              </fieldset>

                  {calendarMode === "availability" ? "Cobertura del dia" : "Reservas del dia"}

{totalCoveredCalendarCells}

Huecos visibles

{totalBlankCalendarCells}

Dia seleccionado

{selectedCalendarDay ? formatCalendarDayLabel(selectedCalendarDay.date) : "Sin dia"}

Canchas

                  {selectedCalendarCourts.length}

            {!selectedCalendarVenue || !selectedCalendarVenueConfig ? (

                No hay sedes disponibles para mostrar en el calendario.

            ) : selectedCalendarCourts.length === 0 ? (

                Esta sede no tiene canchas habilitadas para coaches.

            ) : (

Selecciona el dia

Despues ves todas las canchas de la sede como columnas.

                      {selectedCalendarCourts.length} canchas

                  <fieldset className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7">
                    <legend className="sr-only">Dia del calendario</legend>
                    {calendarDays.map((day) => {
                      const isActive = day.date === selectedCalendarDate;

                      return (

                          <input
                            type="radio"
                            name="calendar-day"
                            value={day.date}
                            checked={isActive}
                            onChange={() => setCalendarDate(day.date)}
                            className="sr-only"
                          />

{formatCalendarDayLabel(day.date)}

                            {day.coveredCells} ocupados · {Math.max(0, day.totalCells - day.coveredCells)} huecos

                      );
                    })}
                  </fieldset>

                {calendarWeekSlots.length === 0 ? (

                    Esta sede no tiene horario coach habilitado para este dia.

                ) : (

{selectedCalendarDay ? formatCalendarDayLabel(selectedCalendarDay.date) : "Dia"}

                          {calendarMode === "availability" ? "Cobertura de coaches por cancha" : "Reservas tomadas por cancha"}

                        {totalCoveredCalendarCells} bloques ocupados

                      <div
                        className="grid min-w-[1120px] gap-2"
                        style={{ gridTemplateColumns: `96px repeat(${selectedCalendarCourts.length}, minmax(160px, 1fr))` }}
                      >

                          Hora

                        {selectedCalendarCourts.map((court) => {
                          const theme = calendarCourtThemesById.get(court.id) ?? calendarCourtThemes[0];

                          return (

                            <div className={`mb-2 h-2 rounded-full ${theme.accent}`} />

{court.name}

{court.covered ? "Techada" : "Descubierta"}

                          );
                        })}

                        {calendarWeekSlots.map((slot) => (
                          <div key={`row-${slot}`} className="contents">

                              {formatSlotLabel(slot)}

                            {selectedCalendarCourts.map((court) => {
                              const theme = calendarCourtThemesById.get(court.id) ?? calendarCourtThemes[0];
                              const block = selectedCalendarDay?.occupancy.get(`${court.id}|${slot}`) ?? null;
                              const slotEnabled = selectedCalendarDay?.slots.includes(slot) ?? false;
                              const activeDate = selectedCalendarDay?.date ?? selectedCalendarDate;
                              const slotPriceMinor = getVenueSlotPrice(selectedCalendarVenueConfig, getDateDayOfWeek(activeDate), slot);
                              const visibleSlotPriceMinor =
                                calendarMode === "reservation" ? (block?.courtPriceMinor ?? slotPriceMinor) : slotPriceMinor;

                              return (
                                <button
                                  key={`${court.id}-${slot}`}
                                  type="button"
                                  disabled={!slotEnabled}
                                  aria-label={`${court.name}, ${formatSlotLabel(slot)}, ${
                                    !slotEnabled
                                      ? "cerrado para coaches"
                                      : block
                                        ? calendarMode === "availability"
                                          ? "cubierta por profesor"
                                          : `reserva cargada${block.playerName ? ` para ${block.playerName}` : ""}`
                                        : calendarMode === "availability"
                                          ? "sin coach cubriendo"
                                          : "sin reserva cargada"
                                  }`}
                                  onClick={() => {
                                    if (!slotEnabled) return;
                                    setManualClassDraft((current) => ({
                                      ...(() => {
                                        const fallbackCoach =
                                          manualClassOptions.coaches.find(
                                            (coach) => coach.venueId === selectedCalendarVenue.id && getActiveCoachPackages(coach).length > 0
                                          ) ??
                                          manualClassOptions.coaches.find((coach) => coach.venueId === selectedCalendarVenue.id) ??
                                          null;
                                        const currentCoachFitsVenue = manualClassOptions.coaches.some(
                                          (coach) => coach.id === current.coachId && coach.venueId === selectedCalendarVenue.id
                                        );
                                        const nextCoachId = block?.coachId ?? (currentCoachFitsVenue ? current.coachId : fallbackCoach?.id ?? "");
                                        const nextCoach = manualClassOptions.coaches.find((coach) => coach.id === nextCoachId) ?? null;
                                        const nextPackage =
                                          current.packageId && getActiveCoachPackages(nextCoach).some((salesPackage) => salesPackage.id === current.packageId)
                                            ? getActiveCoachPackages(nextCoach).find((salesPackage) => salesPackage.id === current.packageId) ?? null
                                            : getActiveCoachPackages(nextCoach)[0] ?? null;
                                        return {
                                          ...applyCoachPackageToDraft(
                                            {
                                              ...current,
                                              venueId: selectedCalendarVenue.id,
                                              courtId: court.id,
                                              coachId: nextCoachId,
                                              date: activeDate,
                                              time: slot,
                                              courtPriceMinor: resolveManualCourtPrice(
                                                selectedCalendarVenue.id,
                                                nextCoachId,
                                                activeDate,
                                                slot
                                              ),
                                            },
                                            nextPackage
                                          ),
                                        };
                                      })(),
                                    }));
                                    setShowManualClassForm(true);
                                  }}
                                  className={`min-h-[88px] rounded-xl border px-3 py-3 text-left transition ${
                                    !slotEnabled
                                      ? "cursor-not-allowed border-transparent bg-[#160909] text-white/25"
                                      : block
                                        ? theme.cell
                                        : "border-dashed border-white/10 bg-[#0c100c] hover:border-white/20 hover:bg-white/[0.03]"
                                  }`}
                                >
                                  {!slotEnabled ? (

Cerrado para coaches

                                  ) : block ? (
                                    <>

                                        {calendarMode === "availability" ? "Cubierta por profesor" : "Reserva cargada"}

                                      {calendarMode === "reservation" && block.playerName ? (

{block.playerName}

                                      ) : null}

{formatMoney(visibleSlotPriceMinor)}

                                        {block.startTime} - {block.endTime}

                                    </>
                                  ) : (
                                    <>

Hueco

                                        {calendarMode === "availability" ? "Sin coach cubriendo" : "Sin reserva cargada"}

{formatMoney(visibleSlotPriceMinor)}

                                    </>
                                  )}
                                </button>
                              );
                            })}

                        ))}

                )}

            )}

        </>
      ) : view === "analytics" ? (
        <>

Profesores

Analytics por profesor

                <select
                  value={coachVenueFilter}
                  onChange={(event) => setCoachVenueFilter(event.target.value)}
                  className="h-10 rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                >
                  {venueFilterOptions.map((option) => (
                    <option key={option.id} value={option.id}>
                      {option.label}
                    </option>
                  ))}
                </select>

                  {filteredProfessorRows.length} profes

Reservations

{dashboard.analytics.summary.reservationCount}

Para club

{formatMoney(dashboard.analytics.summary.clubRevenueMinor)}

Para coaches

{formatMoney(dashboard.analytics.summary.coachRevenueMinor)}

Reviews

{formatAverageReview(dashboard.analytics.summary.averageReview)}

                    Profesor
                    Sede
                    Reservas
                    Alumnos
                    Resenas
                    Para coach
                    Para club

                  {filteredProfessorRows.length === 0 ? (

                        No hay profesores para ese filtro.

                  ) : null}

                  {filteredProfessorRows.map((professor) => (

{professor.coachName}

{professor.studentCount} alumnos activos

                      {professor.venueName ?? "Sin sede"}
                      {professor.reservationCount}
                      {professor.studentCount}
                      {formatAverageReview(professor.averageReview)}
                      {formatMoney(professor.coachRevenueMinor)}
                      {formatMoney(professor.clubRevenueMinor)}

                  ))}

Reservations

Detalle de reservas

                {analyticsReservations.length} reservas

                    Fecha
                    Profesor
                    Alumno
                    Sede / cancha
                    Club
                    Coach

                  {analyticsReservations.length === 0 ? (

                        Todavia no hay reservas tomadas.

                  ) : null}

                  {analyticsReservations.map((reservation) => (

                      {formatRequestDate(reservation.startsAt)}
                      {reservation.coachName}
                      {reservation.playerName}

                        {reservation.venueName}
                        {reservation.courtName ? ` · ${reservation.courtName}` : ""}

                      {formatMoney(reservation.courtPriceMinor ?? 0)}
                      {formatMoney(reservation.coachPriceMinor ?? 0)}

                  ))}

        </>
      ) : view === "venues" ? (

Sedes

Administra tus sedes

                  Crea sedes rapido aqui y luego entra a cada una para terminar horarios, canchas y profesores con mas detalle.

                {venues.length} sedes

Accion principal

Agregar sede

                      Crea una sede nueva y despues entras a terminar horarios, canchas y reservas con una configuracion guiada.

                      {totalCourts} canchas
                      {coaches.length} profesores
                      {totalCoachHours} bloques coach

                      Crear sede

                      Ocultar formulario

Nueva sede

Primer paso rapido

                        Aqui solo defines nombre y cantidad inicial de canchas. Lo mas fino lo terminas dentro de la sede.

                      Luego la sigues configurando

                      Nombre
                      <input
                        value={newVenueName}
                        onChange={(event) => setNewVenueName(event.target.value)}
                        placeholder="Palermo Indoor, Moron, Sede Norte..."
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />

                      Techadas
                      <input
                        type="number"
                        min={0}
                        value={coveredCourtCount}
                        onChange={(event) => setCoveredCourtCount(Math.max(0, Number(event.target.value || 0)))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />

                      No techadas
                      <input
                        type="number"
                        min={0}
                        value={uncoveredCourtCount}
                        onChange={(event) => setUncoveredCourtCount(Math.max(0, Number(event.target.value || 0)))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />

                    <Button
                      type="button"
                      className="h-11 rounded-xl px-5 text-sm font-medium"
                      disabled={creatingVenue || newVenueName.trim().length === 0}
                      onClick={() => void handleCreateVenue()}
                    >
                      {creatingVenue ? "Creando..." : "Crear sede y abrir configuracion"}
                    </Button>

Canchas

{totalCourts}

Coaches

{coaches.length}

Bloques coach

{totalCoachHours}

              {venues.length === 0 ? (

                  Todavia no cargaste sedes. Crea una y entra para configurar horarios, coaches externos y entrenadores.

              ) : (

                        Sede
                        Estado
                        Horario
                        Resumen
                        Accion

                      {venues.map((venue) => {
                        const venueCoaches = coaches.filter((coach) => coach.venue?.id === venue.id);

                        return (

{venue.name}

                                {venue.courtSummary.total} canchas · {venueCoaches.length} profesores

                                  {venue.status === "active" ? "Activa" : "Inactiva"}

                                  {venue.externalCoachesAllowed ? "Acepta externos" : "Solo club"}

                            {formatOpeningWindow(venue.openingTime, venue.closingTime)}

                                {venue.courtSummary.coachesEnabled} canchas para coaches · {venue.coachCount} asignados

                                Externo {formatMoney(venue.coachReservationPrice)} · Club {formatMoney(venue.clubCoachReservationPrice)}

                              {venue.coachPriceOverrides.length > 0 ? (

                                  {venue.coachPriceOverrides.length} horario{venue.coachPriceOverrides.length === 1 ? "" : "s"} especial

                              ) : null}

                                <Link
                                  href={`/clubs/venues/${venue.id}`}
                                  className="inline-flex h-11 items-center justify-center rounded-xl border border-[#c4d600]/35 bg-[#c4d600]/10 px-5 text-sm font-medium text-[#d9ef31] shadow-[0_12px_35px_-24px_rgba(196,214,0,0.9)] transition hover:-translate-y-0.5 hover:border-[#c4d600]/60 hover:bg-[#c4d600]/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#c4d600]/70"
                                >
                                  Editar sede
                                </Link>

                        );
                      })}

              )}

      ) : (

Link de invitacion para coach

Agregar profesores

                No hace falta cargar mail. Cuando el coach entre, luego decides en qué sede trabaja.

              <Button
                type="button"
                className="mt-5 h-11 w-full rounded-xl text-sm font-medium"
                disabled={inviteSaving}
                onClick={() => void handleCreateInvite()}
              >
                {inviteSaving ? "Generando..." : "Generar link"}
              </Button>

              {latestInvite ? (

Ultimo link generado

                  <input
                    readOnly
                    value={latestInvite.inviteUrl}
                    className="mt-3 h-11 w-full rounded-xl border border-white/10 bg-black/30 px-3 text-xs text-white/80"
                  />

                    Generado {formatInviteDate(latestInvite.createdAt)}
                    <Button
                      type="button"
                      variant="outline"
                      className="h-10 rounded-xl border-white/15 bg-transparent text-white hover:bg-white/5"
                      onClick={() => {
                        void copyInvite(latestInvite);
                      }}
                    >
                      {copiedInviteId === latestInvite.id ? "Copiado" : "Copiar link"}
                    </Button>

              ) : null}

Profesores

Tabla de profesores

                  {coaches.length} coaches

                      Profesor
                      Sede
                      Clases publicadas
                      Solicitudes
                      Resenas

                    {coaches.length === 0 ? (

                          Todavia no hay coaches vinculados a este club.

                    ) : null}

                    {coaches.map((coach) => (

{coach.name}

{coach.email ?? "Sin email"}

                        {coach.venue ? coach.venue.name : "Sin sede asignada"}
                        {coach.metrics.classOffers}
                        {coach.metrics.requestCount}
                        {formatAverageReview(coach.metrics.averageReview)}

                    ))}

      )}

      {error ?
{error}
 : null}

  );
}


**Planning calendar cleanup**

I need to work on task 4, which involves cleaning up the action text and status in the calendar grid. Before diving into that, I'll probably want to edit the TASKS and STATE first. Marking it as "in progress" seems like a good way to start, then I can proceed with the edits. Let’s get organized for a smoother workflow ahead!

edit

4. pending - club-coaches-page-client: calendar grid action text/status cleanup and explicit helper copy.

4. in_progress - club-coaches-page-client: calendar grid action text/status cleanup and explicit helper copy.


Successfully replaced 1 block(s) in /home/sebas/runtime/ralph-loops/ballbox-a11y-20260517-120638/TASKS.md.

    ...
  4
  5 1. done - coach-settings-page-client: package format and active state controls to semantic grouped controls.
  6 2. done - coach-settings-page-client: students table contact save/error/status semantics and grouping.
  7 3. done - coach-settings-page-client: availability section audit for html-first labels/help/status.
- 8 4. pending - club-coaches-page-client: calendar grid action text/status cleanup and explicit helper copy.
+ 8 4. in_progress - club-coaches-page-client: calendar grid action text/status cleanup and explicit helper copy.
  9 5. pending - payments-admin-client: remaining CRUD session area form semantics audit.
 10 6. pending - authenticated/public nav final keyboard/a11y polish audit.
 11 7. pending - cross-flow pass: improve error/status live messaging where still weak.
 12 8. pending - add or adjust tests for changed semantics where useful, especially keyboard interaction for weekly availability grid.
    ...


Current owner: agent-4
Current focus: iteration 4
Last result: task 3 done in `components/coach-settings-page-client.tsx` and `components/weekly-availability-grid.tsx`; availability now exposes a live autosave status, semantic grid fieldset/help/error wiring, and keyboard-toggleable half-hour buttons with explicit day/time aria labels. Validation: `pnpm exec eslint components/coach-settings-page-client.tsx components/weekly-availability-grid.tsx` and `pnpm typecheck` passed. Next suggested task: task 4, club coaches calendar grid action text/status cleanup and explicit helper copy.

Current owner: agent-4
Current focus: task 4 — club calendar grid action text/status cleanup and helper copy
Last result: task 3 done in `components/coach-settings-page-client.tsx` and `components/weekly-availability-grid.tsx`; availability now exposes a live autosave status, semantic grid fieldset/help/error wiring, and keyboard-toggleable half-hour buttons with explicit day/time aria labels. Validation: `pnpm exec eslint components/coach-settings-page-client.tsx components/weekly-availability-grid.tsx` and `pnpm typecheck` passed. Next suggested task: finish task 4 in `components/club-coaches-page-client.tsx`.


Successfully replaced 1 block(s) in /home/sebas/runtime/ralph-loops/ballbox-a11y-20260517-120638/STATE.md.

    ...
  7 Rule: work only in /home/sebas/work/projects/ballbox.
  8 Rule: do not push or merge.
  9
 10 Current owner: agent-4
-11 Current focus: iteration 4
-12 Last result: task 3 done in `components/coach-settings-page-client.tsx` and `components/weekly-availability-grid.tsx`; availability now exposes a live autosave status, semantic grid fieldset/help/error wiring, and keyboard-toggleable half-hour buttons with explicit day/time aria labels. Validation: `pnpm exec eslint components/coach-settings-page-client.tsx components/weekly-availability-grid.tsx` and `pnpm typecheck` passed. Next suggested task: task 4, club coaches calendar grid action text/status cleanup and explicit helper copy.
+11 Current focus: task 4 — club calendar grid action text/status cleanup and helper copy
+12 Last result: task 3 done in `components/coach-settings-page-client.tsx` and `components/weekly-availability-grid.tsx`; availability now exposes a live autosave status, semantic grid fieldset/help/error wiring, and keyboard-toggleable half-hour buttons with explicit day/time aria labels. Validation: `pnpm exec eslint components/coach-settings-page-client.tsx components/weekly-availability-grid.tsx` and `pnpm typecheck` passed. Next suggested task: finish task 4 in `components/club-coaches-page-client.tsx`.


**Implementing calendar accessibility improvements**

I need to add some elements to enhance the calendar's accessibility features. The button text seems unclear, so I might include a section explaining its click action. Adding visually hidden captions could also be beneficial. For semantic HTML, I should consider a role grid for the interactive elements. The task points to cleaning up action text and adding helper text. I’ll implement IDs for live status and include helper text above the day selector while ensuring the status region summarizes selected options effectively.

**Enhancing calendar button text**

I think I should add secondary visible text for the calendar slots. For empty slots, it could say "Cargar clase aquí," and for occupied ones, maybe "Usar este horario en carga manual." The task emphasizes cleaning up status and action text, so it might be worth looking into titles for the buttons. Adding concise action lines would help too. I’ll also need to incorporate some helper copy for the day selector to ensure everything is clear and accessible.

  const selectedCalendarDay = calendarDays.find((day) => day.date === selectedCalendarDate) ?? calendarDays[0] ?? null;
  const totalCalendarCells = selectedCalendarDay?.totalCells ?? 0;
  const totalCoveredCalendarCells = selectedCalendarDay?.coveredCells ?? 0;
  const totalBlankCalendarCells = Math.max(0, totalCalendarCells - totalCoveredCalendarCells);
  const calendarWeekSlots = selectedCalendarDay?.slots ?? [];
  const calendarSummaryId = "club-calendar-summary";
  const calendarHelperId = "club-calendar-helper";
  const calendarSelectionSummary = selectedCalendarVenue
    ? [
        `Sede ${selectedCalendarVenue.name}`,
        selectedCalendarDay ? `dia ${formatCalendarDayLabel(selectedCalendarDay.date)}` : null,
        calendarMode === "availability" ? "modo disponibilidad" : "modo reservas",
        effectiveCalendarCoachFilter === "all"
          ? "todos los profesores del club"
          : `profesor ${calendarCoachTagOptions.find((coach) => coach.id === effectiveCalendarCoachFilter)?.name ?? "seleccionado"}`,
      ]
        .filter(Boolean)
        .join(" · ")
    : "Sin sede seleccionada";

Selecciona el dia

Despues ves todas las canchas de la sede como columnas.

                      Cada bloque abre la carga manual con sede, cancha, fecha y hora ya completas.

                      {selectedCalendarCourts.length} canchas

-
-
-                      {selectedCalendarCourts.length} canchas
-
-

                   <fieldset className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7">

                                <button
                                  key={`${court.id}-${slot}`}
                                  type="button"
                                  disabled={!slotEnabled}
                                  aria-label={`${court.name}, ${formatSlotLabel(slot)}, ${
                                    !slotEnabled
                                      ? "cerrado para coaches"
                                      : block
                                        ? calendarMode === "availability"
                                          ? "cubierta por profesor"
                                          : `reserva cargada${block.playerName ? ` para ${block.playerName}` : ""}`
                                        : calendarMode === "availability"
                                          ? "sin coach cubriendo"
                                          : "sin reserva cargada"
                                  }`}

                                <button
                                  key={`${court.id}-${slot}`}
                                  type="button"
                                  disabled={!slotEnabled}
                                  aria-describedby={calendarHelperId}
                                  aria-label={`${court.name}, ${formatSlotLabel(slot)}, ${
                                    !slotEnabled
                                      ? "cerrado para coaches"
                                      : block
                                        ? calendarMode === "availability"
                                          ? `cubierta por ${block.coachName ?? "profesor"}`
                                          : `reserva cargada${block.playerName ? ` para ${block.playerName}` : ""}`
                                        : calendarMode === "availability"
                                          ? "sin coach cubriendo"
                                          : "sin reserva cargada"
                                  }. ${slotEnabled ? "Abrir carga manual para este horario" : "Horario no disponible"}`}

{!slotEnabled ? (

Cerrado para coaches

                                  ) : block ? (
                                    <>

                                        {calendarMode === "availability" ? "Cubierta por profesor" : "Reserva cargada"}

                                      {calendarMode === "reservation" && block.playerName ? (

{block.playerName}

                                      ) : null}

{formatMoney(visibleSlotPriceMinor)}

                                        {block.startTime} - {block.endTime}

                                    </>
                                  ) : (
                                    <>

Hueco

                                        {calendarMode === "availability" ? "Sin coach cubriendo" : "Sin reserva cargada"}

{formatMoney(visibleSlotPriceMinor)}

                                    </>
                                  )}

{!slotEnabled ? (
                                    <>

Cerrado para coaches

Sin accion disponible.

                                    </>
                                  ) : block ? (
                                    <>

                                        {calendarMode === "availability" ? "Cubierta" : "Reserva cargada"}

                                        {calendarMode === "availability"
                                          ? block.coachName ?? "Profesor asignado"
                                          : block.playerName ?? "Alumno sin nombre visible"}

{formatMoney(visibleSlotPriceMinor)}

                                        {block.startTime} - {block.endTime}

Abrir carga manual en este horario.

                                    </>
                                  ) : (
                                    <>

Hueco

                                        {calendarMode === "availability" ? "Sin coach cubriendo" : "Sin reserva cargada"}

{formatMoney(visibleSlotPriceMinor)}

Abrir carga manual en este horario.

                                    </>
                                  )}