Skip to main content

Stała rezerwacja kortu

Stała rezerwacja to ten sam kort, ten sam dzień tygodnia i ta sama godzina przez wybrany okres sezonu — domyślnie od października do kwietnia. Cena pochodzi z osobnego cennika stałych rezerwacji, niezależnego od cennika kortu.

Dokument opisuje pierwszy etap funkcji (AP-1036): włączenie w lokalizacji, zakładanie, zasady okresu, święta i cennik. Płatność z góry za miesiąc, blokada samodzielnych zmian, zwolnienie terminu „na sprzedaż" i podział kosztu między graczy dochodzą w kolejnych etapach (AP-1037–AP-1039).

👤 Instrukcja​

Włączenie w lokalizacji (administrator)​

Ścieżka: Dashboard ➔ Ustawienia rezerwacji ➔ sekcja „Stałe rezerwacje"

Ustawienia działają per zakres — domyślny, miasto albo lokalizacja — tak jak reszta ustawień rezerwacji.

UstawienieDomyślnieCo robi
Włącz stałe rezerwacjewyłączoneBez tego przełącznika nikt — ani recepcja, ani klient — nie zobaczy opcji stałej rezerwacji. Nie da się go zapisać bez stawki większej od zera
Klient może założyć stałą rezerwację samwłączonePo wyłączeniu stałą rezerwację zakłada tylko recepcja
Początek sezonu / Koniec sezonu10-01/04-30Okno, w którym musi zmieścić się cały okres rezerwacji (format MM-DD)
Minimalna długość (pełne miesiące)1Ile pełnych miesięcy kalendarzowych musi objąć rezerwacja
Typ cennika, Stawka godzinowa, regułypodstawowyCena za godzinę gry; w cenniku zaawansowanym — reguły godzinowe, weekendowe i per ulica

Cennik zaawansowany używa tego samego edytora reguł co cennik kortu: godziny od–do, zakres dat, „tylko weekendy" i opcjonalnie ulica. Stawka bazowa obowiązuje w godzinach, których nie obejmuje żadna reguła.

Zakładanie przez recepcję​

  1. W Grafiku kliknij wolne okienko na korcie — w dniu i o godzinie, od której ma zacząć się rezerwacja.
  2. Dodaj klienta.
  3. Włącz przełącznik Stała rezerwacja. Zwykłe ustawienia serii cyklicznej znikają, a Koniec okresu podpowiada się jako ostatni dzień sezonu. Możesz go skrócić.
  4. Sprawdź cenę. Gdy wszystkie terminy kosztują tyle samo, karta pokazuje Cenę za termin i sumę za okres. Gdy cennik ma różne stawki w różnych miesiącach (np. osobna reguła na wrzesień i na październik), karta rozpisuje terminy per miesiąc — „wrzesień 2026: 1 × 60.00 PLN”, „październik 2026: 5 × 70.00 PLN” — i podaje Razem. Każdy termin jest wyceniany według swojej daty, tak samo przy zapisie. Jeśli wpiszesz Cenę niestandardową, zastąpi ona cenę z cennika we wszystkich terminach.
  5. Jeśli w okresie wypadają święta, pojawi się lista Święta w okresie. Zaznacz terminy, które mają zostać zarezerwowane mimo święta — niezaznaczone zostaną pominięte. Dni, w których kort jest zamknięty, pomijane są zawsze.
  6. Zapisz. Terminy zajęte przez kogoś innego pokażą się jak w każdej serii — możesz utworzyć tylko wolne albo zrezygnować.

Zakładanie przez klienta​

Gdy administrator zostawił włączone Klient może założyć stałą rezerwację sam, klient zakłada ją z zakładki Rezerwacje:

  1. Klika Utwórz rezerwację. Zamiast od razu kalendarza terminów pojawia się pytanie Jaką rezerwację chcesz utworzyć? z dwiema opcjami: Jednorazowa i Stała. Gdy stałe rezerwacje są w lokalizacji niedostępne (wyłączone, bez stawki albo tylko dla recepcji), pytania nie ma i przycisk działa jak dotąd.
  2. Jednorazowa otwiera zwykłe okno rezerwacji — bez zmian.
  3. Stała prosi o dzień pierwszego terminu. Kalendarz pozwala wybrać dzień od dziś (albo od początku najbliższego sezonu) do końca sezonu — niezależnie od tego, na ile dni naprzód można rezerwować jednorazowo.
  4. Po Kontynuuj otwiera się formularz z włączoną Stałą rezerwacją: klient wybiera kort i godzinę, sprawdza koniec okresu, cenę i święta, i zapisuje.

Można też, jak wcześniej, kliknąć wolny termin w widoku grafiku i włączyć Stała rezerwacja w formularzu. Klient widzi cenę z cennika i nie może jej zmienić. Obowiązują te same zasady co przy rezerwacji online — blokada przy zaległościach i czas na opłacenie, jeśli lokalizacja go wymaga.

Zasady okresu​

Okres musi zmieścić się w sezonie. Przy sezonie 01.10–30.04 rezerwacja od 5 marca do 28 maja zostanie odrzucona, bo wychodzi poza 30 kwietnia.

Liczą się pełne miesiące kalendarzowe. Miesiąc jest pełny, gdy rezerwacja obejmuje każdy termin w tym miesiącu w danym dniu tygodnia. Przy minimum 1 miesiąca:

Okres (czwartki)WynikDlaczego
1 października – 31 paźdz✅ przyjętaObejmuje wszystkie czwartki października
2 października – 30 paźdz✅ przyjętaDla piątków: 2 października to pierwszy piątek miesiąca, więc miesiąc pełny
15 października – 14 list❌ odrzuconaPaździernik zaczęty w połowie, listopad nieskończony — brak pełnego miesiąca
15 października – 30 list✅ przyjętaPaździernik niepełny, ale listopad pełny

Rezerwacja zaczęta w trakcie miesiąca musi więc objąć jeszcze kolejny pełny miesiąc. Te same zasady sprawdza formularz i serwer — dla recepcji i dla klienta.

Liczą się terminy faktycznie zarezerwowane, nie sam zakres dat. W pełnym miesiącu każdy czwartek musi być zarezerwowany albo wypadać w dzień, w którym kortu nie da się zarezerwować — święto albo zamknięcie kortu. Termin pominięty dlatego, że był zajęty przez kogoś innego („utwórz tylko wolne"), przerywa miesiąc. Przykład: seria listopadowa bez 12 listopada, bo tego dnia kort był zajęty, nie ma pełnego miesiąca; seria bez 19 listopada, bo to święto, ma.

Bez stawki nie ma stałych rezerwacji. Lokalizacja z włączonymi stałymi rezerwacjami, ale bez stawki w cenniku, nie pokaże przełącznika ani recepcji, ani klientowi — inaczej każdy termin kosztowałby 0 zł.

🛠️ Dokumentacja techniczna​

Model danych​

Stała rezerwacja nie jest nowym bytem — to tryb serii rezerwacji kortu (recurring_game_series + game + booking). Grafik, konflikty, PIN-y do drzwi, hardware sync i faktury działają bez zmian.

Migracja migrations/0279_add_standing_reservations.sql:

TabelaKolumnaZnaczenie
recurring_game_seriesseries_kind ('recurring' | 'standing')Jedyny przełącznik trybu; domyślnie 'recurring', więc istniejące serie bez zmian
recurring_game_seriesstanding_priceCena pierwszego terminu (informacyjnie)
recurring_game_seriesstanding_price_source ('pricelist'|'manual')Czy cenę wzięto z cennika, czy wpisała ją recepcja
recurring_game_seriesowner_player_idKlient główny — pod podział kosztu w Etapie 4
booking_settingsstanding_enabled, standing_allow_onlineDostępność w lokalizacji
booking_settingsstanding_season_start, standing_season_endOkno sezonu jako MM-DD
booking_settingsstanding_min_monthsMinimum pełnych miesięcy
booking_settingsstanding_pricing_type, standing_price, standing_pricing_rulesCennik: basic/advanced, stawka godzinowa, JSON PricingRule[]

Cena każdego terminu ląduje w game.custom_price i booking.custom_price, a booking.applied_pricing_rule_id zostaje NULL — reguła cennika stałych rezerwacji nie jest regułą typu aktywności. Dzięki temu płatności, faktury i szczegóły rezerwacji czytają cenę tak jak przy każdej ręcznie wycenionej rezerwacji.

Bramka series_kind = 'standing'​

Żadna reguła stałej rezerwacji nie może wyciec na zwykłe serie cykliczne ani na serie instruktorów. Dlatego generateRecurringReservations (lib/actions/booking.ts) czyta tryb z wiersza serii w bazie, a nie z payloadu, i dopiero dla 'standing' uruchamia resolveStandingSeriesTerms:

Wszystkie odmowy padają przed zapisem pierwszego terminu, więc ReservationForm usuwa pustą serię (SERIES_REFUSED_BEFORE_WRITE zawiera STANDING_REFUSALS). Ustawienia lokalizacji są pobierane na serwerze z kortu — bookingSettings z formularza nie decyduje o dostępie. Hold płatności, blokada przy zaległościach i requiresImmediateCourtPayment (instruktor bez holdu) działają bez zmian dla obu trybów.

Cena wpisana przez klienta jest ignorowana, bo seriesCustomPrice jest już null dla ról spoza ADMIN/BACKOFFICE. Indywidualny cennik klienta (applyClientPricingOverride) nie dotyczy stałej rezerwacji — cena każdego terminu przychodzi jako custom_price, który ma pierwszeństwo w createSystemPaymentForGameParticipation.

Reguły w lib/standing-reservation.ts​

Moduł jest czysty (bez bazy), używany przez serwer i formularz:

  • standingSeasonFor(dateISO, settings) — sezon zawierający datę; obsługuje okno przechodzące przez Nowy Rok.
  • standingMonths(periodStart, periodEnd) — miesiące okresu z flagą fullyCovered: miesiąc jest pełny, gdy okres obejmuje pierwszy i ostatni termin danego dnia tygodnia w tym miesiącu. Pod Etap 2 (płatność miesięczna).
  • validateStandingPeriod(periodStart, periodEnd, settings, coverage?) — STANDING_OUTSIDE_SEASON albo STANDING_PERIOD_TOO_SHORT. Bez coverage sprawdza sam zakres dat (podpowiedź w formularzu, zanim terminy są znane). Z coverage = { bookedDays, excusedDays } miesiąc liczy się tylko wtedy, gdy każdy dzień tygodnia serii w tym miesiącu jest zarezerwowany albo usprawiedliwiony — tak sprawdza serwer i formularz przy zapisie. Bez tego spreparowany payload (seria „na listopad" z jednym terminem) dostawał cenę stałej rezerwacji.
  • standingUnbookedDays(periodStart, periodEnd, bookedDays) — dni tygodnia serii w pełnych miesiącach okresu, których seria nie rezerwuje. Tylko te sprawdza findUnbookableDays w lib/actions/booking.ts: święta z getHolidays(court.city) (jedna lista z cache, affects_booking = 1) i zamknięcia kortu (findClosureForInstant o godzinie serii), bez zapytania na dzień.
  • isStandingPriced(settings) — stawka większa od zera; warunek canCreateStandingReservation i walidacji w updateBookingSettings (INVALID_STANDING_SETTINGS) oraz formularza ustawień (standingPriceRequired).
  • resolveStandingPrice(settings, start, end, street) — buduje syntetyczny typ aktywności z standing_* (w trybie basic bez reguł, w advanced z regułami przefiltrowanymi po ulicy kortu) i liczy cenę przez calculateActivityPrice, więc weekendy i podział godzin działają jak w cenniku kortu.
  • standingPriceBreakdown(settings, occurrences, street) — wycenia każdy termin przez resolveStandingPrice i grupuje je po miesiącu w strefie Europe/Warsaw (toPolandMonthKey) i cenie; zwraca lines, total (sumowany w groszach), occurrenceCount i uniformPrice (null, gdy ceny się różnią). Formularz liczy go z tej samej listy terminów, która idzie do zapisu (freeOccurrences przy pomijaniu kolizji, inaczej plannedOccurrences), dopiero po sprawdzeniu kolizji — wcześniej karta pokazuje cenę pierwszego terminu. recurring_game_series.standing_price nadal trzyma tylko cenę pierwszego terminu.
  • canCreateStandingReservation, holidayOccurrencesToBook, sortOccurrencesByStart, formatSeasonDay.

Daty okresu to dni w strefie Europe/Warsaw (toPolandDateISO z series.start_date i series.end_date).

Święta​

generateRecurringGameOccurrencesShared nadal pomija święta i zamknięcia, ale każdy pominięty termin niesie teraz day oraz start_time/end_time, które by zajął. ReservationForm w trybie stałej rezerwacji:

  1. pokazuje święta z skippedDates jako listę z checkboxami (StandingReservationSettings),
  2. zamienia wybrane dni na terminy przez holidayOccurrencesToBook,
  3. sprawdza je osobnym hookiem useStandingHolidayConflicts (checkRecurringConflicts), bo zwykły useRecurringConflicts nigdy ich nie widzi,
  4. dokleja je do plannedOccurrences / freeOccurrences / recurringConflicts, więc ostrzeżenie o konfliktach, licznik terminów i zapis działają na pełnej liście.

Zamknięcia kortu nie da się zaznaczyć — generateRecurringReservations i tak odrzuca termin w zamknięciu (COURT_CLOSED).

Kody odmów​

KodKiedy
STANDING_NOT_ALLOWEDStałe rezerwacje wyłączone lub bez stawki w lokalizacji albo klient przy standing_allow_online = 0
STANDING_OUTSIDE_SEASONPoczątek poza sezonem albo koniec po końcu sezonu
STANDING_PERIOD_TOO_SHORTMniej pełnych miesięcy niż standing_min_months, licząc tylko miesiące z kompletem zarezerwowanych (lub usprawiedliwionych) terminów
STANDING_INVALID_PERIODSeria bez dat albo terminy poza okresem lub w innym dniu tygodnia (spreparowany payload)

Komunikaty: reservationsView.standing.errors.*.

Testy​

  • lib/standing-reservation.test.ts — okres (start 15. dnia, dokładnie miesiąc, poza sezonem, przełom roku, sezon letni), cennik (basic, advanced, weekend, ulica), dostęp, święta.
  • lib/actions/booking.standingReservation.test.ts — odmowy dla recepcji i klienta, cena z cennika, cena ręczna recepcji, cena klienta ignorowana, wiersz booking; regresja: czterotygodniowa seria w czerwcu i seria instruktora bez holdu.
  • lib/utils/recurring-occurrences.test.ts — okno terminu pominiętego święta.
  • __tests__/lib/standing-reservation.integration.test.ts — na bazie SQLite w pamięci z kompletem migracji: zapis serii, terminów, rezerwacji i płatności z kwotami; odmowy bez zapisanych terminów; konflikt terminu; hold i link płatności klienta; święto wybrane przez recepcję i święto pominięte; seria „na miesiąc" z jednym terminem i z dziurą po konflikcie; zapis ustawień, w tym odmowa włączenia bez stawki; regresja zwykłej serii i serii instruktora.