diff --git a/CHANGELOG.md b/CHANGELOG.md index 912670a..be5bb05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T ### Geändert - Bilderrahmen: hochgeladene Bilder liegen jetzt im Dateibereich des Servers statt in der Datenbank — die Datenbanksicherung bleibt dadurch klein; vorhandene Bilder ziehen beim ersten Start automatisch um +- Dashboard: Kacheln, die zu einem Modul gehören, erscheinen nur noch für Benutzer, die dieses Modul nutzen dürfen; eine nicht mehr freigegebene Kachel erklärt das jetzt, statt leer zu bleiben ## 1.3.0 – 2026-09-22 diff --git a/apps/web/src/components/dashboard/widgets/widget-wrapper.test.tsx b/apps/web/src/components/dashboard/widgets/widget-wrapper.test.tsx index 29cd11d..88a3b86 100644 --- a/apps/web/src/components/dashboard/widgets/widget-wrapper.test.tsx +++ b/apps/web/src/components/dashboard/widgets/widget-wrapper.test.tsx @@ -3,15 +3,21 @@ import { describe, expect, it, vi } from 'vitest'; // quick-260916-iex: Link-Widget entfernt — unbekannte Widget-Typen (z. B. // eine alte Link-Kachel vor dem Einspielen der Migration) muessen weiterhin -// ohne Absturz als grauer Text gerendert werden. +// ohne Absturz gerendert werden. +// quick-260922-m1h: Statt des rohen Typnamens steht dort jetzt ein Satz, der +// den Fall erklaert — derselbe Fall tritt kuenftig auf, wenn eine Kachel zu +// einem Modul gehoert, das dem Benutzer nicht freigegeben ist. vi.mock('next-intl', () => ({ - useTranslations: () => (key: string) => key, + useTranslations: () => (key: string) => + key === 'unavailable' + ? 'Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.' + : key, })); import { WidgetWrapper } from './widget-wrapper'; describe('WidgetWrapper', () => { - it('unbekannter Widget-Typ (z. B. eine alte Link-Kachel vor der Migration) rendert als grauer Text ohne Absturz', () => { + it('unbekannter/gesperrter Widget-Typ erklaert sich mit einem Hinweistext statt leer oder als roher Typname zu rendern', () => { render( { const article = screen.getByRole('article'); expect(article).toHaveAttribute('aria-label', 'link'); - const fallback = screen.getByText('link'); - expect(fallback.className).toContain('text-muted-foreground'); + const hint = screen.getByText( + 'Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.', + ); + expect(hint.className).toContain('text-muted-foreground'); + expect(hint.className).toContain('text-center'); + + // Der rohe Typname steht nicht mehr im Rumpf der Kachel. + expect(screen.queryByText('link')).toBeNull(); + }); + + it('eine bekannte Kachel rendert weiterhin ihre Komponente, nicht den Hinweis', () => { + render( + , + ); + + expect( + screen.queryByText( + 'Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.', + ), + ).toBeNull(); }); }); diff --git a/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx b/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx index bd25948..c6f1500 100644 --- a/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx +++ b/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx @@ -111,8 +111,14 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro isEditMode={isEditMode} /> ) : ( -
- {widget.widgetType} + /* quick-260922-m1h: Kein Bauteil zu diesem Typ — entweder eine alte + Kachel eines entfernten Typs oder (ab der ersten Modul-Kachel) eine + Kachel, deren Modul dem Benutzer nicht freigegeben ist. Vorher + stand hier der rohe Typname, der dem Anwender nichts sagte. */ +
+

+ {t('unavailable')} +

)}
diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 5f0ebb5..03998ea 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -213,6 +213,7 @@ "saveChanges": "Änderungen speichern", "layoutLoadError": "Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.", "widgetSaveError": "Änderungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.", + "unavailable": "Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.", "clock": { "name": "Uhr", "description": "Zeigt die aktuelle Uhrzeit an", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 19f3b1d..d993a28 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -213,6 +213,7 @@ "saveChanges": "Save changes", "layoutLoadError": "Could not load dashboard. Please reload the page.", "widgetSaveError": "Could not save changes. Please try again.", + "unavailable": "This tile is not available — the module it belongs to is not enabled for you.", "clock": { "name": "Clock", "description": "Shows the current time", diff --git a/docs/anleitung-entwicklung.md b/docs/anleitung-entwicklung.md index 3a36145..1edf99d 100644 --- a/docs/anleitung-entwicklung.md +++ b/docs/anleitung-entwicklung.md @@ -400,6 +400,40 @@ Für ein Modul mit Unterrouten (Einstellungsseite, Verwaltungsansicht) orientier `dkv-fleet` oder `tender-radar` — beide haben zusätzliche `settings/page.tsx` bzw. weitere Unterverzeichnisse, die vom selben `layout.tsx` mitgedeckt werden. +### Eine Kachel zum Modul + +Ein Modul kann zusätzlich als Kachel auf dem Dashboard erscheinen. Seit +`quick-260922-m1h` sind dafür **drei** Stellen nötig (vorher waren es sieben): + +1. **Die Kachel-Komponente schreiben** — `apps/web/src/components/dashboard/widgets/-widget.tsx`, + nimmt die `WidgetProps` aus `widget-registry.tsx` (`instanceId`, `config`, `isEditMode`) entgegen. +2. **Den Typ eintragen** — in `WIDGET_TYPES` in `packages/shared/src/index.ts`. Gehört die Kachel zu + einem Modul, zusätzlich `WIDGET_MODULE_SLUGS[''] = ''` in derselben Datei. Das ist + die einzige Liste: die API validiert `POST /dashboard/widgets` per `@IsIn` gegen genau sie, und + das Frontend leitet Registry und Katalog davon ab. +3. **Anmelden** — `registerWidget('', Widget)` in `apps/web/src/app/(portal)/page.tsx`, + neben den übrigen Aufrufen. Der Aufruf steht dort und nicht in der Registry, weil die Kachel über + den Wrapper wieder die Registry importiert — ein Import aus der Registry heraus wäre ein + Zirkelimport. + +Dazu kommen wie bei jeder Oberfläche die **Übersetzungsschlüssel** (`.name` und +`.description` unter `widgets` in `de.json` **und** `en.json`), ein **Symbol** als Inline-SVG +und die **Größenvorgaben** in `WIDGET_CONSTRAINTS` (`minW`/`minH` = kleinste noch bedienbare Kachel, +`defaultW`/`defaultH` = Startgröße) — beides in `widget-registry.tsx`. Ein Test in +`widget-registry.test.tsx` prüft, dass `WIDGET_TYPES`, Registry und Constraints deckungsgleich sind; +vergisst man eine Stelle, schlägt er fehl. + +**Was eine Kachel mit `moduleSlug` automatisch tut:** Sie verschwindet für Benutzer, die das Modul +nicht nutzen dürfen — aus dem Katalog („Widget hinzufügen", `visibleWidgetTypes`) und aus dem +Dashboard selbst. Ist die Modulliste unbekannt, weil ihr Abruf fehlschlug, bleibt die Kachel +ebenfalls verborgen (fail-closed). Eine bereits angelegte Kachel eines gesperrten Moduls rendert +nicht mehr leer, sondern zeigt den Hinweis `widgets.unavailable`. + +**Wie beim Modul-Gate gilt auch hier:** Der Katalogfilter ist Komfort, nicht Zugriffskontrolle. Die +verbindliche Prüfung sitzt serverseitig in `DashboardService.getWidgets()` (filtert Kacheln +gesperrter Module fail-closed aus `GET /dashboard/widgets`) — und die Daten, die eine Modul-Kachel +anzeigt, holt sie über die Endpunkte ihres Moduls, die `@UseModule('')` tragen müssen. + ## Mandantentrennung Der tatsächliche Mechanismus ist `TenantGuard` (`apps/api/src/tenant/tenant.guard.ts`), global als