"use client";

import { useEffect, useRef, useState, type FormEvent } from "react";
import { Button } from "@/components/ui/button";
import { formatBallboxDateTime } from "@/lib/ballbox-datetime";

type ClassAtcSlot = {
  start: string;
  durationMinutes: number;
  priceMinor: number | null;
  currency: string | null;
  sportclubId: string | null;
  courtId: string | null;
  courtName: string | null;
};

type ClassesRequestFormProps = {
  classOfferId: string;
  slots: ClassAtcSlot[];
  loadingSlots?: boolean;
  initialSelectedSlot?: ClassAtcSlot | null;
};

function formatMoney(value: number | null, currency: string | null) {
  if (value === null || !currency) return "Precio a confirmar";

  return new Intl.NumberFormat("es-AR", {
    style: "currency",
    currency,
    maximumFractionDigits: 0,
  }).format(value / 100);
}

function slotKey(slot: ClassAtcSlot) {
  return [
    slot.start,
    slot.durationMinutes,
    slot.priceMinor ?? "null",
    slot.currency ?? "null",
    slot.sportclubId ?? "null",
    slot.courtId ?? "null",
  ].join("|");
}

function resolveInitialSelection(slots: ClassAtcSlot[], initialSelectedSlot: ClassAtcSlot | null) {
  if (initialSelectedSlot) {
    const matchedSlot = slots.find((slot) => slotKey(slot) === slotKey(initialSelectedSlot));
    if (matchedSlot) {
      return {
        selectionMode: "slot" as const,
        selectedSlotKey: slotKey(matchedSlot),
      };
    }
  }

  return {
    selectionMode: slots.length === 0 ? ("flexible" as const) : null,
    selectedSlotKey: "",
  };
}

export function ClassesRequestForm({ classOfferId, slots, loadingSlots = false, initialSelectedSlot = null }: ClassesRequestFormProps) {
  const errorId = "classes-request-error";
  const successId = "classes-request-success";
  const matchedInitialSlot = initialSelectedSlot
    ? slots.find((slot) => slotKey(slot) === slotKey(initialSelectedSlot))
      ?? slots.find(
          (slot) =>
            slot.start === initialSelectedSlot.start
            && slot.durationMinutes === initialSelectedSlot.durationMinutes
            && (initialSelectedSlot.sportclubId ? slot.sportclubId === initialSelectedSlot.sportclubId : true)
        )
      ?? initialSelectedSlot
    : null;
  const displaySlots = matchedInitialSlot ? [matchedInitialSlot] : slots;
  const showFlexibleOption = !matchedInitialSlot;
  const initialSelection = resolveInitialSelection(displaySlots, matchedInitialSlot);
  const [pending, setPending] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState<string | null>(null);
  const [selectionMode, setSelectionMode] = useState<"slot" | "flexible" | null>(initialSelection.selectionMode);
  const [selectedSlotKey, setSelectedSlotKey] = useState<string>(initialSelection.selectedSlotKey);
  const studentNameInputRef = useRef<HTMLInputElement | null>(null);

  useEffect(() => {
    if (selectionMode !== "slot" || selectedSlotKey.length === 0) {
      return;
    }

    studentNameInputRef.current?.focus();
  }, [selectedSlotKey, selectionMode]);

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    setPending(true);
    setError(null);
    setSuccess(null);

    if (!selectionMode) {
      setError("Elige un horario visible o marca cualquier horario / lugar.");
      setPending(false);
      return;
    }

    const selectedSlot = selectionMode === "slot" ? displaySlots.find((slot) => slotKey(slot) === selectedSlotKey) ?? null : null;
    if (selectionMode === "slot" && !selectedSlot) {
      setError("Elige un horario valido antes de enviar la solicitud.");
      setPending(false);
      return;
    }

    const formData = new FormData(form);
    const payload = {
      classOfferId,
      studentName: String(formData.get("studentName") ?? ""),
      studentEmail: String(formData.get("studentEmail") ?? ""),
      studentPhone: String(formData.get("studentPhone") ?? ""),
      message: String(formData.get("message") ?? ""),
      selectionMode,
      ...(selectedSlot
        ? {
            selectedSlotStart: selectedSlot.start,
            selectedSlotDurationMinutes: selectedSlot.durationMinutes,
            selectedSlotPriceMinor: selectedSlot.priceMinor,
            selectedSlotCurrency: selectedSlot.currency,
            selectedAtcSportclubId: selectedSlot.sportclubId,
            selectedAtcCourtId: selectedSlot.courtId,
          }
        : {}),
    };

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

    const body = (await response.json().catch(() => null)) as { ok?: boolean; error?: string; item?: { id: string } } | null;

    if (!response.ok || !body || body.ok !== true) {
      setError(
        body?.error === "SLOT_NOT_AVAILABLE"
          ? "Ese horario ya no aparece disponible. Elige otro o usa cualquier horario / lugar."
          : "No pudimos guardar la solicitud. Revisa los datos e intenta de nuevo."
      );
      setPending(false);
      return;
    }

    form.reset();
    setSelectionMode(slots.length === 0 ? "flexible" : null);
    setSelectedSlotKey("");
    setSuccess(
      selectionMode === "slot"
        ? "Solicitud enviada con el horario elegido. Ballbox la guardo como lead local para seguimiento."
        : "Solicitud enviada con preferencia flexible. Ballbox la guardo como lead local para seguimiento."
    );
    setPending(false);
  }

  return (
    <form
      id="request-form"
      onSubmit={handleSubmit}
      data-local-submit="true"
      className="space-y-4 rounded-[1.75rem] border border-white/10 bg-[#101510]/95 p-6 shadow-[0_24px_60px_-35px_rgba(0,0,0,0.55)]"
    >

Solicitar

Elige horario y deja tus datos

          Elige uno de los horarios visibles o usa cualquier horario / lugar como fallback. Ballbox guarda la solicitud localmente; no hay booking ATC automatico en este MVP.

      <fieldset className="space-y-3 rounded-[1.5rem] border border-white/10 bg-black/20 p-4">
        <legend className="px-2 text-sm font-medium text-white">Horario preferido</legend>
        {loadingSlots ? (

Buscando horarios visibles de ATC...

        ) : null}
        {displaySlots.length > 0 ? (

            {displaySlots.map((slot) => {
              const key = slotKey(slot);
              const checked = selectionMode === "slot" && selectedSlotKey === key;

              return (

                  <input
                    type="radio"
                    name="slotSelection"
                    className="mt-1"
                    checked={checked}
                    onChange={() => {
                      setSelectionMode("slot");
                      setSelectedSlotKey(key);
                    }}
                  />

{formatBallboxDateTime(slot.start)}

                      {slot.durationMinutes} min{slot.courtName ? ` - ${slot.courtName}` : ""}

{formatMoney(slot.priceMinor, slot.currency)}

              );
            })}

        ) : !loadingSlots ? (

No hay horarios visibles ahora mismo. Puedes enviar la solicitud como cualquier horario / lugar.

        ) : null}

        {showFlexibleOption ? (

            <input
              type="radio"
              name="slotSelection"
              className="mt-1"
              checked={selectionMode === "flexible"}
              onChange={() => {
                setSelectionMode("flexible");
                setSelectedSlotKey("");
              }}
            />

Cualquier horario / lugar

Usa esta opcion si las alternativas visibles no te sirven. Luego aclara tus condiciones en el mensaje.

        ) : null}
      </fieldset>

          Nombre
          <input
            ref={studentNameInputRef}
            name="studentName"
            required
            aria-invalid={error ? true : undefined}
            aria-describedby={error ? errorId : success ? successId : undefined}
            className="h-11 w-full rounded-xl border border-white/15 bg-black/20 px-4 text-white outline-none focus:border-[#c4d600]/50"
          />

          Email
          <input
            name="studentEmail"
            type="email"
            required
            aria-invalid={error ? true : undefined}

[43 more lines in file. Use offset=261 to continue.]


      if (!response.ok) {
        setClub(null);
        setVenue(null);
        setAssignedCoaches([]);
        setAvailableCoaches([]);
        setDraft(null);
        setError(response.status === 404 ? "No encontramos la sede." : "No pudimos cargar la sede.");
        return;
      }

      const parsed = clubVenueDetailResponseSchema.parse(payload);
      const nextDraft = createDraft(parsed.venue);
      setClub(parsed.club);
      setVenue(parsed.venue);
      setAssignedCoaches(parsed.assignedCoaches);
      setAvailableCoaches(parsed.availableCoaches);
      setDraft(nextDraft);
      lastSavedDraftSignature.current = createDraftSignature(nextDraft);
      setSaveState("idle");
      setError(null);
    } catch {
      setClub(null);
      setVenue(null);
      setAssignedCoaches([]);
      setAvailableCoaches([]);
      setDraft(null);
      setError("No pudimos cargar la sede.");
    } finally {
      setLoading(false);
    }
  }

  const loadVenueEffect = useEffectEvent(loadVenue);

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

  async function saveDraft(nextDraft: VenueDraft, signature: string, options?: { background?: boolean }) {
    const background = options?.background ?? false;

    if (!background) {
      setSaveState("saving");
      setError(null);
    }

    try {
      const response = await fetch(`/api/clubs/venues/${venueId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: nextDraft.name,
          status: nextDraft.status,
          openingTime: nextDraft.openingTime,
          closingTime: nextDraft.closingTime,
          coachReservationPrice: nextDraft.coachReservationPrice,
          clubCoachReservationDiscount: nextDraft.clubCoachReservationDiscount,
          coachCancellationWindowHours: nextDraft.coachCancellationWindowHours,
          externalCoachesAllowed: nextDraft.externalCoachesAllowed,
          coachHours: nextDraft.coachHours,
          coachPriceOverrides: nextDraft.coachPriceOverrides.map((override) => ({
            dayOfWeek: override.dayOfWeek,
            startTime: override.startTime,
            endTime: override.endTime,
            price: override.price,
          })),
          courts: nextDraft.courts.map((court) => ({
            ...(court.id ? { id: court.id } : {}),
            name: court.name,
            covered: court.covered,
            coachesEnabled: court.coachesEnabled,
          })),
        }),
      });

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

      lastSavedDraftSignature.current = signature;
      pendingSaveRef.current = null;

      if (!background) {
        const parsed = clubVenueMutationResponseSchema.parse(payload);
        const savedDraft = createDraft(parsed.item);
        setVenue(parsed.item);
        setDraft((current) => {
          if (!current || createDraftSignature(current) !== signature) {
            return current;
          }

          lastSavedDraftSignature.current = createDraftSignature(savedDraft);
          return savedDraft;
        });
        setSaveState("saved");
      }
    } catch {
      if (!background) {
        setError("No pudimos guardar la sede.");
        setSaveState("error");
      }
    }
  }

  const saveDraftEffect = useEffectEvent(saveDraft);

  useEffect(
    () => () => {
      if (saveTimer.current) {
        clearTimeout(saveTimer.current);
        saveTimer.current = null;
      }

      if (pendingSaveRef.current) {
        const pending = pendingSaveRef.current;
        pendingSaveRef.current = null;
        void saveDraftEffect(pending.draft, pending.signature, { background: true });
      }
    },
    []
  );

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

    const signature = createDraftSignature(draft);
    if (signature === lastSavedDraftSignature.current) {
      pendingSaveRef.current = null;
      return;
    }

    setSaveState("saving");
    pendingSaveRef.current = { draft, signature };

    if (saveTimer.current) {
      clearTimeout(saveTimer.current);
    }

    saveTimer.current = setTimeout(() => {
      saveTimer.current = null;
      pendingSaveRef.current = null;
      void saveDraftEffect(draft, signature);
    }, 750);
  }, [draft, loading]);

  function updateDraft(patch: Partial<VenueDraft>) {
    setDraft((current) => (current ? { ...current, ...patch } : current));
  }

  function updateCourt(clientId: string, patch: Partial<VenueCourtDraft>) {
    setDraft((current) =>
      current
        ? {
            ...current,
            courts: current.courts.map((court) => (court.clientId === clientId ? { ...court, ...patch } : court)),
          }
        : current
    );
  }

  function updatePriceOverride(clientId: string, patch: Partial<VenuePriceOverrideDraft>) {
    setDraft((current) =>
      current
        ? {
            ...current,
            coachPriceOverrides: current.coachPriceOverrides.map((override) =>
              override.clientId === clientId ? { ...override, ...patch } : override
            ),
          }
        : current
    );
  }

  function addPriceOverride() {
    setDraft((current) =>
      current
        ? {
            ...current,
            coachPriceOverrides: [
              ...current.coachPriceOverrides,
              {
                clientId: createLocalId("price-override"),
                dayOfWeek: 1,
                startTime: current.openingTime,
                endTime: current.closingTime,
                price: current.coachReservationPrice,
              },
            ],
          }
        : current
    );
  }

  function removePriceOverride(clientId: string) {
    setDraft((current) =>
      current
        ? {
            ...current,
            coachPriceOverrides: current.coachPriceOverrides.filter((override) => override.clientId !== clientId),
          }
        : current
    );
  }

  function updateVenueHours(patch: Partial<Pick<VenueDraft, "openingTime" | "closingTime">>) {
    setDraft((current) =>
      current
        ? (() => {
            const next = { ...current, ...patch };
            const availableRanges = buildDailyAvailabilityRanges(next.openingTime, next.closingTime);

            return {
              ...next,
              coachHours:
                availableRanges.length > 0 ? clampWeeklyAvailabilityRanges(current.coachHours, availableRanges) : current.coachHours,
            };
          })()
        : current
    );
  }

  function addCourt(covered: boolean) {
    setDraft((current) =>
      current
        ? {
            ...current,
            courts: [
              ...current.courts,
              {
                clientId: createLocalId("court"),
                name: `Cancha ${current.courts.length + 1}`,
                covered,
                coachesEnabled: true,
              },
            ],
          }
        : current
    );
  }

  function removeCourt(clientId: string) {
    setDraft((current) =>
      current
        ? {
            ...current,
            courts: current.courts.filter((court) => court.clientId !== clientId),
          }
        : current
    );
  }

  async function assignCoach(coachId: string) {
    setAssigningCoachId(coachId);
    setError(null);

[727 more lines in file. Use offset=520 to continue.]


aria-invalid={error ? true : undefined}
            aria-describedby={error ? errorId : success ? successId : undefined}
            className="h-11 w-full rounded-xl border border-white/15 bg-black/20 px-4 text-white outline-none focus:border-[#c4d600]/50"
          />

        Telefono
        <input
          name="studentPhone"
          aria-invalid={error ? true : undefined}
          aria-describedby={error ? errorId : success ? successId : undefined}
          className="h-11 w-full rounded-xl border border-white/15 bg-black/20 px-4 text-white outline-none focus:border-[#c4d600]/50"
        />

        Mensaje
        <textarea
          name="message"
          rows={4}
          aria-invalid={error ? true : undefined}
          aria-describedby={error ? errorId : success ? successId : undefined}
          className="w-full rounded-xl border border-white/15 bg-black/20 px-4 py-3 text-white outline-none focus:border-[#c4d600]/50"
          placeholder="Objetivo, nivel y condiciones. Ejemplo: me sirven martes o jueves despues de las 19, o sedes Palermo/Belgrano."
        />

      {error ?
{error}
 : null}
      {success ?
{success}
 : null}

      <Button
        type="submit"
        loading={pending}
        disabled={pending}
        className="h-11 w-full rounded-xl text-sm font-medium disabled:cursor-not-allowed disabled:bg-[#7f9012] disabled:text-[#0b0e09]"
      >
        {pending ? "Enviando..." : "Solicitar clase"}
      </Button>
    </form>
  );
}


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

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

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

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

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

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

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

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

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

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

      setAuthRequired(false);

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

  const loadClubPanelEffect = useEffectEvent(loadClubPanel);

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

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

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

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

  useEffect(() => {
    if (showManualClassForm) {
      window.requestAnimationFrame(() => {
        manualClassHeadingRef.current?.focus();
        manualClassVenueRef.current?.focus();
      });
    }
  }, [showManualClassForm]);

  useEffect(() => {
    if (showRequestsPanel) {
      window.requestAnimationFrame(() => {
        requestsPanelHeadingRef.current?.focus();
      });
    }
  }, [showRequestsPanel]);

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

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

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

      const parsed = clubInviteMutationResponseSchema.parse(payload);
      setInvites((current) => [parsed.item, ...current.filter((invite) => invite.id !== parsed.item.id)]);
      setCopiedInviteId(null);

[1661 more lines in file. Use offset=480 to continue.]


**Inspecting club-coaches statuses**

I think there might be missing live regions for success or error messages in club-coaches. I need to take a closer look at the rendering process and the statuses. It’s important to search for any roles or statuses that might relate to this issue. By doing this, I'll hopefully uncover what's causing the missing messages and ensure everything works smoothly. There's definitely something here worth exploring further!

bash

cd /home/sebas/work/projects/ballbox && grep -n "role=\|aria-live\|status\|errorId\|successId" -n components/club-coaches-page-client.tsx components/club-venue-page-client.tsx components/classes-request-form.tsx | head -200


components/club-coaches-page-client.tsx:376:        setAuthRequired(response.status === 401);
components/club-coaches-page-client.tsx:377:        setError(response.status === 401 ? null : payload?.error === "INVALID_PAYLOAD" ? "El panel del club devolvio datos invalidos." : "No pudimos cargar el panel del club.");
components/club-coaches-page-client.tsx:1265:
{manualClassError}

components/club-coaches-page-client.tsx:1323:                        {request.statusLabel}
components/club-coaches-page-client.tsx:1385:

components/club-coaches-page-client.tsx:1567:                        role="grid"
components/club-coaches-page-client.tsx:1573:

components/club-coaches-page-client.tsx:1580:

components/club-coaches-page-client.tsx:1591:                              role="rowheader"
components/club-coaches-page-client.tsx:1609:                                  role="gridcell"
components/club-coaches-page-client.tsx:2001:                                  {venue.status === "active" ? "Activa" : "Inactiva"}
components/club-venue-page-client.tsx:46:  status: "active" | "inactive";
components/club-venue-page-client.tsx:103:  status: "active" | "inactive";
components/club-venue-page-client.tsx:121:    status: venue.status,
components/club-venue-page-client.tsx:211:    status: draft.status,
components/club-venue-page-client.tsx:240:  const venueSaveStatusId = "club-venue-save-status";
components/club-venue-page-client.tsx:267:        setError(response.status === 404 ? "No encontramos la sede." : "No pudimos cargar la sede.");
components/club-venue-page-client.tsx:313:          status: nextDraft.status,
components/club-venue-page-client.tsx:611:
{error ?? "No encontramos esta sede."}

components/club-venue-page-client.tsx:674:            role="status"
components/club-venue-page-client.tsx:675:            aria-live="polite"
components/club-venue-page-client.tsx:721:                value={draft.status}
components/club-venue-page-client.tsx:722:                onChange={(event) => updateDraft({ status: event.target.value as "active" | "inactive" })}
components/club-venue-page-client.tsx:1242:      {error ?
{error}
 : null}
components/classes-request-form.tsx:63:  const errorId = "classes-request-error";
components/classes-request-form.tsx:64:  const successId = "classes-request-success";
components/classes-request-form.tsx:250:            aria-describedby={error ? errorId : success ? successId : undefined}
components/classes-request-form.tsx:261:            aria-describedby={error ? errorId : success ? successId : undefined}
components/classes-request-form.tsx:272:          aria-describedby={error ? errorId : success ? successId : undefined}
components/classes-request-form.tsx:283:          aria-describedby={error ? errorId : success ? successId : undefined}
components/classes-request-form.tsx:289:      {error ?
{error}
 : null}
components/classes-request-form.tsx:290:      {success ?
{success}
 : null}


**Investigating alert status issues**

I think there's a need to fix the statuses or labels for club-coaches alerts, as there might be some errors, especially after running asynchronously. I should probably inspect the rendering around the top section and also look into the manual class implementation. It feels important to get this right so that everything works smoothly for those alerts. I'll focus on troubleshooting this!


className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                  />

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

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

              {selectedManualVenue ? (

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

              ) : null}

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

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

              ) : null}

              {manualClassError ? (

{manualClassError}

              ) : null}

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

              </form>

          ) : null}

          {showRequestsPanel ? (

Solicitudes

Pendientes del club

                  {dashboard.requests.length} items

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

                    No hay pedidos ni problemas pendientes para revisar ahora.

                ) : null}

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

{request.title}

{request.subtitle}

                        {request.statusLabel}

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

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

{formatRequestDate(request.createdAt)}

                    {request.student ? (

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

                    ) : null}

                ))}

          ) : null}

Calendario

Semana actual

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

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

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

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

              </fieldset>

              {calendarSelectionSummary}

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

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

                    return (

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

                    );
                  })}

              </fieldset>

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

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

[701 more lines in file. Use offset=1440 to continue.]


**Considering improvements for accessibility**

I'm thinking about some specific improvements for live regions regarding summary messages and error descriptions. It could be worth implementing these in the club-coaches and venue pages. I may need contract tests or lint to address this. I see there's a top-level error in club-coaches that lacks `aria-live`, and there are copied invite messages that might not be accessible. I should also check the calendar summary to ensure it’s live and inspect the top render for any error or auth messages.


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

        if (!targetCourtId) continue;

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

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

      return {
        date,
        slots: daySlots,
        occupancy,
        blockCount: dayBlocks.length,
        coveredCells,
        totalCells: daySlots.length * selectedCalendarCourts.length,
      };
    });
  })();
  const selectedCalendarDay = calendarDays.find((day) => day.date === selectedCalendarDate) ?? calendarDays[0] ?? null;
  const totalCalendarCells = selectedCalendarDay?.totalCells ?? 0;
  const totalCoveredCalendarCells = selectedCalendarDay?.coveredCells ?? 0;
  const totalBlankCalendarCells = Math.max(0, totalCalendarCells - totalCoveredCalendarCells);
  const calendarWeekSlots = selectedCalendarDay?.slots ?? [];
  const calendarSummaryId = "club-calendar-summary";
  const calendarHelperId = "club-calendar-helper";
  const calendarSelectionSummary = selectedCalendarVenue
    ? [
        `Sede ${selectedCalendarVenue.name}`,
        selectedCalendarDay ? `dia ${formatCalendarDayLabel(selectedCalendarDay.date)}` : null,
        calendarMode === "availability" ? "modo disponibilidad" : "modo reservas",
        effectiveCalendarCoachFilter === "all"
          ? "todos los profesores del club"
          : `profesor ${calendarCoachTagOptions.find((coach) => coach.id === effectiveCalendarCoachFilter)?.name ?? "seleccionado"}`,
      ]
        .filter(Boolean)
        .join(" · ")
    : "Sin sede seleccionada";
  const availableManualCoaches = manualClassOptions.coaches.filter((coach) =>
    manualClassDraft.venueId ? coach.venueId === manualClassDraft.venueId : Boolean(coach.venueId)
  );
  const selectedManualCoach = availableManualCoaches.find((coach) => coach.id === manualClassDraft.coachId) ?? null;
  const availableManualPackages = getActiveCoachPackages(selectedManualCoach);
  const selectedManualVenue = venues.find((venue) => venue.id === manualClassDraft.venueId) ?? null;
  const availableManualCourts = selectedManualVenue?.courts.filter((court) => court.coachesEnabled) ?? [];
  const analyticsReservations = dashboard.analytics.reservations;
  const resolveManualCourtPrice = (venueId: string, coachId: string, date: string, time: string) => {
    const venue = venues.find((item) => item.id === venueId) ?? null;
    const slotPrice = getVenueSlotPrice(venue, getDateDayOfWeek(date), time);
    const coach = coaches.find((item) => item.id === coachId) ?? null;

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

    return slotPrice;
  };

  return (

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

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

              <div className={`rounded-[1.35rem] border bg-black/20 ${showManualClassForm ? "border-[#c4d600]/45 bg-[#c4d600]/12 shadow-[0_0_0_1px_rgba(196,214,0,0.22)]" : "border-white/10"}`}>
                <button
                  type="button"
                  aria-expanded={showManualClassForm}
                  aria-controls="club-manual-class-panel"
                  onClick={() => {
                    if (showRequestsPanel) {
                      setShowRequestsPanel(false);
                    }
                    setShowManualClassForm((current) => !current);
                  }}
                  className="w-full px-5 py-5 text-left"

[1301 more lines in file. Use offset=840 to continue.]


cd /home/sebas/work/projects/ballbox && grep -n "authRequired\|No pudimos cargar el panel del club\|El panel del club devolvio datos invalidos" components/club-coaches-page-client.tsx


308:  const [authRequired, setAuthRequired] = useState(false);
377:        setError(response.status === 401 ? null : payload?.error === "INVALID_PAYLOAD" ? "El panel del club devolvio datos invalidos." : "No pudimos cargar el panel del club.");
418:      setError("No pudimos cargar el panel del club.");
624:    if (authRequired) {


setStudentCreateError(payload?.details?.message ?? "No pudimos crear el alumno.");
        return;
      }

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

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

  if (!club || !dashboard) {
    if (authRequired) {
      return (

Acceso club

            Entra desde /coaches y usa sign in o sign up para clubs.

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

      );
    }

    return (

No pudimos abrir Entrenadores

{error ?? "El panel del club fallo al cargar."}

        <button
          type="button"
          onClick={() => {
            setLoading(true);
            void loadClubPanel();
          }}
          className="mt-5 inline-flex h-11 items-center justify-center rounded-xl bg-[#c4d600] px-5 text-sm font-medium text-[#10150d] transition hover:bg-[#d2e63e]"
        >
          Reintentar
        </button>

    );
  }

  const totalCourts = venues.reduce((total, venue) => total + venue.courtSummary.total, 0);
  const totalCoachHours = venues.reduce((total, venue) => total + venue.coachHours.length, 0);
  const latestInvite = invites[0] ?? null;
  const manualClassOptions = dashboard.manualClassOptions;
  const venueFilterOptions = [
    { id: "all", label: "Todas las sedes" },
    ...venues.map((venue) => ({ id: venue.id, label: venue.name })),
    { id: "unassigned", label: "Sin sede" },
  ];
  const coachVenueById = new Map(dashboard.coachTable.map((coach) => [coach.id, coach.venue]));
  const filteredProfessorRows = dashboard.analytics.professors.filter((professor) => {

[1471 more lines in file. Use offset=670 to continue.]


const response = await fetch(`/api/clubs/coaches/${coachId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          affiliation: "club",
          venueId: null,
        }),
      });

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

      await loadVenue();
    } catch {
      setError("No pudimos quitar el coach de la sede.");
    } finally {
      setRemovingCoachId(null);
    }
  }

  if (loading) {
    return
Cargando sede...
;
  }

  if (!club || !venue || !draft) {
    return (

Sede no disponible

{error ?? "No encontramos esta sede."}

        <Link
          href="/clubs/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]"
        >
          Volver al panel del club
        </Link>

    );
  }

  const unassignedOrOtherVenueCoaches = availableCoaches.filter((coach) => coach.venue?.id !== venueId);
  const availableCoachRanges = buildDailyAvailabilityRanges(draft.openingTime, draft.closingTime);
  const enabledCourtOptions = draft.courts.filter((court) => court.coachesEnabled && court.id);

  return (

      <AuthenticatedWorkspaceNavbar
        email={club.contactEmail}
        sections={[
          {
            label: "Calendario",
            href: "/clubs/coaches",
          },
          {
            label: "Analiticas",
            href: "/clubs/coaches?view=analytics",
          },
          {
            label: "Sedes",
            href: "/clubs/coaches?view=venues",
            active: true,
          },
          {
            label: "Profesores",
            href: "/clubs/coaches?view=coaches",
          },
        ]}
        onSignOut={async () => {
          await fetch("/api/clubs/logout", { method: "POST" });
          window.location.href = "/coaches";
        }}
      />

Sede

{venue.name}

{club.name}

Configuracion

Configura la sede paso a paso

              Todo lo importante queda visible de entrada: base, horarios, reservas, canchas y entrenadores. Los cambios se guardan solos.

            {getVenueSaveLabel(saveState)}

          {[
            { href: "#base-sede", step: "01", title: "Base", detail: "Nombre, estado y externos" },
            { href: "#horarios-sede", step: "02", title: "Horarios", detail: "Apertura y bloques coach" },
            { href: "#reservas-sede", step: "03", title: "Reservas", detail: "Precios y reglas" },
            { href: "#canchas-sede", step: "04", title: "Canchas", detail: "Listado y habilitadas" },
            { href: "#coaches-sede", step: "05", title: "Entrenadores", detail: "Asignaciones y cancha fija" },
          ].map((item) => (

{item.step}

{item.title}

{item.detail}

          ))}

            Nombre de la sede
            <input
              value={draft.name}
              onChange={(event) => updateDraft({ 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"
            />

              Estado

[527 more lines in file. Use offset=720 to continue.]


cd /home/sebas/work/projects/ballbox && grep -n "studentCreateError\|inviteSaving\|creatingVenue\|role=alert" components/club-coaches-page-client.tsx


305:  const [inviteSaving, setInviteSaving] = useState(false);
306:  const [creatingVenue, setCreatingVenue] = useState(false);
336:  const [studentCreateError, setStudentCreateError] = useState<string | null>(null);
991:                  {studentCreateError ? (
993:                      {studentCreateError}
1943:                      disabled={creatingVenue || newVenueName.trim().length === 0}
1946:                      {creatingVenue ? "Creando..." : "Crear sede y abrir configuracion"}
2054:                disabled={inviteSaving}
2057:                {inviteSaving ? "Generando..." : "Generar link"}


                        <option value="other">Otro</option>
                      </select>
                    </label>
                    <label className="block text-sm text-white/70">
                      Ano de nacimiento
                      <input
                        type="number"
                        min={1900}
                        max={2100}
                        value={studentCreateDraft.birthYear}
                        onChange={(event) => setStudentCreateDraft((current) => ({ ...current, birthYear: event.target.value }))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      />
                    </label>
                    <label className="block text-sm text-white/70">
                      Categoria
                      <select
                        value={studentCreateDraft.coachCategory}
                        onChange={(event) => setStudentCreateDraft((current) => ({ ...current, coachCategory: event.target.value }))}
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      >
                        <option value="">Sin categoria</option>
                        {Array.from({ length: 10 }, (_, index) => String(index + 1)).map((value) => (
                          <option key={value} value={value}>
                            {value}
                          </option>
                        ))}
                      </select>
                    </label>
                  </div>

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

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

                <label className="block text-sm text-white/70">
                  Profesor
                  <select
                    value={manualClassDraft.coachId}
                    onChange={(event) => {
                      const nextCoachId = event.target.value;
                      const nextCoach = availableManualCoaches.find((coach) => coach.id === nextCoachId) ?? null;
                      const nextPackage = getActiveCoachPackages(nextCoach)[0] ?? null;
                      setManualClassDraft((current) =>

[1091 more lines in file. Use offset=1050 to continue.]


**Patching error alerts**

I’m thinking I need to implement a `studentCreateError`, possibly using a role for alerts. That makes sense! I also want to add an `aria-live` attribute to the `manualClassError`. I'll include a visually hidden live region that summarizes the current save or error state on the club-venue page. Using a role of "status" with a span seems okay, but the top error should definitely have the role of alert, along with `aria-live`. Let’s patch these up!

edit

if (authRequired) {
      return (

Acceso club

            Entra desde /coaches y usa sign in o sign up para clubs.

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

      );
    }

    return (

No pudimos abrir Entrenadores

{error ?? "El panel del club fallo al cargar."}

        <button
          type="button"
          onClick={() => {
            setLoading(true);
            void loadClubPanel();
          }}
          className="mt-5 inline-flex h-11 items-center justify-center rounded-xl bg-[#c4d600] px-5 text-sm font-medium text-[#10150d] transition hover:bg-[#d2e63e]"
        >
          Reintentar
        </button>

    );
  }


Successfully replaced 1 block(s) in /home/sebas/work/projects/ballbox/components/club-coaches-page-client.tsx.

...
  622
  623   if (!club || !dashboard) {
  624     if (authRequired) {
  625       return (
- 626

+ 626

  627
Acceso club

  628

  629             Entra desde /coaches y usa sign in o sign up para clubs.
  630

      ...
  638       );
  639     }
  640
  641     return (
- 642

+ 642

  643
No pudimos abrir Entrenadores

  644
{error ?? "El panel del club fallo al cargar."}

  645         <button
  646           type="button"
      ...


{manualClassError ? (

{manualClassError}

              ) : null}

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

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


Successfully replaced 2 block(s) in /home/sebas/work/projects/ballbox/components/club-coaches-page-client.tsx.


Sede no disponible

{error ?? "No encontramos esta sede."}

          <Link
            href="/clubs/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]"
          >
            Volver al panel del club
          </Link>


Could not find the exact text in /home/sebas/work/projects/ballbox/components/club-venue-page-client.tsx. The old text must match exactly including all whitespace and newlines.


if (loading) {
    return
Cargando sede...
;
  }

  if (!club || !venue || !draft) {
    return (

Sede no disponible

{error ?? "No encontramos esta sede."}

        <Link
          href="/clubs/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]"
        >
          Volver al panel del club
        </Link>

    );
  }

  const unassignedOrOtherVenueCoaches = availableCoaches.filter((coach) => coach.venue?.id !== venueId);
  const availableCoachRanges = buildDailyAvailabilityRanges(draft.openingTime, draft.closingTime);
  const enabledCourtOptions = draft.courts.filter((court) => court.coachesEnabled && court.id);

  return (

      <AuthenticatedWorkspaceNavbar
        email={club.contactEmail}
        sections={[
          {

[615 more lines in file. Use offset=632 to continue.]


if (!club || !venue || !draft) {
    return (

Sede no disponible

{error ?? "No encontramos esta sede."}

        <Link
          href="/clubs/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]"
        >
          Volver al panel del club
        </Link>

    );
  }


Successfully replaced 1 block(s) in /home/sebas/work/projects/ballbox/components/club-venue-page-client.tsx.

...
  605   }
  606
  607   if (!club || !venue || !draft) {
  608     return (
- 609

+ 609

  610
Sede no disponible

- 611
{error ?? "No encontramos esta sede."}

+ 611
{error ?? "No encontramos esta sede."}

  612         <Link
  613           href="/clubs/coaches"
  614           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]"
  615         >
      ...


bash

cd /home/sebas/work/projects/ballbox && grep -n "venueErrorId" -n components/club-venue-page-client.tsx && sed -n '1238,1248p' components/club-venue-page-client.tsx


239:  const venueErrorId = "club-venue-error";
611:
{error ?? "No encontramos esta sede."}

1242:      {error ?
{error}
 : null}

      {error ?
{error}
 : null}

  );
}


**Considering code validation**

I think I need to run some tests and linting, specifically eslint on the changed files. First, I'll check the git status and see if any tests exist for the changes I'm making. To satisfy the task requirements, I might consider adjusting contract coverage if it seems worthwhile. But perhaps there are no tests needed. I want to validate things with linting, so I'll run eslint on those changed components and possibly tests. It seems like there might just be two files involved.

cd /home/sebas/work/projects/ballbox && pnpm eslint components/club-coaches-page-client.tsx components/club-venue-page-client.tsx

cd /home/sebas/work/projects/ballbox && git diff -- components/club-coaches-page-client.tsx components/club-venue-page-client.tsx


(no output)


diff --git a/components/club-coaches-page-client.tsx b/components/club-coaches-page-client.tsx
index 51405e9..4b39520 100644
--- a/components/club-coaches-page-client.tsx
+++ b/components/club-coaches-page-client.tsx
@@ -1,7 +1,7 @@
 "use client";

 import Link from "next/link";
-import { useEffect, useEffectEvent, useState } from "react";
+import { useEffect, useEffectEvent, useRef, useState } from "react";
 import { useRouter, useSearchParams } from "next/navigation";
 import type { z } from "zod";
 import { AuthenticatedWorkspaceNavbar } from "@/components/authenticated-workspace-navbar";
@@ -334,6 +334,9 @@ export function ClubCoachesPageClient() {
   const [manualClassError, setManualClassError] = useState<string | null>(null);
   const [creatingStudent, setCreatingStudent] = useState(false);
   const [studentCreateError, setStudentCreateError] = useState<string | null>(null);
+  const manualClassHeadingRef = useRef<HTMLHeadingElement | null>(null);
+  const manualClassVenueRef = useRef<HTMLSelectElement | null>(null);
+  const requestsPanelHeadingRef = useRef<HTMLHeadingElement | null>(null);

   useEffect(() => {
     const nextView = searchParams.get("view");
@@ -437,6 +440,23 @@ export function ClubCoachesPageClient() {
     setCalendarDate((current) => (days.includes(current) ? current : days[0] ?? ""));
   }, [dashboard]);

+  useEffect(() => {
+    if (showManualClassForm) {
+      window.requestAnimationFrame(() => {
+        manualClassHeadingRef.current?.focus();
+        manualClassVenueRef.current?.focus();
+      });
+    }
+  }, [showManualClassForm]);
+
+  useEffect(() => {
+    if (showRequestsPanel) {
+      window.requestAnimationFrame(() => {
+        requestsPanelHeadingRef.current?.focus();
+      });
+    }
+  }, [showRequestsPanel]);
+
   async function handleCreateInvite() {
     setInviteSaving(true);
     setError(null);
@@ -603,7 +623,7 @@ export function ClubCoachesPageClient() {
   if (!club || !dashboard) {
     if (authRequired) {
       return (
-

+

Acceso club

             Entra desde /coaches y usa sign in o sign up para clubs.
@@ -619,7 +639,7 @@ export function ClubCoachesPageClient() {
     }

     return (
-

+

No pudimos abrir Entrenadores

{error ?? "El panel del club fallo al cargar."}

         <button
@@ -735,6 +755,20 @@ export function ClubCoachesPageClient() {
   const totalCoveredCalendarCells = selectedCalendarDay?.coveredCells ?? 0;
   const totalBlankCalendarCells = Math.max(0, totalCalendarCells - totalCoveredCalendarCells);
   const calendarWeekSlots = selectedCalendarDay?.slots ?? [];
+  const calendarSummaryId = "club-calendar-summary";
+  const calendarHelperId = "club-calendar-helper";
+  const calendarSelectionSummary = selectedCalendarVenue
+    ? [
+        `Sede ${selectedCalendarVenue.name}`,
+        selectedCalendarDay ? `dia ${formatCalendarDayLabel(selectedCalendarDay.date)}` : null,
+        calendarMode === "availability" ? "modo disponibilidad" : "modo reservas",
+        effectiveCalendarCoachFilter === "all"
+          ? "todos los profesores del club"
+          : `profesor ${calendarCoachTagOptions.find((coach) => coach.id === effectiveCalendarCoachFilter)?.name ?? "seleccionado"}`,
+      ]
+        .filter(Boolean)
+        .join(" · ")
+    : "Sin sede seleccionada";
   const availableManualCoaches = manualClassOptions.coaches.filter((coach) =>
     manualClassDraft.venueId ? coach.venueId === manualClassDraft.venueId : Boolean(coach.venueId)
   );
@@ -791,15 +825,18 @@ export function ClubCoachesPageClient() {
         <>

-
-                <summary
+              <div className={`rounded-[1.35rem] border bg-black/20 ${showManualClassForm ? "border-[#c4d600]/45 bg-[#c4d600]/12 shadow-[0_0_0_1px_rgba(196,214,0,0.22)]" : "border-white/10"}`}>
+                <button
+                  type="button"
+                  aria-expanded={showManualClassForm}
+                  aria-controls="club-manual-class-panel"
                   onClick={() => {
                     if (showRequestsPanel) {
                       setShowRequestsPanel(false);
                     }
                     setShowManualClassForm((current) => !current);
                   }}
-                  className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
+                  className="w-full px-5 py-5 text-left"
                 >

Operacion diaria

@@ -813,18 +850,21 @@ export function ClubCoachesPageClient() {
                       {showManualClassForm ? "Ocultar" : "Abrir"}

-
-
+                </button>
+

-
-                <summary
+              <div className={`rounded-[1.35rem] border bg-black/20 ${showRequestsPanel ? "border-sky-400/45 bg-sky-400/10 shadow-[0_0_0_1px_rgba(56,189,248,0.18)]" : "border-white/10"}`}>
+                <button
+                  type="button"
+                  aria-expanded={showRequestsPanel}
+                  aria-controls="club-requests-panel"
                   onClick={() => {
                     if (showManualClassForm) {
                       setShowManualClassForm(false);
                     }
                     setShowRequestsPanel((current) => !current);
                   }}
-                  className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
+                  className="w-full px-5 py-5 text-left"
                 >

Seguimiento

@@ -838,17 +878,17 @@ export function ClubCoachesPageClient() {
                       {dashboard.requests.length} items

-
-
+                </button>
+

           {showManualClassForm ? (
-

+

Agregar clase

-
Carga manual para tu operacion diaria

+
Carga manual para tu operacion diaria

@@ -949,7 +989,7 @@ export function ClubCoachesPageClient() {

                   {studentCreateError ? (
-

+

                       {studentCreateError}

                   ) : null}
@@ -958,6 +998,7 @@ export function ClubCoachesPageClient() {

                   Sede
                   <select
+                    ref={manualClassVenueRef}
                     value={manualClassDraft.venueId}
                     onChange={(event) => {
                       const nextVenueId = event.target.value;
@@ -1221,7 +1262,7 @@ export function ClubCoachesPageClient() {
               ) : null}

               {manualClassError ? (
-
{manualClassError}

+
{manualClassError}

               ) : null}

@@ -1253,11 +1294,11 @@ export function ClubCoachesPageClient() {
           ) : null}

           {showRequestsPanel ? (
-

+

Solicitudes

-
Pendientes del club

+
Pendientes del club

                   {dashboard.requests.length} items
@@ -1341,7 +1382,11 @@ export function ClubCoachesPageClient() {
               </fieldset>

-

+

+              {calendarSelectionSummary}
+

+
+

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

@@ -1459,6 +1504,9 @@ export function ClubCoachesPageClient() {

Selecciona el dia

Despues ves todas las canchas de la sede como columnas.

+

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

                       {selectedCalendarCourts.length} canchas

@@ -1516,17 +1564,20 @@ export function ClubCoachesPageClient() {

                       <div
+                        role="grid"
+                        aria-label={`Calendario de ${calendarMode === "availability" ? "disponibilidad" : "reservas"} para ${selectedCalendarVenue.name} el ${selectedCalendarDay ? formatCalendarDayLabel(selectedCalendarDay.date) : "dia seleccionado"}`}
+                        aria-describedby={calendarHelperId}
                         className="grid min-w-[1120px] gap-2"
                         style={{ gridTemplateColumns: `96px repeat(${selectedCalendarCourts.length}, minmax(160px, 1fr))` }}
                       >
-

+

                           Hora

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

                           return (
-

+

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

{court.name}

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

@@ -1537,6 +1588,7 @@ export function ClubCoachesPageClient() {
                         {calendarWeekSlots.map((slot) => (
                           <div key={`row-${slot}`} className="contents">

                               {formatSlotLabel(slot)}
@@ -1554,18 +1606,20 @@ export function ClubCoachesPageClient() {
                                 <button
                                   key={`${court.id}-${slot}`}
                                   type="button"
+                                  role="gridcell"
                                   disabled={!slotEnabled}
+                                  aria-describedby={calendarHelperId}
                                   aria-label={`${court.name}, ${formatSlotLabel(slot)}, ${
                                     !slotEnabled
                                       ? "cerrado para coaches"
                                       : block
                                         ? calendarMode === "availability"
-                                          ? "cubierta por profesor"
+                                          ? `cubierta por ${block.coachName ?? "profesor"}`
                                           : `reserva cargada${block.playerName ? ` para ${block.playerName}` : ""}`
                                         : calendarMode === "availability"
                                           ? "sin coach cubriendo"
                                           : "sin reserva cargada"
-                                  }`}
+                                  }. ${slotEnabled ? "Abrir carga manual para este horario" : "Horario no disponible"}`}
                                   onClick={() => {
                                     if (!slotEnabled) return;
                                     setManualClassDraft((current) => ({
@@ -1617,19 +1671,25 @@ export function ClubCoachesPageClient() {
                                   }`}
                                 >
                                   {!slotEnabled ? (
-
Cerrado para coaches

+                                    <>
+
Cerrado para coaches

+
Sin accion disponible.

+                                    </>
                                   ) : block ? (
                                     <>

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

+

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

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

-                                      ) : null}

{formatMoney(visibleSlotPriceMinor)}

                                         {block.startTime} - {block.endTime}

+
Abrir carga manual en este horario.

                                     </>
                                   ) : (
                                     <>
@@ -1638,6 +1698,7 @@ export function ClubCoachesPageClient() {
                                         {calendarMode === "availability" ? "Sin coach cubriendo" : "Sin reserva cargada"}

{formatMoney(visibleSlotPriceMinor)}

+
Abrir carga manual en este horario.

                                     </>
                                   )}
                                 </button>
@@ -1700,6 +1761,7 @@ export function ClubCoachesPageClient() {

+
Resumen de ingresos y resenas por profesor para la sede seleccionada.

                     Profesor
@@ -1751,6 +1813,7 @@ export function ClubCoachesPageClient() {

+
Detalle de reservas por fecha, profesor, alumno, sede y reparto.

                     Fecha
@@ -1910,6 +1973,7 @@ export function ClubCoachesPageClient() {
               ) : (

+
Tabla de sedes del club con estado, horario, resumen y accion de edicion.

                         Sede
@@ -2031,6 +2095,7 @@ export function ClubCoachesPageClient() {

+
Tabla de profesores del club con sede, clases publicadas, solicitudes y resenas.

                       Profesor
diff --git a/components/club-venue-page-client.tsx b/components/club-venue-page-client.tsx
index c130c66..0d629b1 100644
--- a/components/club-venue-page-client.tsx
+++ b/components/club-venue-page-client.tsx
@@ -236,6 +236,8 @@ type ClubVenuePageClientProps = {
 };

 export function ClubVenuePageClient({ venueId }: ClubVenuePageClientProps) {
+  const venueErrorId = "club-venue-error";
+  const venueSaveStatusId = "club-venue-save-status";
   const [club, setClub] = useState<ClubProfile | null>(null);
   const [venue, setVenue] = useState<VenueDetail | null>(null);
   const [assignedCoaches, setAssignedCoaches] = useState<VenueCoach[]>([]);
@@ -604,9 +606,9 @@ export function ClubVenuePageClient({ venueId }: ClubVenuePageClientProps) {

   if (!club || !venue || !draft) {
     return (
-

+

Sede no disponible

-
{error ?? "No encontramos esta sede."}

+
{error ?? "No encontramos esta sede."}

         <Link
           href="/clubs/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]"
@@ -668,6 +670,9 @@ export function ClubVenuePageClient({ venueId }: ClubVenuePageClientProps) {

           <span
+            id={venueSaveStatusId}
+            role="status"
+            aria-live="polite"
             className={`rounded-full border px-3 py-1 text-xs ${
               saveState === "error"
                 ? "border-red-500/30 bg-red-500/10 text-red-200"
@@ -1234,7 +1239,7 @@ export function ClubVenuePageClient({ venueId }: ClubVenuePageClientProps) {

-      {error ?
{error}
 : null}
+      {error ?
{error}
 : null}

   );
 }


edit

3. pending - `/clubs/coaches`: final calendar/manual-class semantics pass, especially table/grid helper copy and action naming consistency.
4. pending - cross-flow audit: review remaining live status/error messaging in recently touched Ballbox flows and tighten weak spots.
5. pending - tests: add or adjust contract coverage for any new semantics from this loop where worthwhile.
6. pending - final summary: capture what this loop changed and what remains.

3. pending - `/clubs/coaches`: final calendar/manual-class semantics pass, especially table/grid helper copy and action naming consistency.
4. done - cross-flow audit: review remaining live status/error messaging in recently touched Ballbox flows and tighten weak spots.
5. pending - tests: add or adjust contract coverage for any new semantics from this loop where worthwhile.
6. pending - final summary: capture what this loop changed and what remains.


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

    ...
  4
  5 1. done - `/coaches/settings`: follow-up pass on explicit labels, row action names, and keyboard-only success/error discoverability across the students/contact area.
  6 2. done - `/coaches/settings`: package cards semantic toggle audit for format and active/paused controls, including clearer grouping/help text if still weak.
  7 3. pending - `/clubs/coaches`: final calendar/manual-class semantics pass, especially table/grid helper copy and action naming consistency.
- 8 4. pending - cross-flow audit: review remaining live status/error messaging in recently touched Ballbox flows and tighten weak spots.
+ 8 4. done - cross-flow audit: review remaining live status/error messaging in recently touched Ballbox flows and tighten weak spots.
  9 5. pending - tests: add or adjust contract coverage for any new semantics from this loop where worthwhile.
 10 6. pending - final summary: capture what this loop changed and what remains.


Current owner: agent-3
Current focus: iteration 3 running
Last result: tightened coach settings package toggle semantics: visible label + hidden status help for the active switch, clearer checkbox naming, and preserved format grouping. Validation: pnpm eslint components/coach-settings-page-client.tsx
Next suggested task: cross-flow audit for remaining live status/error messaging

Current owner: agent-3
Current focus: iteration 3 running
Last result: tightened cross-flow live status/error semantics in club workspace and venue setup: added assertive/polite alert/status regions for load/save/fallback states, plus clearer focus/role treatment for manual class and requests panels. Validation: pnpm eslint components/club-coaches-page-client.tsx components/club-venue-page-client.tsx
Next suggested task: final summary or `/clubs/coaches` calendar/manual-class semantics pass


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

    ...
  8 Rule: do not push or merge.
  9
 10 Current owner: agent-3
 11 Current focus: iteration 3 running
-12 Last result: tightened coach settings package toggle semantics: visible label + hidden status help for the active switch, clearer checkbox naming, and preserved format grouping. Validation: pnpm eslint components/coach-settings-page-client.tsx
-13 Next suggested task: cross-flow audit for remaining live status/error messaging
+12 Last result: tightened cross-flow live status/error semantics in club workspace and venue setup: added assertive/polite alert/status regions for load/save/fallback states, plus clearer focus/role treatment for manual class and requests panels. Validation: pnpm eslint components/club-coaches-page-client.tsx components/club-venue-page-client.tsx
+13 Next suggested task: final summary or `/clubs/coaches` calendar/manual-class semantics pass


**Checking git status**

I’m thinking I need to do a final check on the current state of my git repository. It feels important to confirm everything is in order before proceeding. I want to ensure no surprises pop up later. So, I’ll run the `git status` command to verify what changes are staged, what files are modified, and make sure everything is as expected. This way, I can feel confident moving forward!

bash

cd /home/sebas/work/projects/ballbox && git status --short && pnpm eslint components/club-coaches-page-client.tsx components/club-venue-page-client.tsx


 M PLAN.md
 M components/authenticated-workspace-navbar.tsx
 M components/classes-request-form.tsx
 M components/club-coaches-page-client.tsx
 M components/club-venue-page-client.tsx
 M components/coach-access-page-client.tsx
 M components/coach-settings-page-client.tsx
 M components/navbar.tsx
 M components/payments-admin-client.tsx
 M components/weekly-availability-grid.tsx
 M docs/implemented-host-app.md
 M docs/implemented-sports.md
 M tests/contracts/classes/classes-request-form.test.tsx
?? tests/contracts/weekly-availability-grid.test.tsx


**Clarifying final communication**

I’m considering how we should finalize communication. If we’re only editing files, then it seems like the final response could be minimal or even empty. But, as an assistant, I wonder whether I still need to provide some sort of message. I think it makes sense to keep it simple, perhaps just leaving it without any content at all. This way, it aligns with the approach of focusing solely on the edited files.