Filtry i sortowanie w widoku Rezerwacje
Instrukcja
Dotyczy zakładki Rezerwacje w panelu — tej samej dla klienta, instruktora i recepcji. Zmiany obejmują trzy widoki przełączane u góry: Moje rezerwacje (lista), Grafik (siatka dnia) i Dostępność (siatka tygodnia).
Lista „Moje rezerwacje"
Nad listą pojawił się panel filtrów:
- Data od / Data do — zawężają listę do wybranego zakresu. Można podać tylko jedną granicę, np. samo „Data od", żeby zobaczyć wszystko od dziś w przód. Wybrany zakres pokazuje się pod panelem jako plakietka; kliknięcie krzyżyka przy plakietce zdejmuje tę granicę.
- Sortowanie — trzy opcje:
- Najbliższe rezerwacje (domyślne) — na górze jest najbliższy nadchodzący termin, dalej kolejne w przyszłość, a na końcu terminy już rozegrane, od najnowszego. Dzięki temu rezerwacja, którą chce się odwołać, jest zwykle pierwsza na liście.
- Data rosnąco — od najstarszej do najnowszej.
- Data malejąco — od najnowszej do najstarszej.
- Pokaż odwołane rezerwacje — przełącznik przeniesiony tutaj z paska nad tabelą; działa jak wcześniej.
Przycisk Wyczyść filtry pojawia się, gdy cokolwiek jest ustawione, i wraca do stanu domyślnego.
Filtry siedzą w adresie strony, więc link do przefiltrowanej listy można zapisać w zakładkach albo komuś wysłać.
Panel działa też przy globalnym wyborze lokalizacji ustawionym na wszystkie lokalizacje — wcześniej lista była wtedy pusta, więc żaden filtr ani sortowanie nie dawały widocznego efektu.
Na komputerze panel jest rozwinięty nad tabelą. Na telefonie filtry chowają się za pływającym przyciskiem w lewym dolnym rogu — z plakietką pokazującą, ile ustawień jest aktywnych. Kliknięcie wysuwa je od dołu jako szufladę; zamyka ją przycisk Zamknij, gest w dół albo tapnięcie w tło. Dzięki temu lista rezerwacji zajmuje na telefonie cały ekran.
Grafik i Dostępność
W obu siatkach zajęte okienka nadal są szare i podpisane „Rezerwacja". Okienko, które należy do osoby patrzącej, jest teraz niebieskie i podpisane imieniem i nazwiskiem:
- klientowi wyświetla się imię i nazwisko uczestnika z jego kartoteki — rodzic opiekujący się kilkoma zawodnikami widzi więc od razu, czyj to termin (przy kilku swoich uczestnikach w jednej rezerwacji nazwiska są po przecinku);
- instruktorowi wyświetla się jego własne imię i nazwisko na zajęciach, które prowadzi (również jako zastępstwo).
Cudze rezerwacje pozostają anonimowe — bez nazwiska i bez zmiany koloru. Na własnym okienku nie ma też odnośnika Powiadom o dostępności, bo nie ma po co zapisywać się na listę oczekujących na własny termin.
Dokumentacja techniczna
Filtrowanie i sortowanie listy
Cała lista jest stronicowana po stronie serwera, więc filtr i sortowanie muszą trafić do zapytania SQL — sortowanie po stronie klienta obejmowałoby tylko bieżącą stronę wyników.
Parametry adresu obsługiwane przez app/(dashboard)/dashboard/reservations/page.tsx:
| Parametr | Wartości | Uwagi |
|---|---|---|
dateFrom | yyyy-MM-dd | dzień w czasie polskim, włącznie |
dateTo | yyyy-MM-dd | dzień w czasie polskim, włącznie |
sort | upcoming | dateAsc | dateDesc | brak parametru = upcoming |
showCancelled | true | bez zmian względem poprzedniej wersji |
Parsowanie i walidacja siedzą w
app/(dashboard)/dashboard/reservations/components/types.ts
(normalizeDateParam, parseReservationsSort) i są używane po obu stronach —
przez serwerowy page.tsx przy budowaniu zapytania i przez kliencki
ReservationsClient przy odczycie stanu panelu z useSearchParams(). Zmiana
filtra zawsze resetuje page na 0.
Parametr street przychodzi z globalnego selektora lokalizacji i może mieć
wartość wartownika ALL („wszystkie lokalizacje"). To nie jest nazwa ulicy, więc
page.tsx sprowadza go do concreteStreet (undefined dla ALL) i dopiero to
podaje do getClientBookings i getGamesByCity. Przekazany wprost ALL trafiał
do warunku c.street = 'ALL', który nie pasuje do żadnego kortu — lista i siatka
wracały puste, a każdy filtr nałożony na pustą listę wyglądał na niedziałający.
Układ panelu
Cztery pola panelu (dwie daty, sortowanie, przełącznik odwołanych) to na
komputerze jeden wiersz siatki, więc muszą stać w jednej linii. Każde pole ma tę
samą budowę: etykieta o stałej wysokości (flex h-5 items-center) i pod nią
kontrolka o wysokości h-9. Bez tego etykieta bez ikony renderuje się jako
element inline — jej pudełko liniowe jest wyższe od etykiet z ikoną obok,
przez co Select sortowania i przełącznik odwołanych schodziły o kilka pikseli
niżej niż pola dat. Komórka przełącznika dostaje w układzie siatki pusty
span o wysokości etykiety, żeby jej wiersz kontrolki trafił na tę samą
wysokość; w układzie szuflady (telefon) tego wypełniacza nie ma.
SQL
lib/client-bookings-order.ts trzyma wyrażenia ORDER BY i warunki zakresu dat,
osobno od lib/actions/booking.ts, żeby dało się je odpalić na prawdziwym
SQLite w testach (__tests__/lib/client-bookings-order.test.ts).
Kluczowa pułapka: kolumna game.start_time trzyma dwie konwencje — wiersze
zapisane jako UTC mają na końcu Z, starsze są już czasem polskim. Porównanie
surowej kolumny miesza jedno z drugim i wypada dwie godziny obok, a przy
przełomie doby wrzuca termin do złego dnia. Dlatego każde porównanie idzie przez
gamePolandLocalSql('g.start_time') z lib/utils/court-conflict.ts, a „teraz"
przez sqlPolandLocal(SQL_NOW_UTC_ISO) — patrz Konwencje czasu.
Sortowanie upcoming to trzy człony:
ORDER BY CASE WHEN <start_pl> >= <now_pl> THEN 0 ELSE 1 END,
CASE WHEN <start_pl> >= <now_pl> THEN <start_pl> END ASC,
<start_pl> DESC
Pierwszy człon dzieli listę na przyszłość i przeszłość, drugi układa przyszłość rosnąco (najbliższe u góry), trzeci układa przeszłość malejąco. Terminy już rozegrane nie znikają — schodzą na koniec.
buildClientBookingsOrderBy(sort, nowSql) przyjmuje wyrażenie „teraz" jako
argument; produkcja korzysta z domyślnego, testy wstrzykują ustalony moment,
żeby przypiąć granicę przyszłość/przeszłość.
Lista mobilna
MobileBookingsList grupuje rezerwacje po dniach. Grupowanie przepisano z
obiektu na Map, a stałe sortowanie „od najnowszej" w obrębie dnia i między
dniami usunięto — kolejność z serwera musi przetrwać grupowanie, inaczej wybór
w polu Sortowanie nie miałby na telefonie żadnego efektu.
Szuflada z filtrami jest renderowana jako rodzeństwo MobileContainer, a nie
w jego topContent: pływający przycisk i sama szuflada są fixed/portalowane,
więc w topContent zabierałyby tylko miejsce nad listą.
Offset przycisku to bottom-[calc(5rem_+_env(safe-area-inset-bottom))] —
MobileContainer trzyma paginację w pasku fixed bottom-0 … z-10 z własnym
pb-[env(safe-area-inset-bottom)], więc stała wartość lądowałaby na tym pasku na
telefonie z wcięciem. Przycisk ma z-20, żeby był nad paskiem.
Wewnątrz szuflady zostaje DatePicker z PopoverContentHigh (z-[9999]), który
otwiera się nad DrawerContent (z-50) — ten sam układ co w formularzach
player-form-dialog i camp-term-form-dialog. Na iOS DatePicker i tak
przełącza się na natywny <input type="date">.
Nazwisko na własnym okienku
getOwnReservationLabel w
app/(dashboard)/dashboard/reservations/components/utils.ts zwraca podpis dla
okienka albo null, gdy termin należy do kogoś innego. Kolejność sprawdzania:
- czy któryś z uczestników gry (
game.attendees[].id) jest uczestnikiem z kartoteki zalogowanego użytkownika — wtedy podpisem jest jego imię i nazwisko; - czy
game.instructor_idlubgame.substitute_instructor_idtosubzalogowanego — wtedy podpisem jest imię i nazwisko z sesji.
Dane wejściowe dobrano tak, żeby nie poszerzać tego, co i tak trafia do
przeglądarki. Widok kalendarza pobiera gry przez getGamesByCity z
includeAttendeeNames: false, czyli dostaje wyłącznie identyfikatory uczestników
— nigdy cudze nazwiska. Nazwiska własnych uczestników dokłada osobno
getCurrentUserPlayers (lib/actions/payment.ts), a nazwisko instruktora
pochodzi z sesji. Innymi słowy podpis może się rozwiązać wyłącznie do danych,
które przeglądarka i tak ma prawo znać.
getCurrentUserPlayers to jedno zapytanie o id, first_name, last_name dla
owner_email z sesji — zastępuje wcześniejsze getCurrentUserPlayerIds w tym
widoku, więc liczba zapytań się nie zmienia.
ReservationsClient składa z tego CurrentUserIdentity (players, sub,
name) i podaje je do GraphView oraz AvailabilityWeekView. Obie siatki
używają tego samego podpisu do trzech rzeczy: tekstu w okienku, koloru tła
(bg-blue-600 zamiast bg-gray-400) i ukrycia odnośnika Powiadom o
dostępności. Wcześniej ten odnośnik chował się na podstawie osobnego
currentUserPlayerIds; teraz warunek jest jeden i obejmuje też zajęcia własne
instruktora.
Podpis jest ucinany wielokropkiem (truncate) z pełną wartością w title, bo
okienko w siatce dnia ma na telefonie 100 px szerokości.
Testy
__tests__/lib/client-bookings-order.test.ts— sortowania i zakresy dat na prawdziwym SQLite, w tym wiersz22:30Zwpadający do następnego dnia polskiego i wiersz zapisany starą konwencją bezZ.__tests__/app/reservations-own-label.test.ts— rozwiązywanie podpisu: własny uczestnik, kilku własnych uczestników, cudza rezerwacja, instruktor prowadzący i zastępujący, brak tożsamości.