Ustawienia ogólne (klucz–wartość)
Ekran Dashboard ➔ Ustawienia ogólne to edytor surowych ustawień aplikacji przechowywanych w tabeli app_settings. Każde ustawienie jest parą klucz → wartość przypisaną do miasta, a od teraz opcjonalnie także do konkretnej lokalizacji (ulicy).
👤 Instrukcja dla administratora
Ścieżka: Dashboard ➔ Ustawienia ogólne
Zakres ustawienia: miasto czy lokalizacja
Każdy wiersz ma dwie kolumny opisujące zakres:
| Kolumna | Znaczenie |
|---|---|
| Miasto | Miasto, którego dotyczy ustawienie. |
| Lokalizacja | Ulica (obiekt) w tym mieście. Wartość Całe miasto oznacza, że ustawienie obowiązuje we wszystkich lokalizacjach tego miasta. |
Zasada rozstrzygania jest prosta: ustawienie przypisane do lokalizacji wygrywa z ustawieniem „Całe miasto”. Jeżeli dla danej lokalizacji nie ma wyjątku, aplikacja bierze wartość miejską.
Domyślnie wszystkie ustawienia są miejskie — wiersz dla lokalizacji tworzysz dopiero wtedy, gdy dany obiekt ma się różnić od reszty miasta.
Przykład — Opole prowadzi dwa obiekty, Spokojną i Oleską:
| Klucz | Miasto | Lokalizacja | Wartość | Co obowiązuje |
|---|---|---|---|---|
trial_price | Opole | Całe miasto | 29 | Cena zajęć próbnych na Spokojnej i wszędzie indziej… |
trial_price | Opole | Oleska | 50 | …poza Oleską, gdzie obowiązuje 50 zł. |
Dodawanie ustawienia
- Kliknij Dodaj ustawienie.
- Podaj klucz (identyfikator techniczny, np.
reservation_enabled). - Wybierz miasto.
- Wybierz lokalizację — zostaw „— Całe miasto”, jeśli ustawienie ma obowiązywać wszędzie w tym mieście, albo wskaż konkretną ulicę, aby zrobić od niego wyjątek.
- Uzupełnij kategorię (grupuje wiersze na liście), wartość i opcjonalny opis.
Klucz, miasto i lokalizacja tworzą tożsamość ustawienia, więc przy edycji istniejącego wiersza są zablokowane. Aby przenieść ustawienie na inną lokalizację, dodaj nowy wiersz i usuń stary.
Lista lokalizacji pochodzi z kortów przypisanych do wybranego miasta — jeśli miasto ma tylko jeden obiekt, wybór lokalizacji pozostaje nieaktywny.
Filtrowanie listy
Selektory miasta i lokalizacji na górnym pasku aplikacji filtrują tabelę. Po wybraniu lokalizacji lista pokazuje to, co w niej faktycznie obowiązuje: wyjątki utworzone dla tej lokalizacji plus ustawienia miejskie, dla których wyjątku nie ma. Ten sam klucz nigdy nie pojawia się dwa razy — wiersz lokalizacji zastępuje na liście wiersz miejski, który przykrywa.
Dla przykładu z trial_price powyżej: w widoku Oleskiej zobaczysz 50 z etykietą lokalizacji, a w widoku Spokojnej 29 z etykietą Całe miasto.
🛠️ Dokumentacja techniczna
Model danych
CREATE TABLE app_settings (
key TEXT NOT NULL,
city TEXT NOT NULL,
street TEXT NOT NULL DEFAULT '',
value TEXT NOT NULL,
created_at TEXT,
updated_at TEXT,
category TEXT,
description TEXT,
tenant_id TEXT NOT NULL DEFAULT 'ace-park',
PRIMARY KEY (key, city, street, tenant_id)
);
Kolumnę street wprowadza migracja 0193_app_settings_street_scope.sql. Ponieważ w SQLite nie da się rozszerzyć klucza głównego w miejscu, migracja przebudowuje tabelę i tworzy indeks idx_app_settings_lookup (tenant_id, city, street, key).
Pusty ciąg (''), a nie NULL, oznacza „całe miasto” — dzięki temu kolumna wchodzi do klucza głównego (w SQLite NULL nie jest porównywalny i psułby ON CONFLICT).
Bezpieczeństwo przebudowy
Migracja nie zawiera żadnego DROP TABLE. Stara tabela jest przemianowana na app_settings_pre_0193 i zostaje w bazie jako kopia zapasowa — można ją usunąć ręcznie po weryfikacji. Przy ponownym uruchomieniu ALTER TABLE i CREATE TABLE kończą się błędem „already exists”, a przepisujący dane INSERT jest osłonięty przez NOT EXISTS, więc całość staje się operacją pustą i nie nadpisuje niczego, co skonfigurowano ręcznie.
Migracja nie tworzy wierszy dla lokalizacji
Wszystkie istniejące ustawienia pozostają miejskie (street = ''). Wiersze dla lokalizacji powstają wyłącznie ręcznie, z poziomu ekranu ustawień, i tylko tam, gdzie dana lokalizacja ma faktycznie różnić się od miasta. Dzięki temu w bazie nie pojawiają się kopie, które wyglądają na konfigurowalne, a niczego nie zmieniają.
:::warning Zakres działania
Wymiar lokalizacji obsługuje na razie wyłącznie getAppSettingValue. Grupy typowane — getIndividualTrainingSettings, getTrialClassSettings, getVacationActivitySettings, getClientRestrictionsSettings — nadal czytają i zapisują wyłącznie wiersze street = ''. Wyjątek utworzony dla lokalizacji na kluczu obsługiwanym przez którąś z tych grup (trial_*, individual_*, vacation_activity_*, availability_reminder_*) będzie widoczny w panelu, ale nie wpłynie na zachowanie aplikacji, dopóki te gettery nie zaczną przyjmować ulicy.
:::
Odczyt z fallbackiem
getAppSettingValue(key, city, street?) w lib/actions/app-settings.ts rozstrzyga zakres jednym zapytaniem:
SELECT value FROM app_settings
WHERE key = ? AND city = ? AND tenant_id = ? AND street IN (?, '')
ORDER BY street DESC
LIMIT 1;
Pusty street sortuje się najniżej, więc ORDER BY street DESC zwraca wiersz przypisany do lokalizacji, a gdy go nie ma — wiersz miejski. Wynik jest cache'owany pod kluczem zawierającym street, a każdy zapis unieważnia cache przez invalidateAppSettingsCache().
Pozostałe, typowane grupy ustawień (getIndividualTrainingSettings, getTrialClassSettings, getVacationActivitySettings, getClientRestrictionsSettings) celowo działają dalej na poziomie miasta i zapisują/odczytują wiersze z street = ''. Ich ekrany konfiguracyjne nie mają jeszcze wymiaru lokalizacji.
Zapis
Upserty używają pełnego klucza:
INSERT INTO app_settings (key, city, street, value, created_at, updated_at, tenant_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(key, city, street, tenant_id)
DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at;
Warstwa CRUD i UI
| Plik | Rola |
|---|---|
lib/actions/app-settings-general.ts | CRUD dla ekranu; identyfikator wiersza to key::city::street |
app/(dashboard)/dashboard/general-settings/page.tsx | czyta city i street z URL i przekazuje je jako filtry listy |
components/forms/app-settings/app-settings-form-dialog.tsx | formularz; listę ulic pobiera fetchStreetsByCity z /api/streets |
components/tables/app-settings-table/columns.tsx | kolumna Lokalizacja; pusty street renderuje się jako badge „Całe miasto” |
components/layout/global-city-select-client.tsx | /dashboard/general-settings należy do employeeStreetPaths, więc widać selektor ulic |
Domyślną ulicę dla miasta wielolokalizacyjnego wyznacza getDefaultStreetForCity z lib/actions/court.ts — ta sama funkcja, z której korzystają ustawienia rezerwacji, więc filtr tabeli zgadza się z tym, co pokazuje selektor na pasku.