Extract nav tabs, detail modal, table row, stats grid, search/filter, records table, pagination, and data-loading hook into _components/ and _hooks/. page.tsx reduced from 833 to 114 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
172 lines
5.1 KiB
TypeScript
172 lines
5.1 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import {
|
|
ConsentRecord,
|
|
ConsentStatus,
|
|
ConsentType,
|
|
HistoryAction,
|
|
PAGE_SIZE,
|
|
} from '../_types'
|
|
|
|
interface GlobalStats {
|
|
total: number
|
|
active: number
|
|
revoked: number
|
|
}
|
|
|
|
export function useConsents() {
|
|
const [records, setRecords] = useState<ConsentRecord[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [currentPage, setCurrentPage] = useState(1)
|
|
const [totalRecords, setTotalRecords] = useState(0)
|
|
const [globalStats, setGlobalStats] = useState<GlobalStats>({ total: 0, active: 0, revoked: 0 })
|
|
|
|
const loadConsents = useCallback(async (page: number) => {
|
|
setIsLoading(true)
|
|
try {
|
|
const offset = (page - 1) * PAGE_SIZE
|
|
const listResponse = await fetch(
|
|
`/api/sdk/v1/einwilligungen/consent?limit=${PAGE_SIZE}&offset=${offset}`
|
|
)
|
|
if (listResponse.ok) {
|
|
const listData = await listResponse.json()
|
|
setTotalRecords(listData.total ?? 0)
|
|
if (listData.consents?.length > 0) {
|
|
const mapped: ConsentRecord[] = listData.consents.map((c: {
|
|
id: string
|
|
user_id: string
|
|
data_point_id: string
|
|
granted: boolean
|
|
granted_at: string
|
|
revoked_at?: string
|
|
consent_version?: string
|
|
source?: string
|
|
ip_address?: string
|
|
user_agent?: string
|
|
history?: Array<{
|
|
id: string
|
|
action: string
|
|
created_at: string
|
|
consent_version?: string
|
|
ip_address?: string
|
|
user_agent?: string
|
|
source?: string
|
|
}>
|
|
}) => ({
|
|
id: c.id,
|
|
identifier: c.user_id,
|
|
email: c.user_id,
|
|
consentType: (c.data_point_id as ConsentType) || 'privacy',
|
|
status: (c.revoked_at ? 'withdrawn' : 'granted') as ConsentStatus,
|
|
currentVersion: c.consent_version || '1.0',
|
|
grantedAt: c.granted_at ? new Date(c.granted_at) : null,
|
|
withdrawnAt: c.revoked_at ? new Date(c.revoked_at) : null,
|
|
source: c.source ?? null,
|
|
ipAddress: c.ip_address ?? '',
|
|
userAgent: c.user_agent ?? '',
|
|
history: (c.history ?? []).map(h => ({
|
|
id: h.id,
|
|
action: h.action as HistoryAction,
|
|
timestamp: new Date(h.created_at),
|
|
version: h.consent_version || '1.0',
|
|
ipAddress: h.ip_address ?? '',
|
|
userAgent: h.user_agent ?? '',
|
|
source: h.source ?? '',
|
|
})),
|
|
}))
|
|
setRecords(mapped)
|
|
} else {
|
|
setRecords([])
|
|
}
|
|
}
|
|
} catch {
|
|
// Backend nicht erreichbar, leere Liste anzeigen
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
const loadStats = useCallback(async () => {
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/einwilligungen/consent?stats=true')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const s = data.statistics
|
|
if (s) {
|
|
setGlobalStats({
|
|
total: s.total_consents ?? 0,
|
|
active: s.active_consents ?? 0,
|
|
revoked: s.revoked_consents ?? 0,
|
|
})
|
|
setTotalRecords(s.total_consents ?? 0)
|
|
}
|
|
}
|
|
} catch {
|
|
// Statistiken nicht erreichbar — lokale Werte behalten
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
loadStats()
|
|
}, [loadStats])
|
|
|
|
useEffect(() => { loadConsents(currentPage) }, [currentPage, loadConsents])
|
|
|
|
const handleRevoke = useCallback(async (recordId: string) => {
|
|
try {
|
|
const response = await fetch('/api/sdk/v1/einwilligungen/consent', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ consentId: recordId, action: 'revoke' }),
|
|
})
|
|
if (response.ok) {
|
|
const now = new Date()
|
|
setRecords(prev => prev.map(r => {
|
|
if (r.id === recordId) {
|
|
return {
|
|
...r,
|
|
status: 'withdrawn' as ConsentStatus,
|
|
withdrawnAt: now,
|
|
history: [
|
|
...r.history,
|
|
{
|
|
id: `h-${recordId}-${r.history.length + 1}`,
|
|
action: 'withdrawn' as HistoryAction,
|
|
timestamp: now,
|
|
version: r.currentVersion,
|
|
ipAddress: 'Admin-Portal',
|
|
userAgent: 'Admin Action',
|
|
source: 'Manueller Widerruf durch Admin',
|
|
notes: 'Widerruf über Admin-Portal durchgeführt',
|
|
},
|
|
],
|
|
}
|
|
}
|
|
return r
|
|
}))
|
|
loadStats()
|
|
}
|
|
} catch {
|
|
// Fallback: update local state even if API call fails
|
|
const now = new Date()
|
|
setRecords(prev => prev.map(r => {
|
|
if (r.id === recordId) {
|
|
return { ...r, status: 'withdrawn' as ConsentStatus, withdrawnAt: now }
|
|
}
|
|
return r
|
|
}))
|
|
}
|
|
}, [loadStats])
|
|
|
|
return {
|
|
records,
|
|
isLoading,
|
|
currentPage,
|
|
setCurrentPage,
|
|
totalRecords,
|
|
globalStats,
|
|
handleRevoke,
|
|
}
|
|
}
|