Skip to main content

Rodzaje zajęć

Przewodnik po tworzeniu i zarządzaniu rodzajami aktywności w systemie.

👤 Instrukcja dla pracownika (Administracja)

Zakładka Rodzaje zajęć służy do definiowania i zarządzania szablonami aktywności odbywających się w obiekcie. Każdy rodzaj zajęć charakteryzuje się unikalnym zestawem parametrów, takich jak: model cenowy, przedział wiekowy uczestników, limit miejsc oraz identyfikacja wizualna (kolor) w kalendarzu. Podczas dodawania nowej pozycji do harmonogramu (np. na wtorek o 18:00), wystarczy wybrać predefiniowany "Rodzaj zajęć", a system automatycznie zastosuje przypisane do niego reguły wyceny oraz ograniczenia liczebności.

1. Typy zajęć

Kluczowym elementem konfiguracji jest określenie Typu zajęć, który definiuje późniejsze możliwości operacyjne uczestników i administratorów:

  • 🏃 Grupowe (np. szkółka tenisowa) - Zajęcia o charakterze grupowym. Zapewniają klientom możliwość samodzielnego anulowania obecności oraz odrabiania zajęć w innym terminie. Wymagają określenia bezwzględnego limitu miejsc (np. maksymalnie 6 uczestników).
  • 👤 Indywidualne (np. trening personalny) - Format dedykowany dla jednego uczestnika z wyłącznością trenera. W tym wariancie nie funkcjonuje mechanizm odrabiania – anulowanie zajęć skutkuje automatycznym zwrotem środków na Wirtualny Portfel klienta.
  • 📅 Rezerwacja (np. wynajem samego kortu) - Szablon przeznaczony wyłącznie na udostępnienie infrastruktury bez opieki trenerszej.
  • 🔧 Inne (np. turniej) - Szablony konfigurowane pod nietypowe i nieregularne wydarzenia.

2. Identyfikacja wizualna w panelu (Kolory)

Każdy szablon wymaga przypisania koloru, który jest następnie aplikowany w widoku głównego kalendarza. Rekomendacja: Wskazane jest ustalenie jednolitego standardu kodowania kolorystycznego z całym zespołem (np. kolor niebieski dla sekcji dziecięcej, zielony dla grup dorosłych, żółty dla rezerwacji własnych). Ułatwia to błyskawiczną identyfikację grafiku.

3. Ograniczenia i reguły (Wiek, Czas odwołania, Limity)

  • Maksymalna liczba uczestników: Parametr ten określa pojemność grupy. Przykładowo, zdefiniowanie wartości "8" zablokuje możliwość rejestracji dziewiątemu klientowi za pośrednictwem Portalu. Administratorzy posiadają uprawnienia do ręcznego przekroczenia tego limitu, jednakże czynność ta każdorazowo generuje stosowne ostrzeżenie w systemie.
  • Przedział wiekowy: Ustawienie przedziału (np. 6 do 8 lat) gwarantuje, że w Portalu Klienta dana oferta będzie widoczna wyłącznie dla opiekunów, których podopieczni spełniają to kryterium.
  • Godziny przed odwołaniem: Standardowo parametr ten wynosi 24 godziny. Zabezpiecza on przed zbyt późnymi rezygnacjami – np. próba anulacji obecności na 10 godzin przed rozpoczęciem zajęć zostanie zablokowana przez system.
  • Termin płatności: Parametr kontrolujący generowanie statusu "Zaległa". Konfiguracja "W dniu zapisu" nakłada wymagalność zapłaty natychmiast po przypisaniu do grupy. Konfiguracja np. "7 dni przed" wydłuża okres karencji.

4. Miasto (podział na lokalizacje)

Każdy rodzaj zajęć można przypisać do konkretnego miasta. Pole Miasto znajduje się w formularzu dodawania oraz edycji, tuż pod polem "Rodzaj zajęć":

  • Konkretne miasto (np. Opole) – zajęcia są oferowane wyłącznie w tym mieście. Nie pojawią się w grafiku, formularzach zapisu ani na listach wyboru w pozostałych lokalizacjach.
  • Wszystkie miasta – ustawienie domyślne i wartość, którą mają wszystkie zajęcia utworzone przed wprowadzeniem podziału. Takie zajęcia są widoczne w każdej lokalizacji.

Lista Rodzaje zajęć ma kolumnę Miasto, a zajęcia bez przypisanego miasta są oznaczone plakietką "Wszystkie miasta". Listę zawęża wybór miasta w nagłówku panelu — ten sam, którym przełączasz lokalizację na pozostałych ekranach.

Jak to działa w praktyce: po wybraniu miasta widać zawsze zajęcia tego miasta plus zajęcia wspólne. Ta sama reguła obowiązuje wszędzie tam, gdzie wybiera się rodzaj zajęć: dodawanie i edycja pozycji w grafiku, zapisy uczestnika, edycja uczestnika (zarówno z listy Uczestnicy, jak i z profilu klienta), ustawienia treningów indywidualnych, tablica zadań, linki rejestracyjne CRM czy eksport kalendarza.

Wyjątek chroniący dane: przy edycji istniejących zajęć w grafiku system zawsze dokłada do listy rodzaj zajęć, który dana pozycja już ma — nawet jeśli należy do innego miasta. Dzięki temu edycja starej pozycji nigdy nie podmieni ani nie wyczyści jej rodzaju zajęć.

5. Typ a Kategoria — to nie to samo

To dwa różne pola i mylenie ich potrafi kosztować pieniądze:

  • Typ (Grupowe / Indywidualne / Rezerwacja / Inne) steruje działaniem systemu: kto i gdzie może się zapisać, czy dostępne są zajęcia próbne i odrabianie, do której kategorii przychodu trafi wpłata.
  • Kategoria to dowolny tekst, który wyłącznie grupuje pozycje na liście Rodzaje zajęć (lista jest domyślnie zwijana właśnie po kategorii). Nie zmienia żadnej reguły.

Częsty błąd: zajęcia indywidualne sprzedawane w pakiecie dla 2–4 osób ("Grupa 3os.") bywają zapisywane z Typem = Grupowe i Kategorią = Indywidualne. Na liście wyglądają wtedy poprawnie — siedzą pod nagłówkiem "Indywidualne" — ale system traktuje je jak szkółkę: pokazuje je w publicznych zapisach na zajęcia próbne i wlicza przychód do zajęć grupowych. Typ indywidualny obsługuje limit uczestników większy niż 1 (cena liczy się jako stawka za osobę × liczba miejsc), więc pakiety 2–4 osobowe ustawiaj jako Indywidualne i tam ustaw limit miejsc.

6. Zajęcia próbne nigdy nie działają na zajęciach indywidualnych

Zajęcia próbne są dostępne wyłącznie dla zajęć grupowych (oraz wakacyjnych w ich własnym trybie). Rodzaj zajęć z Typem Indywidualne nie pojawi się w publicznej rejestracji na zajęcia próbne ani w oknie zapisu w panelu, a próba zapisu zwróci komunikat "Zajęcia próbne nie są dostępne na zajęciach indywidualnych". Klient chcący trening indywidualny korzysta z opcji zapisu na zajęcia indywidualne, gdzie płaci pełną stawkę.


🛠️ Dokumentacja techniczna

Sekcja opisująca właściwości bazy danych. Konfiguracje te lądują w tabeli activity_types. Właściwości i typy zajęć rzutują kaskadowo (przez activity_type_id) na wszystkie powiązane rekordy w tabeli game. Zmiana ceny lub reguł w istniejącym rodzaju zajęć wpływa wyłącznie na nowe zajęcia; historyczne i zaplanowane już zajęcia (game) trzymają własne snapshoty stawek z momentu ich utworzenia, by zabezpieczyć integralność finansową minionych rezerwacji.

Zasięg miejski (activity_types.city)

Kolumna city istniała wcześniej wyłącznie na potrzeby ustawień rezerwacji (type = 'booking'); teraz obsługuje cały katalog. NULL (oraz pusty string w starszych danych) oznacza "wszystkie miasta".

  • Filtrowanie realizuje jeden warunek: (city IS NULL OR city = '' OR city = ?) — ta sama reguła, którą stosuje lib/actions/trial-level.ts.
  • getActivityTypes, getAllActivityTypes i getIndividualActivityTypes (lib/actions/activity-types.ts) przyjmują opcjonalny parametr city:
    • podane miasto → filtr na to miasto,
    • 'ALL' lub null → brak filtra (używa tego wyłącznie widok katalogu),
    • brak parametru → miasto z ciasteczka lokalizacji (getSelectedLocation), czyli to wybrane w nagłówku panelu.
  • Rozwinięte miasto wchodzi do klucza cache (getCached), więc listy dla różnych miast nie nadpisują się nawzajem.
  • Ekrany edycji uczestnika rozwiązują miasto przez resolveHeaderCity (lib/actions/header-city.ts): searchParams.city, a przy jego braku miasto preferowane użytkownika — dokładnie ten sam fallback, który stosuje select w nagłówku. Dotyczy to listy Uczestnicy (app/(dashboard)/dashboard/player/page.tsx), listy uczestników na profilu klienta (UserPlayers) oraz modala /dashboard/user/[email]/profile/[playerId]. Globalny select publikuje miasto do URL-a, a CityLink / useNavigateWithCity przenoszą je na kolejny ekran.
  • Te ekrany celowo nie opierają się na ciasteczku lokalizacji: zapisuje je efekt w nagłówku dopiero po renderze strony, więc zaraz po zmianie miasta lista pokazywałaby katalog poprzedniej lokalizacji, a przy wejściu z linku bez parametru — miasto inne niż to widoczne w nagłówku.
  • Rodzaje zajęć już przypisane uczestnikowi (player_activity_types) nie są filtrowane miastem: w formularzu edycji wchodzą jako plakietki niezależnie od katalogu, więc zapis nigdy nie kasuje przypisania z innego miasta.
  • getActivityTypesForGame(activityTypeId, city) zwraca listę dla miasta powiększoną o rodzaj zajęć, którego używa edytowana gra — zabezpieczenie przed cichym zgubieniem powiązania w formularzach edycji gry.
  • Migracja 0213 zawęża unikalny indeks idx_activity_types_name_city_street do type = 'booking'. Indeks powstał dla jednorazowego wiersza ustawień rezerwacji; rozciągnięty na cały katalog blokowałby zapis duplikatów nazw, które legalnie istnieją dzisiaj (przy city IS NULL).

type kontra category

activity_types.type jest jedynym polem, na którym opiera się logika biznesowa; activity_types.category to swobodny tekst z pola combobox, używany wyłącznie do zwijania katalogu (defaultGroupBy="category" w components/tables/activity-type-table/activity-type-table.tsx). Jedyny wyjątek to 'Wakacyjne', po którym rozpoznawany jest tryb wakacyjny w oknie zapisu na zajęcia próbne.

Konsekwencje złego type przy poprawnie wyglądającej kategorii:

  • GROUP_TRIAL_SLOT_SOURCE_SQL (lib/registration-services.ts) wybiera terminy próbne przez at.type = 'group', więc zajęcia indywidualne z type = 'group' trafiały do publicznej rejestracji.
  • resolveRevenueCategory (lib/revenue-category.ts) mapuje type na kategorię przychodu, więc wpłata lądowała w lesson_group_* zamiast lesson_individual.
  • getIndividualActivityTypes (lib/actions/activity-types.ts) filtruje type = 'individual', więc taki rodzaj zajęć nie pojawiał się w oknie zapisu na trening indywidualny.

Migracja 0268 naprawia historyczne dane: type = 'group' AND category = 'Indywidualne'type = 'individual'.

Blokada zajęć próbnych na zajęciach indywidualnych

enrollInSkillAssessment (lib/actions/game.ts) odrzuca zapis, gdy rodzaj zajęć powiązanej gry ma type = 'individual', i zwraca errors.individualNotAllowed. Strażnik stoi w akcji, a nie w widoku, bo prowadzą do niej trzy niezależne ścieżki: publiczna rejestracja próbna (lib/trial-registration-payment.ts), okno zapisu w panelu (components/forms/trial-enrollment/trial-enrollment-dialog.tsx) oraz bezpośredni adres /dashboard/user-activities/[playerId]/confirm-join/[gameId]. Filtry w SQL terminów i w oknie zapisu zostają jako pierwsza warstwa — chodzi o to, żeby błędnie ustawiony type nie mógł sam z siebie sprzedać miejsca w slocie wycenianym za osobę po cenie zajęć próbnych.