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:
-
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ą.
-
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.
-
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ą".
-
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ęć":
- Otwórz zajęcia wakacyjne do edycji (typ „Inne").
- W polu „Grupa, dla której są te zajęcia" wybierz szkółkę.
- 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'(typat.type = 'other'). To jedyny wyróżnik odróżniający zajęcia wakacyjne od zwykłych. - Gra: zwykły rekord
gamezactivity_type_idwskazującym na rodzaj „Wakacyjne". - Uczestnik wakacyjny: wpis w
game.attendeesztype = 'vacation'ipaymentId. - Płatność:
payment.payment_type = 'school',related_ids = [gameId]. Zajęcia wakacyjne rozpoznaje się jakopayment_type = 'school'orazactivity_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(...)wlib/actions/activity-types.ts— odsiewa rodzaje z kategoriąWakacyjne, gdyvacation_activity_enableddla danego miasta nie jest'1'. Widok „Wszystkie miasta" nie ma własnego miasta, więc pytaisAppSettingEnabledInAnyCity(„czy którekolwiek miasto to prowadzi") zamiast czytać wierszcity = '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 cacheapp_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.tsxi modal dodawania gry). OpcjonalnykeepActivityTypeIddokłada z powrotem jeden wskazany rodzaj: ekran, który się na coś powołuje, musi umieć to nazwać. Kalendarz podaje tuactivityTypeIdz 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 zkeepActivityTypeIdustawionym 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:ActivityTypesCheckboxListtrzyma zaznaczenie jako obiektyActivityTypei 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(...)wlib/actions/game.ts— właściwa bramka:isEmployeebierze się zsession.user.role, więc zapis klienta kończy sięerrors.vacationDisabledniezależ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. WymagaconsentAccepted(inaczejerrors.consentRequired), blokuje przy zaległościach, zapisuje uczestnika typuvacationoraz zdarzenieconsent_accepteddoevent_log.hasPendingVacationPaymentByPlayer(playerId)— blokada zapisu przy aktywnej, nieopłaconej płatności wakacyjnej (świadoma wygaśnięć).getGamesByDateRangeForPlayer(...)— źródło dostępnych terminów; zwracaactivity_typezcategory,cancellation_hours_beforei 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ą przezgetVacationActivityTypeIdsForGroups(...): hook przekazuje identyfikatory rodzajów zajęć przypisanych uczestnikowi (player.activity_types) i zostawia tylko te gry, którychactivity_type.idjest wśród zwróconych. Zapytanie o gry idzie zignoreAge: true, więc data urodzenia nie ma znaczenia.lib/actions/vacation-activity-groups.ts—getVacationActivityTypeIdsForGroups(jedno zapytanieIN (...)po kolumnievacation_group_activity_type_id, z pominięciem zarchiwizowanych),getUnlinkedVacationActivityTypeIds(etykieta „Bez grupy" w katalogu, zawężona do kategoriiWakacyjne) orazassertVacationGroupWritable(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(migracja0185) — relacja jeden-do-wielu: termin wskazuje jedną grupę, grupa może mieć wiele terminów. Zapisywana wprost przezcreateActivityType/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 sprawdzeniemplayerMustCompleteSkillAssessmentdomyka ona ocenę uczestnikowi, który już chodzi na zajęcia grupowe, a nie ma stempla wskill_assessment_enrolled. Zapis dzieje się wyłącznie tutaj — w server action. Ścieżka odczytu (getTrialEnrollmentStatus, kalendarz klienta) tylko raportuje blokadęexistingClasses, borevalidateTagw trakcie renderu jest w Next.js zabronione. - UI zapisu:
components/forms/vacation-enrollment/vacation-enrollment-dialog.tsx(zajęcia wakacyjne) orazcomponents/forms/trial-enrollment/trial-enrollment-dialog.tsxz propemvacationOnly(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 propemvacationOnly; zwykłe próbne bez zmian. - Link do płatności:
app/pay/[token]/page.tsx+PublicPayButton.tsx; inicjalizacjaapp/api/public/payment/initialize/route.ts. - Wykrywanie płatności wakacyjnej w widoku Płatności: pole
activity_type_categorywystawione z zapytań płatności (lib/actions/payment.ts), warunekpayment_type === 'school' && activity_type_category === 'Wakacyjne'.
Regulamin / zgoda (consent_accepted)
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 wpayments-list-view-user.tsxiPaymentsTableAccordion.tsx(analogicznie do bramki rezerwacji kortów); po akceptacjisaveVacationStatuteAcceptance(paymentIds). - Link do płatności —
app/api/public/payment/initialize/route.tswywołujerecordVacationStatuteConsent(paymentId, acceptedBy, tenantId)dla płatności typugroup(updateBookingStatuteAcceptancejest no-opem dla gier — dlatego trzeba zapisać zdarzenie na grze). - Zapis przy zapisie —
enrollInVacationActivityzapisujeconsent_acceptedprzy 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
buildGameConsentAcceptedSQLwlib/actions/activity-log.ts(typ akcjiconsent_accepted, źródłovacation); render wapp/(dashboard)/dashboard/history/components/ActivityLogItem.tsx(badge „Akceptacja regulaminu"). Zajęcia wakacyjne mają źródłosource_type = 'vacation'. - Historia Zajęć:
app/(dashboard)/dashboard/schedule/components/GameEventsView.tsxrenderuje wszystkie zdarzenia zevent_log; etykiety/opisy zgameEvents.eventTypes.*ieventDescriptions.*(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ścipending → expired;logExpiriesToGameLog(...)dopisuje zdarzenieattendee_expireddoevent_loggry (dla płatnościschoolna grach „Wakacyjne" oraztrialna 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
| Obszar | Plik |
|---|---|
| Ustawienia | lib/actions/app-settings.ts, components/forms/vacation-activity-settings/vacation-activity-settings-form.tsx |
| Listy wyboru | lib/actions/activity-types.ts (withoutDisabledVacationTypes, getSelectableActivityTypes, getCatalogueActivityTypes), lib/actions/app-settings.ts (isAppSettingEnabledInAnyCity) |
| Zapis | lib/actions/game.ts (enrollInVacationActivity), components/forms/vacation-enrollment/* |
| Dostępne terminy | lib/actions/game.ts (getGamesByDateRangeForPlayer), use-vacation-enrollment.ts |
| Regulamin | components/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 |
| Historia | lib/actions/activity-log.ts, ActivityLogItem.tsx, GameEventsView.tsx |
| Wygaśnięcia | lib/actions/expire-payments.ts |
| Powiadomienia | lib/notification-config.ts, mails/vacation-payment-success.html |