Skip to main content

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:

KolumnaZnaczenie
MiastoMiasto, którego dotyczy ustawienie.
LokalizacjaUlica (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ą:

KluczMiastoLokalizacjaWartośćCo obowiązuje
trial_priceOpoleCałe miasto29Cena zajęć próbnych na Spokojnej i wszędzie indziej…
trial_priceOpoleOleska50…poza Oleską, gdzie obowiązuje 50 zł.

Dodawanie ustawienia

  1. Kliknij Dodaj ustawienie.
  2. Podaj klucz (identyfikator techniczny, np. reservation_enabled).
  3. Wybierz miasto.
  4. 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.
  5. 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

PlikRola
lib/actions/app-settings-general.tsCRUD dla ekranu; identyfikator wiersza to key::city::street
app/(dashboard)/dashboard/general-settings/page.tsxczyta city i street z URL i przekazuje je jako filtry listy
components/forms/app-settings/app-settings-form-dialog.tsxformularz; listę ulic pobiera fetchStreetsByCity z /api/streets
components/tables/app-settings-table/columns.tsxkolumna 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.