Przejdź do głównej zawartości

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

  1. Wejdź w Klienci (menu boczne) i kliknij Dodaj nowego klientaDodaj klienta lokalnego.
  2. Uzupełnij imię, nazwisko i numer telefonu w formacie +48XXXXXXXXX.
  3. Wybierz miasto, w którym klient zwykle gra.
  4. 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:

KomunikatCo się stałoCo zrobić
Klient z tym numerem telefonu już istniejeTaki klient lokalny jest już na liścieWyszukaj go na liście klientów zamiast zakładać drugiego
Klient istnieje, ale jest zarchiwizowanyKtoś wcześniej zarchiwizował tę osobęPrzywróć klienta z archiwum – numer telefonu jest zajęty przez tamten wpis
Klient został połączony z zarejestrowanym kontemOsoba założyła własne konto i wpis lokalny został z nim scalonySzukaj klienta wśród kont zarejestrowanych
Ten numer telefonu jest już używanyNumer należy do zarejestrowanego użytkownikaPoproś klienta o zalogowanie się albo użyj innego numeru
Numer jest zajęty przez wpis niewidoczny na liścieNumer istnieje w bazie, ale poza Twoim klubemZgłoś administratorowi – wpis jest poza zasięgiem panelu
Nie udało się sprawdzić, czy numer telefonu jest wolnyBaza kont nie odpowiedziałaSpróbuj ponownie za chwilę; jeśli się powtarza, zgłoś błąd razem ze szczegółami
Nie udało się zapisać klienta w bazie danychZapis do bazy nie przeszedłZgłoś błąd – w komunikacie są szczegóły techniczne
Uczestnik o tym imieniu, nazwisku i dacie urodzenia już istniejeW bazie jest już uczestnik o tych danychSkorzystaj z istniejącego uczestnika lub popraw dane
Konto powiązane z numerem jest zarchiwizowaneKonto właściciela zostało zarchiwizowanePrzywróć konto zamiast zakładać nowego klienta
Nie znaleziono klienta lokalnegoWpis 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 telefonuNumer nie ma postaci +48XXXXXXXXXPopraw numer – z niego wyliczany jest identyfikator klienta i nie da się go zmienić
Nie udało się zapisać zmian klienta lokalnegoZapis 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:

  1. UprawnieniarequireBackofficeOrHigher(); brak roli kończy się kodem unauthorized zamiast wyjątku Server Action (Next.js i tak zaciera jego treść w produkcji).
  2. Kolizje wpisów – jedno zapytanie po phone_number lub wygenerowanym e-mailu, bez filtrów na archived, merged_to_user_id i tenant_id. Kolumny phone_number i email mają unikalny indeks w skali całej tabeli, więc wiersz z innego klubu, zarchiwizowany albo scalony blokuje INSERT, choć nie widać go na liście klientów. Stąd kody local_user_exists, local_user_archived, local_user_merged i phone_number_taken.
  3. Dostępność numeruisPhoneNumberAvailable; zajęty numer to phone_exists_in_auth0, a wyjątek (np. niedostępna baza) phone_check_failed wraz z treścią błędu w errorDetails.
  4. Zapis local_user – błąd UNIQUE mapowany na phone_number_taken, pozostałe na insert_failed.
  5. Utworzenie uczestnikacreatePlayer. Gdy się nie powiedzie, świeżo wstawiony wiersz local_user jest 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:

  1. UprawnieniarequireBackofficeOrHigher(), kod unauthorized.
  2. Odczyt wpisu po id i tenant_id; brak wiersza to local_user_not_found (a nie cichy brak zmian).
  3. Stan wpisu – wiersz scalony z kontem (merged_to_user_id) zwraca local_user_merged, zarchiwizowany local_user_archived. Zapis do takiego wiersza przeszedłby, ale nikt by go już nie zobaczył: listy klientów czytają local_user z pominięciem scalonych i zarchiwizowanych, więc okno raportowałoby sukces, a dane zostałyby przy starych.
  4. UPDATE local_usergiven_name, family_name, preferred_city. Błąd zapisu to update_failed z treścią wyjątku w errorDetails.
  5. Synchronizacja uczestnikagetPlayerByClientId(id) + updatePlayer, tak samo jak updateUser robi to dla kont. Bez tego korekta nazwiska zatrzymałaby się na local_user, a listy obecności zostałyby przy starym.
  6. 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 +48 i dziewięć cyfr); pozostałe trzy dopuszczają + i jedenaście cyfr, więc i numer spoza Polski.
  • createLocalUser odrzuca kodem invalid_phone_number wszystko, 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:

ElementKlient lokalnyPowód
Zapis zakładki Dane osoboweupdateLocalUserupdateUser pisze do tabeli kont Better Auth po user_id
Pola e-mail i telefonzablokowanesą kluczem, po którym wisi cała historia klienta
Wybór roliukrytyrola należy do konta
Zakładka Prywatnośćukrytazgody zapisuje updateUser na wierszu konta
Rozliczenia, Ustawienia, Powiadomieniabez zmianwszystkie 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.