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:
@@ -0,0 +1,181 @@
|
||||
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 }),
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user