+
onLayoutChange(allLayouts)}
+ draggableHandle=".widget-drag-handle"
+ >
+ {widgets.map((widget) => (
+ l.i === widget.id),
+ minW: WIDGET_CONSTRAINTS[widget.widgetType].minW,
+ minH: WIDGET_CONSTRAINTS[widget.widgetType].minH,
+ }}>
+
+
+ ))}
+
+
+ );
+}
+```
+
+### NestJS Dashboard Controller
+
+```typescript
+// Source: NestJS Documentation Pattern + Existing Codebase Pattern
+@Controller('dashboard')
+export class DashboardController {
+ constructor(private dashboardService: DashboardService) {}
+
+ @Get('layout')
+ async getLayout(@Req() req: Request) {
+ const userId = req.user.id;
+ const tenantId = req.tenantId;
+ return this.dashboardService.getLayout(userId, tenantId);
+ }
+
+ @Put('layout')
+ async saveLayout(
+ @Req() req: Request,
+ @Body() dto: SaveLayoutDto,
+ ) {
+ const userId = req.user.id;
+ const tenantId = req.tenantId;
+ return this.dashboardService.saveLayout(userId, tenantId, dto);
+ }
+
+ @Get('widgets')
+ async getWidgets(@Req() req: Request) {
+ return this.dashboardService.getWidgets(req.user.id);
+ }
+
+ @Post('widgets')
+ async addWidget(
+ @Req() req: Request,
+ @Body() dto: CreateWidgetDto,
+ ) {
+ return this.dashboardService.addWidget(req.user.id, req.tenantId, dto);
+ }
+
+ @Patch('widgets/:id/config')
+ async updateWidgetConfig(
+ @Req() req: Request,
+ @Param('id') id: string,
+ @Body() dto: UpdateWidgetConfigDto,
+ ) {
+ return this.dashboardService.updateWidgetConfig(id, req.user.id, dto);
+ }
+
+ @Delete('widgets/:id')
+ async removeWidget(
+ @Req() req: Request,
+ @Param('id') id: string,
+ ) {
+ return this.dashboardService.removeWidget(id, req.user.id);
+ }
+}
+```
+
+### Calendar Source Configuration
+
+```typescript
+// Prisma Schema fuer Calendar Sources
+model CalendarSource {
+ id String @id @default(uuid())
+ userId String
+ tenantId String
+ name String
+ type String // 'caldav' | 'ics' | 'exchange'
+ url String
+ username String?
+ encryptedPassword String? // AES-256 verschluesselt
+ color String? @default("#3B82F6")
+ isVisible Boolean @default(true)
+ syncIntervalMin Int @default(15)
+ lastSyncAt DateTime?
+ lastSyncError String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([userId])
+ @@index([tenantId])
+}
+```
+
+### Markdown Note Widget with Autosave
+
+```typescript
+// Source: @uiw/react-md-editor docs + Autosave Pattern
+'use client';
+
+import { useState, useCallback, useRef, useEffect } from 'react';
+import MDEditor, { commands } from '@uiw/react-md-editor';
+
+const DEBOUNCE_MS = 1500;
+
+// Kompakte Toolbar (D-16)
+const NOTE_COMMANDS = [
+ commands.bold,
+ commands.italic,
+ commands.strikethrough,
+ commands.divider,
+ commands.unorderedListCommand,
+ commands.checkedListCommand,
+ commands.divider,
+ commands.link,
+ commands.code,
+];
+
+export function NoteWidget({ instanceId, config }: WidgetProps) {
+ const [value, setValue] = useState(config.content as string || '');
+ const timerRef = useRef