Przejdź do głównej zawartości

Zajęcia wakacyjne

Kompleksowy przewodnik po module zajęć wakacyjnych — pojedynczych zajęć w kalendarzu (np. „Doubles (wakacyjne)"), na które uczestnik zapisuje się termin po terminie. Obejmuje ustawienia, warunki zapisu, akceptację regulaminu, przepływy płatności, wygaśnięcia oraz zapis wszystkiego w historii.

:::note Rozróżnienie To nie to samo co moduł Półkolonie (camps). Półkolonie to wielodniowe turnusy z własnym systemem sezonów, list rezerwowych i dokumentów. Zajęcia wakacyjne opisane tutaj to zwykłe gry (game) z rodzajem zajęć w kategorii „Wakacyjne", na które klient zapisuje się pojedynczo, z dedykowaną logiką zapisu, regulaminu i płatności. :::

Przepływ w skrócie (diagram)​

👤 Instrukcja dla pracownika (Administracja / Recepcja / Instruktor)​

Czym są zajęcia wakacyjne​

Zajęcia wakacyjne to rodzaj zajęć (Rodzaj zajęć) oznaczony kategorią „Wakacyjne". Zajęcia utworzone z takiego rodzaju trafiają na osobną zakładkę „Zajęcia wakacyjne" w portalu klienta i podlegają odrębnym regułom widoczności, zapisu i płatności.

Ustawienia (Panel administratora → Ustawienia zajęć wakacyjnych)​

Dla każdego miasta konfiguruje się osobno:

  • Włącz zajęcia wakacyjne — nadrzędny przełącznik modułu w danym mieście. Wyłączony chowa wakacyjne rodzaje zajęć z list wyboru także pracownikowi: w kalendarzu (filtr aktywności i wybór rodzaju zajęć przy dodawaniu gry), przy przypisywaniu poziomu uczestnikowi oraz w katalogu „Rodzaje zajęć". Znika też skrót „Utwórz zajęcia wakacyjne" w menu wiersza zajęć grupowych — nie ma po co tworzyć terminu, którego żadna lista i tak nie pokaże. Terminy nie znikają z historii ani z gier, które już się odbyły — zostają tylko schowane przed pracownikiem, dopóki przełącznik jest wyłączony. Rodzaj zajęć przypisany wcześniej uczestnikowi albo użyty przez edytowaną grę nadal jest widoczny, żeby edycja go nie skasowała.
  • Zezwól klientom na samodzielny zapis — widoczny tylko przy włączonym module. Wyłączony zamyka wyłącznie samoobsługę: klient traci zakładkę „Zajęcia wakacyjne" i nie zapisze się sam, a recepcja dalej widzi terminy i zapisuje uczestników przy ladzie. To jest ustawienie na przygotowanie sezonu — terminy trzeba wtedy poustawiać, zanim ruszą zapisy.
  • Natychmiastowa płatność (online) — gdy włączona, klient zapisujący się sam jest od razu kierowany do płatności; Czas na płatność online (minuty) określa, ile ma czasu, zanim zapis wygaśnie.
  • Natychmiastowa płatność (pracownik) — gdy recepcja zapisuje uczestnika, do klienta wysyłany jest link do płatności (SMS); Czas na płatność pracownik (minuty) określa okno na opłacenie.
  • Dni z wyprzedzeniem (klient) / Dni z wyprzedzeniem (pracownik) — jak daleko w przód widać dostępne terminy (osobno dla klienta i pracownika).

Zapis uczestnika​

Zapis możliwy jest przez klienta (portal klienta → zakładka „Zajęcia wakacyjne") oraz przez recepcję (w imieniu uczestnika). System dopuszcza zapis tylko, gdy spełnione są wszystkie warunki:

  1. Ukończona ocena umiejętności (zajęcia próbne) — nowy uczestnik musi najpierw przejść zajęcia próbne / ocenę umiejętności, zanim zobaczy ofertę wakacyjną.

  2. Brak zaległej / oczekującej płatności wakacyjnej — uczestnik z nieopłaconą, aktywną płatnością wakacyjną nie może zapisać się na kolejny termin, dopóki jej nie ureguluje lub nie wygaśnie.

  3. Powiązanie z grupą uczestnika — uczestnik ma w profilu przypisane grupowe rodzaje zajęć (szkółki). Każde zajęcia wakacyjne wskazują z kolei jedną grupę, dla której są przeznaczone — są jej wakacyjnym odpowiednikiem. Uczestnik przypisany do dwóch grup znajdzie więc dwa zestawy terminów: odpowiedniki jednej i drugiej grupy. Wiek uczestnika nie jest tu w ogóle brany pod uwagę.

    Jeśli uczestnik nie ma przypisanego żadnego rodzaju zajęć, widzi komunikat o oczekiwaniu na przypisanie przez recepcję; jeśli ma przypisania, ale żaden termin wakacyjny nie jest z nimi powiązany, widzi „Skontaktuj się z recepcją".

  4. Wolne miejsca i okno widoczności (dni z wyprzedzeniem).

Jak powiązać zajęcia wakacyjne z grupą​

Każda szkółka ma swój wakacyjny odpowiednik, więc dla grup „Skrzaty" i „Żaki" zakładasz dwa osobne rodzaje zajęć wakacyjnych. Uczestnik zapisany do obu grup zobaczy oba.

Najprościej przez skrót „Utwórz zajęcia wakacyjne na podstawie grupy" (menu przy zajęciach grupowych) — grupa, z której startujesz, jest od razu ustawiona w nowym typie zajęć.

Ręcznie, w katalogu „Rodzaje zajęć":

  1. Otwórz zajęcia wakacyjne do edycji (typ „Inne").
  2. W polu „Grupa, dla której są te zajęcia" wybierz szkółkę.
  3. Zapisz. Od tej chwili termin widzą uczestnicy przypisani do tej grupy.

Jedna grupa może mieć kilka rodzajów zajęć wakacyjnych (np. lipiec i sierpień osobno) — uczestnik zobaczy terminy ze wszystkich.

:::warning Termin bez wskazanej grupy nie trafia do nikogo Zajęcia wakacyjne bez wybranej grupy nie pokażą się żadnemu uczestnikowi. W katalogu rodzajów zajęć taki wpis jest oznaczony czerwoną etykietą „Bez grupy", a w formularzu widnieje ostrzeżenie pod polem wyboru. Po wdrożeniu tej zmiany istniejące zajęcia wakacyjne nie mają ustawionej grupy — trzeba je uzupełnić przed sezonem, inaczej oferta będzie pusta. :::

Akceptacja regulaminu (i RODO)​

Zapis na zajęcia wakacyjne wymaga akceptacji regulaminu wakacyjnej szkółki oraz RODO. Akceptacja jest wymuszana na każdej ścieżce płatności:

  • podczas zapisu przez klienta (dialog zgody),
  • na stronie linku do płatności (SMS od recepcji),
  • w widoku Płatności w aplikacji, jeśli klient zamiast klikać w link zaloguje się i opłaci zajęcia z listy płatności.

Bez zaznaczenia zgody nie da się przejść do płatności. Fakt akceptacji jest zapisywany i widoczny w historii (patrz niżej).

Osobna ścieżka obowiązuje, gdy recepcja dopisuje uczestnika z kalendarza (edycja zajęć → lista uczestników), z pominięciem kreatora zapisu. Wtedy klient dostaje SMS z linkiem do regulaminu wakacyjnego i ma wyznaczony czas na akceptację, inaczej zostaje wypisany — opisuje to Akceptacja regulaminu po zapisie przez recepcję. Zgoda trafia do consent_log jako vacation_enrollment, tak samo jak z kreatora.

Rządzą tym dwa ustawienia w sekcji Akceptacja regulaminu na tym ekranie (vacation_statute_acceptance_enabled i vacation_statute_acceptance_time), niezależne od odpowiedników dla zajęć stałych. Domyślnie wyłączone — dopóki nikt ich nie włączy dla danego miasta, dopisanie uczestnika z kalendarza działa jak dotychczas, bez SMS-a i bez wymogu numeru telefonu.

Ostrzeżenie o braku możliwości odwołania​

Jeśli termin zajęć przypada wcześniej niż wymagany czas na anulowanie (próg anulowania rodzaju zajęć), przy wyborze terminu pojawia się ostrzeżenie:

„Uwaga! Termin zajęć przypada wcześniej niż wymagany czas na jej anulowanie. Po dokonaniu zapisu nie będzie możliwości odwołania zajęć."

Dotyczy to zarówno zajęć wakacyjnych, jak i wakacyjnych zajęć próbnych.

Płatność​

  • Klient (natychmiastowa płatność online) — po zapisie klient trafia do widoku Płatności (/dashboard/payments), gdzie po akceptacji regulaminu opłaca zajęcia. Zajęcia wakacyjne nie są automatycznie przekierowywane wprost na bramkę Przelewy24 — klient reguluje je z poziomu listy płatności (bramką lub portfelem).
  • Recepcja — po zapisie do klienta wychodzi link do płatności (SMS); na stronie linku klient akceptuje regulamin i płaci (Przelewy24).
  • Po zaksięgowaniu płatności klient dostaje powiadomienie „Płatność potwierdzona" (vacation_payment_success).

Wygaśnięcie płatności​

Jeśli uczestnik nie opłaci w wyznaczonym czasie, płatność wygasa, a miejsce jest zwalniane automatycznie. Wygaśnięcie jest odnotowane w historii zajęć jako „Wygaśnięcie czasu na opłacenie".

Historia​

Wszystkie zdarzenia związane z zajęciami wakacyjnymi trafiają do dwóch historii:

  • Historia Zajęć (podgląd/edycja pojedynczej gry) — chronologiczny log zdarzeń danej gry: dołączenie, akceptacja regulaminu, płatność, płatność z portfela, zwrot na portfel, usunięcie bez zwrotu, wygaśnięcie, odwołanie.
  • Historia aktywności (/dashboard/history) — ogólny rejestr, w którym zajęcia wakacyjne mają własne źródło „Zajęcia wakacyjne" oraz m.in. wpis „Akceptacja regulaminu".

🛠️ Dokumentacja techniczna​

Model danych​

  • Rodzaj zajęć wakacyjnych: activity_types.category = 'Wakacyjne' (typ at.type = 'other'). To jedyny wyróżnik odróżniający zajęcia wakacyjne od zwykłych.
  • Gra: zwykły rekord game z activity_type_id wskazującym na rodzaj „Wakacyjne".
  • Uczestnik wakacyjny: wpis w game.attendees z type = 'vacation' i paymentId.
  • Płatność: payment.payment_type = 'school', related_ids = [gameId]. Zajęcia wakacyjne rozpoznaje się jako payment_type = 'school' oraz activity_type.category = 'Wakacyjne'.
  • Log zdarzeń gry (game.event_log, JSON): m.in. joined, consent_accepted, payment_paid, paid_with_wallet, refund_to_wallet, removed_without_refund, attendee_expired, cancelled.

Ustawienia​

VacationActivitySettings w lib/actions/app-settings.ts (klucze app_settings per miasto): vacation_activity_enabled, vacation_activity_client_enabled (migracja 0247; brak wiersza = true, czyli zachowanie sprzed rozdzielenia), vacation_activity_immediate_payment_online, vacation_activity_immediate_payment_employee, vacation_activity_payment_time_online, vacation_activity_payment_time_employee, vacation_activity_payment_time (legacy fallback, nie edytowany już w UI), vacation_activity_days_advance_client, vacation_activity_days_advance_employee.

Formularz: components/forms/vacation-activity-settings/vacation-activity-settings-form.tsx (w UI zostały tylko dwa pola czasu płatności: online i pracownik).

Widoczność w listach wyboru​

vacation_activity_enabled steruje nie tylko zapisem, ale i tym, czy wakacyjne rodzaje zajęć w ogóle pojawiają się w listach wyboru (dotyczy to również pracownika — to przełącznik nadrzędny; samoobsługę klienta zamyka się osobno przez vacation_activity_client_enabled, opisane niżej). Termin wakacyjny zostaje w katalogu między sezonami (jest używany ponownie następnego lata, a archiwizacja zepsułaby zeszłoroczne gry), więc to ustawienie jest jedynym znacznikiem „aktualny / nieaktualny".

  • withoutDisabledVacationTypes(...) w lib/actions/activity-types.ts — odsiewa rodzaje z kategorią Wakacyjne, gdy vacation_activity_enabled dla danego miasta nie jest '1'. Widok „Wszystkie miasta" nie ma własnego miasta, więc pyta isAppSettingEnabledInAnyCity („czy którekolwiek miasto to prowadzi") zamiast czytać wiersz city = 'ALL' — ten jest zaseedowany na '0' i ekran ustawień praktycznie nigdy go nie zapisuje, więc zaufanie mu chowałoby terminy miasta, które je prowadzi, bez śladu w UI. Oba odczyty idą przez cache app_settings (unieważniany przy każdym zapisie ustawień), a filtr w ogóle nie pyta bazy, jeśli na liście nie ma żadnego rodzaju wakacyjnego.
  • getSelectableActivityTypes(...) — katalog dla kalendarza (schedule/page.tsx i modal dodawania gry). Opcjonalny keepActivityTypeId dokłada z powrotem jeden wskazany rodzaj: ekran, który się na coś powołuje, musi umieć to nazwać. Kalendarz podaje tu activityTypeId z query stringa (filtr trafia do URL-a, więc zakładka z filtrem po terminie wakacyjnym inaczej filtrowałaby pod pustą etykietą).
  • getActivityTypesForGame(...) — cienka nakładka na powyższe z keepActivityTypeId ustawionym na rodzaj, którego gra już używa, więc edycja wakacyjnej gry przy wyłączonym module nie podmienia jej rodzaju zajęć.
  • isVacationModuleEnabled(city) — ta sama reguła wystawiona dla ekranów, które muszą ukryć akcję, a nie pozycję listy: katalog przekazuje ją w dół (ActivityTypesTable → getColumns / ActivityTypesListView → CellAction), żeby skrót „Utwórz zajęcia wakacyjne" w menu wiersza zajęć grupowych znikał razem z terminami, które tworzy.
  • getCatalogueActivityTypes(...) — ekran „Rodzaje zajęć". Grupy wycofane po rolce sezonu zostają (to ekran, na którym się je edytuje i archiwizuje), znikają tylko terminy wakacyjne wyłączonego miasta. Konsekwencja dla recepcji: żeby poza sezonem poprawić termin wakacyjny, trzeba najpierw włączyć przełącznik.
  • getAssignablePlayerActivityTypes(...) / getAssignableAllActivityTypes(...) — listy poziomów dla uczestników (Uczestnicy, zadania, profil klienta). Poziom już przypisany uczestnikowi nie ginie przy edycji: ActivityTypesCheckboxList trzyma zaznaczenie jako obiekty ActivityType i renderuje je jako badge niezależnie od listy dostępnych.

Rozdzielenie pracownik / klient​

vacation_activity_client_enabled zamyka samoobsługę klienta, nie ruszając strony recepcyjnej. Rola jest liczona po stronie serwera z sesji (ADMIN, BACKOFFICE, INSTRUCTOR to pracownik), więc klient nie obejdzie tego z przeglądarki.

  • ResponsiveActivities.tsx — zakładka „Zajęcia wakacyjne" w portalu: pracownik otwierający ten sam ekran dla klienta zakładkę zachowuje.
  • use-vacation-enrollment.ts — hook nie pobiera terminów dla klienta przy wyłączonej samoobsłudze (enabled: false), pracownikowi zwraca je normalnie.
  • enrollInVacationActivity(...) w lib/actions/game.ts — właściwa bramka: isEmployee bierze się z session.user.role, więc zapis klienta kończy się errors.vacationDisabled niezależnie od tego, co wyśle klient.

Listy wyboru pracownika (kalendarz, katalog, poziomy uczestnika) patrzą wyłącznie na przełącznik nadrzędny — samoobsługa klienta ich nie dotyczy.

Zapis i widoczność​

  • enrollInVacationActivity(gameId, playerId, city, consentAccepted) — serwerowa akcja zapisu. Wymaga consentAccepted (inaczej errors.consentRequired), blokuje przy zaległościach, zapisuje uczestnika typu vacation oraz zdarzenie consent_accepted do event_log.
  • hasPendingVacationPaymentByPlayer(playerId) — blokada zapisu przy aktywnej, nieopłaconej płatności wakacyjnej (świadoma wygaśnięć).
  • getGamesByDateRangeForPlayer(...) — źródło dostępnych terminów; zwraca activity_type z category, cancellation_hours_before i innymi polami potrzebnymi UI.
  • useVacationEnrollment(...) (components/forms/vacation-enrollment/use-vacation-enrollment.ts) — hook ładujący ustawienia, dostępne terminy, status zaległości i informację o konieczności przypisania do grupy. Terminy filtrowane są przez getVacationActivityTypeIdsForGroups(...): hook przekazuje identyfikatory rodzajów zajęć przypisanych uczestnikowi (player.activity_types) i zostawia tylko te gry, których activity_type.id jest wśród zwróconych. Zapytanie o gry idzie z ignoreAge: true, więc data urodzenia nie ma znaczenia.
  • lib/actions/vacation-activity-groups.ts — getVacationActivityTypeIdsForGroups (jedno zapytanie IN (...) po kolumnie vacation_group_activity_type_id, z pominięciem zarchiwizowanych), getUnlinkedVacationActivityTypeIds (etykieta „Bez grupy" w katalogu, zawężona do kategorii Wakacyjne) oraz assertVacationGroupWritable (rola + sprawdzenie, że wskazany identyfikator to zajęcia grupowe tego samego najemcy; wołane przed zapisem, żeby odrzucenie nie zostawiło zapisanego typu zajęć).
  • Kolumna activity_types.vacation_group_activity_type_id (migracja 0185) — relacja jeden-do-wielu: termin wskazuje jedną grupę, grupa może mieć wiele terminów. Zapisywana wprost przez createActivityType / updateActivityType, więc formularz katalogu zapisuje wszystko jednym submitem.
  • Bramka zajęć próbnych (ocena umiejętności) przed ofertą wakacyjną: reconcileSkillAssessmentWithClassEnrollment(...) (lib/actions/skill-assessment.ts). Poza sprawdzeniem playerMustCompleteSkillAssessment domyka ona ocenę uczestnikowi, który już chodzi na zajęcia grupowe, a nie ma stempla w skill_assessment_enrolled. Zapis dzieje się wyłącznie tutaj — w server action. Ścieżka odczytu (getTrialEnrollmentStatus, kalendarz klienta) tylko raportuje blokadę existingClasses, bo revalidateTag w trakcie renderu jest w Next.js zabronione.
  • UI zapisu: components/forms/vacation-enrollment/vacation-enrollment-dialog.tsx (zajęcia wakacyjne) oraz components/forms/trial-enrollment/trial-enrollment-dialog.tsx z propem vacationOnly (wakacyjne zajęcia próbne).

Płatności​

  • Brak auto-redirectu na bramkę dla wakacji: po zapisie klient trafia do /dashboard/payments (a nie wprost na Przelewy24). Wakacyjne zajęcia próbne bramkowane propem vacationOnly; zwykłe próbne bez zmian.
  • Link do płatności: app/pay/[token]/page.tsx + PublicPayButton.tsx; inicjalizacja app/api/public/payment/initialize/route.ts.
  • Wykrywanie płatności wakacyjnej w widoku Płatności: pole activity_type_category wystawione z zapytań płatności (lib/actions/payment.ts), warunek payment_type === 'school' && activity_type_category === 'Wakacyjne'.

Akceptacja regulaminu jest zapisywana jako zdarzenie consent_accepted w event_log gry na każdej ścieżce płatności:

  • Widok Płatności (w aplikacji) — bramka VacationStatuteAcceptanceDialog (components/modal/vacation-statute-acceptance-dialog.tsx) wpięta w payments-list-view-user.tsx i PaymentsTableAccordion.tsx (analogicznie do bramki rezerwacji kortów); po akceptacji saveVacationStatuteAcceptance(paymentIds).
  • Link do płatności — app/api/public/payment/initialize/route.ts wywołuje recordVacationStatuteConsent(paymentId, acceptedBy, tenantId) dla płatności typu group (updateBookingStatuteAcceptance jest no-opem dla gier — dlatego trzeba zapisać zdarzenie na grze).
  • Zapis przy zapisie — enrollInVacationActivity zapisuje consent_accepted przy dołączeniu.

Rdzeń zapisu: recordVacationStatuteConsent(...) w lib/actions/payment.ts — bez zależności od sesji, best-effort, z dedupem po paymentId (nie dubluje zgody). Dialog linkuje do Regulaminu wakacyjnej szkółki oraz RODO.

Historia — renderowanie​

  • Historia aktywności: builder buildGameConsentAcceptedSQL w lib/actions/activity-log.ts (typ akcji consent_accepted, źródło vacation); render w app/(dashboard)/dashboard/history/components/ActivityLogItem.tsx (badge „Akceptacja regulaminu"). Zajęcia wakacyjne mają źródło source_type = 'vacation'.
  • Historia Zajęć: app/(dashboard)/dashboard/schedule/components/GameEventsView.tsx renderuje wszystkie zdarzenia z event_log; etykiety/opisy z gameEvents.eventTypes.* i eventDescriptions.* (m.in. consent_accepted, payment_paid, paid_with_wallet, removed_without_refund, game_cancelled).

Wygaśnięcia i zwolnienie miejsca​

  • Cron expirePendingPayments(env) (lib/actions/expire-payments.ts) przełącza wygasłe płatności pending → expired; logExpiriesToGameLog(...) dopisuje zdarzenie attendee_expired do event_log gry (dla płatności school na grach „Wakacyjne" oraz trial na dowolnej grze). Miejsce zwalniane jest przez leniwe filtry uczestników.

Powiadomienia​

  • vacation_payment_success (lib/notification-config.ts) — SMS/e-mail po zaksięgowaniu płatności; szablon e-mail: mails/vacation-payment-success.html.

Kluczowe pliki​

ObszarPlik
Ustawienialib/actions/app-settings.ts, components/forms/vacation-activity-settings/vacation-activity-settings-form.tsx
Listy wyborulib/actions/activity-types.ts (withoutDisabledVacationTypes, getSelectableActivityTypes, getCatalogueActivityTypes), lib/actions/app-settings.ts (isAppSettingEnabledInAnyCity)
Zapislib/actions/game.ts (enrollInVacationActivity), components/forms/vacation-enrollment/*
Dostępne terminylib/actions/game.ts (getGamesByDateRangeForPlayer), use-vacation-enrollment.ts
Regulamincomponents/modal/vacation-statute-acceptance-dialog.tsx, lib/actions/payment.ts (recordVacationStatuteConsent, saveVacationStatuteAcceptance)
Płatność (link)app/pay/[token]/*, app/api/public/payment/initialize/route.ts
Płatność (widok)components/tables/payments-table/payments-list-view-user.tsx, PaymentsTableAccordion.tsx
Historialib/actions/activity-log.ts, ActivityLogItem.tsx, GameEventsView.tsx
Wygaśnięcialib/actions/expire-payments.ts
Powiadomienialib/notification-config.ts, mails/vacation-payment-success.html