wip: phase 05 plan-phase paused at revision verify (checker v2 pending)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
# 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):
|
||||
```typescript
|
||||
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):
|
||||
```typescript
|
||||
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):
|
||||
```typescript
|
||||
@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):
|
||||
```typescript
|
||||
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):
|
||||
```typescript
|
||||
async findAll() {
|
||||
return this.prisma.module.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**CRUD Pattern - upsert** (Zeilen 53-82):
|
||||
```typescript
|
||||
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):
|
||||
```typescript
|
||||
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):
|
||||
```prisma
|
||||
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):
|
||||
```typescript
|
||||
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`:
|
||||
```typescript
|
||||
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):
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useTranslations('dashboard');
|
||||
```
|
||||
|
||||
**Tailwind + OKLCH Farbklassen** (Zeilen 9-11):
|
||||
```typescript
|
||||
<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):
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
{/* 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)
|
||||
```typescript
|
||||
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)
|
||||
```typescript
|
||||
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
|
||||
```typescript
|
||||
// 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
|
||||
```typescript
|
||||
'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
|
||||
Reference in New Issue
Block a user