Files
cannamanage/cannamanage-frontend/src/services/compliance-dashboard.ts
T
Patrick Plate 57f418f7c9 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.
2026-06-15 14:12:01 +02:00

182 lines
4.4 KiB
TypeScript

import { apiClient } from "@/lib/api-client"
export interface ComplianceStatus {
KCANG: "GREEN" | "YELLOW" | "RED"
FINANCE: "GREEN" | "YELLOW" | "RED"
DSGVO: "GREEN" | "YELLOW" | "RED"
VEREIN: "GREEN" | "YELLOW" | "RED"
}
export interface ComplianceDeadline {
id: string
clubId: string
area: "KCANG" | "FINANCE" | "DSGVO" | "VEREIN"
title: string
description: string | null
dueDate: string
isRecurring: boolean
recurrenceRule: string | null
completedAt: string | null
completedBy: string | null
}
export interface ComplianceDashboardResponse {
status: ComplianceStatus
upcomingDeadlines: ComplianceDeadline[]
overdueDeadlines: ComplianceDeadline[]
}
export interface RetentionReport {
totalAnonymized: number
upcomingAnonymizations: number
currentCutoffDate: string
retentionSchedule: RetentionScheduleItem[]
}
export interface RetentionScheduleItem {
legalBasis: string
description: string
retentionYears: number
}
export interface RetentionPreview {
affectedCount: number
items: RetentionPreviewItem[]
}
export interface RetentionPreviewItem {
memberId: string
membershipNumber: string
membershipDate: string
reason: string
}
// --- Mock data for development ---
const mockDashboard: ComplianceDashboardResponse = {
status: {
KCANG: "GREEN",
FINANCE: "YELLOW",
DSGVO: "GREEN",
VEREIN: "GREEN",
},
upcomingDeadlines: [
{
id: "d1",
clubId: "c1",
area: "FINANCE",
title: "Kassenprüfung durchführen",
description: "Prüfung der Vereinskasse durch gewählte Kassenprüfer",
dueDate: new Date(Date.now() + 14 * 86400000).toISOString().split("T")[0],
isRecurring: true,
recurrenceRule: "YEARLY",
completedAt: null,
completedBy: null,
},
{
id: "d2",
clubId: "c1",
area: "DSGVO",
title: "VVT aktualisieren",
description:
"Jährliche Überprüfung des Verzeichnisses von Verarbeitungstätigkeiten",
dueDate: new Date(Date.now() + 28 * 86400000).toISOString().split("T")[0],
isRecurring: true,
recurrenceRule: "YEARLY",
completedAt: null,
completedBy: null,
},
],
overdueDeadlines: [
{
id: "d3",
clubId: "c1",
area: "FINANCE",
title: "EÜR erstellen",
description: "Einnahmen-Überschuss-Rechnung für das Vorjahr",
dueDate: new Date(Date.now() - 10 * 86400000).toISOString().split("T")[0],
isRecurring: true,
recurrenceRule: "YEARLY",
completedAt: null,
completedBy: null,
},
],
}
const mockRetentionReport: RetentionReport = {
totalAnonymized: 3,
upcomingAnonymizations: 1,
currentCutoffDate: new Date(Date.now() - 5 * 365 * 86400000)
.toISOString()
.split("T")[0],
retentionSchedule: [
{
legalBasis: "KCanG §24",
description: "Mitgliederdaten nach Austritt",
retentionYears: 5,
},
{
legalBasis: "AO §147",
description: "Finanzdaten (Aufbewahrungspflicht)",
retentionYears: 10,
},
{
legalBasis: "DSGVO",
description: "Kommunikationsdaten (inaktiv)",
retentionYears: 2,
},
],
}
// --- API functions ---
export async function getComplianceDashboard(
upcomingDays = 30
): Promise<ComplianceDashboardResponse> {
if (process.env.NEXT_PUBLIC_USE_MOCK === "true") {
return mockDashboard
}
return apiClient<ComplianceDashboardResponse>(
`/api/v1/compliance/dashboard?upcomingDays=${upcomingDays}`
)
}
export async function getRetentionReport(): Promise<RetentionReport> {
if (process.env.NEXT_PUBLIC_USE_MOCK === "true") {
return mockRetentionReport
}
return apiClient<RetentionReport>("/api/v1/compliance/dashboard/retention")
}
export async function previewRetention(): Promise<RetentionPreview> {
if (process.env.NEXT_PUBLIC_USE_MOCK === "true") {
return { affectedCount: 0, items: [] }
}
return apiClient<RetentionPreview>(
"/api/v1/compliance/dashboard/retention/preview",
{
method: "POST",
}
)
}
export async function completeDeadline(
deadlineId: string,
completedBy: string
): Promise<ComplianceDeadline> {
if (process.env.NEXT_PUBLIC_USE_MOCK === "true") {
return {
...mockDashboard.upcomingDeadlines[0],
completedAt: new Date().toISOString(),
completedBy,
}
}
return apiClient<ComplianceDeadline>(
`/api/v1/compliance/deadlines/${deadlineId}/complete`,
{
method: "POST",
body: JSON.stringify({ completedBy }),
}
)
}