Klient lokalny
Klient lokalny to osoba założona przez pracownika, która nigdy nie logowała się do aplikacji. Nie ma konta ani hasła – recepcja zakłada ją po numerze telefonu, żeby móc zapisać ją na rezerwację, obóz albo kurs.
👤 Instrukcja dla pracownika
Jak dodać klienta
- Wejdź w Klienci (menu boczne) i kliknij Dodaj nowego klienta → Dodaj klienta lokalnego.
- Uzupełnij imię, nazwisko i numer telefonu w formacie
+48XXXXXXXXX. - Wybierz miasto, w którym klient zwykle gra.
- Kliknij Stwórz.
Ten sam formularz znajdziesz w oknie rezerwacji (lista uczestników → Dodaj gracza) oraz w zapisach na obozy i kursy tenisowe.
Razem z klientem zakładany jest automatycznie powiązany z nim uczestnik – to on trafia na listy obecności i do rezerwacji.
Jak zmienić dane klienta
Klienta lokalnego edytujesz tak samo jak każdego innego: na jego profilu przyciskiem Modyfikuj użytkownika, albo z listy klientów przez menu … → Edytuj.
Zmienić możesz:
- imię i nazwisko – zmiana przepisuje się od razu na powiązanego uczestnika, żeby na listach obecności nie zostało stare nazwisko,
- miasto – wraz z nim zmienia się miasto wszystkich uczestników klienta,
- dane do faktury (zakładka Rozliczenia),
- ustawienia klienta i cenniki indywidualne (zakładka Ustawienia) – to tu przypisujesz klientowi własną cenę zajęć albo rezerwacji.
Numeru telefonu i adresu e-mail nie da się zmienić. Klient lokalny jest po nich rozpoznawany – cała jego historia (płatności, portfel, zapisy, uczestnicy) jest podpięta pod ten adres. Jeśli numer został wpisany błędnie, poproś klienta o rejestrację właściwym numerem: przy rejestracji jego historia zostanie przeniesiona na nowe konto. Z tego samego powodu klient lokalny nie ma zakładki Prywatność ani wyboru roli – zgody i role należą do konta, którego on nie ma.
Co oznacza komunikat błędu
Jeżeli klient nie zostanie utworzony, komunikat mówi, na czym dokładnie zatrzymało się zakładanie konta:
| Komunikat | Co się stało | Co zrobić |
|---|---|---|
| Klient z tym numerem telefonu już istnieje | Taki klient lokalny jest już na liście | Wyszukaj go na liście klientów zamiast zakładać drugiego |
| Klient istnieje, ale jest zarchiwizowany | Ktoś wcześniej zarchiwizował tę osobę | Przywróć klienta z archiwum – numer telefonu jest zajęty przez tamten wpis |
| Klient został połączony z zarejestrowanym kontem | Osoba założyła własne konto i wpis lokalny został z nim scalony | Szukaj klienta wśród kont zarejestrowanych |
| Ten numer telefonu jest już używany | Numer należy do zarejestrowanego użytkownika | Poproś klienta o zalogowanie się albo użyj innego numeru |
| Numer jest zajęty przez wpis niewidoczny na liście | Numer istnieje w bazie, ale poza Twoim klubem | Zgłoś administratorowi – wpis jest poza zasięgiem panelu |
| Nie udało się sprawdzić, czy numer telefonu jest wolny | Baza kont nie odpowiedziała | Spróbuj ponownie za chwilę; jeśli się powtarza, zgłoś błąd razem ze szczegółami |
| Nie udało się zapisać klienta w bazie danych | Zapis do bazy nie przeszedł | Zgłoś błąd – w komunikacie są szczegóły techniczne |
| Uczestnik o tym imieniu, nazwisku i dacie urodzenia już istnieje | W bazie jest już uczestnik o tych danych | Skorzystaj z istniejącego uczestnika lub popraw dane |
| Konto powiązane z numerem jest zarchiwizowane | Konto właściciela zostało zarchiwizowane | Przywróć konto zamiast zakładać nowego klienta |
| Nie znaleziono klienta lokalnego | Wpis zniknął w trakcie edycji (np. został scalony) | Odśwież listę klientów i sprawdź, czy klient nie ma już własnego konta |
| Nieprawidłowy format numeru telefonu | Numer nie ma postaci +48XXXXXXXXX | Popraw numer – z niego wyliczany jest identyfikator klienta i nie da się go zmienić |
| Nie udało się zapisać zmian klienta lokalnego | Zapis do bazy nie przeszedł | Zgłoś błąd – w komunikacie są szczegóły techniczne |
Komunikaty kończące się dopiskiem „Szczegóły techniczne: …” warto zgłaszać przyciskiem Zgłoś błąd w powiadomieniu – ten fragment trafia do zgłoszenia i wskazuje developerowi dokładną przyczynę.
🔧 Dokumentacja techniczna
Przebieg zakładania klienta
Akcja createLocalUser (lib/actions/users.ts) wykonuje kroki w kolejności:
- Uprawnienia –
requireBackofficeOrHigher(); brak roli kończy się kodemunauthorizedzamiast wyjątku Server Action (Next.js i tak zaciera jego treść w produkcji). - Kolizje wpisów – jedno zapytanie po
phone_numberlub wygenerowanym e-mailu, bez filtrów naarchived,merged_to_user_iditenant_id. Kolumnyphone_numberiemailmają unikalny indeks w skali całej tabeli, więc wiersz z innego klubu, zarchiwizowany albo scalony blokujeINSERT, choć nie widać go na liście klientów. Stąd kodylocal_user_exists,local_user_archived,local_user_mergediphone_number_taken. - Dostępność numeru –
isPhoneNumberAvailable; zajęty numer tophone_exists_in_auth0, a wyjątek (np. niedostępna baza)phone_check_failedwraz z treścią błędu werrorDetails. - Zapis
local_user– błądUNIQUEmapowany naphone_number_taken, pozostałe nainsert_failed. - Utworzenie uczestnika –
createPlayer. Gdy się nie powiedzie, świeżo wstawiony wierszlocal_userjest kasowany: klient bez uczestnika i tak nie da się zapisać na nic, a pozostawiony wiersz odpowiadałby na kolejną próbę komunikatem „klient już istnieje”.
Przebieg edycji klienta
Akcja updateLocalUser (lib/actions/users.ts) zapisuje wyłącznie te pola,
na które nic innego nie wskazuje:
- Uprawnienia –
requireBackofficeOrHigher(), kodunauthorized. - Odczyt wpisu po
iditenant_id; brak wiersza tolocal_user_not_found(a nie cichy brak zmian). - Stan wpisu – wiersz scalony z kontem (
merged_to_user_id) zwracalocal_user_merged, zarchiwizowanylocal_user_archived. Zapis do takiego wiersza przeszedłby, ale nikt by go już nie zobaczył: listy klientów czytająlocal_userz pominięciem scalonych i zarchiwizowanych, więc okno raportowałoby sukces, a dane zostałyby przy starych. UPDATE local_user–given_name,family_name,preferred_city. Błąd zapisu toupdate_failedz treścią wyjątku werrorDetails.- Synchronizacja uczestnika –
getPlayerByClientId(id)+updatePlayer, tak samo jakupdateUserrobi to dla kont. Bez tego korekta nazwiska zatrzymałaby się nalocal_user, a listy obecności zostałyby przy starym. - Miasto uczestników – tylko gdy miasto faktycznie się zmieniło:
UPDATE player SET city = ? WHERE owner_email = ? AND tenant_id = ?.
id (local|<telefon>) i email (<telefon>@local.acepark.pl) są wyliczane
z numeru telefonu i stanowią klucz, po którym payment, wallet, invoice,
client_settings, client_pricing_rules i player odnajdują tego klienta
(pełną listę kolumn ma ADDRESS_COLUMNS w lib/local-user-merge.ts). Dlatego
formularz blokuje pola numeru i e-maila: przepisanie adresu to ta sama operacja,
którą wykonuje mergeLocalUserIntoAccount, i musi iść jednym batchem razem z
portfelem i uczestnikami. Zmiana numeru odbywa się więc przez rejestrację klienta
i scalenie, nie przez edycję.
Format numeru telefonu
Numer musi być zapisany międzynarodowo, z + na początku. Kontrola jest po
obu stronach, bo z numeru wyliczane są id i email, a te są nieodwracalne:
wpis założony z 796616646 albo 48796616646 byłby klientem, którego nie da
się już poprawić.
- Formularze wymagają pełnego numeru, zanim pozwolą kliknąć „Utwórz".
Szybki formularz w oknie rezerwacji sprawdza
POLISH_PHONE_PATTERN(lib/login-identifier.ts, czyli+48i dziewięć cyfr); pozostałe trzy dopuszczają+i jedenaście cyfr, więc i numer spoza Polski. createLocalUserodrzuca kodeminvalid_phone_numberwszystko, co nie jest+i od 9 do 15 cyfr — celowo luźniej niż formularze, żeby nie odciąć poprawnego numeru zagranicznego, a mimo to nie wpuścić kształtu, z którego powstaje wpis nie do naprawienia.
Wpisy założone zanim wzorzec był wymuszany nadal istnieją, więc formularz edycji nie waliduje numeru klienta lokalnego — pole i tak jest zablokowane i nic z niego nie jest zapisywane. Gdyby walidował, okno odmawiałoby zapisu poprawionego nazwiska z powodu pola, którego nikt nie może zmienić.
Formularz UserFormDialog dla klienta lokalnego
components/forms/user/user-form-dialog.tsx rozpoznaje klienta lokalnego przez
isLocalUser(email) i przełącza się na zapis do local_user:
| Element | Klient lokalny | Powód |
|---|---|---|
| Zapis zakładki Dane osobowe | updateLocalUser | updateUser pisze do tabeli kont Better Auth po user_id |
| Pola e-mail i telefon | zablokowane | są kluczem, po którym wisi cała historia klienta |
| Wybór roli | ukryty | rola należy do konta |
| Zakładka Prywatność | ukryta | zgody zapisuje updateUser na wierszu konta |
| Rozliczenia, Ustawienia, Powiadomienia | bez zmian | wszystkie trzy są kluczowane adresem e-mail, nie user_id |
Imię i nazwisko są w schemacie formularza opcjonalne (konta bywają bez nich),
a w local_user mają NOT NULL, więc ścieżka lokalna waliduje je dodatkowo
przed wysłaniem.
Kody błędów
Typ LocalUserErrorCode (lib/local-user-errors.ts) jest kontraktem między akcją
a formularzami. Kody są stabilnymi identyfikatorami, a nie zdaniami, bo Next.js
zaciera treści błędów Server Actions w produkcji, a moduł 'use server' może
eksportować wyłącznie funkcje asynchroniczne.
Odpowiedź akcji zawiera dodatkowo errorDetails – oryginalną treść wyjątku
(np. komunikat D1). Hook useLocalUserErrorMessage
(hooks/useLocalUserErrorMessage.ts) tłumaczy kod na zdanie z
userViewForm.localUserErrors.* i dokleja szczegóły techniczne, jeśli przyszły.
Nieznany kod spada na notification.userCreateFailedDescription, dzięki czemu
nowy kod po stronie serwera nie psuje starszego klienta.
Wszystkie cztery miejsca zakładające klienta lokalnego (lista klientów, autouzupełnianie uczestników w kalendarzu, zapis na obóz, zapis na kurs) korzystają z tego samego hooka.
Błędy createPlayer
createPlayer (lib/actions/players.ts) rzuca PlayerCreationError z kodem
(duplicate_name, owner_archived, insert_failed, missing_owner_email,
create_failed) i opcjonalnym polem details. Wcześniej każda przyczyna była
zamieniana na jedno zdanie „Nie udało się utworzyć uczestnika w bazie danych.”,
więc wywołujący nie odróżniał duplikatu od awarii bazy. Treść komunikatu dla
istniejących dialogów pozostała bez zmian – kod i szczegóły dochodzą obok niej.