Przejdź do głównej zawartości

Rezerwacje kortów – instrukcja dla recepcji

Krótki przewodnik po codziennej obsłudze rezerwacji kortów: tworzenie, edycja, rozliczanie, odwoływanie i szukanie rezerwacji.

👤 Instrukcja dla pracownika

Gdzie pracujesz

Podstawowym narzędziem jest Kalendarz (menu boczne → Kalendarz, nagłówek strony „Harmonogram”). Widzisz w nim siatkę: kolumny to korty, wiersze to godziny.

  • Miasto/lokalizację wybierasz w selektorze na górze panelu – filtruje on całą aplikację, nie tylko kalendarz.
  • Widok przełączasz między dniem, tygodniem i miesiącem paskiem nad siatką.
  • Zielone/puste pola to terminy wolne, kafelki to zajęte terminy.

Tworzenie rezerwacji

  1. Kliknij wolne pole w siatce, na korcie i o godzinie, o którą prosi klient.
  2. Otworzy się okno z dwiema zakładkami:
    • Rezerwacja – zwykły wynajem kortu (domyślna, tej używasz najczęściej),
    • Aktywność – zajęcia z instruktorem, treningi, zajęcia grupowe.
  3. Uzupełnij formularz:
    • Kort – możesz zmienić, jeśli kliknąłeś nie w ten, co trzeba.
    • Czas rezerwacji – godzina startu i końca. Długość zmienia się skokowo, zgodnie z ustawieniami klubu (np. co 30 min), i nie wyjdzie poza godziny otwarcia kortu.
    • Cena – wyliczana automatycznie z cennika (uwzględnia reguły godzinowe i indywidualne zniżki klienta).
    • Cena niestandardowa – wpisz, tylko jeśli chcesz nadpisać cennik.
    • Notatki – widoczne wyłącznie dla pracowników, nie dla klienta.
    • UczestnicyDodaj gracza – zacznij wpisywać imię, nazwisko lub telefon. Jeśli klienta nie ma w bazie, z tej samej listy założysz nowego (może być wymagany numer telefonu).
  4. Kliknij Utwórz rezerwację.

:::tip Bez powiadomienia Strzałka obok przycisku daje opcję Dodaj aktywność bez powiadomienia – rezerwacja powstanie, ale klient nie dostanie potwierdzenia (mail/SMS/push). Używaj, gdy poprawiasz coś „na już” i nie chcesz zasypywać klienta wiadomościami. :::

Bez uczestnika nie utworzysz rezerwacji – przycisk pozostanie nieaktywny do momentu dodania co najmniej jednej osoby.

Jeśli klub ma włączoną natychmiastową płatność, zobaczysz żółte ostrzeżenie: rezerwacja wygaśnie, jeśli nie zostanie opłacona w podanym czasie (np. 10 minut).

:::tip Rezerwacja za 0 zł Gdy wpiszesz cenę 0 (rezerwacja gratisowa), ostrzeżenie znika. Nie ma czego opłacić, więc kort nie jest blokowany na 10 minut, klient nie dostaje SMS-a z linkiem do płatności, a rezerwacja jest potwierdzona od razu – tak samo jak w klubie bez natychmiastowej płatności. :::

Rezerwacje cykliczne (stali klienci)

W sekcji Zajęcia cykliczne zaznacz powtarzalność (co tydzień / codziennie / co miesiąc) oraz zakończenie – po liczbie powtórzeń albo do konkretnej daty. Pod spodem zobaczysz, ile terminów zostanie utworzonych – po sprawdzeniu konfliktów licznik pokazuje realną liczbę, razem z informacją, ile terminów wypadło na dni wolne lub na zamknięcie kortu.

System sam sprawdzi konflikty z istniejącymi rezerwacjami. Jeśli któreś terminy są zajęte, wybierz Utwórz N rezerwacji (pomiń konflikty) – powstaną tylko wolne terminy, a zajęte zostaną pominięte. Dni wolne, święta oraz okresy zamknięcia kortu są pomijane zawsze, także przy pomijaniu konfliktów – seria przechodząca przez remont kortu powstanie, ale bez terminów z tego okresu.

Terminy są sprawdzane ponownie w chwili zapisu. Jeśli w międzyczasie ktoś zajął którykolwiek z nich, cała seria zostanie odrzucona z komunikatem o zajętym terminie – nic nie powstanie połowicznie. Odśwież konflikty i zapisz ponownie.

Cena niestandardowa wpisana przy serii obowiązuje na każdym terminie – tak samo jak przy rezerwacji jednorazowej.

Klient dostaje potwierdzenie rezerwacji cyklicznej: kort, adres, dzień tygodnia i godzina, zakres dat, liczba terminów, cena za termin i PIN do drzwi. Nie jest to już wiadomość o zapisie na zajęcia – nie ma w niej instruktora ani nazwy grupy.

:::warning Seria z natychmiastową płatnością Jeśli klub ma włączoną natychmiastową płatność, seria jest tylko zablokowana do czasu opłacenia – zobaczysz żółte ostrzeżenie z czasem (np. 10 minut). Klient dostaje jeden link, do pierwszego terminu. Opłacenie go potwierdza całą serię; pozostałe terminy rozliczasz normalnie przy ladzie. Jeśli nikt nie zapłaci w tym czasie, wszystkie terminy serii zostają zwolnione i korty wracają do sprzedaży. Wysłanie serii „bez powiadomienia" nie wyśle też linku – przy natychmiastowej płatności używaj zwykłego przycisku. :::

Podgląd i edycja rezerwacji

Kliknij kafelek rezerwacji w kalendarzu – otworzy się Szczegóły rezerwacji: typ (jednorazowa/cykliczna), kort, termin, cena, dane klienta i notatka.

  • Kliknięcie w imię i nazwisko przenosi do profilu klienta.
  • Kliknięcie w telefon lub e-mail kopiuje je do schowka.

Na dole są trzy przyciski: Odwołaj, Edytuj, Rozlicz (ten ostatni znika, gdy rezerwacja jest już opłacona).

Edytuj pozwala zmienić kort, datę, godziny, cenę, notatki i listę uczestników. Zapisujesz przyciskiem Zaktualizuj (lub „bez powiadomienia”). W oknie edycji jest też Podziel – dzieli jedną długą rezerwację na kilka segmentów z osobno wyliczoną ceną (np. gdy część godzin idzie po innej stawce).

Rozliczenie płatności

Przy ladzie: Szczegóły rezerwacjiRozlicz.

  1. Sprawdź kwotę do zapłaty (pole z imieniem i nazwiskiem możesz poprawić, np. na potrzeby paragonu).
  2. Jeśli klient płaci kartą benefitową – kliknij Multisport / Medicover / FitProfit / PZU Sport tyle razy, ile kart wnosi. Każde kliknięcie obniża kwotę o 15 zł (do zapłaty zostaje minimum 1 zł). Minus w rogu kafelka odejmuje kartę, Resetuj czyści wszystkie.
  3. Zaznacz Drukuj paragon, jeśli klient chce paragon.
  4. Wybierz formę płatności:
    • Gotówka / Karta – rozliczenie w klubie,
    • Portfel – pobiera środki z wirtualnego portfela klienta (w nawiasie widzisz jego saldo; przy niewystarczających środkach pobierze ile się da, a resztę trzeba dopłacić),
    • Zaległość – klient wychodzi bez płacenia, kwota trafia na jego zaległości,
    • Więcej opcji – płatność mieszana (np. część gotówką, część kartą).

Jeśli klient prosił o fakturę, nad kwotą pojawi się niebieska ramka z danymi do faktury – to sygnał, żeby nie wystawiać zwykłego paragonu.

Odwoływanie rezerwacji

Szczegóły rezerwacjiOdwołaj. W oknie potwierdzenia:

  • przy rezerwacji opłaconej zobaczysz zaznaczony checkbox „Zwróć X zł na portfel klienta” – zostaw go zaznaczonego przy zwykłym odwołaniu; odznacz tylko wtedy, gdy zwrot się nie należy,
  • Odwołaj rezerwację – klient dostaje powiadomienie o odwołaniu,
  • strzałka Odwołaj bez powiadomienia,
  • przy rezerwacji cyklicznej dostępne jest Odwołaj całą serię – zobaczysz listę wszystkich przyszłych terminów, zanim potwierdzisz.

:::warning Zwrot to portfel, nie przelew Zwrócone pieniądze trafiają do wirtualnego portfela klienta i pokrywają kolejne rezerwacje. Nie jest to przelew na konto ani zwrot na kartę. :::

Szukanie rezerwacji

Gdy klient dzwoni i nie wiesz, kiedy ma kort: menu → Historia → filtr Przegląd rezerwacji. Masz tam wyszukiwarkę (nazwisko, telefon) oraz statusy (aktywna / odwołana / wygasła) i źródło rezerwacji (online albo „rezerwacja telefoniczna”, czyli założona przez pracownika). Kliknięcie pozycji otwiera to samo okno Szczegółów rezerwacji, z którego odwołasz i rozliczysz.

PIN do drzwi

Każdy uczestnik ma jeden stały 4-cyfrowy PIN, który otwiera drzwi na wszystkich jego rezerwacjach. Nie generuje się nowego kodu do każdej rezerwacji.

Aby go podejrzeć lub zmienić: menu → Uczestnicy przy wierszu → PIN do drzwi. Klient widzi swój PIN w aplikacji w sekcji Moi gracze.

Najczęstsze sytuacje

SytuacjaCo zrobić
Klient chce przesunąć godzinęSzczegółyEdytuj → zmień godziny → Zaktualizuj
Klient chce inny kortSzczegółyEdytuj → zmień Kort
Klient nie przyszedł i nie zapłaciłRozliczZaległość
Klient płaci połowę gotówką, połowę kartąRozliczWięcej opcji
Rezerwacja cykliczna kończy się wcześniejOdwołajOdwołaj całą serię (odwoła przyszłe terminy)
Nie widać wolnego terminu, choć kort pustySprawdź miasto/lokalizację na górze i godziny otwarcia kortu (przycisk Godziny otwarcia)

🛠️ Dokumentacja techniczna

Ścieżki i komponenty

ElementPlik
Kalendarz recepcjiapp/(dashboard)/dashboard/schedule (BookingClient.tsx)
Formularz nowej rezerwacjiapp/(dashboard)/dashboard/schedule/components/ReservationForm.tsx
Przełącznik Rezerwacja/Aktywnośćapp/(dashboard)/dashboard/schedule/components/GameFormContainer.tsx
Edycja rezerwacji + podziałEditReservationForm.tsx, SplitReservationDialog.tsx
Szczegóły rezerwacjicomponents/schedule/reservation-details-dialog.tsx
Rozliczenie / płatność mieszanareservation-settle-dialog.tsx, reservation-advanced-payment-dialog.tsx
Odwołaniecomponents/schedule/reservation-cancel-dialog.tsx
Lista rezerwacji dla pracownikaapp/(dashboard)/dashboard/history (/reservations-overview przekierowuje tutaj)

Logika serwerowa

  • createAdminReservation (lib/actions/booking.ts) – tworzy grę, booking, opcjonalną płatność i wysyła powiadomienia. Gdy immediate_payment_employee (lub indywidualne require_online_payment klienta) jest włączone, rezerwacja i płatność dostają expires_at = teraz + employee_payment_time minut, a gra powstaje bez płatności (createGameWithoutPayment).
  • Cena liczona jest przez calculateActivityPrice z uwzględnieniem reguł cenowych i nadpisań klienta (applyClientPricingOverride); custom_price z formularza nadpisuje wynik. Cena jest ustalana przed decyzją o holdzie, bo to ona o niej współdecyduje.
  • Rezerwacja za 0 zł nigdy nie dostaje holdu – requiresImmediateCourtPayment (lib/utils/immediate-payment.ts) zwraca false dla ceny <= 0, niezależnie od ustawień lokalizacji, nadpisania klienta i zaległości. createPayment rozlicza zerową kwotę już przy INSERT (status paid), więc żadna późniejsza zmiana statusu nie nadejdzie, a clearGameExpiresAt / clearBookingExpiresAt – jedyne miejsca zdejmujące blokadę – nigdy by się nie uruchomiły: kort stałby za terminem, którego nic nie potrafi wyczyścić, i po cichu przepadł. Zamiast tego gra powstaje przez createGame, uczestnik nie jest draft, booking_expires_at zostaje puste, a klient dostaje zwykłe potwierdzenie zamiast SMS-a z linkiem do płatności. Ta sama reguła obowiązuje w rezerwacji klienckiej i w serii. Formularze (ReservationForm, court-booking-dialog) czytają ten sam predykat i podają mu kwotę, która faktycznie zostanie policzona, więc żółte ostrzeżenie o czasie na płatność nie pokazuje się dla ceny 0.
  • Obniżenie ceny do 0 na już istniejącej rezerwacji z holdem domyka się w updateReservation: handlePriceChangeForPayments tylko przepisuje kwotę i zostawia płatność jako pending, a linku na 0 zł nie da się opłacić (/api/public/payment/initialize odrzuca kwoty <= 0), więc żadna zmiana statusu by nie nadeszła i hold wygasłby po cichu. Dlatego akcja sama rozlicza taką płatność (status = 'paid', payment_expires_at = NULL) i zdejmuje blokadę przez clearGameExpiresAt + clearBookingExpiresAt – ten sam chokepoint, co przy potwierdzeniu wpłaty. Płatność już opłaconą zostawia w spokoju (obniżka idzie wtedy zwykłą ścieżką zwrotu do portfela).
  • generateRecurringReservations (lib/actions/booking.ts) – odpowiednik dla serii. Sprawdza zaległości, wyrównanie do siatki oraz zamknięcia kortu dla każdego terminu z osobna (godziny otwarcia mogą się zmienić w trakcie serii), a przed pierwszym zapisem ponownie weryfikuje dostępność wszystkich terminów i przerywa całą serię błędem COURT_SCHEDULE_OVERLAP. custom_price (tylko dla ADMIN/BACKOFFICE) trafia do kolumny custom_price każdej gry, do wiersza booking i do kwoty płatności.
  • Hold serii: gdy requiresImmediateCourtPayment zwróci true, generateRecurringReservations zapisuje termin blokady w game_expires_at każdej gry, booking_expires_at każdego wiersza booking, w polach draft/paymentStartedAt/paymentExpiresAt uczestnika oraz w payment_expires_at każdej płatności. Link do płatności idzie tylko dla pierwszego terminu (sendReservationNotifications), a potwierdzenie tej płatności zwalnia całą serię przez releaseRecurringCourtSeriesHold (lib/series-reservation-hold-sql.ts) wpięte w clearGameExpiresAt – ten sam chokepoint, przez który przechodzą wszystkie potwierdzenia płatności. Zwolnienie omija terminy, których slot ktoś zajął w czasie, gdy blokada była wygasła – wskrzeszenie ich oznaczałoby podwójną rezerwację; taki termin zostaje wygaszony, a jego płatność wygasza expirePendingPayments, więc klient nie płaci za kort, który przepadł. Gdy seria nie wygenerowała płatności za pierwszy termin (typ aktywności, której cena nie trafiła na płatność), akcja zakłada ją sama – bez płatności nie byłoby linku, a blokada wygasłaby po cichu. Seria wyceniona na 0 zł w ogóle nie dostaje holdu (patrz reguła zerowej ceny wyżej). Brak wpłaty = expirePendingPayments wygasza płatności, a leniwe filtry na game_expires_at zwalniają korty. Kształt skopiowany z addPlayerToRecurringSeries (zapis na zajęcia stałe).
  • generateRecurringGames (lib/actions/game.ts) z enforceCourtAvailability: true wstawia każde wystąpienie zapytaniem INSERT ... WHERE NOT EXISTS, tak jak createGame. Przegrany wyścig o termin cofa wszystkie wystąpienia zapisane w tym wywołaniu razem z ich płatnościami, a formularz kasuje pustą serię.
  • Powiadomienie o serii: player_added_to_recurring_reservation (lib/notification-config.ts, szablon mails/recurring-reservation-confirmation.html, migracja 0242). Wysyła je generateRecurringReservations po utworzeniu bookingów – dopiero wtedy znany jest PIN serii. generateRecurringGames dostaje sendNotification: false z tej ścieżki, żeby nie poszła wiadomość o zapisie na zajęcia (player_added_to_recurring_series), która mówiła o instruktorze. Klient z kontem dostaje ją kanałami ze swoich ustawień, a klient założony przy ladzie (konto local, adres bez skrzynki) – SMS-em, tak samo jak przy jednorazowej. Przy aktywnym holdzie zamiast potwierdzenia idzie SMS z linkiem, a po zapłacie booking_payment_success.
  • Zamknięcia kortu (court.closures) sprawdza findClosureForInstant (lib/utils/opening-hours.ts) – godzinę i dzień czyta w strefie Europe/Warsaw, więc Worker w UTC widzi to samo, co kalendarz. Serwer odrzuca zamknięty termin błędem COURT_CLOSED również przy rezerwacji jednorazowej.
  • Terminy serii generuje generateRecurringGameOccurrencesShared (lib/utils/recurring-occurrences.ts) – pomija dni wolne i zamknięcia kortu, raportuje je w skippedDates (kind: 'holiday' | 'closure') i liczy godziny w strefie Europe/Warsaw. Ten sam wynik zasila sprawdzanie konfliktów (useRecurringConflicts) i zapis, także przy „pomiń konflikty”.
  • cancelReservation przyjmuje flagi refundToWallet i skipNotification; odwołanie serii idzie przez deleteRecurringGameSeries.
  • Rozliczenie: settlePaymentInClub (gotówka/karta/portfel, karty benefitowe jako lista {type, amount, count}, 15 zł za sztukę) oraz markPaymentAsArrears dla przycisku Zaległość.
  • Po utworzeniu i po zmianie rezerwacji wywoływane jest syncBookingToHardware, które przekazuje termin i PIN do kontroli dostępu.

Uprawnienia

Formularz pokazuje pola Cena niestandardowa, Notatki i sekcję Uczestnicy tylko rolom ADMIN i BACKOFFICE (useRole). Klient w tym samym oknie widzi wyłącznie kort, termin i cenę.