Prepinac firmy do sidebaru a prepnuti prekresli obsah cely

Dodelavka. Tvrzeni "prepinac plati pro cely system" nebylo pravdive:
useApiQuery se na zmenu firmy zeptat umel, ale komponenty volajici
apiFetch primo - EntityAdmin, a tim cele Nastaveni a zalozky Resitele
a Skupiny v Lidech - o zmene nevedely a zustala na nich data predchozi
firmy. Presne ta vada, kvuli ktere se to delalo, jen o patro vedle.

- obsah dashboardu ma key podle vybrane firmy, takze prepnuti prestavi
  cely strom komponent. Zadny dotaz nemuze zustat stary, protoze zadna
  stara komponenta nezustane. Doplnovat zavislost do kazde komponenty
  zvlast je totez zapomenuti, jen o patro niz.
- prepinac je v sidebaru nad navigaci, ne v hlavicce, a vypada jako
  ovladaci prvek: ram, ikona firmy, sipka. Predchozi verze mela pruhledny
  ram a splyvala s popiskem.
- v hlavicce zustal jen nazev aktivni firmy. Driv tam stalo natvrdo
  tenants[0], takze po prepnuti ukazovala porad prvni firmu ze seznamu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-26 08:04:22 +02:00
co-authored by Claude Opus 5
parent 2e0b9456a9
commit 2e863c1f8b
3 changed files with 95 additions and 18 deletions
+14 -2
View File
@@ -48,8 +48,9 @@ muze mit vic. Bez `tenantId` se pouzije prvni.
## Prepinac firmy je nad celym portalem
Vybrana firma je **jedna hodnota pro cely dashboard** (`web/src/lib/tenant.ts`),
ne stav jedne stranky. Prepinac je v hlavicce nad obsahem a `apiFetch` doplnuje
`tenantId` do kazdeho dotazu na `/api/dashboard`.
ne stav jedne stranky. Prepinac je v sidebaru nad navigaci, tedy nad vsim, co se
pod nim kresli, a `apiFetch` doplnuje `tenantId` do kazdeho dotazu
na `/api/dashboard`.
Doplnuje se to v API vrstve schvalne, ne na strankach. Kdyz si to mela pridavat
kazda stranka sama, vetsina na to zapomnela a server sahnul po vychozi firme -
@@ -65,6 +66,17 @@ Vyjimky, kdy se nedoplnuje:
| Neni vybrano | server pouzije vychozi firmu uctu |
| Cesta mimo `/api/dashboard` | prihlaseni a verejny web s firmou nepracuji |
**Prepnuti prekresli obsah cely.** Obsah dashboardu ma `key` podle vybrane
firmy, takze jina firma znamena novy strom komponent.
Bez toho to nestacilo: `useApiQuery` se sice zeptat umi, ale komponenty, ktere
volaji `apiFetch` primo - `EntityAdmin`, a tim cele Nastaveni a zalozky
Resitele a Skupiny - o zmene nevi a zustala by na nich data predchozi firmy.
Doplnovat zavislost do kazde z nich je totez zapomenuti, jen o patro niz.
Ztrata rozepsaneho stavu je pritom spravne: filtr na cloveka z firmy A nema
ve firme B co znamenat.
Volba prezije obnoveni stranky (localStorage). Kdyz ulozena firma uz mezi
`access.tenants` neni - clovek z ni odesel, prihlasil se nekdo jiny na tomtez
pocitaci - spadne se na vychozi firmu uctu, aby kazdy dotaz nekoncil na 403.
+25
View File
@@ -2,6 +2,31 @@
Nejnovejsi nahore.
## 2026-08-26 - prepinac firmy: do sidebaru a prekresli obsah cely
Dodelavka predchoziho bodu. Tvrzeni "prepinac plati pro cely system" nebylo
pravdive: `useApiQuery` se na zmenu firmy zeptat umel, ale komponenty, ktere
volaji `apiFetch` primo - `EntityAdmin`, a tim cele Nastaveni a zalozky
Resitele a Skupiny v Lidech - o zmene nevedely a zustala na nich data
predchozi firmy.
### Opraveno
- **Obsah dashboardu ma `key` podle vybrane firmy**, takze prepnuti prestavi
cely strom komponent. Zadny dotaz nemuze zustat stary, protoze zadna stara
komponenta nezustane. Doplnovat zavislost do kazde komponenty zvlast je
totez zapomenuti, jen o patro niz.
### Zmeneno
- **Prepinac je v sidebaru nad navigaci**, ne v hlavicce. Vypada jako ovladaci
prvek: ram, ikona firmy, sipka. Predchozi verze mela pruhledny ram a splyvala
s popiskem - prepinac, ktery neni videt, je k nicemu.
- **V hlavicce zustal jen nazev aktivni firmy.** Driv tam stalo natvrdo
`tenants[0]`, takze po prepnuti ukazovala porad prvni firmu ze seznamu.
- Kdo patri do jedne firmy, vidi misto prepinace jeji nazev. Vyber z jedne
moznosti neni vyber.
## 2026-08-26 - prava plati za firmu, ne za cloveka
`permissionsOf(user)` scitalo role pres vsechna clenstvi. Kdo byl spravce
@@ -1,13 +1,15 @@
import {
AlarmClock,
Blocks,
Building2,
ChevronDown,
ExternalLink,
LayoutDashboard,
LayoutGrid,
LifeBuoy,
MessageSquarePlus,
LogOut,
Menu,
MessageSquarePlus,
MousePointerClick,
Plug,
ScrollText,
@@ -92,27 +94,36 @@ function TenantSwitch({ access }: { access: Access | null }) {
const current = tenants.find((tenant) => tenant.id === activeTenant);
// Vyber z jedne moznosti neni vyber, tak se vykresli jen nazev.
if (tenants.length <= 1) {
return (
<p className="truncate text-sm font-semibold text-white">
<p className="truncate px-2 py-1 text-sm font-semibold text-white">
{current?.name ?? tenants[0]?.name ?? 'Klientský portál'}
</p>
);
}
/*
Vypada jako ovladaci prvek, ne jako popisek. Predchozi verze mela pruhledny
ram a splyvala s textem - prepinac, ktery neni videt, je k nicemu.
*/
return (
<select
value={activeTenant ?? ''}
onChange={(event) => setActiveTenant(event.target.value)}
aria-label="Firma, za kterou pracujete"
className="-ml-2 max-w-full truncate rounded-lg border border-transparent bg-transparent px-2 py-0.5 text-sm font-semibold text-white transition-colors hover:border-ink-600/70 hover:bg-ink-850/70 focus:border-brand-400/70 focus:outline-none"
>
{tenants.map((tenant) => (
<option key={tenant.id} value={tenant.id} className="bg-ink-850">
{tenant.name}
</option>
))}
</select>
<div className="relative flex w-full items-center">
<Building2 className="pointer-events-none absolute left-3 size-4 text-white/45" />
<select
value={activeTenant ?? ''}
onChange={(event) => setActiveTenant(event.target.value)}
aria-label="Firma, za kterou pracujete"
className="w-full appearance-none truncate rounded-xl border border-ink-600/70 bg-ink-850/70 py-2 pr-8 pl-9 text-sm font-semibold text-white transition-colors hover:border-brand-400/50 focus:border-brand-400/70 focus:outline-none"
>
{tenants.map((tenant) => (
<option key={tenant.id} value={tenant.id} className="bg-ink-850">
{tenant.name}
</option>
))}
</select>
<ChevronDown className="pointer-events-none absolute right-3 size-4 text-white/45" />
</div>
);
}
@@ -145,6 +156,8 @@ function DashboardShell() {
setSidebarOpen(false);
}, [location.pathname]);
const activeTenant = useActiveTenant();
async function handleLogout() {
await logout();
navigate('/', { replace: true });
@@ -171,6 +184,15 @@ function DashboardShell() {
</button>
</div>
{/*
Prepinac firmy nad navigaci, tedy nad vsim, co se pod nim kresli.
Vybrana firma plati pro cely portal, ne pro jednu stranku, takze
patri do ramu portalu a ne dovnitr obsahu.
*/}
<div className="border-b border-ink-600/60 p-3">
<TenantSwitch access={access.data} />
</div>
<nav className="flex-1 space-y-1 overflow-y-auto p-3">
{(access.data?.nav ?? []).map((entry) => {
const route = moduleRoutes[entry.key];
@@ -253,7 +275,15 @@ function DashboardShell() {
</button>
<div className="min-w-0 flex-1">
<TenantSwitch access={access.data} />
{/*
Jen nazev, prepinac je v sidebaru. Driv tu stalo natvrdo
`tenants[0]`, takze po prepnuti ukazovalo porad prvni firmu
ze seznamu bez ohledu na vyber.
*/}
<p className="truncate text-sm font-semibold text-white">
{access.data?.tenants.find((tenant) => tenant.id === activeTenant)?.name ??
'Klientský portál'}
</p>
<p className="truncate text-xs text-white/40">{accessLabel(user, access.data)}</p>
</div>
@@ -270,7 +300,17 @@ function DashboardShell() {
</div>
</header>
<main className="px-5 py-7 sm:px-7">
{/*
Prepnuti firmy prekresli obsah **cely**, ne jen to, co si o zmene
reklo. `useApiQuery` se sice zeptat umi, ale komponenty, ktere volaji
`apiFetch` primo (EntityAdmin, a tim cele Nastaveni), o zmene nevi -
zustala by na nich data predchozi firmy. Klic to resi jednou provzdy:
jina firma znamena novy strom, takze zadny dotaz nemuze zustat stary.
Ztrata rozepsaneho stavu je pritom spravne. Filtr na cloveka z firmy A
nema ve firme B co znamenat.
*/}
<main key={activeTenant ?? 'bez-firmy'} className="px-5 py-7 sm:px-7">
<Outlet />
</main>
</div>