$ node scripts/next-build-safe.mjs
[build] linux/arm64 detected: using webpack + SWC wasm fallback to avoid native next-swc crash
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Skipping creating a lockfile at /home/sebas/work/projects/ballbox/.next/lock because we're using WASM bindings
▲ Next.js 16.2.1 (webpack)
- Environments: .env.local, .env.production

  Creating an optimized production build ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
✓ Compiled successfully in 14.9s
  Skipping validation of types
  Finished TypeScript config validation in 12ms ...
  Collecting page data using 3 workers ...
  Generating static pages using 3 workers (0/56) ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Generating static pages using 3 workers (14/56)
  Generating static pages using 3 workers (28/56)
  Generating static pages using 3 workers (42/56)
✓ Generating static pages using 3 workers (56/56) in 1402ms
  Finalizing page optimization ...
  Collecting build traces ...

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /admin
├ ƒ /admin/login
├ ƒ /admin/network
├ ƒ /admin/payments
├ ƒ /admin/players
├ ƒ /admin/screens
├ ƒ /admin/sports
├ ƒ /admin/tvs
├ ƒ /api/account/session
├ ƒ /api/admin/logout
├ ƒ /api/admin/payments/operations
├ ƒ /api/admin/payments/sessions
├ ƒ /api/admin/payments/sessions/[sessionId]
├ ƒ /api/admin/players
├ ƒ /api/admin/players/[playerId]
├ ƒ /api/admin/players/[playerId]/memberships
├ ƒ /api/admin/players/memberships/[membershipId]
├ ƒ /api/admin/session
├ ƒ /api/admin/sports/classes
├ ƒ /api/admin/sports/classes/[classId]
├ ƒ /api/admin/sports/coaches
├ ƒ /api/admin/sports/coaches/[coachId]
├ ƒ /api/admin/sports/open-matches
├ ƒ /api/admin/sports/open-matches/[matchId]
├ ƒ /api/admin/sports/operations
├ ƒ /api/admin/summary
├ ƒ /api/admin/tvs/assignments/[assignmentId]
├ ƒ /api/admin/tvs/campaigns
├ ƒ /api/admin/tvs/campaigns/[campaignId]
├ ƒ /api/admin/tvs/campaigns/[campaignId]/assignments
├ ƒ /api/admin/tvs/campaigns/[campaignId]/creatives
├ ƒ /api/admin/tvs/creatives/[creativeId]
├ ƒ /api/admin/tvs/operations
├ ƒ /api/admin/tvs/screens
├ ƒ /api/admin/tvs/screens/[screenId]
├ ƒ /api/admin/tvs/tv-devices
├ ƒ /api/admin/tvs/tv-devices/[tvDeviceId]
├ ƒ /api/atc-public/availability/sportclubs/[sportclubId]
├ ƒ /api/classes/logout
├ ƒ /api/classes/offers
├ ƒ /api/classes/offers/[offerId]
├ ƒ /api/classes/requests
├ ƒ /api/classes/session
├ ƒ /api/clubs/accounts
├ ƒ /api/clubs/coaches
├ ƒ /api/clubs/coaches/[coachId]
├ ƒ /api/clubs/invites
├ ƒ /api/clubs/logout
├ ƒ /api/clubs/manual-classes
├ ƒ /api/clubs/me
├ ƒ /api/clubs/players
├ ƒ /api/clubs/session
├ ƒ /api/clubs/venues
├ ƒ /api/clubs/venues/[venueId]
├ ƒ /api/coaches/accounts
├ ƒ /api/coaches/availability
├ ƒ /api/coaches/clubs
├ ƒ /api/coaches/invites/[token]
├ ƒ /api/coaches/logout
├ ƒ /api/coaches/me
├ ƒ /api/coaches/session
├ ƒ /api/coaches/students
├ ƒ /api/coaches/students/[bookingId]
├ ƒ /api/coaches/students/players
├ ƒ /api/coaches/students/players/[playerId]
├ ƒ /api/integrations/payments/approved
├ ƒ /api/integrations/payments/health
├ ƒ /api/integrations/payments/orders
├ ƒ /api/integrations/payments/sessions
├ ƒ /api/network
├ ƒ /api/network/[clubId]
├ ƒ /api/network/[clubId]/venues
├ ƒ /api/network/[clubId]/venues/[venueId]
├ ƒ /api/network/venues/[venueId]/vending-machines
├ ƒ /api/network/venues/[venueId]/vending-machines/[vendingMachineId]
├ ƒ /api/players
├ ƒ /api/sports
├ ƒ /api/tvs/health
├ ƒ /api/tvs/heartbeat
├ ƒ /api/tvs/impression
├ ƒ /api/tvs/manifest/[screenId]
├ ƒ /api/tvs/screens
├ ƒ /classes
├ ƒ /classes-by-date
├ ƒ /classes/[offerId]
├ ƒ /classes/login
├ ƒ /classes/me
├ ƒ /clubs
├ ƒ /clubs/coaches
├ ƒ /clubs/venues/[venueId]
├ ○ /coaches
├ ƒ /coaches/settings
├ ○ /manifest.webmanifest
├ ƒ /notification/mercadopago
├ ○ /offline
└ ƒ /player/[screenId]

ƒ Proxy (Middleware)

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand


bash

cd /home/sebas/work/projects/ballbox && git add components/club-coaches-page-client.tsx && git commit -m "Use semantic calendar day selection" && git push origin main


[main 1ad344c] Use semantic calendar day selection
 1 file changed, 13 insertions(+), 6 deletions(-)
pre-push: pushing main, running quality checks...
$ node scripts/check-doc-references.mjs
Docs reference check passed for README.md, PLAN.md, and docs/index.md
$ ./scripts/db-local-up.sh
Local Postgres already ready.
$ ./scripts/prisma-migrate-deploy.sh
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "ballbox_local", schema "public" at "127.0.0.1:5432"

2 migrations found in prisma/migrations

No pending migrations to apply.
$ ./scripts/prisma-check-migrations.sh
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "ballbox_local", schema "public" at "127.0.0.1:5432"

2 migrations found in prisma/migrations

Database schema is up to date!
Prisma schema loaded from prisma/schema.prisma
The schema at prisma/schema.prisma is valid 🚀
$ eslint .
$ rm -rf .next/types .next/dev/types
$ tsc --noEmit --incremental false
$ vitest run tests/contracts

 RUN  v4.1.0 /home/sebas/work/projects/ballbox

 ✓ tests/contracts/players/players-admin-player-card.test.tsx (2 tests) 468ms
     ✓ renders the empty memberships state for a player without memberships  378ms
 ✓ tests/contracts/network/network-admin-venue-card.test.tsx (2 tests) 456ms
     ✓ updates the targeted machine active flag through setClubs  361ms
 ✓ tests/contracts/classes/classes-request-form.test.tsx (3 tests) 747ms
     ✓ submits the selected visible slot as part of the request  569ms
 ✓ tests/contracts/payments/auth.test.ts (2 tests) 340ms
 ✓ tests/contracts/clubs/routes.test.ts (9 tests) 374ms
 ✓ tests/contracts/coaches/routes.test.ts (7 tests) 286ms
 ✓ tests/contracts/players/admin-routes.test.ts (9 tests) 302ms
 ✓ tests/contracts/coaches/backend.test.ts (1 test) 217ms
 ✓ tests/contracts/players/players-admin-create-player-section.test.tsx (1 test) 465ms
     ✓ renders the extracted section shell and forwards player form updates  463ms
 ✓ tests/contracts/coaches/students-routes.test.ts (5 tests) 244ms
 ✓ tests/contracts/tvs/backend-modules.test.ts (14 tests) 194ms
 ✓ tests/contracts/payments/orders-route.test.ts (2 tests) 154ms
 ✓ tests/contracts/tvs/routes.test.ts (10 tests) 218ms
 ✓ tests/contracts/classes/routes.test.ts (5 tests) 158ms
 ✓ tests/contracts/network/routes.test.ts (12 tests) 194ms
 ✓ tests/contracts/payments/admin-routes.test.ts (6 tests) 202ms
 ✓ tests/contracts/accounts/routes.test.ts (3 tests) 202ms
 ✓ tests/contracts/tvs/admin-routes.test.ts (7 tests) 264ms
 ✓ tests/contracts/tvs/screen-player.test.tsx (3 tests) 228ms
 ✓ tests/contracts/players/backend-modules.test.ts (3 tests) 88ms
 ✓ tests/contracts/sports/routes.test.ts (8 tests) 127ms
 ✓ tests/contracts/players/routes.test.ts (1 test) 115ms
 ✓ tests/contracts/network/network-admin-club-card.test.tsx (2 tests) 174ms
 ✓ tests/contracts/tvs/backend-runtime.test.ts (4 tests) 69ms
 ✓ tests/contracts/players/players-admin-stats.test.tsx (1 test) 79ms
 ✓ tests/contracts/players/players-admin-player-list.test.tsx (2 tests) 96ms
 ✓ tests/contracts/network/backend-modules.test.ts (6 tests) 90ms
 ✓ tests/contracts/players/players-admin-shell.test.tsx (1 test) 111ms
 ✓ tests/contracts/network/network-admin-shell.test.tsx (1 test) 59ms
 ✓ tests/contracts/classes/by-date.test.ts (1 test) 11ms
 ✓ tests/contracts/network/network-admin-club-list.test.tsx (1 test) 39ms
 ✓ tests/contracts/network/network-admin-create-club-section.test.tsx (1 test) 41ms

 Test Files  32 passed (32)
      Tests  135 passed (135)
   Start at  08:23:20
   Duration  13.18s (transform 2.79s, setup 0ms, import 6.42s, tests 6.81s, environment 16.53s)

$ node scripts/next-build-safe.mjs
[build] linux/arm64 detected: using webpack + SWC wasm fallback to avoid native next-swc crash
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Skipping creating a lockfile at /home/sebas/work/projects/ballbox/.next/lock because we're using WASM bindings
▲ Next.js 16.2.1 (webpack)
- Environments: .env.local, .env.production

  Creating an optimized production build ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
✓ Compiled successfully in 8.0s
  Skipping validation of types
  Finished TypeScript config validation in 8ms ...
  Collecting page data using 3 workers ...
  Generating static pages using 3 workers (0/56) ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Generating static pages using 3 workers (14/56)
  Generating static pages using 3 workers (28/56)
  Generating static pages using 3 workers (42/56)
✓ Generating static pages using 3 workers (56/56) in 1024ms
  Finalizing page optimization ...
  Collecting build traces ...

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /admin
├ ƒ /admin/login
├ ƒ /admin/network
├ ƒ /admin/payments
├ ƒ /admin/players
├ ƒ /admin/screens
├ ƒ /admin/sports
├ ƒ /admin/tvs
├ ƒ /api/account/session
├ ƒ /api/admin/logout
├ ƒ /api/admin/payments/operations
├ ƒ /api/admin/payments/sessions
├ ƒ /api/admin/payments/sessions/[sessionId]
├ ƒ /api/admin/players
├ ƒ /api/admin/players/[playerId]
├ ƒ /api/admin/players/[playerId]/memberships
├ ƒ /api/admin/players/memberships/[membershipId]
├ ƒ /api/admin/session
├ ƒ /api/admin/sports/classes
├ ƒ /api/admin/sports/classes/[classId]
├ ƒ /api/admin/sports/coaches
├ ƒ /api/admin/sports/coaches/[coachId]
├ ƒ /api/admin/sports/open-matches
├ ƒ /api/admin/sports/open-matches/[matchId]
├ ƒ /api/admin/sports/operations
├ ƒ /api/admin/summary
├ ƒ /api/admin/tvs/assignments/[assignmentId]
├ ƒ /api/admin/tvs/campaigns
├ ƒ /api/admin/tvs/campaigns/[campaignId]
├ ƒ /api/admin/tvs/campaigns/[campaignId]/assignments
├ ƒ /api/admin/tvs/campaigns/[campaignId]/creatives
├ ƒ /api/admin/tvs/creatives/[creativeId]
├ ƒ /api/admin/tvs/operations
├ ƒ /api/admin/tvs/screens
├ ƒ /api/admin/tvs/screens/[screenId]
├ ƒ /api/admin/tvs/tv-devices
├ ƒ /api/admin/tvs/tv-devices/[tvDeviceId]
├ ƒ /api/atc-public/availability/sportclubs/[sportclubId]
├ ƒ /api/classes/logout
├ ƒ /api/classes/offers
├ ƒ /api/classes/offers/[offerId]
├ ƒ /api/classes/requests
├ ƒ /api/classes/session
├ ƒ /api/clubs/accounts
├ ƒ /api/clubs/coaches
├ ƒ /api/clubs/coaches/[coachId]
├ ƒ /api/clubs/invites
├ ƒ /api/clubs/logout
├ ƒ /api/clubs/manual-classes
├ ƒ /api/clubs/me
├ ƒ /api/clubs/players
├ ƒ /api/clubs/session
├ ƒ /api/clubs/venues
├ ƒ /api/clubs/venues/[venueId]
├ ƒ /api/coaches/accounts
├ ƒ /api/coaches/availability
├ ƒ /api/coaches/clubs
├ ƒ /api/coaches/invites/[token]
├ ƒ /api/coaches/logout
├ ƒ /api/coaches/me
├ ƒ /api/coaches/session
├ ƒ /api/coaches/students
├ ƒ /api/coaches/students/[bookingId]
├ ƒ /api/coaches/students/players
├ ƒ /api/coaches/students/players/[playerId]
├ ƒ /api/integrations/payments/approved
├ ƒ /api/integrations/payments/health
├ ƒ /api/integrations/payments/orders
├ ƒ /api/integrations/payments/sessions
├ ƒ /api/network
├ ƒ /api/network/[clubId]
├ ƒ /api/network/[clubId]/venues
├ ƒ /api/network/[clubId]/venues/[venueId]
├ ƒ /api/network/venues/[venueId]/vending-machines
├ ƒ /api/network/venues/[venueId]/vending-machines/[vendingMachineId]
├ ƒ /api/players
├ ƒ /api/sports
├ ƒ /api/tvs/health
├ ƒ /api/tvs/heartbeat
├ ƒ /api/tvs/impression
├ ƒ /api/tvs/manifest/[screenId]
├ ƒ /api/tvs/screens
├ ƒ /classes
├ ƒ /classes-by-date
├ ƒ /classes/[offerId]
├ ƒ /classes/login
├ ƒ /classes/me
├ ƒ /clubs
├ ƒ /clubs/coaches
├ ƒ /clubs/venues/[venueId]
├ ○ /coaches
├ ƒ /coaches/settings
├ ○ /manifest.webmanifest
├ ƒ /notification/mercadopago
├ ○ /offline
└ ƒ /player/[screenId]

ƒ Proxy (Middleware)

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

pre-push: all checks passed.
To https://github.com/sebasfavaron/ballbox.git
   f3a9d73..1ad344c  main -> main


Updated #7 (in_progress → completed)

Plan and implement html-first accessibility work for Ballbox

Find Ballbox repo/context, define implementation plan for html-first accessibility in Next, then execute changes to completion with validation.

planning and implementing Ballbox accessibility work

Phase 2 html-first accessibility in Ballbox

After shipping phase 1 to main, continue iterative html-first accessibility improvements on additional Ballbox flows until major custom-widget hotspots are reduced.

Phase 3 html-first accessibility in Ballbox

Continue replacing JS-only disclosure patterns with native HTML disclosure/navigation where possible.

Phase 4 html-first accessibility in Ballbox

Continue with the largest html-first accessibility hotspot, prioritizing dense coach/club admin flows with JS-heavy selection controls.

Phase 5 html-first accessibility in Ballbox

Convert coach settings student/class composer flow from JS-only open/close controls toward native disclosure and stronger form semantics.

Phase 6 html-first accessibility in Ballbox

Replace another JS-heavy selection area with more semantic controls, prioritizing calendar mode toggles in club coaches flow.

Phase 7 html-first accessibility in Ballbox

Convert calendar day selection in club coaches flow to semantic controls, then keep reducing JS-only selection patterns.


Seguí.

Subido a `main`:
- `1ad344c` Use semantic calendar day selection

Qué hice:
- `club-coaches-page-client`
  - selección de día: botones → radios semánticos
  - agrupado con `fieldset/legend`

Validado:
- `typecheck`
- `lint`
- `build`
- contract tests
- push con hooks completos

Siguiente:
- celdas del grid del calendario / alta manual de clase
- ahí todavía hay bastante JS-only interaction

Si querés, sigo ahí.


[telegram] si


todo

Phase 8 html-first accessibility in Ballbox

Improve semantic structure around calendar grid actions and manual class flow in club coaches page.


Created #8: Phase 8 html-first accessibility in Ballbox (pending)


Updated #8 (pending → in_progress)


bash

cd /home/sebas/work/projects/ballbox && rg -n "showManualClassForm|setShowManualClassForm|manualClassDraft|handleCreateManualClass|Manual" components/club-coaches-page-client.tsx


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

                      );
                    })}
                  </fieldset>

                {calendarWeekSlots.length === 0 ? (

                    Esta sede no tiene horario coach habilitado para este dia.

                ) : (

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

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

                        {totalCoveredCalendarCells} bloques ocupados

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

                          Hora

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

                          return (

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

{court.name}

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

                          );
                        })}

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

                              {formatSlotLabel(slot)}

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

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

Cerrado para coaches

                                  ) : block ? (
                                    <>

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

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

{block.playerName}

                                      ) : null}

{formatMoney(visibleSlotPriceMinor)}

                                        {block.startTime} - {block.endTime}

                                    </>
                                  ) : (
                                    <>

Hueco

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

{formatMoney(visibleSlotPriceMinor)}

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

                        ))}

                )}

            )}

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

Profesores

Analytics por profesor

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

                  {filteredProfessorRows.length} profes

Reservations

{dashboard.analytics.summary.reservationCount}

Para club

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

Para coaches

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

Reviews

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

                    Profesor
                    Sede
                    Reservas
                    Alumnos
                    Resenas
                    Para coach
                    Para club

                  {filteredProfessorRows.length === 0 ? (

                        No hay profesores para ese filtro.

                  ) : null}

                  {filteredProfessorRows.map((professor) => (

{professor.coachName}

{professor.studentCount} alumnos activos

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

                  ))}

Reservations

Detalle de reservas

                {analyticsReservations.length} reservas

                    Fecha
                    Profesor
                    Alumno
                    Sede / cancha
                    Club
                    Coach

[314 more lines in file. Use offset=1750 to continue.]


18:type ClubManualClassOptions = ClubDashboard["manualClassOptions"];
21:type ManualClassDraft = {
144:type ManualCoachOption = ClubManualClassOptions["coaches"][number];
145:type ManualCoachPackage = ManualCoachOption["packages"][number];
147:function getActiveCoachPackages(coach: ManualCoachOption | null | undefined) {
151:function applyCoachPackageToDraft(draft: ManualClassDraft, salesPackage: ManualCoachPackage | null): ManualClassDraft {
247:function createManualClassDraft(options: ClubManualClassOptions, venues: ClubVenue[]): ManualClassDraft {
315:  const [showManualClassForm, setShowManualClassForm] = useState(false);
317:  const [manualClassDraft, setManualClassDraft] = useState<ManualClassDraft>({
333:  const [savingManualClass, setSavingManualClass] = useState(false);
334:  const [manualClassError, setManualClassError] = useState<string | null>(null);
385:      setShowManualClassForm(false);
387:      setManualClassDraft(createManualClassDraft(parsed.dashboard.manualClassOptions, parsed.configuration.venues));
395:      setShowManualClassForm(false);
397:      setManualClassDraft({
509:  async function handleCreateManualClass() {
512:    setSavingManualClass(true);
513:    setManualClassError(null);
516:      const startsAt = new Date(`${manualClassDraft.date}T${manualClassDraft.time}:00`);
521:          venueId: manualClassDraft.venueId,
522:          courtId: manualClassDraft.courtId || null,
523:          coachId: manualClassDraft.coachId,
524:          packageId: manualClassDraft.packageId,
525:          playerId: manualClassDraft.playerId,
526:          playerWhatsappPhone: manualClassDraft.playerWhatsappPhone || null,
528:          durationMinutes: manualClassDraft.durationMinutes,
530:          courtPriceMinor: manualClassDraft.courtPriceMinor,
531:          coachPriceMinor: manualClassDraft.coachPriceMinor,
533:            manualClassDraft.repeatWeeklyCount > 1
534:              ? { frequency: "weekly", occurrences: manualClassDraft.repeatWeeklyCount }
536:          note: manualClassDraft.note || null,
542:        setManualClassError(payload?.details?.message ?? "No pudimos cargar la clase manual.");
547:      setShowManualClassForm(false);
549:      setManualClassError("No pudimos cargar la clase manual.");
551:      setSavingManualClass(false);
586:      setManualClassDraft((current) => ({
738:  const availableManualCoaches = manualClassOptions.coaches.filter((coach) =>
739:    manualClassDraft.venueId ? coach.venueId === manualClassDraft.venueId : Boolean(coach.venueId)
741:  const selectedManualCoach = availableManualCoaches.find((coach) => coach.id === manualClassDraft.coachId) ?? null;
742:  const availableManualPackages = getActiveCoachPackages(selectedManualCoach);
743:  const selectedManualVenue = venues.find((venue) => venue.id === manualClassDraft.venueId) ?? null;
744:  const availableManualCourts = selectedManualVenue?.courts.filter((court) => court.coachesEnabled) ?? [];
746:  const resolveManualCourtPrice = (venueId: string, coachId: string, date: string, time: string) => {
797:                  setShowManualClassForm((current) => !current);
803:                  showManualClassForm
817:                    {showManualClassForm ? "Ocultar" : "Abrir"}
826:                  if (showManualClassForm) {
827:                    setShowManualClassForm(false);
852:          {showManualClassForm ? (
960:                    value={manualClassDraft.venueId}
970:                      setManualClassDraft((current) => ({
977:                            courtPriceMinor: resolveManualCourtPrice(
1002:                    value={manualClassDraft.coachId}
1005:                      const nextCoach = availableManualCoaches.find((coach) => coach.id === nextCoachId) ?? null;
1007:                      setManualClassDraft((current) =>
1012:                            courtPriceMinor: resolveManualCourtPrice(current.venueId, nextCoachId, current.date, current.time),
1021:                    {availableManualCoaches.map((coach) => (
1032:                    value={manualClassDraft.packageId}
1033:                    disabled={!manualClassDraft.coachId || availableManualPackages.length === 0}
1035:                      const nextPackage = availableManualPackages.find((salesPackage) => salesPackage.id === event.target.value) ?? null;
1036:                      setManualClassDraft((current) => applyCoachPackageToDraft(current, nextPackage));
1041:                      {manualClassDraft.coachId
1042:                        ? availableManualPackages.length === 0
1047:                    {availableManualPackages.map((salesPackage) => (
1058:                    value={manualClassDraft.courtId}
1059:                    onChange={(event) => setManualClassDraft((current) => ({ ...current, courtId: event.target.value }))}
1063:                    {availableManualCourts.map((court) => (
1074:                    value={manualClassDraft.playerId}
1077:                      setManualClassDraft((current) => ({
1097:                    value={manualClassDraft.playerWhatsappPhone}
1098:                    onChange={(event) => setManualClassDraft((current) => ({ ...current, playerWhatsappPhone: event.target.value }))}
1107:                    value={manualClassDraft.date}
1109:                      setManualClassDraft((current) => ({
1112:                        courtPriceMinor: resolveManualCourtPrice(current.venueId, current.coachId, event.target.value, current.time),
1124:                    value={manualClassDraft.time}
1126:                      setManualClassDraft((current) => ({
1129:                        courtPriceMinor: resolveManualCourtPrice(current.venueId, current.coachId, current.date, event.target.value),
1139:                    value={String(manualClassDraft.durationMinutes)}
1141:                      setManualClassDraft((current) => ({ ...current, durationMinutes: Number(event.target.value || 60) }))
1156:                    value={String(manualClassDraft.repeatWeeklyCount)}
1158:                      setManualClassDraft((current) => ({ ...current, repeatWeeklyCount: Number(event.target.value || 1) }))
1174:                    value={manualClassDraft.courtPriceMinor}
1176:                      setManualClassDraft((current) => ({ ...current, courtPriceMinor: Math.max(0, Number(event.target.value || 0)) }))
1188:                    value={manualClassDraft.coachPriceMinor}
1197:                  value={manualClassDraft.note}
1198:                  onChange={(event) => setManualClassDraft((current) => ({ ...current, note: event.target.value }))}
1203:              {selectedManualVenue ? (
1205:                  Precio actual del horario: {formatMoney(resolveManualCourtPrice(
1206:                    selectedManualVenue.id,
1207:                    manualClassDraft.coachId,
1208:                    manualClassDraft.date,
1209:                    manualClassDraft.time
1211:                  . Base de la sede: {formatMoney(selectedManualVenue.coachReservationPrice)}. Descuento coach del club:{" "}
1212:                  {formatMoney(selectedManualVenue.clubCoachReservationDiscount)}.
1216:              {manualClassDraft.coachId && availableManualPackages.length === 0 ? (
1231:                    savingManualClass ||
1232:                    !manualClassDraft.venueId ||
1233:                    !manualClassDraft.coachId ||
1234:                    !manualClassDraft.packageId ||
1235:                    !manualClassDraft.playerId ||
1236:                    !manualClassDraft.date ||
1237:                    !manualClassDraft.time
1239:                  onClick={() => void handleCreateManualClass()}
1241:                  {savingManualClass ? "Guardando..." : "Guardar clase"}
1245:                  onClick={() => setShowManualClassForm(false)}
1559:                                    setManualClassDraft((current) => ({
1585:                                              courtPriceMinor: resolveManualCourtPrice(
1597:                                    setShowManualClassForm(true);


active: view === "coaches",
            onClick: () => handleViewChange("coaches"),
          },
        ]}
        onSignOut={async () => {
          await fetch("/api/clubs/logout", { method: "POST" });
          window.location.href = "/coaches";
        }}
      />

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

              <button
                type="button"
                onClick={() => {
                  setShowManualClassForm((current) => !current);
                  if (showRequestsPanel) {
                    setShowRequestsPanel(false);
                  }
                }}
                className={`rounded-[1.35rem] border px-5 py-5 text-left transition ${
                  showManualClassForm
                    ? "border-[#c4d600]/45 bg-[#c4d600]/12 shadow-[0_0_0_1px_rgba(196,214,0,0.22)]"
                    : "border-white/10 bg-black/20 hover:border-[#c4d600]/30 hover:bg-[#c4d600]/[0.06]"
                }`}
              >

Operacion diaria

Agregar clase

                      Carga una reserva manual para una sede, profesor, cancha y alumno.

                    {showManualClassForm ? "Ocultar" : "Abrir"}

              </button>

              <button
                type="button"
                onClick={() => {
                  setShowRequestsPanel((current) => !current);
                  if (showManualClassForm) {
                    setShowManualClassForm(false);
                  }
                }}
                className={`rounded-[1.35rem] border px-5 py-5 text-left transition ${
                  showRequestsPanel
                    ? "border-sky-400/45 bg-sky-400/10 shadow-[0_0_0_1px_rgba(56,189,248,0.18)]"
                    : "border-white/10 bg-black/20 hover:border-sky-400/30 hover:bg-sky-400/[0.05]"
                }`}
              >

Seguimiento

Ver solicitudes

                      Revisa pedidos, cambios o problemas pendientes de alumnos y profesores.

                    {dashboard.requests.length} items

              </button>

          {showManualClassForm ? (

Agregar clase

Carga manual para tu operacion diaria

Nuevo alumno

Lo creas para el club y despues se lo asignas al profesor que corresponda.

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

                      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 ClubStudentCreateDraft["gender"],
                          }))
                        }
                        className="mt-2 h-11 w-full rounded-xl border border-white/15 bg-black/30 px-3 text-sm text-white"
                      >
                        <option value="">Sin dato</option>
                        <option value="female">Mujer</option>
                        <option value="male">Hombre</option>
                        <option value="other">Otro</option>
                      </select>

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

                      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>

                  {studentCreateError ? (

                      {studentCreateError}

                  ) : null}

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

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

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

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

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

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

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

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

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

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

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

                  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="button"
                  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
                  }
                  onClick={() => void handleCreateManualClass()}
                >
                  {savingManualClass ? "Guardando..." : "Guardar clase"}
                </Button>
                <button
                  type="button"
                  onClick={() => setShowManualClassForm(false)}
                  className="text-sm text-white/60 transition hover:text-white"
                >
                  Cancelar
                </button>

          ) : 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}

[784 more lines in file. Use offset=1280 to continue.]


**Improving class disclosure**

I see there's a need to enhance the manual class details. I think being more specific in the disclosures will help make things clearer for everyone. Maybe I'll focus on adding more comprehensive explanations about what each class entails. It's important that users understand exactly what they’re signing up for, so let’s make sure we do that effectively! I’ll plan a way to tackle these improvements soon.

edit

              <button
                type="button"
                onClick={() => {
                  setShowManualClassForm((current) => !current);
                  if (showRequestsPanel) {
                    setShowRequestsPanel(false);
                  }
                }}
                className={`rounded-[1.35rem] border px-5 py-5 text-left transition ${
                  showManualClassForm
                    ? "border-[#c4d600]/45 bg-[#c4d600]/12 shadow-[0_0_0_1px_rgba(196,214,0,0.22)]"
                    : "border-white/10 bg-black/20 hover:border-[#c4d600]/30 hover:bg-[#c4d600]/[0.06]"
                }`}
              >

Operacion diaria

Agregar clase

                      Carga una reserva manual para una sede, profesor, cancha y alumno.

                    {showManualClassForm ? "Ocultar" : "Abrir"}

              </button>

                 {
                    if (showRequestsPanel) {
                      setShowRequestsPanel(false);
                    }
                    setShowManualClassForm((current) => !current);
                  }}
                  className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
                >

Operacion diaria

Agregar clase

                        Carga una reserva manual para una sede, profesor, cancha y alumno.

                      {showManualClassForm ? "Ocultar" : "Abrir"}

          {showManualClassForm ? (
            <article className="rounded-[1.75rem] border border-white/10 bg-[#0f140f]/80 p-6">

          {showManualClassForm ? (
            <article className="rounded-[1.75rem] border border-white/10 bg-[#0f140f]/80 p-6" aria-labelledby="manual-class-heading">

Agregar clase

Carga manual para tu operacion diaria


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

...
  790       {view === "calendar" ? (
  791         <>
  792

  793

- 794               <button
- 795                 type="button"
- 796                 onClick={() => {
- 797                   setShowManualClassForm((current) => !current);
- 798                   if (showRequestsPanel) {
- 799                     setShowRequestsPanel(false);
- 800                   }
- 801                 }}
- 802                 className={`rounded-[1.35rem] border px-5 py-5 text-left transition ${
- 803                   showManualClassForm
- 804                     ? "border-[#c4d600]/45 bg-[#c4d600]/12 shadow-[0_0_0_1px_rgba(196,214,0,0.22)]"
- 805                     : "border-white/10 bg-black/20 hover:border-[#c4d600]/30 hover:bg-[#c4d600]/[0.06]"
- 806                 }`}
- 807               >
- 808
Operacion diaria

- 809

- 810

- 811
Agregar clase

- 812

- 813                       Carga una reserva manual para una sede, profesor, cancha y alumno.
- 814

+ 794
+ 795                  {
+ 797                     if (showRequestsPanel) {
+ 798                       setShowRequestsPanel(false);
+ 799                     }
+ 800                     setShowManualClassForm((current) => !current);
+ 801                   }}
+ 802                   className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
+ 803                 >
+ 804
Operacion diaria

+ 805

+ 806

+ 807
Agregar clase

+ 808

+ 809                         Carga una reserva manual para una sede, profesor, cancha y alumno.
+ 810

+ 811

+ 812
+ 813                       {showManualClassForm ? "Ocultar" : "Abrir"}
+ 814
  815

- 816
- 817                     {showManualClassForm ? "Ocultar" : "Abrir"}
- 818
- 819

- 820               </button>
+ 816
+ 817
  821
  822               <button
  823                 type="button"
  824                 onClick={() => {
      ...
  849

  850

  851
  852           {showManualClassForm ? (
- 853

+ 850

  854

  855

  856
Agregar clase

- 857
Carga manual para tu operacion diaria

+ 854
Carga manual para tu operacion diaria

  858

  859

  860
  861

      ...


bash

cd /home/sebas/work/projects/ballbox && pnpm typecheck && pnpm lint

cd /home/sebas/work/projects/ballbox && pnpm build


$ rm -rf .next/types .next/dev/types
$ tsc --noEmit --incremental false
$ eslint .


$ node scripts/next-build-safe.mjs
[build] linux/arm64 detected: using webpack + SWC wasm fallback to avoid native next-swc crash
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Skipping creating a lockfile at /home/sebas/work/projects/ballbox/.next/lock because we're using WASM bindings
▲ Next.js 16.2.1 (webpack)
- Environments: .env.local, .env.production

  Creating an optimized production build ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
✓ Compiled successfully in 15.6s
  Skipping validation of types
  Finished TypeScript config validation in 11ms ...
  Collecting page data using 3 workers ...
  Generating static pages using 3 workers (0/56) ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Generating static pages using 3 workers (14/56)
  Generating static pages using 3 workers (28/56)
  Generating static pages using 3 workers (42/56)
✓ Generating static pages using 3 workers (56/56) in 1317ms
  Finalizing page optimization ...
  Collecting build traces ...

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /admin
├ ƒ /admin/login
├ ƒ /admin/network
├ ƒ /admin/payments
├ ƒ /admin/players
├ ƒ /admin/screens
├ ƒ /admin/sports
├ ƒ /admin/tvs
├ ƒ /api/account/session
├ ƒ /api/admin/logout
├ ƒ /api/admin/payments/operations
├ ƒ /api/admin/payments/sessions
├ ƒ /api/admin/payments/sessions/[sessionId]
├ ƒ /api/admin/players
├ ƒ /api/admin/players/[playerId]
├ ƒ /api/admin/players/[playerId]/memberships
├ ƒ /api/admin/players/memberships/[membershipId]
├ ƒ /api/admin/session
├ ƒ /api/admin/sports/classes
├ ƒ /api/admin/sports/classes/[classId]
├ ƒ /api/admin/sports/coaches
├ ƒ /api/admin/sports/coaches/[coachId]
├ ƒ /api/admin/sports/open-matches
├ ƒ /api/admin/sports/open-matches/[matchId]
├ ƒ /api/admin/sports/operations
├ ƒ /api/admin/summary
├ ƒ /api/admin/tvs/assignments/[assignmentId]
├ ƒ /api/admin/tvs/campaigns
├ ƒ /api/admin/tvs/campaigns/[campaignId]
├ ƒ /api/admin/tvs/campaigns/[campaignId]/assignments
├ ƒ /api/admin/tvs/campaigns/[campaignId]/creatives
├ ƒ /api/admin/tvs/creatives/[creativeId]
├ ƒ /api/admin/tvs/operations
├ ƒ /api/admin/tvs/screens
├ ƒ /api/admin/tvs/screens/[screenId]
├ ƒ /api/admin/tvs/tv-devices
├ ƒ /api/admin/tvs/tv-devices/[tvDeviceId]
├ ƒ /api/atc-public/availability/sportclubs/[sportclubId]
├ ƒ /api/classes/logout
├ ƒ /api/classes/offers
├ ƒ /api/classes/offers/[offerId]
├ ƒ /api/classes/requests
├ ƒ /api/classes/session
├ ƒ /api/clubs/accounts
├ ƒ /api/clubs/coaches
├ ƒ /api/clubs/coaches/[coachId]
├ ƒ /api/clubs/invites
├ ƒ /api/clubs/logout
├ ƒ /api/clubs/manual-classes
├ ƒ /api/clubs/me
├ ƒ /api/clubs/players
├ ƒ /api/clubs/session
├ ƒ /api/clubs/venues
├ ƒ /api/clubs/venues/[venueId]
├ ƒ /api/coaches/accounts
├ ƒ /api/coaches/availability
├ ƒ /api/coaches/clubs
├ ƒ /api/coaches/invites/[token]
├ ƒ /api/coaches/logout
├ ƒ /api/coaches/me
├ ƒ /api/coaches/session
├ ƒ /api/coaches/students
├ ƒ /api/coaches/students/[bookingId]
├ ƒ /api/coaches/students/players
├ ƒ /api/coaches/students/players/[playerId]
├ ƒ /api/integrations/payments/approved
├ ƒ /api/integrations/payments/health
├ ƒ /api/integrations/payments/orders
├ ƒ /api/integrations/payments/sessions
├ ƒ /api/network
├ ƒ /api/network/[clubId]
├ ƒ /api/network/[clubId]/venues
├ ƒ /api/network/[clubId]/venues/[venueId]
├ ƒ /api/network/venues/[venueId]/vending-machines
├ ƒ /api/network/venues/[venueId]/vending-machines/[vendingMachineId]
├ ƒ /api/players
├ ƒ /api/sports
├ ƒ /api/tvs/health
├ ƒ /api/tvs/heartbeat
├ ƒ /api/tvs/impression
├ ƒ /api/tvs/manifest/[screenId]
├ ƒ /api/tvs/screens
├ ƒ /classes
├ ƒ /classes-by-date
├ ƒ /classes/[offerId]
├ ƒ /classes/login
├ ƒ /classes/me
├ ƒ /clubs
├ ƒ /clubs/coaches
├ ƒ /clubs/venues/[venueId]
├ ○ /coaches
├ ƒ /coaches/settings
├ ○ /manifest.webmanifest
├ ƒ /notification/mercadopago
├ ○ /offline
└ ƒ /player/[screenId]

ƒ Proxy (Middleware)

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand


cd /home/sebas/work/projects/ballbox && git add components/club-coaches-page-client.tsx && git commit -m "Add semantic manual class disclosure shell" && git push origin main


[main 121454a] Add semantic manual class disclosure shell
 1 file changed, 25 insertions(+), 28 deletions(-)
pre-push: pushing main, running quality checks...
$ node scripts/check-doc-references.mjs
Docs reference check passed for README.md, PLAN.md, and docs/index.md
$ ./scripts/db-local-up.sh
Local Postgres already ready.
$ ./scripts/prisma-migrate-deploy.sh
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "ballbox_local", schema "public" at "127.0.0.1:5432"

2 migrations found in prisma/migrations

No pending migrations to apply.
$ ./scripts/prisma-check-migrations.sh
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "ballbox_local", schema "public" at "127.0.0.1:5432"

2 migrations found in prisma/migrations

Database schema is up to date!
Prisma schema loaded from prisma/schema.prisma
The schema at prisma/schema.prisma is valid 🚀
$ eslint .
$ rm -rf .next/types .next/dev/types
$ tsc --noEmit --incremental false
$ vitest run tests/contracts

 RUN  v4.1.0 /home/sebas/work/projects/ballbox

 ✓ tests/contracts/players/players-admin-create-player-section.test.tsx (1 test) 462ms
     ✓ renders the extracted section shell and forwards player form updates  460ms
 ✓ tests/contracts/players/players-admin-player-card.test.tsx (2 tests) 535ms
     ✓ renders the empty memberships state for a player without memberships  445ms
 ✓ tests/contracts/classes/classes-request-form.test.tsx (3 tests) 823ms
     ✓ submits the selected visible slot as part of the request  612ms
 ✓ tests/contracts/clubs/routes.test.ts (9 tests) 311ms
 ✓ tests/contracts/payments/auth.test.ts (2 tests) 405ms
 ✓ tests/contracts/players/admin-routes.test.ts (9 tests) 205ms
 ✓ tests/contracts/coaches/routes.test.ts (7 tests) 308ms
 ✓ tests/contracts/tvs/admin-routes.test.ts (7 tests) 207ms
 ✓ tests/contracts/network/network-admin-venue-card.test.tsx (2 tests) 497ms
     ✓ updates the targeted machine active flag through setClubs  347ms
 ✓ tests/contracts/coaches/students-routes.test.ts (5 tests) 270ms
 ✓ tests/contracts/coaches/backend.test.ts (1 test) 291ms
 ✓ tests/contracts/tvs/routes.test.ts (10 tests) 262ms
 ✓ tests/contracts/accounts/routes.test.ts (3 tests) 233ms
 ✓ tests/contracts/payments/admin-routes.test.ts (6 tests) 187ms
 ✓ tests/contracts/tvs/backend-modules.test.ts (14 tests) 187ms
 ✓ tests/contracts/tvs/screen-player.test.tsx (3 tests) 225ms
 ✓ tests/contracts/network/routes.test.ts (12 tests) 186ms
 ✓ tests/contracts/classes/routes.test.ts (5 tests) 218ms
 ✓ tests/contracts/payments/orders-route.test.ts (2 tests) 197ms
 ✓ tests/contracts/players/routes.test.ts (1 test) 120ms
 ✓ tests/contracts/sports/routes.test.ts (8 tests) 153ms
 ✓ tests/contracts/network/network-admin-club-card.test.tsx (2 tests) 169ms
 ✓ tests/contracts/network/backend-modules.test.ts (6 tests) 92ms
 ✓ tests/contracts/players/backend-modules.test.ts (3 tests) 103ms
 ✓ tests/contracts/players/players-admin-shell.test.tsx (1 test) 58ms
 ✓ tests/contracts/players/players-admin-player-list.test.tsx (2 tests) 73ms
 ✓ tests/contracts/tvs/backend-runtime.test.ts (4 tests) 80ms
 ✓ tests/contracts/players/players-admin-stats.test.tsx (1 test) 123ms
 ✓ tests/contracts/network/network-admin-shell.test.tsx (1 test) 63ms
 ✓ tests/contracts/classes/by-date.test.ts (1 test) 8ms
 ✓ tests/contracts/network/network-admin-create-club-section.test.tsx (1 test) 55ms
 ✓ tests/contracts/network/network-admin-club-list.test.tsx (1 test) 37ms

 Test Files  32 passed (32)
      Tests  135 passed (135)
   Start at  09:06:55
   Duration  13.49s (transform 2.96s, setup 0ms, import 6.09s, tests 7.14s, environment 16.55s)

$ node scripts/next-build-safe.mjs
[build] linux/arm64 detected: using webpack + SWC wasm fallback to avoid native next-swc crash
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Skipping creating a lockfile at /home/sebas/work/projects/ballbox/.next/lock because we're using WASM bindings
▲ Next.js 16.2.1 (webpack)
- Environments: .env.local, .env.production

  Creating an optimized production build ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
✓ Compiled successfully in 8.1s
  Skipping validation of types
  Finished TypeScript config validation in 9ms ...
  Collecting page data using 3 workers ...
  Generating static pages using 3 workers (0/56) ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Generating static pages using 3 workers (14/56)
  Generating static pages using 3 workers (28/56)
  Generating static pages using 3 workers (42/56)
✓ Generating static pages using 3 workers (56/56) in 959ms
  Finalizing page optimization ...
  Collecting build traces ...

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /admin
├ ƒ /admin/login
├ ƒ /admin/network
├ ƒ /admin/payments
├ ƒ /admin/players
├ ƒ /admin/screens
├ ƒ /admin/sports
├ ƒ /admin/tvs
├ ƒ /api/account/session
├ ƒ /api/admin/logout
├ ƒ /api/admin/payments/operations
├ ƒ /api/admin/payments/sessions
├ ƒ /api/admin/payments/sessions/[sessionId]
├ ƒ /api/admin/players
├ ƒ /api/admin/players/[playerId]
├ ƒ /api/admin/players/[playerId]/memberships
├ ƒ /api/admin/players/memberships/[membershipId]
├ ƒ /api/admin/session
├ ƒ /api/admin/sports/classes
├ ƒ /api/admin/sports/classes/[classId]
├ ƒ /api/admin/sports/coaches
├ ƒ /api/admin/sports/coaches/[coachId]
├ ƒ /api/admin/sports/open-matches
├ ƒ /api/admin/sports/open-matches/[matchId]
├ ƒ /api/admin/sports/operations
├ ƒ /api/admin/summary
├ ƒ /api/admin/tvs/assignments/[assignmentId]
├ ƒ /api/admin/tvs/campaigns
├ ƒ /api/admin/tvs/campaigns/[campaignId]
├ ƒ /api/admin/tvs/campaigns/[campaignId]/assignments
├ ƒ /api/admin/tvs/campaigns/[campaignId]/creatives
├ ƒ /api/admin/tvs/creatives/[creativeId]
├ ƒ /api/admin/tvs/operations
├ ƒ /api/admin/tvs/screens
├ ƒ /api/admin/tvs/screens/[screenId]
├ ƒ /api/admin/tvs/tv-devices
├ ƒ /api/admin/tvs/tv-devices/[tvDeviceId]
├ ƒ /api/atc-public/availability/sportclubs/[sportclubId]
├ ƒ /api/classes/logout
├ ƒ /api/classes/offers
├ ƒ /api/classes/offers/[offerId]
├ ƒ /api/classes/requests
├ ƒ /api/classes/session
├ ƒ /api/clubs/accounts
├ ƒ /api/clubs/coaches
├ ƒ /api/clubs/coaches/[coachId]
├ ƒ /api/clubs/invites
├ ƒ /api/clubs/logout
├ ƒ /api/clubs/manual-classes
├ ƒ /api/clubs/me
├ ƒ /api/clubs/players
├ ƒ /api/clubs/session
├ ƒ /api/clubs/venues
├ ƒ /api/clubs/venues/[venueId]
├ ƒ /api/coaches/accounts
├ ƒ /api/coaches/availability
├ ƒ /api/coaches/clubs
├ ƒ /api/coaches/invites/[token]
├ ƒ /api/coaches/logout
├ ƒ /api/coaches/me
├ ƒ /api/coaches/session
├ ƒ /api/coaches/students
├ ƒ /api/coaches/students/[bookingId]
├ ƒ /api/coaches/students/players
├ ƒ /api/coaches/students/players/[playerId]
├ ƒ /api/integrations/payments/approved
├ ƒ /api/integrations/payments/health
├ ƒ /api/integrations/payments/orders
├ ƒ /api/integrations/payments/sessions
├ ƒ /api/network
├ ƒ /api/network/[clubId]
├ ƒ /api/network/[clubId]/venues
├ ƒ /api/network/[clubId]/venues/[venueId]
├ ƒ /api/network/venues/[venueId]/vending-machines
├ ƒ /api/network/venues/[venueId]/vending-machines/[vendingMachineId]
├ ƒ /api/players
├ ƒ /api/sports
├ ƒ /api/tvs/health
├ ƒ /api/tvs/heartbeat
├ ƒ /api/tvs/impression
├ ƒ /api/tvs/manifest/[screenId]
├ ƒ /api/tvs/screens
├ ƒ /classes
├ ƒ /classes-by-date
├ ƒ /classes/[offerId]
├ ƒ /classes/login
├ ƒ /classes/me
├ ƒ /clubs
├ ƒ /clubs/coaches
├ ƒ /clubs/venues/[venueId]
├ ○ /coaches
├ ƒ /coaches/settings
├ ○ /manifest.webmanifest
├ ƒ /notification/mercadopago
├ ○ /offline
└ ƒ /player/[screenId]

ƒ Proxy (Middleware)

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

pre-push: all checks passed.
To https://github.com/sebasfavaron/ballbox.git
   1ad344c..121454a  main -> main


Updated #8 (in_progress → completed)


Seguí.

Subido a `main`:
- `121454a` Add semantic manual class disclosure shell

Qué hice:
- `club-coaches-page-client`
  - tarjeta “Agregar clase” ahora usa shell semántico de disclosure
  - mejor relación entre trigger y panel
  - heading del panel manual conectado con `aria-labelledby`

Validado:
- `typecheck`
- `lint`
- `build`
- contract tests
- push con hooks completos

Queda fuerte:
- requests panel todavía sigue con toggle JS
- grid de celdas sigue siendo interacción compleja, pero ya con base bastante mejor

Si querés, próxima fase hago:
- requests panel → disclosure semántico
- y después reviso el grid para nombres/estado más explícitos.


[telegram] si


todo

Phase 9 html-first accessibility in Ballbox

Convert requests panel to semantic disclosure and keep reducing JS-only dashboard toggles in club coaches flow.


Created #9: Phase 9 html-first accessibility in Ballbox (pending)


Updated #9 (pending → in_progress)


edit

<button
                type="button"
                onClick={() => {
                  setShowRequestsPanel((current) => !current);
                  if (showManualClassForm) {
                    setShowManualClassForm(false);
                  }
                }}
                className={`rounded-[1.35rem] border px-5 py-5 text-left transition ${
                  showRequestsPanel
                    ? "border-sky-400/45 bg-sky-400/10 shadow-[0_0_0_1px_rgba(56,189,248,0.18)]"
                    : "border-white/10 bg-black/20 hover:border-sky-400/30 hover:bg-sky-400/[0.05]"
                }`}
              >

Seguimiento

Ver solicitudes

                      Revisa pedidos, cambios o problemas pendientes de alumnos y profesores.

                    {dashboard.requests.length} items

              </button>

                 {
                    if (showManualClassForm) {
                      setShowManualClassForm(false);
                    }
                    setShowRequestsPanel((current) => !current);
                  }}
                  className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
                >

Seguimiento

Ver solicitudes

                        Revisa pedidos, cambios o problemas pendientes de alumnos y profesores.

                      {dashboard.requests.length} items

          {showRequestsPanel ? (
            <article className="rounded-[1.75rem] border border-white/10 bg-[#0f140f]/80 p-6">

          {showRequestsPanel ? (
            <article className="rounded-[1.75rem] border border-white/10 bg-[#0f140f]/80 p-6" aria-labelledby="requests-panel-heading">


...
  815

  816
  817
  818
- 819               <button
- 820                 type="button"
- 821                 onClick={() => {
- 822                   setShowRequestsPanel((current) => !current);
- 823                   if (showManualClassForm) {
- 824                     setShowManualClassForm(false);
- 825                   }
- 826                 }}
- 827                 className={`rounded-[1.35rem] border px-5 py-5 text-left transition ${
- 828                   showRequestsPanel
- 829                     ? "border-sky-400/45 bg-sky-400/10 shadow-[0_0_0_1px_rgba(56,189,248,0.18)]"
- 830                     : "border-white/10 bg-black/20 hover:border-sky-400/30 hover:bg-sky-400/[0.05]"
- 831                 }`}
- 832               >
- 833
Seguimiento

- 834

- 835

- 836
Ver solicitudes

- 837

- 838                       Revisa pedidos, cambios o problemas pendientes de alumnos y profesores.
- 839

+ 819
+ 820                  {
+ 822                     if (showManualClassForm) {
+ 823                       setShowManualClassForm(false);
+ 824                     }
+ 825                     setShowRequestsPanel((current) => !current);
+ 826                   }}
+ 827                   className="cursor-pointer list-none px-5 py-5 text-left [&::-webkit-details-marker]:hidden"
+ 828                 >
+ 829
Seguimiento

+ 830

+ 831

+ 832
Ver solicitudes

+ 833

+ 834                         Revisa pedidos, cambios o problemas pendientes de alumnos y profesores.
+ 835

+ 836

+ 837
+ 838                       {dashboard.requests.length} items
+ 839
  840

- 841
- 842                     {dashboard.requests.length} items
- 843
- 844

- 845               </button>
+ 841
+ 842
  846

  847

  848
  849           {showManualClassForm ? (
      ...
 1248

 1249           ) : null}
 1250
 1251           {showRequestsPanel ? (
-1252

+1249

 1253

 1254

 1255
Solicitudes

-1256
Pendientes del club

+1253
Pendientes del club

 1257

 1258
 1259                   {dashboard.requests.length} items
 1260
      ...


$ node scripts/next-build-safe.mjs
[build] linux/arm64 detected: using webpack + SWC wasm fallback to avoid native next-swc crash
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Skipping creating a lockfile at /home/sebas/work/projects/ballbox/.next/lock because we're using WASM bindings
▲ Next.js 16.2.1 (webpack)
- Environments: .env.local, .env.production

  Creating an optimized production build ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
✓ Compiled successfully in 16.2s
  Skipping validation of types
  Finished TypeScript config validation in 12ms ...
  Collecting page data using 3 workers ...
  Generating static pages using 3 workers (0/56) ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Generating static pages using 3 workers (14/56)
  Generating static pages using 3 workers (28/56)
  Generating static pages using 3 workers (42/56)
✓ Generating static pages using 3 workers (56/56) in 1611ms
  Finalizing page optimization ...
  Collecting build traces ...

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /admin
├ ƒ /admin/login
├ ƒ /admin/network
├ ƒ /admin/payments
├ ƒ /admin/players
├ ƒ /admin/screens
├ ƒ /admin/sports
├ ƒ /admin/tvs
├ ƒ /api/account/session
├ ƒ /api/admin/logout
├ ƒ /api/admin/payments/operations
├ ƒ /api/admin/payments/sessions
├ ƒ /api/admin/payments/sessions/[sessionId]
├ ƒ /api/admin/players
├ ƒ /api/admin/players/[playerId]
├ ƒ /api/admin/players/[playerId]/memberships
├ ƒ /api/admin/players/memberships/[membershipId]
├ ƒ /api/admin/session
├ ƒ /api/admin/sports/classes
├ ƒ /api/admin/sports/classes/[classId]
├ ƒ /api/admin/sports/coaches
├ ƒ /api/admin/sports/coaches/[coachId]
├ ƒ /api/admin/sports/open-matches
├ ƒ /api/admin/sports/open-matches/[matchId]
├ ƒ /api/admin/sports/operations
├ ƒ /api/admin/summary
├ ƒ /api/admin/tvs/assignments/[assignmentId]
├ ƒ /api/admin/tvs/campaigns
├ ƒ /api/admin/tvs/campaigns/[campaignId]
├ ƒ /api/admin/tvs/campaigns/[campaignId]/assignments
├ ƒ /api/admin/tvs/campaigns/[campaignId]/creatives
├ ƒ /api/admin/tvs/creatives/[creativeId]
├ ƒ /api/admin/tvs/operations
├ ƒ /api/admin/tvs/screens
├ ƒ /api/admin/tvs/screens/[screenId]
├ ƒ /api/admin/tvs/tv-devices
├ ƒ /api/admin/tvs/tv-devices/[tvDeviceId]
├ ƒ /api/atc-public/availability/sportclubs/[sportclubId]
├ ƒ /api/classes/logout
├ ƒ /api/classes/offers
├ ƒ /api/classes/offers/[offerId]
├ ƒ /api/classes/requests
├ ƒ /api/classes/session
├ ƒ /api/clubs/accounts
├ ƒ /api/clubs/coaches
├ ƒ /api/clubs/coaches/[coachId]
├ ƒ /api/clubs/invites
├ ƒ /api/clubs/logout
├ ƒ /api/clubs/manual-classes
├ ƒ /api/clubs/me
├ ƒ /api/clubs/players
├ ƒ /api/clubs/session
├ ƒ /api/clubs/venues
├ ƒ /api/clubs/venues/[venueId]
├ ƒ /api/coaches/accounts
├ ƒ /api/coaches/availability
├ ƒ /api/coaches/clubs
├ ƒ /api/coaches/invites/[token]
├ ƒ /api/coaches/logout
├ ƒ /api/coaches/me
├ ƒ /api/coaches/session
├ ƒ /api/coaches/students
├ ƒ /api/coaches/students/[bookingId]
├ ƒ /api/coaches/students/players
├ ƒ /api/coaches/students/players/[playerId]
├ ƒ /api/integrations/payments/approved
├ ƒ /api/integrations/payments/health
├ ƒ /api/integrations/payments/orders
├ ƒ /api/integrations/payments/sessions
├ ƒ /api/network
├ ƒ /api/network/[clubId]
├ ƒ /api/network/[clubId]/venues
├ ƒ /api/network/[clubId]/venues/[venueId]
├ ƒ /api/network/venues/[venueId]/vending-machines
├ ƒ /api/network/venues/[venueId]/vending-machines/[vendingMachineId]
├ ƒ /api/players
├ ƒ /api/sports
├ ƒ /api/tvs/health
├ ƒ /api/tvs/heartbeat
├ ƒ /api/tvs/impression
├ ƒ /api/tvs/manifest/[screenId]
├ ƒ /api/tvs/screens
├ ƒ /classes
├ ƒ /classes-by-date
├ ƒ /classes/[offerId]
├ ƒ /classes/login
├ ƒ /classes/me
├ ƒ /clubs
├ ƒ /clubs/coaches
├ ƒ /clubs/venues/[venueId]
├ ○ /coaches
├ ƒ /coaches/settings
├ ○ /manifest.webmanifest
├ ƒ /notification/mercadopago
├ ○ /offline
└ ƒ /player/[screenId]

ƒ Proxy (Middleware)

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand


bash

cd /home/sebas/work/projects/ballbox && git add components/club-coaches-page-client.tsx && git commit -m "Add semantic requests disclosure shell" && git push origin main


[main 31befb3] Add semantic requests disclosure shell
 1 file changed, 25 insertions(+), 28 deletions(-)
pre-push: pushing main, running quality checks...
$ node scripts/check-doc-references.mjs
Docs reference check passed for README.md, PLAN.md, and docs/index.md
$ ./scripts/db-local-up.sh
Local Postgres already ready.
$ ./scripts/prisma-migrate-deploy.sh
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "ballbox_local", schema "public" at "127.0.0.1:5432"

2 migrations found in prisma/migrations

No pending migrations to apply.
$ ./scripts/prisma-check-migrations.sh
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "ballbox_local", schema "public" at "127.0.0.1:5432"

2 migrations found in prisma/migrations

Database schema is up to date!
Prisma schema loaded from prisma/schema.prisma
The schema at prisma/schema.prisma is valid 🚀
$ eslint .
$ rm -rf .next/types .next/dev/types
$ tsc --noEmit --incremental false
$ vitest run tests/contracts

 RUN  v4.1.0 /home/sebas/work/projects/ballbox

 ✓ tests/contracts/players/players-admin-player-card.test.tsx (2 tests) 447ms
     ✓ renders the empty memberships state for a player without memberships  363ms
 ✓ tests/contracts/network/network-admin-venue-card.test.tsx (2 tests) 450ms
     ✓ updates the targeted machine active flag through setClubs  349ms
 ✓ tests/contracts/classes/classes-request-form.test.tsx (3 tests) 874ms
     ✓ submits the selected visible slot as part of the request  601ms
 ✓ tests/contracts/payments/auth.test.ts (2 tests) 375ms
 ✓ tests/contracts/clubs/routes.test.ts (9 tests) 406ms
 ✓ tests/contracts/coaches/routes.test.ts (7 tests) 298ms
 ✓ tests/contracts/coaches/backend.test.ts (1 test) 330ms
     ✓ keeps coach profile venue summarized while preserving assigned venue detail  323ms
 ✓ tests/contracts/coaches/students-routes.test.ts (5 tests) 215ms
 ✓ tests/contracts/players/players-admin-create-player-section.test.tsx (1 test) 403ms
     ✓ renders the extracted section shell and forwards player form updates  401ms
 ✓ tests/contracts/tvs/routes.test.ts (10 tests) 178ms
 ✓ tests/contracts/accounts/routes.test.ts (3 tests) 192ms
 ✓ tests/contracts/classes/routes.test.ts (5 tests) 214ms
 ✓ tests/contracts/tvs/admin-routes.test.ts (7 tests) 196ms
 ✓ tests/contracts/payments/orders-route.test.ts (2 tests) 237ms
 ✓ tests/contracts/players/admin-routes.test.ts (9 tests) 313ms
 ✓ tests/contracts/payments/admin-routes.test.ts (6 tests) 189ms
 ✓ tests/contracts/tvs/backend-modules.test.ts (14 tests) 249ms
 ✓ tests/contracts/tvs/screen-player.test.tsx (3 tests) 272ms
 ✓ tests/contracts/network/routes.test.ts (12 tests) 168ms
 ✓ tests/contracts/sports/routes.test.ts (8 tests) 220ms
 ✓ tests/contracts/players/routes.test.ts (1 test) 116ms
 ✓ tests/contracts/players/backend-modules.test.ts (3 tests) 106ms
 ✓ tests/contracts/network/network-admin-club-card.test.tsx (2 tests) 177ms
 ✓ tests/contracts/network/backend-modules.test.ts (6 tests) 61ms
 ✓ tests/contracts/players/players-admin-stats.test.tsx (1 test) 81ms
 ✓ tests/contracts/tvs/backend-runtime.test.ts (4 tests) 79ms
 ✓ tests/contracts/network/network-admin-shell.test.tsx (1 test) 80ms
 ✓ tests/contracts/players/players-admin-player-list.test.tsx (2 tests) 76ms
 ✓ tests/contracts/players/players-admin-shell.test.tsx (1 test) 57ms
 ✓ tests/contracts/classes/by-date.test.ts (1 test) 8ms
 ✓ tests/contracts/network/network-admin-club-list.test.tsx (1 test) 43ms
 ✓ tests/contracts/network/network-admin-create-club-section.test.tsx (1 test) 60ms

 Test Files  32 passed (32)
      Tests  135 passed (135)
   Start at  09:16:42
   Duration  13.27s (transform 2.93s, setup 0ms, import 6.24s, tests 7.17s, environment 16.57s)

$ node scripts/next-build-safe.mjs
[build] linux/arm64 detected: using webpack + SWC wasm fallback to avoid native next-swc crash
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Skipping creating a lockfile at /home/sebas/work/projects/ballbox/.next/lock because we're using WASM bindings
▲ Next.js 16.2.1 (webpack)
- Environments: .env.local, .env.production

  Creating an optimized production build ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
✓ Compiled successfully in 8.4s
  Skipping validation of types
  Finished TypeScript config validation in 7ms ...
  Collecting page data using 3 workers ...
  Generating static pages using 3 workers (0/56) ...
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
⚠ experimental.useWasmBinary is not an option for supported platform linux/arm64 and will be ignored.
  Generating static pages using 3 workers (14/56)
  Generating static pages using 3 workers (28/56)
  Generating static pages using 3 workers (42/56)
✓ Generating static pages using 3 workers (56/56) in 877ms
  Finalizing page optimization ...
  Collecting build traces ...

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /admin
├ ƒ /admin/login
├ ƒ /admin/network
├ ƒ /admin/payments
├ ƒ /admin/players
├ ƒ /admin/screens
├ ƒ /admin/sports
├ ƒ /admin/tvs
├ ƒ /api/account/session
├ ƒ /api/admin/logout
├ ƒ /api/admin/payments/operations
├ ƒ /api/admin/payments/sessions
├ ƒ /api/admin/payments/sessions/[sessionId]
├ ƒ /api/admin/players
├ ƒ /api/admin/players/[playerId]
├ ƒ /api/admin/players/[playerId]/memberships
├ ƒ /api/admin/players/memberships/[membershipId]
├ ƒ /api/admin/session
├ ƒ /api/admin/sports/classes
├ ƒ /api/admin/sports/classes/[classId]
├ ƒ /api/admin/sports/coaches
├ ƒ /api/admin/sports/coaches/[coachId]
├ ƒ /api/admin/sports/open-matches
├ ƒ /api/admin/sports/open-matches/[matchId]
├ ƒ /api/admin/sports/operations
├ ƒ /api/admin/summary
├ ƒ /api/admin/tvs/assignments/[assignmentId]
├ ƒ /api/admin/tvs/campaigns
├ ƒ /api/admin/tvs/campaigns/[campaignId]
├ ƒ /api/admin/tvs/campaigns/[campaignId]/assignments
├ ƒ /api/admin/tvs/campaigns/[campaignId]/creatives
├ ƒ /api/admin/tvs/creatives/[creativeId]
├ ƒ /api/admin/tvs/operations
├ ƒ /api/admin/tvs/screens
├ ƒ /api/admin/tvs/screens/[screenId]
├ ƒ /api/admin/tvs/tv-devices
├ ƒ /api/admin/tvs/tv-devices/[tvDeviceId]
├ ƒ /api/atc-public/availability/sportclubs/[sportclubId]
├ ƒ /api/classes/logout
├ ƒ /api/classes/offers
├ ƒ /api/classes/offers/[offerId]
├ ƒ /api/classes/requests
├ ƒ /api/classes/session
├ ƒ /api/clubs/accounts
├ ƒ /api/clubs/coaches
├ ƒ /api/clubs/coaches/[coachId]
├ ƒ /api/clubs/invites
├ ƒ /api/clubs/logout
├ ƒ /api/clubs/manual-classes
├ ƒ /api/clubs/me
├ ƒ /api/clubs/players
├ ƒ /api/clubs/session
├ ƒ /api/clubs/venues
├ ƒ /api/clubs/venues/[venueId]
├ ƒ /api/coaches/accounts
├ ƒ /api/coaches/availability
├ ƒ /api/coaches/clubs
├ ƒ /api/coaches/invites/[token]
├ ƒ /api/coaches/logout
├ ƒ /api/coaches/me
├ ƒ /api/coaches/session
├ ƒ /api/coaches/students
├ ƒ /api/coaches/students/[bookingId]
├ ƒ /api/coaches/students/players
├ ƒ /api/coaches/students/players/[playerId]
├ ƒ /api/integrations/payments/approved
├ ƒ /api/integrations/payments/health
├ ƒ /api/integrations/payments/orders
├ ƒ /api/integrations/payments/sessions
├ ƒ /api/network
├ ƒ /api/network/[clubId]
├ ƒ /api/network/[clubId]/venues
├ ƒ /api/network/[clubId]/venues/[venueId]
├ ƒ /api/network/venues/[venueId]/vending-machines
├ ƒ /api/network/venues/[venueId]/vending-machines/[vendingMachineId]
├ ƒ /api/players
├ ƒ /api/sports
├ ƒ /api/tvs/health
├ ƒ /api/tvs/heartbeat
├ ƒ /api/tvs/impression
├ ƒ /api/tvs/manifest/[screenId]
├ ƒ /api/tvs/screens
├ ƒ /classes
├ ƒ /classes-by-date
├ ƒ /classes/[offerId]
├ ƒ /classes/login
├ ƒ /classes/me
├ ƒ /clubs
├ ƒ /clubs/coaches
├ ƒ /clubs/venues/[venueId]
├ ○ /coaches
├ ƒ /coaches/settings
├ ○ /manifest.webmanifest
├ ƒ /notification/mercadopago
├ ○ /offline
└ ƒ /player/[screenId]

ƒ Proxy (Middleware)

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

pre-push: all checks passed.
To https://github.com/sebasfavaron/ballbox.git
   121454a..31befb3  main -> main


Updated #9 (in_progress → completed)


Seguí.

Subido a `main`:
- `31befb3` Add semantic requests disclosure shell

Qué hice:
- `club-coaches-page-client`
  - tarjeta “Ver solicitudes” ahora usa disclosure semántico
  - panel conectado con heading accesible
  - menos toggle-only UI

Validado:
- `typecheck`
- `lint`
- `build`
- contract tests
- push con hooks completos

Estado:
- los toggles grandes de dashboard quedaron bastante mejor
- siguiente zona dura: grid interactivo de calendario y formulario manual interno

Si querés, sigo con esa última zona fuerte.


todo

Phase 10 html-first accessibility in Ballbox

Improve the interactive calendar grid and manual class form semantics in club coaches flow.

implementing phase 10 accessibility work


Created #10: Phase 10 html-first accessibility in Ballbox (pending)


Updated #10 (pending → in_progress)


bash

cd /home/sebas/work/projects/ballbox && rg -n "handleCreateManualClass\(|manualClassError|studentCreateError|<form|Guardar clase|Crear alumno" components/club-coaches-page-client.tsx


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

                          Hora

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

                          return (

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

{court.name}

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

                          );
                        })}

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

                              {formatSlotLabel(slot)}

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

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

Cerrado para coaches

                                  ) : block ? (
                                    <>

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

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

{block.playerName}

                                      ) : null}

{formatMoney(visibleSlotPriceMinor)}

                                        {block.startTime} - {block.endTime}

                                    </>
                                  ) : (
                                    <>

Hueco

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

{formatMoney(visibleSlotPriceMinor)}

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

                        ))}

                )}

            )}

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

Profesores

Analytics por profesor

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

                  {filteredProfessorRows.length} profes

Reservations

{dashboard.analytics.summary.reservationCount}

Para club

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

Para coaches

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

Reviews

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

                    Profesor
                    Sede
                    Reservas

[368 more lines in file. Use offset=1690 to continue.]