Files
tessera-ctl/.planning/phases/05-dashboard-calendar/05-PATTERNS.md
T

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.


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