feat(sprint9): Phase 6 — Compliance dashboard, RetentionService, testing

Backend:
- ComplianceDashboardService: traffic-light status per ComplianceArea
  (KCANG/FINANCE/DSGVO/VEREIN) based on deadlines, payments, board positions
- RetentionService: scheduled anonymization of expired member data (KCanG §24,
  5 years), with dry-run preview and retention report endpoints
- ComplianceDeadlineSeeder: seeds 5 standard recurring deadlines on club creation
- ComplianceDashboardController: GET /api/v1/compliance/dashboard,
  GET /retention, POST /retention/preview
- Repository additions: countOverdue, countActive board positions/members

Frontend:
- /compliance page with traffic-light status cards per area
- Overdue deadlines section (highlighted red) with 'days overdue' badges
- Upcoming deadlines with 'days until due' badges and 'Complete' buttons
- Retention info cards (KCanG §24: 5y, AO §147: 10y, DSGVO: 2y)
- Navigation: added 'Compliance-Status' to sidebar under Compliance group
- compliance-dashboard.ts service with mock data for dev mode

Build verified: pnpm build passes clean.
This commit is contained in:
Patrick Plate
2026-06-15 14:12:01 +02:00
parent 87511e0485
commit 57f418f7c9
15 changed files with 1273 additions and 3 deletions
@@ -0,0 +1,73 @@
package de.cannamanage.api.controller;
import de.cannamanage.domain.entity.ComplianceDeadline;
import de.cannamanage.domain.entity.TenantContext;
import de.cannamanage.domain.enums.ComplianceArea;
import de.cannamanage.domain.enums.ComplianceStatus;
import de.cannamanage.service.ComplianceDashboardService;
import de.cannamanage.service.RetentionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Compliance Dashboard controller.
* Provides traffic-light compliance status, upcoming/overdue deadlines,
* and retention management endpoints.
*/
@RestController
@RequestMapping("/api/v1/compliance/dashboard")
@RequiredArgsConstructor
@Tag(name = "Compliance Dashboard", description = "Compliance status overview and retention management")
public class ComplianceDashboardController {
private final ComplianceDashboardService dashboardService;
private final RetentionService retentionService;
@GetMapping
@Operation(summary = "Get compliance dashboard status",
description = "Returns traffic-light status per compliance area + upcoming and overdue deadlines")
@PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE)")
public ResponseEntity<ComplianceDashboardResponse> getDashboard(
@RequestParam(defaultValue = "30") int upcomingDays) {
UUID clubId = TenantContext.getCurrentTenant();
Map<ComplianceArea, ComplianceStatus> statusMap = dashboardService.getComplianceStatus(clubId);
List<ComplianceDeadline> upcoming = dashboardService.getUpcomingDeadlines(clubId, upcomingDays);
List<ComplianceDeadline> overdue = dashboardService.getOverdueDeadlines(clubId);
return ResponseEntity.ok(new ComplianceDashboardResponse(statusMap, upcoming, overdue));
}
@GetMapping("/retention")
@Operation(summary = "Get retention report",
description = "Shows what was deleted, what will be deleted, and retention schedule")
@PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)")
public ResponseEntity<RetentionService.RetentionReport> getRetentionReport() {
UUID clubId = TenantContext.getCurrentTenant();
return ResponseEntity.ok(retentionService.getRetentionReport(clubId));
}
@PostMapping("/retention/preview")
@Operation(summary = "Preview retention actions (dry-run)",
description = "Shows what WOULD be affected by retention processing without making changes")
@PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).MANAGE_COMPLIANCE)")
public ResponseEntity<RetentionService.RetentionPreview> previewRetention() {
UUID clubId = TenantContext.getCurrentTenant();
return ResponseEntity.ok(retentionService.previewRetention(clubId));
}
public record ComplianceDashboardResponse(
Map<ComplianceArea, ComplianceStatus> status,
List<ComplianceDeadline> upcomingDeadlines,
List<ComplianceDeadline> overdueDeadlines
) {}
}