Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
Phase 5: Dashboard & Calendar - Pattern Map
Mapped: 2026-06-23 Files analyzed: 30 Analogs found: 24 / 30
File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
apps/api/prisma/schema.prisma (modify) |
model | CRUD | selbst | exact |
apps/api/src/dashboard/dashboard.module.ts |
config | request-response | apps/api/src/module-registry/module-registry.module.ts |
exact |
apps/api/src/dashboard/dashboard.controller.ts |
controller | CRUD | apps/api/src/module-registry/module-registry.controller.ts |
exact |
apps/api/src/dashboard/dashboard.service.ts |
service | CRUD | apps/api/src/module-registry/module-registry.service.ts |
exact |
apps/api/src/dashboard/dto/save-layout.dto.ts |
model | transform | apps/api/src/domaincheck/dto/check-domain.dto.ts |
exact |
apps/api/src/dashboard/dto/update-widget-config.dto.ts |
model | transform | apps/api/src/domaincheck/dto/check-domain.dto.ts |
exact |
apps/api/src/dashboard/dto/create-widget.dto.ts |
model | transform | apps/api/src/domaincheck/dto/check-domain.dto.ts |
exact |
apps/api/src/calendar/calendar.module.ts |
config | request-response | apps/api/src/module-registry/module-registry.module.ts |
exact |
apps/api/src/calendar/calendar.controller.ts |
controller | CRUD | apps/api/src/module-registry/module-registry.controller.ts |
exact |
apps/api/src/calendar/calendar.service.ts |
service | CRUD | apps/api/src/module-registry/module-registry.service.ts |
role-match |
apps/api/src/calendar/providers/caldav.provider.ts |
service | request-response | -- | no-analog |
apps/api/src/calendar/providers/ics.provider.ts |
service | request-response | -- | no-analog |
apps/api/src/calendar/providers/exchange.provider.ts |
service | request-response | -- | no-analog |
apps/api/src/calendar/dto/create-calendar-source.dto.ts |
model | transform | apps/api/src/domaincheck/dto/check-domain.dto.ts |
exact |
apps/api/src/calendar/dto/calendar-events-query.dto.ts |
model | transform | apps/api/src/domaincheck/dto/check-domain.dto.ts |
exact |
apps/web/src/app/(portal)/page.tsx (modify) |
component | request-response | selbst | exact |
apps/web/src/app/(portal)/settings/layout.tsx |
component | request-response | apps/web/src/app/(portal)/layout.tsx |
role-match |
apps/web/src/app/(portal)/settings/page.tsx |
component | request-response | apps/web/src/app/(portal)/page.tsx |
role-match |
apps/web/src/app/(portal)/settings/dashboard/page.tsx |
component | request-response | apps/web/src/app/(portal)/page.tsx |
role-match |
apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx |
component | request-response | apps/web/src/app/(portal)/page.tsx |
role-match |
apps/web/src/app/(portal)/settings/dashboard/search/page.tsx |
component | request-response | apps/web/src/app/(portal)/page.tsx |
role-match |
apps/web/src/components/dashboard/dashboard-grid.tsx |
component | event-driven | -- | no-analog |
apps/web/src/components/dashboard/edit-mode-toggle.tsx |
component | event-driven | apps/web/src/components/theme-toggle.tsx |
role-match |
apps/web/src/components/dashboard/widget-catalog-modal.tsx |
component | event-driven | -- | no-analog |
apps/web/src/components/dashboard/widgets/clock-widget.tsx |
component | event-driven | -- | no-analog |
apps/web/src/components/dashboard/widgets/search-widget.tsx |
component | request-response | -- | no-analog |
apps/web/src/components/dashboard/widgets/calendar-widget.tsx |
component | request-response | -- | no-analog |
apps/web/src/components/dashboard/widgets/note-widget.tsx |
component | event-driven | -- | no-analog |
apps/web/src/components/dashboard/widgets/widget-wrapper.tsx |
component | request-response | -- | no-analog |
apps/web/src/components/settings/settings-sidebar.tsx |
component | request-response | apps/web/src/components/layout/sidebar.tsx |
role-match |
apps/web/src/lib/stores/dashboard-store.ts |
store | CRUD | apps/web/src/lib/stores/sidebar-store.ts |
role-match |
apps/web/src/components/layout/header.tsx (modify) |
component | request-response | selbst | exact |
Pattern Assignments
NestJS Module Pattern (dashboard.module.ts, calendar.module.ts)
Analog: apps/api/src/module-registry/module-registry.module.ts
Vollstaendiges Pattern (Zeilen 1-22):
import { Module } from '@nestjs/common';
import { ModuleRegistryController } from './module-registry.controller';
import { ModuleRegistryService } from './module-registry.service';
import { ModuleGuard } from './module.guard';
@Module({
controllers: [ModuleRegistryController],
providers: [ModuleRegistryService, ModuleGuard],
exports: [ModuleRegistryService, ModuleGuard],
})
export class ModuleRegistryModule {}
NestJS Controller Pattern (dashboard.controller.ts, calendar.controller.ts)
Analog: apps/api/src/module-registry/module-registry.controller.ts
Imports Pattern (Zeilen 1-14):
import {
Controller,
ForbiddenException,
Get,
Param,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { Request } from 'express';
import { Roles } from '../auth/decorators/roles.decorator';
import { RolesGuard } from '../auth/guards/roles.guard';
Tenant-Context Extraction (Zeilen 48-53, wiederholt in allen Methoden):
@Get('active')
async findActive(@Req() req: Request) {
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
return this.moduleRegistryService.findActiveForTenant(tenantId);
}
Hinweis fuer Dashboard: Dashboard-Endpoints brauchen userId zusaetzlich zu tenantId. Pattern: const userId = (req as any).user?.id;
NestJS Service Pattern (dashboard.service.ts, calendar.service.ts)
Analog: apps/api/src/module-registry/module-registry.service.ts
Imports + Constructor (Zeilen 1-12):
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ModuleRegistryService {
constructor(private readonly prisma: PrismaService) {}
CRUD Pattern - findMany (Zeilen 17-21):
async findAll() {
return this.prisma.module.findMany({
orderBy: { name: 'asc' },
});
}
CRUD Pattern - upsert (Zeilen 53-82):
async activateForTenant(tenantId: string, moduleId: string) {
const moduleExists = await this.prisma.module.findUnique({
where: { id: moduleId },
});
if (!moduleExists) {
throw new NotFoundException(`Module with id '${moduleId}' not found`);
}
return this.prisma.tenantModuleActivation.upsert({
where: { tenantId_moduleId: { tenantId, moduleId } },
update: { isActive: true, activatedAt: new Date() },
create: { tenantId, moduleId, isActive: true },
include: { module: true },
});
}
DTO Pattern (alle dto/*.dto.ts)
Analog: apps/api/src/domaincheck/dto/check-domain.dto.ts
Vollstaendiges Pattern (Zeilen 1-34):
import {
ArrayMaxSize,
IsArray,
IsNotEmpty,
IsOptional,
IsString,
Matches,
} from 'class-validator';
export class CheckDomainDto {
@IsString()
@IsNotEmpty()
@Matches(/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, {
message: 'Domain must be a valid label...',
})
domain!: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
@ArrayMaxSize(10, { message: 'Maximum 10 TLDs per request' })
tlds?: string[];
}
Wichtig fuer Dashboard DTOs: save-layout.dto.ts braucht IsObject oder IsJSON fuer das JSONB layouts-Feld. class-validator hat IsObject() und IsNotEmptyObject().
Prisma Schema Pattern (schema.prisma erweitern)
Analog: apps/api/prisma/schema.prisma — bestehende Models
Model mit JSONB + userId + tenantId (Pattern aus User + LdapConfig):
model LdapConfig {
id String @id @default(uuid())
tenantId String @unique
tenant Tenant @relation(fields: [tenantId], references: [id])
serverUrl String
// ... fields ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId])
}
Neue Models folgen diesem Pattern: @id @default(uuid()), userId String, tenantId String, createdAt/updatedAt, @@index auf userId und tenantId. JSONB-Felder als Json Typ.
Zustand Store Pattern (dashboard-store.ts)
Analog: apps/web/src/lib/stores/sidebar-store.ts
Store mit Persist (Zeilen 1-21):
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface SidebarState {
isCollapsed: boolean;
isMobileOpen: boolean;
toggle: () => void;
setMobileOpen: (open: boolean) => void;
}
export const useSidebarStore = create<SidebarState>()(
persist(
(set) => ({
isCollapsed: false,
isMobileOpen: false,
toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })),
setMobileOpen: (open) => set({ isMobileOpen: open }),
}),
{ name: 'tessera-sidebar' }
)
);
Hinweis: Dashboard-Store soll NICHT persist verwenden (Layout kommt aus DB, nicht LocalStorage). Einfacher Store ohne persist wie marketplace-store.ts:
import { create } from 'zustand';
export const useMarketplaceStore = create<MarketplaceState>()((set) => ({
// ...
}));
Next.js Page Pattern (alle settings pages)
Analog: apps/web/src/app/(portal)/page.tsx
Client Component mit next-intl (Zeilen 1-6):
'use client';
import { useTranslations } from 'next-intl';
export default function DashboardPage() {
const t = useTranslations('dashboard');
Tailwind + OKLCH Farbklassen (Zeilen 9-11):
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
<div className="rounded-lg bg-muted p-4">
<span className="text-muted-foreground">
Next.js Layout Pattern (settings/layout.tsx)
Analog: apps/web/src/app/(portal)/layout.tsx
Nested Layout (Zeilen 1-13):
import { AppShell } from '@/components/layout/app-shell';
export default function PortalLayout({
children,
}: {
children: React.ReactNode;
}) {
return <AppShell>{children}</AppShell>;
}
Settings-Layout wird aehnlich: Statt <AppShell> ein <div className="flex h-full"> mit <SettingsSidebar /> und Content-Bereich.
Header Modification (Settings-Link hinzufuegen)
Analog: apps/web/src/components/layout/header.tsx
Dropdown-Menu Einfuegepunkt (Zeilen 130-155): Settings-Link kommt als neues Item zwischen User-Info Block und Logout-Button:
{/* Logout button - Zeile 132 */}
<div className="px-2 py-1">
<button
onClick={() => { setDropdownOpen(false); logout(); }}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-muted transition-colors"
>
Pattern fuer neuen Settings-Link: Gleiche CSS-Klassen wie Logout-Button, aber mit <Link href="/settings"> statt <button onClick={logout}>. Einfuegen vor dem Logout-<div> (vor Zeile 132).
Shared Patterns
Tenant-Context Extraction
Source: apps/api/src/module-registry/module-registry.controller.ts Zeilen 48-53
Apply to: Alle Backend-Controller (dashboard.controller.ts, calendar.controller.ts)
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
User-Context Extraction (neu fuer Dashboard)
Source: Abgeleitet aus Auth-Pattern Apply to: Alle Dashboard/Calendar-Endpoints (user-scoped, nicht tenant-scoped)
const userId = (req as any).user?.id;
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
API Fetch Pattern (Frontend)
Source: apps/web/src/components/layout/header.tsx Zeile 21 (fetchCurrentUser)
Apply to: Dashboard-Store, Widget-Komponenten, Settings-Seiten
// Alle API-Calls nutzen credentials: 'include' fuer Cookie-Auth
await fetch('/api/dashboard/layout', {
credentials: 'include',
});
i18n Pattern
Source: apps/web/src/app/(portal)/page.tsx Zeilen 1-6
Apply to: Alle Frontend-Komponenten
'use client';
import { useTranslations } from 'next-intl';
const t = useTranslations('dashboard');
// Verwendung: {t('empty')}, {t('addWidget')}
Tailwind UI Pattern
Source: apps/web/src/components/layout/header.tsx
Apply to: Alle neuen Komponenten
- Farben:
bg-muted,text-muted-foreground,bg-primary,text-primary-foreground,border-border,bg-card,text-foreground - Hover:
hover:bg-muted,hover:opacity-90,transition-colors - Layout:
flex,items-center,gap-2,rounded-md,px-4 py-2
No Analog Found
| File | Role | Data Flow | Reason |
|---|---|---|---|
apps/api/src/calendar/providers/caldav.provider.ts |
service | request-response | Kein externer API-Client-Pattern im Projekt. RESEARCH.md Pattern (tsdav) verwenden |
apps/api/src/calendar/providers/ics.provider.ts |
service | request-response | Kein HTTP-Fetch + Parse Pattern. RESEARCH.md Pattern (node-ical) verwenden |
apps/api/src/calendar/providers/exchange.provider.ts |
service | request-response | Kein EWS/Graph Pattern. RESEARCH.md Pattern verwenden |
apps/web/src/components/dashboard/dashboard-grid.tsx |
component | event-driven | react-grid-layout ist neu im Projekt. RESEARCH.md Code-Beispiel verwenden |
apps/web/src/components/dashboard/widget-catalog-modal.tsx |
component | event-driven | Kein Modal-Pattern im Projekt. shadcn/ui Dialog verwenden |
apps/web/src/components/dashboard/widgets/*.tsx |
component | event-driven | Widget-Komponenten sind neues Pattern. RESEARCH.md WidgetProps Interface verwenden |
Metadata
Analog search scope: apps/api/src/, apps/web/src/
Files scanned: ~60
Pattern extraction date: 2026-06-23