feat(quick-260923-ad9): Reiter anlegen, umbenennen, loeschen, umsortieren - Task 2

dashboard.service.ts: createDashboard() (Namensvergabe "Dashboard N" fuellt
Luecken, D-08; Obergrenze 20 Reiter, T-AD9-06), renameDashboard() (Riegel
zuerst), deleteDashboard() (letzter Reiter bleibt, D-10; Loeschen + Neu-
Nummerierung als EINE withTenantTransaction), reorderDashboards() (woertlich
nach FavoritesService.reorder-Muster: Exakt-Abgleich vor jedem Schreiben,
kein Teilschreiben, dieselbe Abweisung fuer unvollstaendige/unbekannte/
fremde Kennungen - T-AD9-04).

dashboard.controller.ts: fuenf neue Wege unter tabs; PUT tabs/order VOR
PATCH/DELETE tabs/:id deklariert (Routenreihenfolge).

dashboard.controller.spec.ts (neu, 8 Tests): Durchreichung, Abweisung ohne
Kontext, quelltextlesender Waechter fuer die Routenreihenfolge.

dashboard.service.spec.ts: 61 Tests (43 alte + 18 neue fuer Anlegen,
Umbenennen inkl. DTO-Beschneidung/-Laengenpruefung, Loeschen und
Umsortieren - je ein Fall fuer "fremder Reiter" und "letzter Reiter bleibt").

Deviation (Rule 1): widget-module-map.spec.ts's CreateWidgetDto-Whitelist
helper needed a dashboardId fixture after Task 1 made the field required -
fixed inline, out of the plan's files_modified list but directly caused by
Task 1's DTO change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 08:00:59 +02:00
parent 9c518238f5
commit df7a5e7e8e
6 changed files with 698 additions and 1 deletions
@@ -15,6 +15,8 @@ import type { AuthenticatedRequest } from '../auth/types/auth-user';
import { DashboardService } from './dashboard.service';
import { CreateSearchProviderDto } from './dto/create-search-provider.dto';
import { CreateWidgetDto } from './dto/create-widget.dto';
import { RenameDashboardDto } from './dto/rename-dashboard.dto';
import { ReorderDashboardsDto } from './dto/reorder-dashboards.dto';
import { SaveLayoutDto } from './dto/save-layout.dto';
import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto';
@@ -27,6 +29,12 @@ import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto';
*
* Routes:
* - GET /dashboard/tabs — list the user's dashboard tabs (quick-260923-ad9)
* - POST /dashboard/tabs — create a new, empty tab
* - PUT /dashboard/tabs/order — persist the tab order (MUST be declared
* before the `:id` routes below, see the
* source-order guard in dashboard.controller.spec.ts)
* - PATCH /dashboard/tabs/:id — rename a tab
* - DELETE /dashboard/tabs/:id — delete a tab, its widgets and its layout
* - GET /dashboard/layout — get the saved layout of one tab
* - PUT /dashboard/layout — upsert the layout of one tab
* - GET /dashboard/widgets — list the widget instances of one tab
@@ -78,6 +86,44 @@ export class DashboardController {
return this.dashboardService.listDashboards(userId, tenantId);
}
@Post('tabs')
async createDashboard(@Req() req: AuthenticatedRequest) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.createDashboard(userId, tenantId);
}
/**
* MUSS vor `PATCH tabs/:id` / `DELETE tabs/:id` stehen — in dieser
* Anwendung hat eine Route mit Platzhalter schon einmal eine dahinter
* stehende feste Route verdeckt (siehe Projektnotiz „NestJS Route-
* Order“); ein quelltextlesender Wächter in
* `dashboard.controller.spec.ts` prüft die Reihenfolge im Dateitext.
*/
@Put('tabs/order')
async reorderDashboards(
@Req() req: AuthenticatedRequest,
@Body() dto: ReorderDashboardsDto,
) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.reorderDashboards(userId, tenantId, dto);
}
@Patch('tabs/:id')
async renameDashboard(
@Param('id') id: string,
@Req() req: AuthenticatedRequest,
@Body() dto: RenameDashboardDto,
) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.renameDashboard(id, userId, tenantId, dto);
}
@Delete('tabs/:id')
async deleteDashboard(@Param('id') id: string, @Req() req: AuthenticatedRequest) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.deleteDashboard(id, userId, tenantId);
}
@Get('layout')
async getLayout(
@Req() req: AuthenticatedRequest,