8:import { Button } from "@/components/ui/button";
17:} from "@/lib/contracts/coach-students";
27:type CoachPackage = CoachSettingsResponse["commercial"]["packages"][number];
29:type CoachPageView = "calendar" | "performance" | "configuration" | "students";
65:  whatsappPhone: string;
66:  telegramHandle: string;
70:  whatsappPhone: string;
71:  telegramHandle: string;
76:type AutoSaveState = "idle" | "saving" | "saved" | "error";
84:    clientId: createLocalId("package"),
85:    id: pkg?.id ?? createLocalId("package-id"),
90:    format: pkg?.format ?? "individual",
91:    active: pkg?.active ?? true,
95:function formatMoney(value: number) {
99:function formatDuration(value: number) {
103:function formatAverageReview(value: number | null) {
111:function packageSignature(packages: CoachPackageDraft[]) {
113:    packages.map((pkg) => ({
118:      format: pkg.format,
119:      active: pkg.active,
146:function formatDateTime(value: string) {
152:  }).format(new Date(value));
155:function formatLongDate(value: string) {
160:  }).format(new Date(`${value}T12:00:00`));
163:function formatShortDateLabel(value: string) {
167:  }).format(new Date(`${value}T12:00:00`));
178:function formatBookedHours(minutes: number) {
227:function formatSlotLabel(value: string) {
280:  if (state === "error") return "Error al guardar";
286:    whatsappPhone: player.whatsappPhone ?? "",
287:    telegramHandle: player.telegramHandle ?? "",
307:  const firstPackage = settings.commercial.packages.find((pkg) => pkg.active) ?? settings.commercial.packages[0] ?? null;
328:    whatsappPhone: "",
329:    telegramHandle: "",
341:    if (nextView === "performance" || nextView === "configuration" || nextView === "students") return nextView;
348:  const [packagesDraft, setPackagesDraft] = useState<CoachPackageDraft[]>([]);
349:  const [studentsPanel, setStudentsPanel] = useState<CoachStudentsResponse | null>(null);
366:  const [studentsLoading, setStudentsLoading] = useState(true);
368:  const [studentsError, setStudentsError] = useState<string | null>(null);
389:    if (nextView === "performance" || nextView === "configuration" || nextView === "students") {
412:    handleViewChange("students");
428:    setPackagesDraft(nextSettings.commercial.packages.map((pkg) => createPackageDraft(pkg)));
434:      const response = await fetch("/api/coaches/students", { cache: "no-store" });
439:        setStudentsError(response.status === 401 ? null : "No pudimos cargar tus alumnos.");
447:        for (const student of parsed.students) {
468:        setPageError(response.status === 401 ? null : "No pudimos cargar tu panel.");
492:    if (!settings || !studentsPanel) return;
495:      const nextDefaults = createStudentBookingDraft(settings, studentsPanel);
496:      const playerStillExists = studentsPanel.players.some((player) => player.id === current.playerId);
497:      const venueStillExists = studentsPanel.venues.some((venue) => venue.id === current.venueId);
509:  }, [settings, studentsPanel]);
511:  const activeStudentBookings = useMemo(
512:    () => studentsPanel?.bookings.filter((booking) => booking.status !== "canceled") ?? [],
513:    [studentsPanel]
516:  const trackedStudents = useMemo(() => studentsPanel?.students ?? [], [studentsPanel]);
520:    () => activeStudentBookings.filter((booking) => currentWeekDateSet.has(buildDateKey(booking.startsAt))),
521:    [activeStudentBookings, currentWeekDateSet]
686:      packageSignature(settings.commercial.packages.map((pkg) => createPackageDraft(pkg))) !== packageSignature(packagesDraft));
752:          setAvailabilitySaveState("error");
773:        setAvailabilitySaveState("error");
802:          setClubsSaveState("error");
815:        setClubsSaveState("error");
839:          packages: packagesDraft.map((pkg) => ({
845:            format: pkg.format,
846:            active: pkg.active,
855:          setCommercialSaveState("error");
869:        setCommercialSaveState("error");
934:  }, [packagesDraft, mercadoPagoHandle, commercialDirty, settings, loading]);
937:    if (!studentsPanel || !settings) return;
943:      const response = await fetch("/api/coaches/students", {
962:        if (payload?.error === "BOOKING_OVERLAP") {
964:        } else if (payload?.error === "BOOKING_OUTSIDE_AVAILABILITY") {
966:        } else if (payload?.error === "PLAYER_NOT_FOUND") {
968:        } else if (payload?.error === "VENUE_NOT_FOUND") {
1000:      const response = await fetch("/api/coaches/students/players", {
1005:          whatsappPhone: studentCreateDraft.whatsappPhone.trim() || null,
1006:          telegramHandle: studentCreateDraft.telegramHandle.trim() || null,
1037:        whatsappPhone: current[playerId]?.whatsappPhone ?? "",
1038:        telegramHandle: current[playerId]?.telegramHandle ?? "",
1057:      const response = await fetch(`/api/coaches/students/players/${playerId}`, {
1061:          whatsappPhone: draft.whatsappPhone.trim() || null,
1062:          telegramHandle: draft.telegramHandle.trim() || null,
1068:        const message = payload?.details?.message ?? "No pudimos guardar el contacto del alumno.";
1069:        setStudentContactSaveStates((current) => ({ ...current, [playerId]: "error" }));
1078:      setStudentContactSaveStates((current) => ({ ...current, [playerId]: "error" }));
1081:        [playerId]: "No pudimos guardar el contacto del alumno.",
1089:    return `Tu precio por clase: ${formatMoney(price)}`;
1116:  const availabilityStatusId = "coach-availability-status";
1120:  const availabilityErrorId = "coach-availability-error";
1122:    !!studentsPanel &&
1123:    studentsPanel.venues.length > 0 &&
1124:    studentsPanel.players.length > 0 &&
1125:    !studentsPanel.summary.note &&
1137:            active: view === "calendar",
1142:            active: view === "performance",
1146:            label: "Alumnos",
1147:            active: view === "students",
1148:            onClick: () => handleViewChange("students"),
1152:            active: view === "configuration",
1165:            <button
1166:              type="button"
1174:                  Te llevamos a Alumnos para cargar una clase nueva, elegir un alumno existente
1198:            </button>
1223:                  {formatBookedHours(currentWeekSessionMinutes)}
1233:            {studentsLoading ? (
1235:            ) : studentsError ? (
1236:
{studentsError}

1237:            ) : studentsPanel?.summary.note ? (
1239:                {studentsPanel.summary.note}
1259:
{formatLongDate(dateKey)}

1270:                            {formatSlotLabel(slot)}
1283:                                  <button
1284:                                    type="button"
1330:                                  </button>
1371:                          {formatLongDate(selectedCalendarSession.dateKey)} · {selectedCalendarSession.startTime} - {selectedCalendarSession.endTime}
1402:                        const whatsappHref = buildWhatsAppHref(participant.player.whatsappPhone);
1403:                        const telegramHref = buildTelegramHref(participant.player.telegramHandle);
1427:                              {whatsappHref ? (
1429:                                  href={whatsappHref}
1437:                              {telegramHref ? (
1439:                                  href={telegramHref}
1496:                        {formatAverageReview(clubItem.averageReview)}
1512:
{formatMoney(clubItem.listedRevenue)}

1516:
{formatMoney(clubItem.estimatedClubFees)}

1531:                {formatShortDateLabel(currentWeekDays[0] ?? todayDateValue())} - {formatShortDateLabel(currentWeekDays[6] ?? todayDateValue())}
1538:
{formatMoney(currentWeekEarnings)}

1545:
Alumnos semana

1550:
{formatBookedHours(currentWeekMinutes)}

1585:                      {formatMoney(row.weekRevenue)}
1654:                    clubsSaveState === "error"
1718:                      <button
1720:                        type="button"
1746:                      </button>
1768:                role="status"
1771:                  availabilitySaveState === "error"
1843:                <button
1844:                  type="button"
1849:                </button>
1851:                  role="status"
1854:                    commercialSaveState === "error"
1867:              {packagesDraft.length === 0 ? (
1873:              {packagesDraft.map((pkg, packageIndex) => {
1874:                const packageTitle = pkg.title.trim().length > 0 ? pkg.title : `Paquete ${packageIndex + 1}`;
1875:                const packageBaseId = `coach-package-${pkg.clientId}`;
1876:                const formatHelpId = `${packageBaseId}-format-help`;
1877:                const studentHelpId = `${packageBaseId}-students-help`;
1882:                      <legend className="text-sm font-medium text-white">{packageTitle}</legend>
1883:                      <button
1884:                        type="button"
1889:                      </button>
1921:                              {formatDuration(value)}
1930:                          min={pkg.format === "group" ? 2 : 1}
1935:                              maxStudents: Math.max(pkg.format === "group" ? 2 : 1, Number(event.target.value || (pkg.format === "group" ? 2 : 1))),
1938:                          disabled={pkg.format === "individual"}
1948:

1951:

1954:                              pkg.format === "individual"
1961:                              name={`${packageBaseId}-format`}
1962:                              checked={pkg.format === "individual"}
1963:                              onChange={() => updatePackage(pkg.clientId, { format: "individual", maxStudents: 1 })}
1970:                              pkg.format === "group"
1977:                              name={`${packageBaseId}-format`}
1978:                              checked={pkg.format === "group"}
1979:                              onChange={() => updatePackage(pkg.clientId, { format: "group", maxStudents: pkg.maxStudents < 2 ? 4 : pkg.maxStudents })}
1989:                          pkg.active
1996:                          checked={pkg.active}
1997:                          onChange={(event) => updatePackage(pkg.clientId, { active: event.target.checked })}
2001:                        {pkg.active ? "Si" : "No"}
2006:                      {pkg.format === "individual" ? "Las clases individuales usan un solo alumno." : "Para clases grupales define el maximo de alumnos."}
2052:
Alumnos

2059:                <Button type="button" className="h-11 rounded-xl px-5 text-sm font-medium" onClick={openStudentsComposer}>
2065:            {studentsLoading ? (
2067:            ) : studentsError ? (
2068:
{studentsError}

2082:                        type="button"
2120:                          value={studentCreateDraft.whatsappPhone}
2122:                            setStudentCreateDraft((current) => ({ ...current, whatsappPhone: event.target.value }))
2130:                          value={studentCreateDraft.telegramHandle}
2132:                            setStudentCreateDraft((current) => ({ ...current, telegramHandle: event.target.value }))
2193:                          {studentsPanel?.players.map((player) => (
2210:                          {studentsPanel?.venues.map((venue) => (
2249:                              {formatDuration(value)}
2287:                        type="button"
2298:                {studentsPanel?.summary.note ? (
2300:                    {studentsPanel.summary.note}
2303:                {studentsPanel && studentsPanel.players.length === 0 ? (
2334:                      const whatsappHref = buildWhatsAppHref(student.player.whatsappPhone);
2335:                      const telegramHref = buildTelegramHref(student.player.telegramHandle);
2336:                      const contactBaseId = `student-contact-${student.player.id}`;
2337:                      const whatsappInputId = `${contactBaseId}-whatsapp`;
2338:                      const telegramInputId = `${contactBaseId}-telegram`;
2339:                      const contactHelpId = `${contactBaseId}-help`;
2340:                      const contactStatusId = `${contactBaseId}-status`;
2341:                      const contactErrorId = `${contactBaseId}-error`;
2342:                      const contactState = studentContactSaveStates[student.player.id] ?? "idle";
2343:                      const contactError = studentContactErrors[student.player.id] ?? null;
2344:                      const describedByIds = [contactHelpId, contactStatusId, contactError ? contactErrorId : null]
2362:
2366:                                    id={whatsappInputId}
2367:                                    value={draft.whatsappPhone}
2368:                                    onChange={(event) => updateStudentContactDraft(student.player.id, { whatsappPhone: event.target.value })}
2375:
2379:                                    id={telegramInputId}
2380:                                    value={draft.telegramHandle}
2381:                                    onChange={(event) => updateStudentContactDraft(student.player.id, { telegramHandle: event.target.value })}
2382:                                    placeholder="@telegram"
2388:

2391:

2392:                                {getAutoSaveLabel(contactState)}
2394:                              {contactError ? (
2395:

2396:                                  {contactError}
2403:                            {student.nextLessonAt ? formatDateTime(student.nextLessonAt) : "Sin clase futura"}
2406:                            {student.lastLessonAt ? formatDateTime(student.lastLessonAt) : "Sin historial"}
2412:                                type="button"
2417:                                {savingStudentContactId === student.player.id ? "Guardando..." : "Guardar"}
2419:                              {whatsappHref ? (
2421:                                  href={whatsappHref}
2429:                              {telegramHref ? (
2431:                                  href={telegramHref}


  const minutes = String(date.getMinutes()).padStart(2, "0");
  return `${hours}:${minutes}`;
}

function getGenderLabel(value: CoachStudentsResponse["players"][number]["gender"]) {
  if (value === "female") return "Mujer";
  if (value === "male") return "Hombre";
  if (value === "other") return "Otro";
  return "Sin dato";
}

function getStudentAgeLabel(birthYear: number | null) {
  if (!birthYear) return "Edad sin dato";
  const age = new Date().getFullYear() - birthYear;
  return age > 0 ? `${age} anos` : "Edad sin dato";
}

function getAutoSaveLabel(state: AutoSaveState) {
  if (state === "saving") return "Guardando...";
  if (state === "saved") return "Guardado";
  if (state === "error") return "Error al guardar";
  return "Se guarda automatico";
}

function createStudentContactDraft(player: CoachStudentsResponse["players"][number]): CoachStudentContactDraft {
  return {
    whatsappPhone: player.whatsappPhone ?? "",
    telegramHandle: player.telegramHandle ?? "",
  };
}

function buildWhatsAppHref(value: string | null) {
  if (!value) return null;
  const digits = value.replace(/\D/g, "");
  return digits.length > 0 ? `https://wa.me/${digits}` : null;
}

function buildTelegramHref(value: string | null) {
  if (!value) return null;
  const handle = value.replace(/^@/, "").trim();
  return handle.length > 0 ? `https://t.me/${handle}` : null;
}

function createStudentBookingDraft(
  settings: CoachSettingsResponse,
  panel: CoachStudentsResponse
): CoachStudentBookingDraft {
  const firstPackage = settings.commercial.packages.find((pkg) => pkg.active) ?? settings.commercial.packages[0] ?? null;
  const firstAvailability =
    settings.availability.items[0] ??
    settings.availability.context.allowedHours[0] ??
    null;

  return {
    playerId: panel.players[0]?.id ?? "",
    venueId: panel.venues[0]?.id ?? "",
    date: todayDateValue(),
    startTime: firstAvailability?.startTime ?? "09:00",
    durationMinutes: firstPackage?.durationMinutes ?? 60,
    repeatWeeklyCount: 1,
    note: "",
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? "America/Montevideo",
  };
}

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

export function CoachSettingsPageClient() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [view, setView] = useState<CoachPageView>(() => {
    const nextView = searchParams.get("view");
    if (nextView === "performance" || nextView === "configuration" || nextView === "students") return nextView;
    return "calendar";
  });
  const [settings, setSettings] = useState<CoachSettingsResponse | null>(null);
  const [availabilityDraft, setAvailabilityDraft] = useState<WeeklyAvailabilityRange[]>([]);
  const [availabilityTimezone, setAvailabilityTimezone] = useState("America/Montevideo");
  const [clubSelectionDraft, setClubSelectionDraft] = useState<CoachClubSelection>({ mode: "all", clubIds: [] });
  const [packagesDraft, setPackagesDraft] = useState<CoachPackageDraft[]>([]);
  const [studentsPanel, setStudentsPanel] = useState<CoachStudentsResponse | null>(null);
  const [studentContactDrafts, setStudentContactDrafts] = useState<Record<string, CoachStudentContactDraft>>({});
  const [studentContactSaveStates, setStudentContactSaveStates] = useState<Record<string, AutoSaveState>>({});
  const [studentContactErrors, setStudentContactErrors] = useState<Record<string, string>>({});
  const [studentBookingDraft, setStudentBookingDraft] = useState<CoachStudentBookingDraft>({
    playerId: "",
    venueId: "",
    date: "",
    startTime: "09:00",
    durationMinutes: 60,
    repeatWeeklyCount: 1,
    note: "",
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? "America/Montevideo",
  });
  const [studentCreateDraft, setStudentCreateDraft] = useState<CoachStudentCreateDraft>(() => createStudentCreateDraft());
  const [mercadoPagoHandle, setMercadoPagoHandle] = useState("");
  const [loading, setLoading] = useState(true);
  const [studentsLoading, setStudentsLoading] = useState(true);
  const [pageError, setPageError] = useState<string | null>(null);
  const [studentsError, setStudentsError] = useState<string | null>(null);
  const [availabilityError, setAvailabilityError] = useState<string | null>(null);
  const [clubsError, setClubsError] = useState<string | null>(null);
  const [commercialError, setCommercialError] = useState<string | null>(null);
  const [studentBookingError, setStudentBookingError] = useState<string | null>(null);
  const [studentCreateError, setStudentCreateError] = useState<string | null>(null);
  const [, setSavingAvailability] = useState(false);
  const [, setSavingClubs] = useState(false);
  const [, setSavingCommercial] = useState(false);
  const [savingStudentBooking, setSavingStudentBooking] = useState(false);
  const [creatingStudent, setCreatingStudent] = useState(false);
  const [savingStudentContactId, setSavingStudentContactId] = useState<string | null>(null);
  const [selectedCalendarSessionKey, setSelectedCalendarSessionKey] = useState<string | null>(null);
  const [clubsSaveState, setClubsSaveState] = useState<AutoSaveState>("idle");
  const [availabilitySaveState, setAvailabilitySaveState] = useState<AutoSaveState>("idle");
  const [commercialSaveState, setCommercialSaveState] = useState<AutoSaveState>("idle");
  const clubsSaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const availabilitySaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const commercialSaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    const nextView = searchParams.get("view");
    if (nextView === "performance" || nextView === "configuration" || nextView === "students") {
      setView(nextView);
      return;
    }

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

  function handleViewChange(nextView: CoachPageView) {
    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 ? `/coaches/settings?${nextQuery}` : "/coaches/settings", { scroll: false });
  }

  function openStudentsComposer() {
    handleViewChange("students");
  }

  function syncDrafts(nextSettings: CoachSettingsResponse) {
    setSettings(nextSettings);
    setAvailabilityDraft(
      nextSettings.availability.items.map((item) => ({
        dayOfWeek: item.dayOfWeek,
        startTime: item.startTime,
        endTime: item.endTime,
      }))
    );
    setAvailabilityTimezone(
      nextSettings.availability.items[0]?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "America/Montevideo"
    );
    setClubSelectionDraft(nextSettings.externalClubs.selection);
    setPackagesDraft(nextSettings.commercial.packages.map((pkg) => createPackageDraft(pkg)));
    setMercadoPagoHandle(nextSettings.commercial.mercadoPagoHandle ?? "");
  }

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

      if (!response.ok) {
        setStudentsPanel(null);
        setStudentsError(response.status === 401 ? null : "No pudimos cargar tus alumnos.");
        return;
      }

      const parsed = coachStudentsResponseSchema.parse(payload);
      setStudentsPanel(parsed);
      setStudentContactDrafts((current) => {
        const nextDrafts: Record<string, CoachStudentContactDraft> = {};
        for (const student of parsed.students) {
          nextDrafts[student.player.id] = current[student.player.id] ?? createStudentContactDraft(student.player);
        }
        return nextDrafts;
      });
      setStudentsError(null);
    } catch {
      setStudentsPanel(null);
      setStudentsError("No pudimos cargar tus alumnos.");
    } finally {
      setStudentsLoading(false);
    }
  }

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

      if (!response.ok) {
        setSettings(null);
        setPageError(response.status === 401 ? null : "No pudimos cargar tu panel.");
        return;
      }

      const parsed = coachMeResponseSchema.parse(payload);
      syncDrafts(parsed);
      setPageError(null);
    } catch {
      setSettings(null);
      setPageError("No pudimos cargar tu panel.");
    } finally {
      setLoading(false);
    }
  }

  const loadSettingsEffect = useEffectEvent(loadSettings);
  const loadStudentsPanelEffect = useEffectEvent(loadStudentsPanel);

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

  useEffect(() => {
    if (!settings || !studentsPanel) return;

    setStudentBookingDraft((current) => {
      const nextDefaults = createStudentBookingDraft(settings, studentsPanel);
      const playerStillExists = studentsPanel.players.some((player) => player.id === current.playerId);
      const venueStillExists = studentsPanel.venues.some((venue) => venue.id === current.venueId);
      return {
        playerId: current.playerId && playerStillExists ? current.playerId : nextDefaults.playerId,
        venueId: current.venueId && venueStillExists ? current.venueId : nextDefaults.venueId,
        date: current.date || nextDefaults.date,
        startTime: current.startTime || nextDefaults.startTime,
        durationMinutes: current.durationMinutes || nextDefaults.durationMinutes,
        repeatWeeklyCount: current.repeatWeeklyCount || nextDefaults.repeatWeeklyCount,
        note: current.note,
        timezone: current.timezone || nextDefaults.timezone,
      };
    });
  }, [settings, studentsPanel]);

  const activeStudentBookings = useMemo(
    () => studentsPanel?.bookings.filter((booking) => booking.status !== "canceled") ?? [],
    [studentsPanel]
  );

  const trackedStudents = useMemo(() => studentsPanel?.students ?? [], [studentsPanel]);
  const currentWeekDays = useMemo(() => getCurrentWeekDays(), []);
  const currentWeekDateSet = useMemo(() => new Set(currentWeekDays), [currentWeekDays]);
  const currentWeekBookings = useMemo(
    () => activeStudentBookings.filter((booking) => currentWeekDateSet.has(buildDateKey(booking.startsAt))),
    [activeStudentBookings, currentWeekDateSet]
  );
  const currentWeekEarnings = useMemo(
    () => currentWeekBookings.reduce((total, booking) => total + (booking.coachPriceMinor ?? 0), 0),
    [currentWeekBookings]
  );
  const currentWeekMinutes = useMemo(
    () => currentWeekBookings.reduce((total, booking) => total + booking.durationMinutes, 0),
    [currentWeekBookings]
  );
  const currentWeekStudentCount = useMemo(
    () => new Set(currentWeekBookings.map((booking) => booking.player.id)).size,
    [currentWeekBookings]
  );
  const currentWeekPaidCount = useMemo(
    () => currentWeekBookings.filter((booking) => booking.paymentStatus === "paid").length,
    [currentWeekBookings]
  );
  const currentWeekPendingCount = useMemo(
    () => currentWeekBookings.filter((booking) => booking.paymentStatus === "pending").length,
    [currentWeekBookings]
  );
  const availabilityByDate = useMemo(() => {
    const grouped = new Map<string, WeeklyAvailabilityRange[]>();

    for (const availability of settings?.availability.items ?? []) {
      const dateKey = getDateForWeeklyAvailability(currentWeekDays, availability.dayOfWeek);
      const current = grouped.get(dateKey) ?? [];
      current.push({
        dayOfWeek: availability.dayOfWeek,
        startTime: availability.startTime,
        endTime: availability.endTime,
      });
      grouped.set(dateKey, current);
    }

    return grouped;
  }, [settings, currentWeekDays]);
  const availabilitySlotsByDate = useMemo(() => {
    const grouped = new Map<string, Set<string>>();

    for (const dateKey of currentWeekDays) {
      grouped.set(dateKey, new Set(buildDaySlots(availabilityByDate.get(dateKey) ?? [])));
    }

    return grouped;
  }, [availabilityByDate, currentWeekDays]);
  const coachCalendarSessions = useMemo(() => {
    const grouped = new Map<string, CoachCalendarSession>();

    for (const booking of currentWeekBookings) {
      const dateKey = buildDateKey(booking.startsAt);
      const startsAtMillis = new Date(booking.startsAt).getTime();
      const endsAtMillis = new Date(booking.endsAt).getTime();
      const sessionKey = [
        dateKey,
        booking.venue.id,
        booking.court?.id ?? "sin-cancha",
        startsAtMillis,
        endsAtMillis,
      ].join("|");

      const current = grouped.get(sessionKey) ?? {
        key: sessionKey,
        startsAt: booking.startsAt,
        endsAt: booking.endsAt,
        startTime: getLocalTimeValue(booking.startsAt),
        endTime: getLocalTimeValue(booking.endsAt),
        dateKey,
        durationMinutes: booking.durationMinutes,
        venue: booking.venue,
        court: booking.court,
        note: booking.note,
        participants: [],
      };

      current.note = current.note ?? booking.note;
      current.participants.push({
        bookingId: booking.id,
        player: booking.player,
        paymentStatus: booking.paymentStatus,
      });
      grouped.set(sessionKey, current);
    }

    return Array.from(grouped.values())
      .map((session) => ({
        ...session,
        participants: [...session.participants].sort((left, right) => left.player.name.localeCompare(right.player.name)),
      }))
      .sort((left, right) => new Date(left.startsAt).getTime() - new Date(right.startsAt).getTime());
  }, [currentWeekBookings]);
  const coachCalendarSessionsByDayAndSlot = useMemo(() => {
    const grouped = new Map<string, CoachCalendarSession>();

    for (const session of coachCalendarSessions) {
      for (const slot of listHalfHourSlots(session.startTime, session.endTime)) {
        grouped.set(`${session.dateKey}|${slot}`, session);
      }
    }

    return grouped;
  }, [coachCalendarSessions]);
  const calendarWeekSlots = useMemo(() => {
    const slotSet = new Set<string>();

    for (const dateKey of currentWeekDays) {
      for (const slot of buildDaySlots(availabilityByDate.get(dateKey) ?? [])) {
        slotSet.add(slot);
      }
    }

    for (const session of coachCalendarSessions) {
      for (const slot of listHalfHourSlots(session.startTime, session.endTime)) {
        slotSet.add(slot);
      }
    }

    return Array.from(slotSet).sort((left, right) => (parseTimeToMinutes(left) ?? 0) - (parseTimeToMinutes(right) ?? 0));
  }, [availabilityByDate, coachCalendarSessions, currentWeekDays]);
  const currentWeekSessionMinutes = useMemo(
    () => coachCalendarSessions.reduce((total, session) => total + session.durationMinutes, 0),
    [coachCalendarSessions]
  );
  const todaySessionCount = useMemo(
    () => coachCalendarSessions.filter((session) => session.dateKey === todayDateValue()).length,
    [coachCalendarSessions]
  );
  const selectedCalendarSession = useMemo(
    () => coachCalendarSessions.find((session) => session.key === selectedCalendarSessionKey) ?? null,
    [coachCalendarSessions, selectedCalendarSessionKey]
  );
  useEffect(() => {
    if (selectedCalendarSessionKey && coachCalendarSessions.some((session) => session.key === selectedCalendarSessionKey)) return;
    setSelectedCalendarSessionKey(coachCalendarSessions[0]?.key ?? null);
  }, [coachCalendarSessions, selectedCalendarSessionKey]);
  const studentAnalyticsRows = useMemo(
    () =>
      trackedStudents.map((student) => {
        const weekBookings = currentWeekBookings.filter((booking) => booking.player.id === student.player.id);

        return {
          student,
          weekClassCount: weekBookings.length,
          weekRevenue: weekBookings.reduce((total, booking) => total + (booking.coachPriceMinor ?? 0), 0),
          consistency: getStudentConsistencyLabel(student.lessonCount, student.upcomingLessonCount),
        };
      }),
    [trackedStudents, currentWeekBookings]
  );

  const selectionDirty = settings ? selectionSignature(settings.externalClubs.selection) !== selectionSignature(clubSelectionDraft) : false;
  const availabilityDirty =
    settings !== null &&
    availabilitySignature(
      settings.availability.items.map((item) => ({
        dayOfWeek: item.dayOfWeek,
        startTime: item.startTime,
        endTime: item.endTime,
      })),
      settings.availability.items[0]?.timezone ?? availabilityTimezone
    ) !== availabilitySignature(availabilityDraft, availabilityTimezone);
  const commercialDirty =
    settings !== null &&
    ((settings.commercial.mercadoPagoHandle ?? "") !== mercadoPagoHandle ||
      packageSignature(settings.commercial.packages.map((pkg) => createPackageDraft(pkg))) !== packageSignature(packagesDraft));

  function updatePackage(clientId: string, patch: Partial<CoachPackageDraft>) {
    setPackagesDraft((current) =>
      current.map((pkg) =>
        pkg.clientId === clientId
          ? {
              ...pkg,
              ...patch,
            }
          : pkg
      )
    );
  }

  function removePackage(clientId: string) {
    setPackagesDraft((current) => current.filter((pkg) => pkg.clientId !== clientId));
  }

  function addPackage() {
    setPackagesDraft((current) => [...current, createPackageDraft()]);
  }

  function toggleClub(clubId: string) {
    setClubSelectionDraft((current) => {
      if (current.mode !== "custom") return current;

      const selectedIds = new Set(current.clubIds);
      if (selectedIds.has(clubId)) {
        selectedIds.delete(clubId);
      } else {
        selectedIds.add(clubId);
      }

      return {
        ...current,
        clubIds: [...selectedIds],
      };
    });
  }

  async function handleSaveAvailability(options?: { background?: boolean }) {
    if (!settings) return;
    const background = options?.background ?? false;

    if (!background) {
      setSavingAvailability(true);
      setAvailabilityError(null);
    }

    try {
      const response = await fetch("/api/coaches/availability", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          items: availabilityDraft.map((item) => ({
            ...item,
            timezone: availabilityTimezone,
          })),
        }),
      });
      const payload = await response.json().catch(() => null);

      if (!response.ok) {
        if (!background) {
          setAvailabilityError(payload?.details?.message ?? "No pudimos guardar tus horarios.");
          setAvailabilitySaveState("error");
        }
        return;
      }

      if (!background) {
        const parsed = coachAvailabilityResponseSchema.parse(payload);
        setAvailabilityDraft(
          parsed.items.map((item) => ({


parsed.items.map((item) => ({
            dayOfWeek: item.dayOfWeek,
            startTime: item.startTime,
            endTime: item.endTime,
          }))
        );
        setAvailabilityTimezone(parsed.items[0]?.timezone ?? availabilityTimezone);
        await loadSettings();
        setAvailabilitySaveState("saved");
      }
    } catch {
      if (!background) {
        setAvailabilityError("No pudimos guardar tus horarios.");
        setAvailabilitySaveState("error");
      }
    } finally {
      if (!background) {
        setSavingAvailability(false);
      }
    }
  }

  async function handleSaveClubs(options?: { background?: boolean }) {
    if (!settings) return;
    const background = options?.background ?? false;

    if (!background) {
      setSavingClubs(true);
      setClubsError(null);
    }

    try {
      const response = await fetch("/api/coaches/clubs", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(clubSelectionDraft),
      });
      const payload = await response.json().catch(() => null);

      if (!response.ok) {
        if (!background) {
          setClubsError(payload?.details?.message ?? "No pudimos guardar tus clubes.");
          setClubsSaveState("error");
        }
        return;
      }

      if (!background) {
        coachClubsResponseSchema.parse(payload);
        await loadSettings();
        setClubsSaveState("saved");
      }
    } catch {
      if (!background) {
        setClubsError("No pudimos guardar tus clubes.");
        setClubsSaveState("error");
      }
    } finally {
      if (!background) {
        setSavingClubs(false);
      }
    }
  }

  async function handleSaveCommercial(options?: { background?: boolean }) {
    if (!settings) return;
    const background = options?.background ?? false;

    if (!background) {
      setSavingCommercial(true);
      setCommercialError(null);
    }

    try {
      const response = await fetch("/api/coaches/me", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          mercadoPagoHandle: mercadoPagoHandle.trim() || null,
          packages: packagesDraft.map((pkg) => ({
            id: pkg.id,
            title: pkg.title,
            price: pkg.price,
            durationMinutes: pkg.durationMinutes,
            maxStudents: pkg.maxStudents,
            format: pkg.format,
            active: pkg.active,
          })),
        }),
      });
      const payload = await response.json().catch(() => null);

      if (!response.ok) {
        if (!background) {
          setCommercialError(payload?.details?.message ?? "No pudimos guardar tu configuracion comercial.");
          setCommercialSaveState("error");
        }
        return;
      }

      if (!background) {
        const parsed = coachMeResponseSchema.parse(payload);
        syncDrafts(parsed);
        setCommercialError(null);
        setCommercialSaveState("saved");
      }
    } catch {
      if (!background) {
        setCommercialError("No pudimos guardar tu configuracion comercial.");
        setCommercialSaveState("error");
      }
    } finally {
      if (!background) {
        setSavingCommercial(false);
      }
    }
  }

  const handleSaveClubsEffect = useEffectEvent(handleSaveClubs);
  const handleSaveAvailabilityEffect = useEffectEvent(handleSaveAvailability);
  const handleSaveCommercialEffect = useEffectEvent(handleSaveCommercial);

  useEffect(
    () => () => {
      if (clubsSaveTimer.current) {
        clearTimeout(clubsSaveTimer.current);
        clubsSaveTimer.current = null;
        void handleSaveClubsEffect({ background: true });
      }
      if (availabilitySaveTimer.current) {
        clearTimeout(availabilitySaveTimer.current);
        availabilitySaveTimer.current = null;
        void handleSaveAvailabilityEffect({ background: true });
      }
      if (commercialSaveTimer.current) {
        clearTimeout(commercialSaveTimer.current);
        commercialSaveTimer.current = null;
        void handleSaveCommercialEffect({ background: true });
      }
    },
    []
  );

  useEffect(() => {
    if (!settings || loading || !selectionDirty) return;

    setClubsSaveState("saving");
    if (clubsSaveTimer.current) clearTimeout(clubsSaveTimer.current);
    clubsSaveTimer.current = setTimeout(() => {
      clubsSaveTimer.current = null;
      void handleSaveClubsEffect();
    }, 700);
  }, [clubSelectionDraft, selectionDirty, settings, loading]);

  useEffect(() => {
    if (!settings || loading || selectionDirty || !availabilityDirty) return;

    setAvailabilitySaveState("saving");
    if (availabilitySaveTimer.current) clearTimeout(availabilitySaveTimer.current);
    availabilitySaveTimer.current = setTimeout(() => {
      availabilitySaveTimer.current = null;
      void handleSaveAvailabilityEffect();
    }, 700);
  }, [availabilityDraft, availabilityTimezone, availabilityDirty, selectionDirty, settings, loading]);

  useEffect(() => {
    if (!settings || loading || !commercialDirty) return;

    setCommercialSaveState("saving");
    if (commercialSaveTimer.current) clearTimeout(commercialSaveTimer.current);
    commercialSaveTimer.current = setTimeout(() => {
      commercialSaveTimer.current = null;
      void handleSaveCommercialEffect();
    }, 700);
  }, [packagesDraft, mercadoPagoHandle, commercialDirty, settings, loading]);

  async function handleCreateStudentBooking() {
    if (!studentsPanel || !settings) return;

    setSavingStudentBooking(true);
    setStudentBookingError(null);

    try {
      const response = await fetch("/api/coaches/students", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          playerId: studentBookingDraft.playerId,
          venueId: studentBookingDraft.venueId,
          startsAt: new Date(`${studentBookingDraft.date}T${studentBookingDraft.startTime}:00`).toISOString(),
          durationMinutes: studentBookingDraft.durationMinutes,
          timezone: studentBookingDraft.timezone,
          note: studentBookingDraft.note.trim() || null,
          recurrence:
            studentBookingDraft.repeatWeeklyCount > 1
              ? { frequency: "weekly", occurrences: studentBookingDraft.repeatWeeklyCount }
              : undefined,
        }),
      });
      const payload = await response.json().catch(() => null);

      if (!response.ok) {
        if (payload?.error === "BOOKING_OVERLAP") {
          setStudentBookingError("Ya tienes una clase cargada en ese horario.");
        } else if (payload?.error === "BOOKING_OUTSIDE_AVAILABILITY") {
          setStudentBookingError("Ese horario debe caer dentro de tu disponibilidad activa y de la sede elegida.");
        } else if (payload?.error === "PLAYER_NOT_FOUND") {
          setStudentBookingError("Ese alumno ya no existe.");
        } else if (payload?.error === "VENUE_NOT_FOUND") {
          setStudentBookingError("Esa sede ya no esta disponible para ti.");
        } else {
          setStudentBookingError(payload?.details?.message ?? "No pudimos guardar la clase tomada.");
        }
        return;
      }

      coachStudentBookingMutationResponseSchema.parse(payload);
      await loadStudentsPanel();
      setStudentBookingDraft((current) => ({
        ...current,
        repeatWeeklyCount: 1,
        note: "",
      }));
    } catch {
      setStudentBookingError("No pudimos guardar la clase tomada.");
    } finally {
      setSavingStudentBooking(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/coaches/students/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 loadStudentsPanel();
      setStudentBookingDraft((current) => ({
        ...current,
        playerId: parsed.item.id,
      }));
      setStudentCreateDraft(createStudentCreateDraft());
    } catch {
      setStudentCreateError("No pudimos crear el alumno.");
    } finally {
      setCreatingStudent(false);
    }
  }

  function updateStudentContactDraft(playerId: string, patch: Partial<CoachStudentContactDraft>) {
    setStudentContactDrafts((current) => ({
      ...current,
      [playerId]: {
        whatsappPhone: current[playerId]?.whatsappPhone ?? "",
        telegramHandle: current[playerId]?.telegramHandle ?? "",
        ...patch,
      },
    }));
  }

  async function handleSaveStudentContact(playerId: string) {
    const draft = studentContactDrafts[playerId];
    if (!draft) return;

    setSavingStudentContactId(playerId);
    setStudentContactSaveStates((current) => ({ ...current, [playerId]: "saving" }));
    setStudentContactErrors((current) => {
      const next = { ...current };
      delete next[playerId];
      return next;
    });

    try {
      const response = await fetch(`/api/coaches/students/players/${playerId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          whatsappPhone: draft.whatsappPhone.trim() || null,
          telegramHandle: draft.telegramHandle.trim() || null,
        }),
      });
      const payload = await response.json().catch(() => null);

      if (!response.ok) {
        const message = payload?.details?.message ?? "No pudimos guardar el contacto del alumno.";
        setStudentContactSaveStates((current) => ({ ...current, [playerId]: "error" }));
        setStudentContactErrors((current) => ({ ...current, [playerId]: message }));
        return;
      }

      coachStudentContactMutationResponseSchema.parse(payload);
      setStudentContactSaveStates((current) => ({ ...current, [playerId]: "saved" }));
      await loadStudentsPanel();
    } catch {
      setStudentContactSaveStates((current) => ({ ...current, [playerId]: "error" }));
      setStudentContactErrors((current) => ({
        ...current,
        [playerId]: "No pudimos guardar el contacto del alumno.",
      }));
    } finally {
      setSavingStudentContactId(null);
    }
  }

  function getPackageFinalPriceNote(price: number) {
    return `Tu precio por clase: ${formatMoney(price)}`;
  }

  if (loading) {
    return
Cargando panel del coach...
;
  }

  if (!settings) {
    return (

Acceso coach

          Entra desde /coaches para hacer sign in o sign up.

        <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>
        {pageError ?
{pageError}
 : null}

    );
  }

  const { coach, dashboard } = settings;
  const { context } = settings.availability;
  const availabilityStatusId = "coach-availability-status";
  const availabilityHelpId = "coach-availability-help";
  const availabilitySummaryId = "coach-availability-summary";
  const availabilityInstructionsId = "coach-availability-instructions";
  const availabilityErrorId = "coach-availability-error";
  const canSubmitStudentBooking =
    !!studentsPanel &&
    studentsPanel.venues.length > 0 &&
    studentsPanel.players.length > 0 &&
    !studentsPanel.summary.note &&
    studentBookingDraft.playerId.length > 0 &&
    studentBookingDraft.venueId.length > 0 &&
    studentBookingDraft.date.length > 0 &&
    studentBookingDraft.startTime.length > 0;
  return (

      <AuthenticatedWorkspaceNavbar
        email={coach.email}
        sections={[
          {
            label: "Calendario",
            active: view === "calendar",
            onClick: () => handleViewChange("calendar"),
          },
          {
            label: "Performance",
            active: view === "performance",
            onClick: () => handleViewChange("performance"),
          },
          {
            label: "Alumnos",
            active: view === "students",
            onClick: () => handleViewChange("students"),
          },
          {
            label: "Horarios y Clases",
            active: view === "configuration",
            onClick: () => handleViewChange("configuration"),
          },
        ]}
        onSignOut={async () => {
          await fetch("/api/coaches/logout", { method: "POST" });
          window.location.href = "/coaches";
        }}
      />

      {view === "calendar" ? (
        <>

            <button
              type="button"
              onClick={openStudentsComposer}
              className="group flex w-full flex-col justify-between gap-6 rounded-[1.5rem] border border-[#c4d600]/20 bg-[radial-gradient(circle_at_top_left,_rgba(196,214,0,0.16),_transparent_48%),linear-gradient(180deg,rgba(255,255,255,0.02),rgba(255,255,255,0))] p-6 text-left transition hover:border-[#c4d600]/40 hover:bg-[radial-gradient(circle_at_top_left,_rgba(196,214,0,0.22),_transparent_52%),linear-gradient(180deg,rgba(255,255,255,0.03),rgba(255,255,255,0))]"
            >

Accion rapida

Agregar clase

                  Te llevamos a Alumnos para cargar una clase nueva, elegir un alumno existente
                  o crear uno en el momento.

                    {context.mode === "venue"
                      ? coach.venue
                        ? `Sede ${coach.venue.name}`
                        : "Pendiente de sede"
                      : context.mode === "external-clubs"
                        ? "Coach externo"
                        : "Esperando habilitacion"}

                    {summarizeAllowedHours(context.allowedHours)}

                  Ir a alumnos

            </button>

Calendario

Tus alumnos por semana y por hora

                {coachCalendarSessions.length} clases cargadas

Clases hoy

{todaySessionCount}

Semana

{coachCalendarSessions.length}

Horas semana

                  {formatBookedHours(currentWeekSessionMinutes)}

Pagos

{currentWeekPaidCount}

{currentWeekPendingCount} pendientes

            {studentsLoading ? (

Cargando alumnos y agenda...

            ) : studentsError ? (

{studentsError}

            ) : studentsPanel?.summary.note ? (

                {studentsPanel.summary.note}

            ) : calendarWeekSlots.length === 0 ? (

                Todavia no tienes horarios ni clases en esta semana.

            ) : (

                          Hora

                        {currentWeekDays.map((dateKey) => {
                          const daySessions = coachCalendarSessions.filter((session) => session.dateKey === dateKey).length;

                          return (

{formatLongDate(dateKey)}

{daySessions} clases

                          );
                        })}

                      {calendarWeekSlots.map((slot) => (

                            {formatSlotLabel(slot)}

                          {currentWeekDays.map((dateKey) => {
                            const session = coachCalendarSessionsByDayAndSlot.get(`${dateKey}|${slot}`) ?? null;
                            const isAllowed = availabilitySlotsByDate.get(dateKey)?.has(slot) ?? false;

                            if (session) {
                              const paidCount = session.participants.filter((participant) => participant.paymentStatus === "paid").length;
                              const pendingCount = session.participants.length - paidCount;
                              const isSelected = selectedCalendarSession?.key === session.key;

                              return (
                                <td key={`${dateKey}-${slot}`} className="border-b border-l border-white/10 p-2 align-top">
                                  <button
                                    type="button"
                                    onClick={() => setSelectedCalendarSessionKey(session.key)}
                                    className={`flex min-h-[10.5rem] w-full flex-col rounded-[1rem] border p-3 text-left transition ${
                                      isSelected
                                        ? "border-[#c4d600]/45 bg-[#1b2411]"
                                        : "border-[#c4d600]/25 bg-[#141b10] hover:border-[#c4d600]/40"
                                    }`}
                                  >

                                          {session.participants.length > 1 ? `Grupal · ${session.participants.length} alumnos` : "Individual"}

                                          {session.venue.name}
                                          {session.court ? ` · ${session.court.name}` : ""}

                                        {paidCount}/{session.participants.length} pagos

                                      {session.participants.slice(0, 2).map((participant) => (

{participant.player.name}

                                            {getGenderLabel(participant.player.gender)} · {getStudentAgeLabel(participant.player.birthYear)} ·{" "}
                                            {participant.player.coachCategory ?? "Sin categoria"}

                                      ))}
                                      {session.participants.length > 2 ? (

+{session.participants.length - 2} alumnos mas

                                      ) : null}


{session.startTime} - {session.endTime}

                                      {pendingCount > 0 ? (

{pendingCount} reserva{pendingCount === 1 ? "" : "s"} sin pagar

                                      ) : (

Todo pago

                                      )}

                                  </button>

                              );
                            }

                            return (
                              <td key={`${dateKey}-${slot}`} className="border-b border-l border-white/10 p-2 align-top">

                                    {isAllowed ? "Disponible" : "Fuera de disponibilidad"}

                                    {isAllowed
                                      ? "Ese bloque esta abierto para que tomes una clase."
                                      : "No tienes horario abierto para dar clase en esta media hora."}

                            );
                          })}

                      ))}

                {selectedCalendarSession ? (

Detalle de clase

                          {selectedCalendarSession.participants.length > 1 ? "Clase grupal" : "Clase individual"}

                          {formatLongDate(selectedCalendarSession.dateKey)} · {selectedCalendarSession.startTime} - {selectedCalendarSession.endTime}

                          {selectedCalendarSession.venue.clubName} · {selectedCalendarSession.venue.name}
                          {selectedCalendarSession.court ? ` · ${selectedCalendarSession.court.name}` : ""}

                          {
                            selectedCalendarSession.participants.filter((participant) => participant.paymentStatus === "paid").length
                          }{" "}
                          pagos

                          {
                            selectedCalendarSession.participants.filter((participant) => participant.paymentStatus === "pending").length
                          }{" "}
                          pendientes

                    {selectedCalendarSession.note ? (

                        {selectedCalendarSession.note}

                    ) : null}

                      {selectedCalendarSession.participants.map((participant) => {
                        const whatsappHref = buildWhatsAppHref(participant.player.whatsappPhone);
                        const telegramHref = buildTelegramHref(participant.player.telegramHandle);

                        return (

{participant.player.name}

                                  {getGenderLabel(participant.player.gender)} · {getStudentAgeLabel(participant.player.birthYear)} ·{" "}
                                  {participant.player.coachCategory ?? "Sin categoria"}

                                {participant.paymentStatus === "paid" ? "Pago" : "Pendiente"}

                              {whatsappHref ? (

                                  WhatsApp

                              ) : null}
                              {telegramHref ? (

                                  Telegram

                              ) : null}

                        );
                      })}

                ) : null}

            )}

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

Clubs

Tu performance por club

                {dashboard.clubs.length} clubs

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

                  Todavia no tienes actividad en clubs. En configuracion puedes elegir donde vender y abrir tus horarios.

              ) : null}

              {dashboard.clubs.map((clubItem) => (

{clubItem.name}

                        {clubItem.venueNames.length > 0
                          ? clubItem.venueNames.join(" · ")
                          : clubItem.role === "club-coach"
                            ? "Sin sede asignada"
                            : "Sin sedes elegidas"}

                        {clubItem.role === "club-coach" ? "Coach del club" : "Coach externo"}

                        {formatAverageReview(clubItem.averageReview)}

Clases

{clubItem.publishedLessons}

Solicitudes

{clubItem.requestCount}

Valor publicado

{formatMoney(clubItem.listedRevenue)}

Fees del club

{formatMoney(clubItem.estimatedClubFees)}

              ))}

Performance

Como te esta yendo esta semana

                {formatShortDateLabel(currentWeekDays[0] ?? todayDateValue())} - {formatShortDateLabel(currentWeekDays[6] ?? todayDateValue())}

Ganaste esta semana

{formatMoney(currentWeekEarnings)}

Clases semana

{currentWeekBookings.length}

Alumnos semana

{currentWeekStudentCount}

Horas semana

{formatBookedHours(currentWeekMinutes)}

                    Alumno
                    Clases totales
                    Esta semana
                    Consistencia
                    Reviews
                    Ingreso semana

                  {studentAnalyticsRows.length === 0 ? (

                        Todavia no hay alumnos con seguimiento.

                  ) : null}

                  {studentAnalyticsRows.map((row) => (

{row.student.player.name}

{row.student.venueNames.join(" · ") || "Sin sede"}

                      {row.student.lessonCount}
                      {row.weekClassCount}
                      {row.consistency}
                      Sin resenas
                      {formatMoney(row.weekRevenue)}

                  ))}

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

Afiliacion

            {coach.club ? (
              <>

{coach.club.name}

                  {coach.venue ? `Trabajas desde la sede ${coach.venue.name}.` : "Todavia no te asignaron una sede dentro del club."}

                {coach.court ?
Cancha asignada: {coach.court.name}
 : null}
              </>
            ) : (
              <>

Coach independiente

                  Puedes abrir horarios en clubes que acepten profesores externos y vender ahi tus clases.

              </>
            )}

            {context.assignedVenue ? (

Tu sede

{context.assignedVenue.name}

Tu club define internamente el valor de la cancha para cada horario.

                    Cancelacion tardia: {context.assignedVenue.coachCancellationWindowHours} hs antes

Canchas habilitadas

                    {context.assignedVenue.availableCourts.length === 0 ? (
                      El club todavia no habilito canchas para coaches.
                    ) : (
                      context.assignedVenue.availableCourts.map((court) => (

                          {court.name} · {court.covered ? "techada" : "no techada"}

                      ))
                    )}

            ) : null}

          {coach.affiliation === "independent" ? (

Clubes externos

Donde quieres dar clases

                    Eliges en que clubes vender, y despues tu disponibilidad se adapta a los horarios que ellos habilitaron para coaches externos.

                  {getAutoSaveLabel(clubsSaveState)}

              <fieldset className="mt-5 flex flex-wrap gap-3">
                <legend className="sr-only">Modo de seleccion de clubes</legend>

                  <input
                    type="radio"
                    name="club-selection-mode"
                    value="all"
                    checked={clubSelectionDraft.mode === "all"}
                    onChange={() => setClubSelectionDraft({ mode: "all", clubIds: settings.externalClubs.clubs.map((club) => club.id) })}
                    className="sr-only"
                  />
                  Todos los clubes abiertos

                  <input
                    type="radio"
                    name="club-selection-mode"
                    value="custom"
                    checked={clubSelectionDraft.mode === "custom"}
                    onChange={() =>
                      setClubSelectionDraft({
                        mode: "custom",
                        clubIds: clubSelectionDraft.mode === "custom" ? clubSelectionDraft.clubIds : [],
                      })
                    }
                    className="sr-only"
                  />
                  Elegir manualmente

              </fieldset>

                {settings.externalClubs.clubs.length === 0 ? (

                    Todavia no hay clubes con sedes activas para coaches externos.

                ) : (
                  settings.externalClubs.clubs.map((clubItem) => {
                    const selected = clubSelectionDraft.mode === "all" || clubSelectionDraft.clubIds.includes(clubItem.id);

                    return (
                      <button
                        key={clubItem.id}
                        type="button"
                        disabled={clubSelectionDraft.mode !== "custom"}
                        aria-pressed={selected}
                        onClick={() => toggleClub(clubItem.id)}
                        className={`rounded-[1.25rem] border p-5 text-left transition ${
                          selected
                            ? "border-[#c4d600]/35 bg-[#13180f]"
                            : "border-white/10 bg-black/20 hover:border-white/20"
                        } ${clubSelectionDraft.mode !== "custom" ? "cursor-default" : ""}`}
                      >

{clubItem.name}

{clubItem.externalVenueCount} sedes externas habilitadas

                            {selected ? "Seleccionado" : "Disponible"}

                          {clubItem.venues.map((venue) => (

                              {venue.name}

                          ))}

                      </button>
                    );
                  })
                )}

              {selectionDirty ?
Estamos recalculando tus clubes y horarios habilitados.
 : null}
              {clubsError ?
{clubsError}
 : null}

          ) : null}

Disponibilidad

Cuando quieres trabajar

                  Marcás tus bloques de media hora dentro de lo que te permite tu sede o tus clubes externos.

              <span
                id={availabilityStatusId}
                role="status"
                aria-live="polite"
                className={`rounded-full border px-3 py-1 text-xs ${
                  availabilitySaveState === "error"
                    ? "border-red-500/30 bg-red-500/10 text-red-200"
                    : availabilitySaveState === "saved"
                      ? "border-[#c4d600]/30 bg-[#c4d600]/10 text-[#d9ef31]"
                      : "border-white/10 bg-black/20 text-white/55"
                }`}
              >
                {getAutoSaveLabel(availabilitySaveState)}

                    {context.mode === "venue"
                      ? "Horarios de tu sede"
                      : context.mode === "external-clubs"
                        ? "Horarios permitidos por tus clubes"
                        : "Todavia no tienes horarios habilitados"}

                    {context.note ?? "Lo rojo marca franjas cerradas o no habilitadas para coaches."}

                  {summarizeAllowedHours(context.allowedHours)}

            {selectionDirty ? (

Esperando que se guarde la seleccion de clubes para actualizar esta grilla.

            ) : context.allowedHours.length === 0 ? (

                {context.note ?? "Todavia no hay horarios disponibles para marcar."}

            ) : (
              <fieldset className="mt-4 rounded-[1.25rem] border border-white/10 bg-black/10 p-4">
                <legend className="px-2 text-sm font-medium text-white">Grilla semanal de disponibilidad</legend>

Marca solo los bloques donde quieres tomar clases. La grilla se guarda automaticamente.

                <div aria-describedby={`${availabilitySummaryId} ${availabilityHelpId} ${availabilityInstructionsId} ${availabilityStatusId}${availabilityError ? ` ${availabilityErrorId}` : ""}`}>
                  <WeeklyAvailabilityGrid
                    value={availabilityDraft}
                    onChange={setAvailabilityDraft}
                    blockedRanges={context.allowedHours}
                    selectedLabel="Disponible"
                    openLabel="Disponible para marcar"
                    blockedLabel="Fuera del horario permitido"
                    instructionsId={availabilityInstructionsId}
                  />

              </fieldset>
            )}

            {availabilityError ? (

                {availabilityError}

            ) : null}

Paquetes

Como quieres vender tus clases

                  Defines tu precio, duracion, cantidad maxima de alumnos y si la clase es individual o grupal.

                <button
                  type="button"
                  onClick={addPackage}
                  className="rounded-full border border-white/10 px-4 py-2 text-sm text-white/70 transition hover:border-white/20 hover:text-white"
                >
                  Agregar paquete
                </button>
                <span
                  role="status"
                  aria-live="polite"
                  className={`rounded-full border px-3 py-1 text-xs ${
                    commercialSaveState === "error"
                      ? "border-red-500/30 bg-red-500/10 text-red-200"
                      : commercialSaveState === "saved"
                        ? "border-[#c4d600]/30 bg-[#c4d600]/10 text-[#d9ef31]"
                        : "border-white/10 bg-black/20 text-white/55"
                  }`}
                >
                  {getAutoSaveLabel(commercialSaveState)}

              {packagesDraft.length === 0 ? (

                  Todavia no armaste paquetes. Puedes empezar con una clase individual y despues sumar grupales.

              ) : null}

              {packagesDraft.map((pkg, packageIndex) => {
                const packageTitle = pkg.title.trim().length > 0 ? pkg.title : `Paquete ${packageIndex + 1}`;
                const packageBaseId = `coach-package-${pkg.clientId}`;
                const formatHelpId = `${packageBaseId}-format-help`;
                const studentHelpId = `${packageBaseId}-students-help`;

                return (
                  <fieldset key={pkg.clientId} className="rounded-[1.25rem] border border-white/10 bg-black/20 p-5">

                      <legend className="text-sm font-medium text-white">{packageTitle}</legend>
                      <button
                        type="button"
                        className="rounded-xl border border-red-500/25 px-4 py-2 text-sm text-red-200 transition hover:bg-red-500/10"
                        onClick={() => removePackage(pkg.clientId)}
                      >
                        Quitar
                      </button>

                        Nombre del paquete
                        <input
                          value={pkg.title}
                          onChange={(event) => updatePackage(pkg.clientId, { title: 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"
                        />

                        Tu precio
                        <input
                          type="number"
                          min={0}
                          step={1}
                          value={pkg.price}
                          onChange={(event) => updatePackage(pkg.clientId, { price: 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"
                        />

                        Duracion
                        <select
                          value={pkg.durationMinutes}
                          onChange={(event) => updatePackage(pkg.clientId, { durationMinutes: Number(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"
                        >
                          {[30, 60, 90, 120].map((value) => (
                            <option key={value} value={value}>
                              {formatDuration(value)}
                            </option>
                          ))}
                        </select>

                        Max alumnos
                        <input
                          type="number"
                          min={pkg.format === "group" ? 2 : 1}
                          step={1}
                          value={pkg.maxStudents}
                          onChange={(event) =>
                            updatePackage(pkg.clientId, {
                              maxStudents: Math.max(pkg.format === "group" ? 2 : 1, Number(event.target.value || (pkg.format === "group" ? 2 : 1))),
                            })
                          }
                          disabled={pkg.format === "individual"}
                          aria-describedby={studentHelpId}
                          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"
                        />

                      <fieldset>
                        <legend className="text-sm text-white/70">Formato de la clase</legend>

                          Individual usa un alumno. Grupal habilita dos o mas alumnos.

                            <input
                              type="radio"
                              name={`${packageBaseId}-format`}
                              checked={pkg.format === "individual"}
                              onChange={() => updatePackage(pkg.clientId, { format: "individual", maxStudents: 1 })}
                              className="sr-only"
                            />
                            Individual

                            <input
                              type="radio"
                              name={`${packageBaseId}-format`}
                              checked={pkg.format === "group"}
                              onChange={() => updatePackage(pkg.clientId, { format: "group", maxStudents: pkg.maxStudents < 2 ? 4 : pkg.maxStudents })}
                              className="sr-only"
                            />
                            Grupal

                      </fieldset>

                        <input
                          type="checkbox"
                          checked={pkg.active}
                          onChange={(event) => updatePackage(pkg.clientId, { active: event.target.checked })}
                          className="sr-only"
                        />
                        Activo para vender
                        {pkg.active ? "Si" : "No"}

                      {pkg.format === "individual" ? "Las clases individuales usan un solo alumno." : "Para clases grupales define el maximo de alumnos."}

{getPackageFinalPriceNote(pkg.price)}

Ese valor es el que defines tu como entrenador para vender tus clases.

                  </fieldset>
                );
              })}

            {commercialError ?
{commercialError}
 : null}
            {commercialDirty && !commercialError ?
Guardando cambios comerciales...
 : null}

Cobros

Mercado Pago

              Por ahora guardamos el alias o mail donde quieres cobrar. La conexion real la hacemos despues.

                Alias o mail de Mercado Pago
                <input
                  value={mercadoPagoHandle}
                  onChange={(event) => setMercadoPagoHandle(event.target.value)}
                  placeholder="tu-alias o tu@mail.com"
                  className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                />

                  {mercadoPagoHandle.trim() ? "Listo para conectar" : "Pendiente"}

        </>
      ) : (
        <>

Alumnos

Tus alumnos

                  Aqui ves tu base de alumnos y abres la carga de clases solo cuando la necesitas.

                <Button type="button" className="h-11 rounded-xl px-5 text-sm font-medium" onClick={openStudentsComposer}>
                  Ir a alumnos
                </Button>

            {studentsLoading ? (

Cargando alumnos y agenda...

            ) : studentsError ? (

{studentsError}

            ) : (

                  Carga de clase y alta de alumno

Nuevo alumno

Lo agregas una vez y despues lo reutilizas en tus clases.

                      <Button
                        type="button"
                        className="h-10 rounded-xl px-4 text-sm font-medium"
                        disabled={creatingStudent}
                        onClick={() => void handleCreateStudent()}
                      >
                        {creatingStudent ? "Creando..." : "Crear alumno"}
                      </Button>

                        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"
                        />

                        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>

                        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"
                        />

                        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"
                        />

                        Sexo
                        <select
                          value={studentCreateDraft.gender}
                          onChange={(event) =>
                            setStudentCreateDraft((current) => ({
                              ...current,
                              gender: event.target.value as CoachStudentCreateDraft["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>

                        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"
                        />

                    {studentCreateError ? (

                        {studentCreateError}

                    ) : null}

Carga de clase

Elige el alumno, la sede y el horario que quieres ocupar.

                        Alumno existente
                        <select
                          value={studentBookingDraft.playerId}
                          onChange={(event) => setStudentBookingDraft((current) => ({ ...current, playerId: 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="">Selecciona un alumno</option>
                          {studentsPanel?.players.map((player) => (
                            <option key={player.id} value={player.id}>
                              {player.name}
                              {player.clubNames.length > 0 ? ` · ${player.clubNames.join(", ")}` : ""}
                            </option>
                          ))}
                        </select>

                        Sede
                        <select
                          value={studentBookingDraft.venueId}
                          onChange={(event) => setStudentBookingDraft((current) => ({ ...current, venueId: 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="">Selecciona una sede</option>
                          {studentsPanel?.venues.map((venue) => (
                            <option key={venue.id} value={venue.id}>
                              {venue.clubName} · {venue.name}
                            </option>
                          ))}
                        </select>

                        Fecha
                        <input
                          type="date"
                          value={studentBookingDraft.date}
                          onChange={(event) => setStudentBookingDraft((current) => ({ ...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"
                        />

                        Hora
                        <input
                          type="time"
                          step={1800}
                          value={studentBookingDraft.startTime}
                          onChange={(event) => setStudentBookingDraft((current) => ({ ...current, startTime: 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"
                        />

[Showing lines 1320-2237 of 2457 (50.0KB limit). Use offset=2238 to continue.]


  });

  it("validates coach me response for a coach assigned to a club venue", async () => {
    resolveCoachFromSession.mockResolvedValue({
      ok: true,
      coach: {
        id: "coach_001",
        name: "Coach One",
        email: "coach@example.com",
        affiliation: "club",
        club: {
          id: "club_001",
          name: "Club One",
        },
        venue: {
          id: "venue_001",
          name: "Sede Centro",
        },
        clubSelectionMode: "all",
      },
      passwordHash: "pw_hash",
    });
    getCoachSettingsFromDb.mockResolvedValue({
      coach: {
        id: "coach_001",
        name: "Coach One",
        email: "coach@example.com",
        affiliation: "club",
        club: {
          id: "club_001",
          name: "Club One",
        },
        venue: {
          id: "venue_001",
          name: "Sede Centro",
        },
        clubSelectionMode: "all",
      },
      dashboard: {
        summary: {
          clubCount: 1,
          activePackageCount: 0,
          availabilityBlocks: 0,
          publishedLessons: 1,
          requestCount: 2,
          listedRevenue: 22000,
          estimatedClubFees: 1400,
        },
        clubs: [
          {
            id: "club_001",
            name: "Club One",
            role: "club-coach" as const,
            venueCount: 1,
            venueNames: ["Sede Centro"],
            publishedLessons: 1,
            requestCount: 2,
            averageReview: null,
            listedRevenue: 22000,
            estimatedClubFees: 1400,
          },
        ],
      },
      commercial: {
        mercadoPagoHandle: null,
        packages: [],
      },
      availability: {
        items: [],
        context: {
          mode: "venue",
          note: null,
          allowedHours: [
            {
              dayOfWeek: 1,
              startTime: "09:00",
              endTime: "18:00",
            },
          ],
          assignedVenue: {
            id: "venue_001",
            name: "Sede Centro",
            club: {
              id: "club_001",
              name: "Club One",
            },
            coachReservationPrice: 1400,
            coachReservationBasePrice: 2000,
            clubCoachReservationDiscount: 600,
            coachCancellationWindowHours: 24,
            externalCoachesAllowed: true,
            availableCourts: [
              {
                id: "court_001",
                name: "Cancha 1",
                covered: true,
              },
            ],
          },
        },
      },
      externalClubs: {
        selection: { mode: "all", clubIds: [] },
        clubs: [],
      },
    });

    const { GET } = await import("@/app/api/coaches/me/route");
    const response = await GET();
    const payload = await response.json();

    expect(coachMeResponseSchema.parse(payload)).toEqual(payload);
  });

  it("validates coach me update response", async () => {
    updateCoachCommercialSettingsInDb.mockResolvedValue({ ok: true });

    const { PUT } = await import("@/app/api/coaches/me/route");
    const response = await PUT(
      new Request("http://localhost/api/coaches/me", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          mercadoPagoHandle: "coach.mp",
          packages: [
            {
              title: "Clase individual",
              price: 15000,
              durationMinutes: 60,
              maxStudents: 1,
              format: "individual",
              active: true,
            },
          ],
        }),
      })
    );
    const payload = await response.json();

    expect(coachMeResponseSchema.parse(payload)).toEqual(payload);
  });

  it("validates coach availability response", async () => {
    getCoachAvailabilityFromDb.mockResolvedValue([
      {
        id: "availability_001",
        dayOfWeek: 1,
        startTime: "10:00",
        endTime: "13:00",
        timezone: "America/Argentina/Buenos_Aires",
      },
    ]);

    const { GET } = await import("@/app/api/coaches/availability/route");
    const response = await GET();
    const payload = await response.json();

    expect(coachAvailabilityResponseSchema.parse(payload)).toEqual(payload);
  });

  it("validates coach clubs response", async () => {
    resolveCoachFromSession.mockResolvedValue({
      ok: true,
      coach: {
        id: "coach_001",
        name: "Coach One",
        email: "coach@example.com",
        affiliation: "independent",
        club: null,
        venue: null,
        clubSelectionMode: "custom",
      },
      passwordHash: "pw_hash",
    });
    getCoachClubOptionsFromDb.mockResolvedValue([
      {
        id: "club_001",
        name: "Club One",
        atcSportclubId: "106",
        externalVenueCount: 1,
        coachReservationPriceFrom: 2000,
        coachReservationPriceTo: 2000,
        venues: [
          {
            id: "venue_001",
            name: "Sede Centro",
            coachReservationPrice: 2000,
            coachCancellationWindowHours: 24,
            coachHours: [
              {
                dayOfWeek: 1,
                startTime: "09:00",
                endTime: "18:00",
              },
            ],
          },
        ],
      },
    ]);
    getCoachClubSelectionFromDb.mockResolvedValue({ mode: "custom", clubIds: ["club_001"] });

    const { GET } = await import("@/app/api/coaches/clubs/route");
    const response = await GET();
    const payload = await response.json();

    expect(coachClubsResponseSchema.parse(payload)).toEqual(payload);
  });
});


**Improving accessibility labels**

I’m thinking it might be beneficial to enhance the explicit labels and row action names for better clarity. Okay, I should consider adding aria-labels or help text to the contact rows and possibly the package toggles too. Also, I need to inspect the rest of the file, especially around the continuation for the student area, to ensure everything is clear and accessible. Let’s get on that!


                        Duracion
                        <select
                          value={studentBookingDraft.durationMinutes}
                          onChange={(event) =>
                            setStudentBookingDraft((current) => ({ ...current, durationMinutes: Number(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"
                        >
                          {[30, 60, 90, 120].map((value) => (
                            <option key={value} value={value}>
                              {formatDuration(value)}
                            </option>
                          ))}
                        </select>

                        Repetir semanalmente
                        <select
                          value={studentBookingDraft.repeatWeeklyCount}
                          onChange={(event) =>
                            setStudentBookingDraft((current) => ({ ...current, repeatWeeklyCount: Number(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={1}>Solo esta clase</option>
                          {[4, 8, 12].map((value) => (
                            <option key={value} value={value}>
                              {value} semanas
                            </option>
                          ))}
                        </select>

                        Nota interna
                        <textarea
                          value={studentBookingDraft.note}
                          onChange={(event) => setStudentBookingDraft((current) => ({ ...current, note: event.target.value }))}
                          rows={4}
                          placeholder="Objetivo de la clase, observaciones, seguimiento..."
                          className="mt-2 w-full rounded-xl border border-white/15 bg-black/30 px-3 py-3 text-sm text-white"
                        />

                      <Button
                        type="button"
                        className="h-11 rounded-xl px-5 text-sm font-medium"
                        disabled={savingStudentBooking || !canSubmitStudentBooking}
                        onClick={() => void handleCreateStudentBooking()}
                      >
                        {savingStudentBooking ? "Guardando..." : "Confirmar clase"}
                      </Button>

                {studentsPanel?.summary.note ? (

                    {studentsPanel.summary.note}

                ) : null}
                {studentsPanel && studentsPanel.players.length === 0 ? (

                    Todavia no hay alumnos existentes en Ballbox. Primero cargalos desde Players.

                ) : null}
                {studentBookingError ?
{studentBookingError}
 : null}

            )}

            {trackedStudents.length === 0 ? (

                Aun no tienes alumnos vinculados. Cuando cargues la primera clase tomada, apareceran aqui.

            ) : (

                      Alumno
                      Categoria
                      Contacto
                      Sedes
                      Proxima clase
                      Ultima clase
                      Clases
                      Acciones

                    {trackedStudents.map((student) => {
                      const draft = studentContactDrafts[student.player.id] ?? createStudentContactDraft(student.player);
                      const whatsappHref = buildWhatsAppHref(student.player.whatsappPhone);
                      const telegramHref = buildTelegramHref(student.player.telegramHandle);
                      const contactBaseId = `student-contact-${student.player.id}`;
                      const whatsappInputId = `${contactBaseId}-whatsapp`;
                      const telegramInputId = `${contactBaseId}-telegram`;
                      const contactHelpId = `${contactBaseId}-help`;
                      const contactStatusId = `${contactBaseId}-status`;
                      const contactErrorId = `${contactBaseId}-error`;
                      const contactState = studentContactSaveStates[student.player.id] ?? "idle";
                      const contactError = studentContactErrors[student.player.id] ?? null;
                      const describedByIds = [contactHelpId, contactStatusId, contactError ? contactErrorId : null]
                        .filter(Boolean)
                        .join(" ");

                      return (

{student.player.name}

                              {getGenderLabel(student.player.gender)} · {getStudentAgeLabel(student.player.birthYear)}

                          {student.player.coachCategory ?? "Sin categoria"}

                            <fieldset className="min-w-[14rem] rounded-[1rem] border border-white/10 bg-black/20 p-3">
                              <legend className="px-1 text-xs font-medium uppercase tracking-[0.16em] text-white/45">Contacto</legend>

                                    WhatsApp

                                  <input
                                    id={whatsappInputId}
                                    value={draft.whatsappPhone}
                                    onChange={(event) => updateStudentContactDraft(student.player.id, { whatsappPhone: event.target.value })}
                                    placeholder="WhatsApp"
                                    aria-describedby={describedByIds}
                                    className="h-10 rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                                  />

                                    Telegram

                                  <input
                                    id={telegramInputId}
                                    value={draft.telegramHandle}
                                    onChange={(event) => updateStudentContactDraft(student.player.id, { telegramHandle: event.target.value })}
                                    placeholder="@telegram"
                                    aria-describedby={describedByIds}
                                    className="h-10 rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                                  />

                                Guarda los datos antes de abrir accesos rapidos a WhatsApp o Telegram.

                                {getAutoSaveLabel(contactState)}

                              {contactError ? (

                                  {contactError}

                              ) : null}
                            </fieldset>

                          {student.venueNames.join(" · ") || "Sin sedes"}

                            {student.nextLessonAt ? formatDateTime(student.nextLessonAt) : "Sin clase futura"}

                            {student.lastLessonAt ? formatDateTime(student.lastLessonAt) : "Sin historial"}

                          {student.lessonCount}

                              <Button
                                type="button"
                                className="h-10 rounded-xl px-4 text-sm font-medium"
                                disabled={savingStudentContactId === student.player.id}
                                onClick={() => void handleSaveStudentContact(student.player.id)}
                              >
                                {savingStudentContactId === student.player.id ? "Guardando..." : "Guardar"}
                              </Button>
                              {whatsappHref ? (

                                  WhatsApp

                              ) : null}
                              {telegramHref ? (

                                  Telegram

                              ) : null}

                      );
                    })}

            )}

        </>
      )}

      {pageError ?
{pageError}
 : null}

  );
}


**Improving HTML accessibility**

I need to kick off some semantic HTML improvements for accessibility. I’m thinking about adding labels, clearer action names for rows, and making it easier for keyboard-only users to navigate. It might be beneficial to include aria-labels or descriptions for save buttons and toggle packages, and make row actions clearer. Should I also consider a live region for page errors? I guess I’ll look for existing contract tests related to coach settings and see if we need any e2e tests.

bash

rg -n "CoachSettingsPageClient|coach-settings-page-client|Alumnos|Guardar|aria-label|details" tests -S

ls tests/contracts/coaches && sed -n '1,220p' tests/contracts/coaches/page.test.tsx 2>/dev/null || true


tests/unit/network/use-network-admin-client-state.test.tsx:55:    expect(fetch).toHaveBeenCalledWith("/api/network?include=details", { cache: "no-store" });


backend.test.ts
routes.test.ts
students-routes.test.ts