Skip to main content

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:

ParametrWartościUwagi
dateFromyyyy-MM-dddzień w czasie polskim, włącznie
dateToyyyy-MM-dddzień w czasie polskim, włącznie
sortupcoming | dateAsc | dateDescbrak parametru = upcoming
showCancelledtruebez 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:

  1. czy któryś z uczestników gry (game.attendees[].id) jest uczestnikiem z kartoteki zalogowanego użytkownika — wtedy podpisem jest jego imię i nazwisko;
  2. czy game.instructor_id lub game.substitute_instructor_id to sub zalogowanego — 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 wiersz 22:30Z wpadający do następnego dnia polskiego i wiersz zapisany starą konwencją bez Z.
  • __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.