Files
KanzlAI-mGMT/frontend/src/components/layout/TenantSwitcher.tsx
m f81a2492c6 feat: UI polish — responsive, loading/empty/error states, German fixes (Phase 3Q)
- Responsive sidebar: collapses on mobile with hamburger menu, slide-in animation
- Skeleton loaders: dashboard cards, case table, case detail page
- Empty states: friendly messages with icons for cases, deadlines, parties, documents
- Error states: retry button on dashboard, proper error message on case not found
- Form validation: inline error messages on case creation form
- German language: fix all missing umlauts (Zurück, wählen, Anhängig, Verfügung, etc.)
- Status labels: display German translations instead of raw status values
- Transitions: fade-in animations on page load, hover/transition-colors on all interactive elements
- Focus states: focus-visible ring for keyboard accessibility
- Mobile layout: stacking for filters, forms, tabs; horizontal scroll for tables
- Extraction results: card layout on mobile, table on desktop
- Missing types: add DashboardData, DeadlineSummary, CaseSummary, ExtractedDeadline etc.
- Fix QuickActions links to use correct routes (/cases/new, /ai/extract)
- Consistent input focus styles across all forms
2026-03-25 14:16:30 +01:00

85 lines
2.7 KiB
TypeScript

"use client";
import { api } from "@/lib/api";
import type { TenantWithRole } from "@/lib/types";
import { ChevronsUpDown } from "lucide-react";
import { useEffect, useRef, useState } from "react";
export function TenantSwitcher() {
const [tenants, setTenants] = useState<TenantWithRole[]>([]);
const [current, setCurrent] = useState<TenantWithRole | null>(null);
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
api
.get<TenantWithRole[]>("/tenants")
.then((data) => {
setTenants(data);
const savedId = localStorage.getItem("kanzlai_tenant_id");
const match = data.find((t) => t.id === savedId) || data[0];
if (match) {
setCurrent(match);
localStorage.setItem("kanzlai_tenant_id", match.id);
}
})
.catch(() => {
// Not authenticated or no tenants
});
}, []);
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
function switchTenant(tenant: TenantWithRole) {
setCurrent(tenant);
localStorage.setItem("kanzlai_tenant_id", tenant.id);
setOpen(false);
window.location.reload();
}
if (!current) return null;
return (
<div ref={ref} className="relative">
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2.5 py-1.5 text-sm text-neutral-700 transition-colors hover:bg-neutral-50"
>
<span className="max-w-[120px] truncate sm:max-w-[160px]">
{current.name}
</span>
<ChevronsUpDown className="h-3.5 w-3.5 text-neutral-400" />
</button>
{open && tenants.length > 1 && (
<div className="animate-fade-in absolute right-0 top-full z-50 mt-1 w-56 rounded-md border border-neutral-200 bg-white py-1 shadow-lg">
{tenants.map((tenant) => (
<button
key={tenant.id}
onClick={() => switchTenant(tenant)}
className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors ${
tenant.id === current.id
? "bg-neutral-50 font-medium text-neutral-900"
: "text-neutral-600 hover:bg-neutral-50"
}`}
>
<span className="truncate">{tenant.name}</span>
<span className="ml-auto text-xs text-neutral-400">
{tenant.role}
</span>
</button>
))}
</div>
)}
</div>
);
}