feat(cmp): unified consent view — Website-Besucher + Login-Nutzer tabs

Merges two separate consent views into one unified page at /sdk/einwilligungen:
- Tab "Website-Besucher": device-based banner consents with site selector
- Tab "Login-Nutzer": user-based DSGVO consents (existing, unchanged)

Backend:
- New endpoint GET /admin/consents for paginated banner consent records
- Fix: categories JSON string parsing (was iterating chars instead of array)

CMP Dashboard:
- Dynamic site selector replacing hardcoded "preview-test-site"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-10 22:41:56 +02:00
parent 9c0d471277
commit bdbc30e47b
7 changed files with 478 additions and 52 deletions
@@ -2,7 +2,7 @@
import { useState } from 'react'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { History } from 'lucide-react'
import { History, Globe, User } from 'lucide-react'
import { ConsentRecord } from './_types'
import { useConsents } from './_hooks/useConsents'
@@ -12,8 +12,13 @@ import { SearchAndFilter } from './_components/SearchAndFilter'
import { RecordsTable } from './_components/RecordsTable'
import { Pagination } from './_components/Pagination'
import { ConsentDetailModal } from './_components/ConsentDetailModal'
import BannerConsentsTab from './_components/BannerConsentsTab'
type ConsentTab = 'visitors' | 'users'
export default function EinwilligungenPage() {
const [activeTab, setActiveTab] = useState<ConsentTab>('visitors')
const {
records,
currentPage,
@@ -63,51 +68,84 @@ export default function EinwilligungenPage() {
{/* Navigation Tabs */}
<EinwilligungenNavTabs />
{/* Stats */}
<StatsGrid
total={globalStats.total}
active={globalStats.active}
revoked={globalStats.revoked}
versionUpdates={versionUpdates}
/>
{/* Info Banner */}
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl border border-purple-200 p-4 flex items-start gap-3">
<History className="w-5 h-5 text-purple-600 mt-0.5" />
<div>
<div className="font-medium text-purple-900">Consent-Historie aktiviert</div>
<div className="text-sm text-purple-700">
Alle Änderungen an Einwilligungen werden protokolliert, inkl. Zustimmungen zu neuen Versionen von AGB, DSI und anderen Dokumenten.
Klicken Sie auf "Details" um die vollständige Historie eines Nutzers einzusehen.
</div>
</div>
{/* Consent Type Tabs: Website-Besucher / Login-Nutzer */}
<div className="flex gap-1 p-1 bg-gray-100 rounded-xl w-fit">
<button
onClick={() => setActiveTab('visitors')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'visitors'
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<Globe className="w-4 h-4" />
Website-Besucher
</button>
<button
onClick={() => setActiveTab('users')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'users'
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<User className="w-4 h-4" />
Login-Nutzer
</button>
</div>
{/* Search and Filter */}
<SearchAndFilter
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
filter={filter}
onFilterChange={setFilter}
/>
{/* Tab Content */}
{activeTab === 'visitors' ? (
<BannerConsentsTab />
) : (
<>
{/* Stats */}
<StatsGrid
total={globalStats.total}
active={globalStats.active}
revoked={globalStats.revoked}
versionUpdates={versionUpdates}
/>
{/* Records Table */}
<RecordsTable records={filteredRecords} onShowDetails={setSelectedRecord} />
{/* Info Banner */}
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl border border-purple-200 p-4 flex items-start gap-3">
<History className="w-5 h-5 text-purple-600 mt-0.5" />
<div>
<div className="font-medium text-purple-900">Consent-Historie aktiviert</div>
<div className="text-sm text-purple-700">
Alle Änderungen an Einwilligungen werden protokolliert, inkl. Zustimmungen zu neuen Versionen von AGB, DSI und anderen Dokumenten.
Klicken Sie auf &quot;Details&quot; um die vollständige Historie eines Nutzers einzusehen.
</div>
</div>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalRecords={totalRecords}
onPageChange={setCurrentPage}
/>
{/* Search and Filter */}
<SearchAndFilter
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
filter={filter}
onFilterChange={setFilter}
/>
{/* Detail Modal */}
{selectedRecord && (
<ConsentDetailModal
record={selectedRecord}
onClose={() => setSelectedRecord(null)}
onRevoke={handleRevoke}
/>
{/* Records Table */}
<RecordsTable records={filteredRecords} onShowDetails={setSelectedRecord} />
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalRecords={totalRecords}
onPageChange={setCurrentPage}
/>
{/* Detail Modal */}
{selectedRecord && (
<ConsentDetailModal
record={selectedRecord}
onClose={() => setSelectedRecord(null)}
onRevoke={handleRevoke}
/>
)}
</>
)}
</div>
)