Round 19: DSGVO/GDPR compliance, cookie consent, data export, account deletion, Jugendschutz
- Add Cookie Consent component (TTDSG-compliant, essential + analytics cookies) - Add GDPR Data Export API (DSGVO Art. 20 - data portability) - Add Account Deletion API (DSGVO Art. 17 - right to erasure, 30-day grace period) - Add Datenschutz (Privacy) tab in Settings with: - Privacy policy (data collected, purpose, retention, third-party sharing) - User rights (6 DSGVO rights with descriptions) - Data export button (download all personal data as JSON) - Account deletion with confirmation dialog and grace period - Jugendschutz (youth protection) notices - DPA/AVV section for admin (Auftragsverarbeitungsvertrag) - Add Jugendschutz notice in student login (parental consent for under 16) - Add 65+ i18n keys for DE and EN (DSGVO, privacy, Jugendschutz, DPA) - All German law terms accurate (DSGVO, Jugendschutzgesetz, TTDSG, AVV)
This commit is contained in:
1 parent
849b48a5a4
commit
6306217353
8 files changed
+1430
-2
No files matched your search
@@ -0,0 +1,208 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession, verifyPassword } from '@/lib/auth';
|
||||
|
||||
const requestDeletionSchema = z.object({
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
const GRACE_PERIOD_DAYS = 30;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = requestDeletionSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Validation failed', details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = session.userId;
|
||||
|
||||
// Fetch the user with password hash
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, passwordHash: true, deletedAt: true, role: true, email: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if already scheduled for deletion
|
||||
if (user.deletedAt) {
|
||||
const deletionDate = new Date(user.deletedAt);
|
||||
deletionDate.setDate(deletionDate.getDate() + GRACE_PERIOD_DAYS);
|
||||
return NextResponse.json({
|
||||
error: 'Account already scheduled for deletion',
|
||||
scheduledDeletionDate: deletionDate.toISOString(),
|
||||
}, { status: 409 });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const passwordValid = await verifyPassword(parsed.data.password, user.passwordHash);
|
||||
if (!passwordValid) {
|
||||
return NextResponse.json({ error: 'Wrong password' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Soft delete: set deletedAt to now
|
||||
const now = new Date();
|
||||
const scheduledDeletionDate = new Date(now);
|
||||
scheduledDeletionDate.setDate(scheduledDeletionDate.getDate() + GRACE_PERIOD_DAYS);
|
||||
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: { deletedAt: now },
|
||||
});
|
||||
|
||||
// Create audit log entry
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
schoolId: session.user?.schoolId ?? null,
|
||||
action: 'ACCOUNT_DELETION_REQUESTED',
|
||||
entity: 'User',
|
||||
entityId: userId,
|
||||
details: JSON.stringify({
|
||||
scheduledDeletionDate: scheduledDeletionDate.toISOString(),
|
||||
gracePeriodDays: GRACE_PERIOD_DAYS,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
scheduledDeletionDate: scheduledDeletionDate.toISOString(),
|
||||
gracePeriodDays: GRACE_PERIOD_DAYS,
|
||||
message: `Account scheduled for deletion in ${GRACE_PERIOD_DAYS} days.`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Account deletion error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel deletion (within grace period)
|
||||
export async function PUT() {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = session.userId;
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, deletedAt: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.deletedAt) {
|
||||
return NextResponse.json({
|
||||
error: 'Account is not scheduled for deletion',
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if within grace period
|
||||
const deletionRequestedAt = new Date(user.deletedAt);
|
||||
const gracePeriodEnd = new Date(deletionRequestedAt);
|
||||
gracePeriodEnd.setDate(gracePeriodEnd.getDate() + GRACE_PERIOD_DAYS);
|
||||
|
||||
if (new Date() > gracePeriodEnd) {
|
||||
return NextResponse.json({
|
||||
error: 'Grace period has expired. Account cannot be recovered.',
|
||||
}, { status: 410 });
|
||||
}
|
||||
|
||||
// Cancel deletion: clear deletedAt
|
||||
await db.user.update({
|
||||
where: { id: userId },
|
||||
data: { deletedAt: null },
|
||||
});
|
||||
|
||||
// Create audit log entry
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
schoolId: session.user?.schoolId ?? null,
|
||||
action: 'ACCOUNT_DELETION_CANCELLED',
|
||||
entity: 'User',
|
||||
entityId: userId,
|
||||
details: JSON.stringify({
|
||||
cancelledAt: new Date().toISOString(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Account deletion has been cancelled.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Account deletion cancellation error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check deletion status
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = session.userId;
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, deletedAt: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!user.deletedAt) {
|
||||
return NextResponse.json({
|
||||
scheduledForDeletion: false,
|
||||
});
|
||||
}
|
||||
|
||||
const deletionRequestedAt = new Date(user.deletedAt);
|
||||
const scheduledDeletionDate = new Date(deletionRequestedAt);
|
||||
scheduledDeletionDate.setDate(scheduledDeletionDate.getDate() + GRACE_PERIOD_DAYS);
|
||||
|
||||
return NextResponse.json({
|
||||
scheduledForDeletion: true,
|
||||
deletionRequestedAt: deletionRequestedAt.toISOString(),
|
||||
scheduledDeletionDate: scheduledDeletionDate.toISOString(),
|
||||
gracePeriodDays: GRACE_PERIOD_DAYS,
|
||||
canCancel: new Date() < scheduledDeletionDate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Account deletion status error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = session.userId;
|
||||
|
||||
// Fetch user profile
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
role: true,
|
||||
locale: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
schoolId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Fetch school info
|
||||
const school = user.schoolId
|
||||
? await db.school.findUnique({
|
||||
where: { id: user.schoolId },
|
||||
select: { id: true, name: true, schoolType: true, address: true },
|
||||
})
|
||||
: null;
|
||||
|
||||
// Fetch class groups the user is associated with
|
||||
const classGroupTeachers = await db.classGroupTeacher.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
classGroup: {
|
||||
select: { id: true, name: true, gradeLevel: true, schoolType: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch learning progress entries created by the user
|
||||
const learningProgressEntries = await db.learningProgressEntry.findMany({
|
||||
where: { teacherId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
competencyId: true,
|
||||
level: true,
|
||||
comment: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
take: 500,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch assessments created by the user
|
||||
const assessments = await db.assessment.findMany({
|
||||
where: { teacherId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
type: true,
|
||||
date: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch generated reports
|
||||
const reports = await db.report.findMany({
|
||||
where: { generatedByUserId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch audit log entries
|
||||
const auditLogs = await db.auditLog.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
action: true,
|
||||
entity: true,
|
||||
entityId: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch data export requests
|
||||
const dataExportRequests = await db.dataExportRequest.findMany({
|
||||
where: { requestedByUserId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
scope: true,
|
||||
status: true,
|
||||
requestedAt: true,
|
||||
completedAt: true,
|
||||
},
|
||||
take: 100,
|
||||
orderBy: { requestedAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch teacher notes
|
||||
const teacherNotes = await db.teacherNote.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
category: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch attendance sessions
|
||||
const attendanceSessions = await db.attendanceSession.findMany({
|
||||
where: { teacherId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch notifications
|
||||
const notifications = await db.notification.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
title: true,
|
||||
message: true,
|
||||
read: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch notebooks
|
||||
const notebooks = await db.notebook.findMany({
|
||||
where: { ownerId: userId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
take: 100,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Fetch homework
|
||||
const homeworks = await db.homework.findMany({
|
||||
where: { createdById: userId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
dueDate: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 100,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// For students: fetch their own data
|
||||
let studentData = null;
|
||||
if (user.role === 'STUDENT') {
|
||||
const student = await db.student.findFirst({
|
||||
where: { email: user.email, deletedAt: null },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
dateOfBirth: true,
|
||||
gender: true,
|
||||
enrollments: {
|
||||
select: {
|
||||
id: true,
|
||||
classGroup: { select: { name: true, gradeLevel: true } },
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
},
|
||||
},
|
||||
learningProgressEntries: {
|
||||
select: {
|
||||
id: true,
|
||||
level: true,
|
||||
comment: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
assessmentResults: {
|
||||
select: {
|
||||
id: true,
|
||||
score: true,
|
||||
comment: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 200,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
studentData = student;
|
||||
}
|
||||
|
||||
// For parents: fetch linked students
|
||||
let parentLinks = null;
|
||||
if (user.role === 'PARENT') {
|
||||
parentLinks = await db.parentStudentLink.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
relationship: true,
|
||||
student: {
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the export data
|
||||
const exportData: Record<string, unknown> = {
|
||||
exportMetadata: {
|
||||
exportDate: new Date().toISOString(),
|
||||
platform: 'CompetenceTrack',
|
||||
legalBasis: 'DSGVO Art. 20 - Recht auf Datenübertragbarkeit',
|
||||
userId: user.id,
|
||||
},
|
||||
userProfile: user,
|
||||
school,
|
||||
classAssociations: classGroupTeachers.map((cgt) => ({
|
||||
role: cgt.role,
|
||||
classGroup: cgt.classGroup,
|
||||
})),
|
||||
learningProgress: learningProgressEntries,
|
||||
assessments,
|
||||
reports,
|
||||
auditLogs,
|
||||
dataExportRequests,
|
||||
teacherNotes,
|
||||
attendanceSessions,
|
||||
notifications,
|
||||
notebooks,
|
||||
homeworks,
|
||||
};
|
||||
|
||||
if (studentData) {
|
||||
exportData.studentData = studentData;
|
||||
}
|
||||
|
||||
if (parentLinks) {
|
||||
exportData.parentLinks = parentLinks;
|
||||
}
|
||||
|
||||
return NextResponse.json(exportData, {
|
||||
headers: {
|
||||
'Content-Disposition': `attachment; filename="competencetrack-data-export-${new Date().toISOString().split('T')[0]}.json"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('GDPR data export error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -5,6 +5,7 @@ import { useAppStore } from '@/lib/store';
|
||||
import { fetchCurrentUser } from '@/lib/api';
|
||||
import AuthView from '@/components/auth-view';
|
||||
import AppLayout from '@/components/app-layout';
|
||||
import CookieConsent from '@/components/cookie-consent';
|
||||
|
||||
export default function Home() {
|
||||
const currentUser = useAppStore((s) => s.currentUser);
|
||||
@@ -48,8 +49,18 @@ export default function Home() {
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return <AuthView />;
|
||||
return (
|
||||
<>
|
||||
<AuthView />
|
||||
<CookieConsent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <AppLayout />;
|
||||
return (
|
||||
<>
|
||||
<AppLayout />
|
||||
<CookieConsent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -464,6 +464,13 @@ export default function AuthView() {
|
||||
{t('auth.create_student_account')} — {t('auth.student_login_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{/* Jugendschutz notice for students */}
|
||||
<div className="flex items-center justify-center gap-2 px-4 py-3 rounded-lg bg-emerald-50/50 dark:bg-emerald-900/10 border border-emerald-200/30 dark:border-emerald-900/20 mt-2">
|
||||
<Shield className="h-4 w-4 text-emerald-500" />
|
||||
<p className="text-xs text-emerald-700 dark:text-emerald-300 font-medium">
|
||||
{t('dsgvo.jugendschutz_login_notice')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Cookie, Shield, BarChart3, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
const CONSENT_KEY = 'ct_cookie_consent';
|
||||
|
||||
interface CookieConsentData {
|
||||
essential: boolean;
|
||||
analytics: boolean;
|
||||
acceptedAt: string;
|
||||
}
|
||||
|
||||
function getStoredConsent(): CookieConsentData | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(CONSENT_KEY);
|
||||
if (stored) return JSON.parse(stored);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function storeConsent(data: CookieConsentData): void {
|
||||
try {
|
||||
localStorage.setItem(CONSENT_KEY, JSON.stringify(data));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function hasAnalyticsConsent(): boolean {
|
||||
const consent = getStoredConsent();
|
||||
return consent?.analytics === true;
|
||||
}
|
||||
|
||||
export function hasCookieConsent(): boolean {
|
||||
return getStoredConsent() !== null;
|
||||
}
|
||||
|
||||
export default function CookieConsent() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [analyticsEnabled, setAnalyticsEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if consent already given
|
||||
const consent = getStoredConsent();
|
||||
if (!consent) {
|
||||
setVisible(true);
|
||||
} else {
|
||||
setAnalyticsEnabled(consent.analytics);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
storeConsent({
|
||||
essential: true,
|
||||
analytics: true,
|
||||
acceptedAt: new Date().toISOString(),
|
||||
});
|
||||
setAnalyticsEnabled(true);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleRejectOptional = () => {
|
||||
storeConsent({
|
||||
essential: true,
|
||||
analytics: false,
|
||||
acceptedAt: new Date().toISOString(),
|
||||
});
|
||||
setAnalyticsEnabled(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleSavePreferences = () => {
|
||||
storeConsent({
|
||||
essential: true,
|
||||
analytics: analyticsEnabled,
|
||||
acceptedAt: new Date().toISOString(),
|
||||
});
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<motion.div
|
||||
initial={{ y: 100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
transition={{ duration: 0.4, ease: 'easeOut' }}
|
||||
className="fixed bottom-0 left-0 right-0 z-50 p-4 md:p-6"
|
||||
>
|
||||
<div className="mx-auto max-w-3xl rounded-2xl bg-white dark:bg-gray-900 border border-emerald-200/50 dark:border-emerald-900/30 shadow-2xl shadow-emerald-200/20 dark:shadow-emerald-900/20 overflow-hidden">
|
||||
<div className="p-5 md:p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 shrink-0">
|
||||
<Cookie className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-bold text-gray-900 dark:text-gray-100">
|
||||
{t('dsgvo.cookie_settings')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{t('dsgvo.cookie_consent')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cookie categories */}
|
||||
<AnimatePresence>
|
||||
{showDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="space-y-3 mb-4">
|
||||
{/* Essential cookies */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5">
|
||||
<Shield className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{t('dsgvo.cookie_essential')}
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={true}
|
||||
disabled
|
||||
className="border-emerald-300 dark:border-emerald-700 data-[state=checked]:bg-emerald-500 data-[state=checked]:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('dsgvo.cookie_essential_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analytics cookies */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5">
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{t('dsgvo.cookie_analytics')}
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={analyticsEnabled}
|
||||
onCheckedChange={(v) => setAnalyticsEnabled(v === true)}
|
||||
className="border-amber-300 dark:border-amber-700 data-[state=checked]:bg-amber-500 data-[state=checked]:border-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('dsgvo.cookie_analytics_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Toggle details */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="flex items-center gap-1 text-xs text-emerald-600 dark:text-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-300 font-medium mb-4 transition-colors min-h-[44px]"
|
||||
>
|
||||
{showDetails ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
|
||||
{showDetails ? t('action.close') : t('dsgvo.cookie_settings')}
|
||||
</button>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
onClick={handleAcceptAll}
|
||||
className="flex-1 min-h-[44px] bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white rounded-xl shadow-md font-semibold"
|
||||
>
|
||||
{t('dsgvo.cookie_accept')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRejectOptional}
|
||||
variant="outline"
|
||||
className="flex-1 min-h-[44px] rounded-xl border-emerald-200 dark:border-emerald-900/30 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 font-semibold"
|
||||
>
|
||||
{t('dsgvo.cookie_reject')}
|
||||
</Button>
|
||||
{showDetails && (
|
||||
<Button
|
||||
onClick={handleSavePreferences}
|
||||
variant="outline"
|
||||
className="flex-1 min-h-[44px] rounded-xl border-amber-200 dark:border-amber-900/30 text-amber-700 dark:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-900/20 font-semibold"
|
||||
>
|
||||
{t('action.save')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -695,6 +695,485 @@ function BadgeManagementTab() {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Privacy / DSGVO Tab Component ─────────────────────────────────────
|
||||
function PrivacyTab({ currentUser }: { currentUser: { id: string; role: string; email: string; firstName: string; lastName: string } | null }) {
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deletePassword, setDeletePassword] = useState('');
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [deletionStatus, setDeletionStatus] = useState<{ scheduledForDeletion: boolean; scheduledDeletionDate?: string; canCancel?: boolean } | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
|
||||
const loadDeletionStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/account-deletion');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setDeletionStatus(data);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadDeletionStatus();
|
||||
}, [loadDeletionStatus]);
|
||||
|
||||
const handleExportData = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const res = await fetch('/api/gdpr-export');
|
||||
if (!res.ok) throw new Error('Export failed');
|
||||
const data = await res.json();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `competencetrack-data-export-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success(t('dsgvo.export_data_success'));
|
||||
} catch {
|
||||
toast.error(t('dsgvo.export_data_error'));
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
if (!deletePassword) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
const res = await fetch('/api/account-deletion', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: deletePassword }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
if (data.error === 'Wrong password') {
|
||||
toast.error(t('dsgvo.delete_wrong_password'));
|
||||
} else {
|
||||
toast.error(data.error || t('dsgvo.delete_error'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
toast.success(t('dsgvo.delete_success'));
|
||||
setDeleteDialogOpen(false);
|
||||
setDeletePassword('');
|
||||
loadDeletionStatus();
|
||||
} catch {
|
||||
toast.error(t('dsgvo.delete_error'));
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelDeletion = async () => {
|
||||
setCancelling(true);
|
||||
try {
|
||||
const res = await fetch('/api/account-deletion', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
toast.error(data.error || t('dsgvo.delete_error'));
|
||||
return;
|
||||
}
|
||||
toast.success(t('dsgvo.delete_cancel_success'));
|
||||
loadDeletionStatus();
|
||||
} catch {
|
||||
toast.error(t('dsgvo.delete_error'));
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isAdmin = currentUser?.role === 'SCHOOL_ADMIN' || currentUser?.role === 'SUPER_ADMIN';
|
||||
|
||||
const userRights = [
|
||||
{ key: 'right_access', icon: Eye, color: 'emerald' },
|
||||
{ key: 'right_rectification', icon: Pencil, color: 'teal' },
|
||||
{ key: 'right_erasure', icon: Trash2, color: 'rose' },
|
||||
{ key: 'right_portability', icon: Download, color: 'amber' },
|
||||
{ key: 'right_restriction', icon: Shield, color: 'violet' },
|
||||
{ key: 'right_objection', icon: AlertTriangle, color: 'orange' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* ── Privacy Policy Card ─────────────────────────────── */}
|
||||
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-emerald-500 overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-emerald-50/50 to-transparent dark:from-emerald-900/10 dark:to-transparent">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400">
|
||||
<Shield className="h-4 w-4" />
|
||||
</div>
|
||||
{t('dsgvo.privacy_policy')}
|
||||
</CardTitle>
|
||||
<CardDescription>{t('dsgvo.data_processing')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{t('dsgvo.privacy_policy_text')}
|
||||
</p>
|
||||
|
||||
{/* Data collected */}
|
||||
<div className="p-4 rounded-xl bg-emerald-50/50 dark:bg-emerald-900/10 border border-emerald-200/30 dark:border-emerald-900/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Database className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
||||
<h4 className="text-sm font-semibold text-emerald-700 dark:text-emerald-300">{t('dsgvo.data_collected')}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('dsgvo.data_collected_list')}</p>
|
||||
</div>
|
||||
|
||||
{/* Purpose */}
|
||||
<div className="p-4 rounded-xl bg-teal-50/50 dark:bg-teal-900/10 border border-teal-200/30 dark:border-teal-900/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Target className="h-4 w-4 text-teal-600 dark:text-teal-400" />
|
||||
<h4 className="text-sm font-semibold text-teal-700 dark:text-teal-300">{t('dsgvo.data_purpose')}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('dsgvo.data_purpose_list')}</p>
|
||||
</div>
|
||||
|
||||
{/* Retention */}
|
||||
<div className="p-4 rounded-xl bg-amber-50/50 dark:bg-amber-900/10 border border-amber-200/30 dark:border-amber-900/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
<h4 className="text-sm font-semibold text-amber-700 dark:text-amber-300">{t('dsgvo.data_retention')}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('dsgvo.data_retention_list')}</p>
|
||||
</div>
|
||||
|
||||
{/* Third party */}
|
||||
<div className="p-4 rounded-xl bg-violet-50/50 dark:bg-violet-900/10 border border-violet-200/30 dark:border-violet-900/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Users className="h-4 w-4 text-violet-600 dark:text-violet-400" />
|
||||
<h4 className="text-sm font-semibold text-violet-700 dark:text-violet-300">{t('dsgvo.data_third_party')}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('dsgvo.data_third_party_list')}</p>
|
||||
</div>
|
||||
|
||||
{/* DPO Contact */}
|
||||
<div className="p-4 rounded-xl bg-gray-50/50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Mail className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t('dsgvo.contact_dpo')}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">datenschutz@competencetrack.org</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── User Rights Card ─────────────────────────────── */}
|
||||
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-teal-500 overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-teal-50/50 to-transparent dark:from-teal-900/10 dark:to-transparent">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-teal-100 dark:bg-teal-900/30 text-teal-600 dark:text-teal-400">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
</div>
|
||||
{t('dsgvo.rights_title')}
|
||||
</CardTitle>
|
||||
<CardDescription>{t('dsgvo.rights_desc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{userRights.map((right) => {
|
||||
const Icon = right.icon;
|
||||
const colorMap: Record<string, { bg: string; text: string; border: string }> = {
|
||||
emerald: { bg: 'bg-emerald-100 dark:bg-emerald-900/30', text: 'text-emerald-600 dark:text-emerald-400', border: 'border-emerald-200/30 dark:border-emerald-900/20' },
|
||||
teal: { bg: 'bg-teal-100 dark:bg-teal-900/30', text: 'text-teal-600 dark:text-teal-400', border: 'border-teal-200/30 dark:border-teal-900/20' },
|
||||
rose: { bg: 'bg-rose-100 dark:bg-rose-900/30', text: 'text-rose-600 dark:text-rose-400', border: 'border-rose-200/30 dark:border-rose-900/20' },
|
||||
amber: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-600 dark:text-amber-400', border: 'border-amber-200/30 dark:border-amber-900/20' },
|
||||
violet: { bg: 'bg-violet-100 dark:bg-violet-900/30', text: 'text-violet-600 dark:text-violet-400', border: 'border-violet-200/30 dark:border-violet-900/20' },
|
||||
orange: { bg: 'bg-orange-100 dark:bg-orange-900/30', text: 'text-orange-600 dark:text-orange-400', border: 'border-orange-200/30 dark:border-orange-900/20' },
|
||||
};
|
||||
const c = colorMap[right.color] || colorMap.emerald;
|
||||
return (
|
||||
<div key={right.key} className={`p-3 rounded-xl ${c.bg} border ${c.border}`}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Icon className={`h-4 w-4 ${c.text}`} />
|
||||
<h5 className={`text-sm font-semibold ${c.text}`}>{t(`dsgvo.${right.key}`)}</h5>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">{t(`dsgvo.${right.key}_desc`)}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── Data Export Card ─────────────────────────────── */}
|
||||
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-amber-500 overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400">
|
||||
<Download className="h-4 w-4" />
|
||||
</div>
|
||||
{t('dsgvo.data_portability')}
|
||||
</CardTitle>
|
||||
<CardDescription>DSGVO Art. 20</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('dsgvo.export_data_desc')}</p>
|
||||
<Button
|
||||
onClick={handleExportData}
|
||||
disabled={exporting}
|
||||
className="min-h-[44px] bg-gradient-to-r from-amber-500 to-emerald-500 hover:from-amber-600 hover:to-emerald-600 text-white rounded-xl shadow-md font-semibold"
|
||||
>
|
||||
{exporting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <FileJson className="h-4 w-4 mr-2" />}
|
||||
{t('dsgvo.export_data')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── Account Deletion Card ─────────────────────────────── */}
|
||||
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-rose-500 overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-rose-50/50 to-transparent dark:from-rose-900/10 dark:to-transparent">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</div>
|
||||
{t('dsgvo.delete_account')}
|
||||
</CardTitle>
|
||||
<CardDescription>DSGVO Art. 17</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('dsgvo.delete_account_desc')}</p>
|
||||
|
||||
{/* Deletion status */}
|
||||
{deletionStatus?.scheduledForDeletion && (
|
||||
<div className="p-4 rounded-xl bg-rose-50/50 dark:bg-rose-900/10 border border-rose-200/30 dark:border-rose-900/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle className="h-4 w-4 text-rose-600 dark:text-rose-400" />
|
||||
<span className="text-sm font-semibold text-rose-700 dark:text-rose-300">
|
||||
{t('dsgvo.delete_scheduled')} {new Date(deletionStatus.scheduledDeletionDate!).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{deletionStatus.canCancel && (
|
||||
<Button
|
||||
onClick={handleCancelDeletion}
|
||||
disabled={cancelling}
|
||||
variant="outline"
|
||||
className="min-h-[44px] rounded-xl border-emerald-200 dark:border-emerald-900/30 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 mt-2"
|
||||
>
|
||||
{cancelling ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <RotateCcw className="h-4 w-4 mr-2" />}
|
||||
{t('dsgvo.delete_cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warning */}
|
||||
<div className="flex items-start gap-2 p-3 rounded-xl bg-rose-50/50 dark:bg-rose-900/10 border border-rose-200/30 dark:border-rose-900/20">
|
||||
<AlertTriangle className="h-4 w-4 text-rose-600 dark:text-rose-400 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-rose-700 dark:text-rose-300">{t('dsgvo.delete_warning')}</p>
|
||||
</div>
|
||||
|
||||
{!deletionStatus?.scheduledForDeletion && (
|
||||
<Button
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
variant="outline"
|
||||
className="min-h-[44px] rounded-xl border-rose-200 dark:border-rose-900/30 text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-900/20 font-semibold"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
{t('dsgvo.delete_account')}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── Jugendschutz Card ─────────────────────────────── */}
|
||||
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-amber-400 overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-amber-50/50 to-transparent dark:from-amber-900/10 dark:to-transparent">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400">
|
||||
<GraduationCap className="h-4 w-4" />
|
||||
</div>
|
||||
{t('dsgvo.jugendschutz')}
|
||||
</CardTitle>
|
||||
<CardDescription>Jugendschutzgesetz</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="p-4 rounded-xl bg-amber-50/50 dark:bg-amber-900/10 border border-amber-200/30 dark:border-amber-900/20">
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300 font-medium">{t('dsgvo.jugendschutz_notice')}</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 shrink-0">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{t('dsgvo.parental_consent')}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t('dsgvo.jugendschutz_notice')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-teal-100 dark:bg-teal-900/30 text-teal-600 dark:text-teal-400 shrink-0">
|
||||
<Eye className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{t('dsgvo.no_advertising')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400 shrink-0">
|
||||
<Shield className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{t('dsgvo.no_tracking')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 shrink-0">
|
||||
<Trophy className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{t('dsgvo.jugendschutz_rewards')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── DPA / AVV Card (Admin only) ─────────────────────── */}
|
||||
{isAdmin && (
|
||||
<Card className="card-hover-lift border-0 shadow-sm rounded-xl border-l-3 border-l-violet-500 overflow-hidden">
|
||||
<CardHeader className="bg-gradient-to-r from-violet-50/50 to-transparent dark:from-violet-900/10 dark:to-transparent">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">
|
||||
<FileText className="h-4 w-4" />
|
||||
</div>
|
||||
{t('dsgvo.dpa')}
|
||||
</CardTitle>
|
||||
<CardDescription>Art. 28 DSGVO</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{t('dsgvo.dpa_template_text')}</p>
|
||||
|
||||
{/* DPA Status */}
|
||||
<div className="p-4 rounded-xl bg-violet-50/50 dark:bg-violet-900/10 border border-violet-200/30 dark:border-violet-900/20">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-semibold text-violet-700 dark:text-violet-300">{t('dsgvo.dpa')}</span>
|
||||
<Badge className="bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 border-amber-200/50 dark:border-amber-900/30">
|
||||
{t('dsgvo.dpa_status_pending')}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-[44px] rounded-xl border-violet-200 dark:border-violet-900/30 text-violet-700 dark:text-violet-300 hover:bg-violet-50 dark:hover:bg-violet-900/20 font-semibold"
|
||||
onClick={() => {
|
||||
// Generate a simple DPA template as a downloadable text file
|
||||
const dpaTemplate = `Auftragsverarbeitungsvertrag (AVV)\ngemäß Art. 28 DSGVO\n\nVerantwortliche Stelle (Schule):\n[Name der Schule]\n[Adresse]\n\nAuftragsverarbeiter:\nCompetenceTrack\n[Adresse]\n\n1. Gegenstand und Dauer der Verarbeitung\nDer Auftragsverarbeiter verarbeitet personenbezogene Daten im Auftrag der verantwortlichen Stelle gemäß den Bestimmungen dieses Vertrages.\n\n2. Art und Zweck der Verarbeitung\n- Bereitstellung und Betrieb der CompetenceTrack-Plattform\n- Verwaltung von Schüler- und Klassendaten\n- Dokumentation von Lernfortschritten\n- Erstellung von Zeugnissen und Berichten\n\n3. Art der personenbezogenen Daten\n- Name, E-Mail-Adresse\n- Schülereinschreibungsdaten\n- Lernfortschrittsdaten\n- Bewertungsdaten\n- Anwesenheitsdaten\n\n4. Betroffene Personenkreise\n- Schülerinnen und Schüler\n- Lehrkräfte\n- Eltern / Erziehungsberechtigte\n\n5. Technisch-organisatorische Maßnahmen\nDer Auftragsverarbeiter implementiert angemessene technisch-organisatorische Maßnahmen gemäß Art. 32 DSGVO.\n\n6. Löschung und Rückgabe von Daten\nNach Beendigung des Auftragsverhältnisses werden alle personenbezogenen Daten gelöscht.\n\n7. Kontrolle durch die verantwortliche Stelle\nDie verantwortliche Stelle hat das Recht, die Einhaltung der Maßnahmen zu kontrollieren.\n\n8. Unterauftragsverhältnisse\nEine Weitergabe an Unterauftragnehmer bedarf der vorherigen Zustimmung der verantwortlichen Stelle.\n\nOrt, Datum: _______________\n\nUnterschrift Verantwortliche Stelle: _______________\nUnterschrift Auftragsverarbeiter: _______________`;
|
||||
const blob = new Blob([dpaTemplate], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'AVV_CompetenceTrack_Vorlage.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{t('dsgvo.dpa_download')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Data Processing Register */}
|
||||
<div className="p-4 rounded-xl bg-gray-50/50 dark:bg-gray-800/50 border border-gray-200/30 dark:border-gray-700/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Database className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t('dsgvo.data_register')}</h4>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">{t('dsgvo.data_register_desc')}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-[44px] rounded-xl border-gray-200 dark:border-gray-700/30 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 font-semibold"
|
||||
onClick={() => {
|
||||
const registerData = {
|
||||
title: 'Verzeichnis von Verarbeitungstätigkeiten',
|
||||
legalBasis: 'Art. 30 DSGVO',
|
||||
controller: '[Name der Schule]',
|
||||
processor: 'CompetenceTrack',
|
||||
processingActivities: [
|
||||
{ name: 'Benutzerverwaltung', purpose: 'Bereitstellung der Plattform', dataCategories: 'Name, E-Mail, Rolle', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||
{ name: 'Schülerverwaltung', purpose: 'Verwaltung von Klassen und Schülern', dataCategories: 'Name, Geburtsdatum, Geschlecht', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||
{ name: 'Lernfortschrittsdokumentation', purpose: 'Dokumentation des Lernfortschritts', dataCategories: 'Kompetenzlevel, Kommentare', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||
{ name: 'Bewertung', purpose: 'Leistungsüberprüfung und Benotung', dataCategories: 'Bewertungsergebnisse, Noten', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||
{ name: 'Anwesenheitserfassung', purpose: 'Dokumentation der Anwesenheit', dataCategories: 'Anwesenheitsstatus, Datum', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||
{ name: 'Berichtserstellung', purpose: 'Erstellung von Zeugnissen und Berichten', dataCategories: 'Berichtsinhalte, Noten', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||
{ name: 'Kommunikation mit Eltern', purpose: 'Elterninformation und Kommunikation', dataCategories: 'Nachrichten, Kontaktdaten', retention: 'Dauer der Nutzung + 30 Tage', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO' },
|
||||
],
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(registerData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'Verzeichnis_Verarbeitungstaetigkeiten.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{t('dsgvo.data_register_download')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ── Delete Account Confirmation Dialog ─────────────────────── */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={(open) => { if (!open) { setDeleteDialogOpen(false); setDeletePassword(''); } }}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-rose-500" />
|
||||
{t('dsgvo.delete_confirm_title')}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>{t('dsgvo.delete_confirm_desc')}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="flex items-start gap-2 p-3 rounded-xl bg-rose-50/50 dark:bg-rose-900/10 border border-rose-200/30 dark:border-rose-900/20">
|
||||
<AlertTriangle className="h-4 w-4 text-rose-600 dark:text-rose-400 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-rose-700 dark:text-rose-300">{t('dsgvo.delete_warning')}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="delete-password" className="text-sm font-medium">{t('dsgvo.delete_password_label')}</Label>
|
||||
<Input
|
||||
id="delete-password"
|
||||
type="password"
|
||||
value={deletePassword}
|
||||
onChange={(e) => setDeletePassword(e.target.value)}
|
||||
className="rounded-xl border-rose-200 dark:border-rose-900/30"
|
||||
placeholder="••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleting}>{t('action.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteAccount}
|
||||
disabled={deleting || !deletePassword}
|
||||
className="bg-rose-600 hover:bg-rose-700 text-white"
|
||||
>
|
||||
{deleting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Trash2 className="h-4 w-4 mr-2" />}
|
||||
{t('dsgvo.delete_account')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsView() {
|
||||
const currentUser = useAppStore((s) => s.currentUser);
|
||||
const [activeTab, setActiveTab] = useState('school');
|
||||
@@ -1859,6 +2338,10 @@ export default function SettingsView() {
|
||||
<Trophy className="h-4 w-4 mr-1.5" />
|
||||
<span className="hidden sm:inline">{t('badges.manage')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="privacy" className="rounded-lg min-h-[44px] data-[state=active]:bg-emerald-600 data-[state=active]:text-white">
|
||||
<Shield className="h-4 w-4 mr-1.5" />
|
||||
<span className="hidden sm:inline">{t('dsgvo.tab_privacy')}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* ── School Info Tab ─────────────────────────────────── */}
|
||||
@@ -4568,6 +5051,11 @@ export default function SettingsView() {
|
||||
<TabsContent value="badges">
|
||||
<BadgeManagementTab />
|
||||
</TabsContent>
|
||||
|
||||
{/* ── Datenschutz / Privacy Tab ─────────────────────────────── */}
|
||||
<TabsContent value="privacy">
|
||||
<PrivacyTab currentUser={currentUser} />
|
||||
</TabsContent>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
+144
@@ -3416,6 +3416,78 @@ const de: TranslationDict = {
|
||||
'grading.ausreichend': 'Ausreichend',
|
||||
'grading.mangelhaft': 'Mangelhaft',
|
||||
'grading.ungenuegend': 'Ungenügend',
|
||||
|
||||
// DSGVO / Datenschutz
|
||||
'dsgvo.title': 'Datenschutz',
|
||||
'dsgvo.privacy_policy': 'Datenschutzerklärung',
|
||||
'dsgvo.data_processing': 'Datenverarbeitung',
|
||||
'dsgvo.data_collected': 'Erhobene Daten',
|
||||
'dsgvo.data_purpose': 'Verarbeitungszweck',
|
||||
'dsgvo.data_retention': 'Speicherdauer',
|
||||
'dsgvo.data_third_party': 'Datenweitergabe an Dritte',
|
||||
'dsgvo.data_portability': 'Datenübertragbarkeit',
|
||||
'dsgvo.right_access': 'Auskunftsrecht',
|
||||
'dsgvo.right_rectification': 'Berichtigungsrecht',
|
||||
'dsgvo.right_erasure': 'Löschrecht',
|
||||
'dsgvo.right_portability': 'Recht auf Datenübertragbarkeit',
|
||||
'dsgvo.right_restriction': 'Recht auf Einschränkung der Verarbeitung',
|
||||
'dsgvo.right_objection': 'Widerspruchsrecht',
|
||||
'dsgvo.contact_dpo': 'Datenschutzbeauftragter',
|
||||
'dsgvo.export_data': 'Meine Daten exportieren',
|
||||
'dsgvo.export_data_desc': 'Exportieren Sie alle Ihre persönlichen Daten im JSON-Format (DSGVO Art. 20)',
|
||||
'dsgvo.export_data_success': 'Ihre Daten wurden erfolgreich exportiert.',
|
||||
'dsgvo.export_data_error': 'Fehler beim Exportieren der Daten.',
|
||||
'dsgvo.delete_account': 'Konto löschen',
|
||||
'dsgvo.delete_account_desc': 'Löschen Sie Ihr Konto und alle zugehörigen Daten (DSGVO Art. 17)',
|
||||
'dsgvo.delete_warning': 'Diese Aktion kann nicht rückgängig gemacht werden. Alle Ihre Daten werden nach 30 Tagen unwiderruflich gelöscht.',
|
||||
'dsgvo.delete_confirm_title': 'Konto wirklich löschen?',
|
||||
'dsgvo.delete_confirm_desc': 'Geben Sie Ihr Passwort ein, um die Löschung zu bestätigen. Ihr Konto wird in 30 Tagen unwiderruflich gelöscht.',
|
||||
'dsgvo.delete_password_label': 'Passwort zur Bestätigung',
|
||||
'dsgvo.delete_success': 'Ihr Konto wurde zur Löschung vorgemerkt. Sie haben 30 Tage Zeit, um den Vorgang abzubrechen.',
|
||||
'dsgvo.delete_error': 'Fehler beim Löschen des Kontos.',
|
||||
'dsgvo.delete_wrong_password': 'Falsches Passwort.',
|
||||
'dsgvo.delete_cancel_success': 'Die Löschung wurde abgebrochen.',
|
||||
'dsgvo.delete_scheduled': 'Löschung geplant am',
|
||||
'dsgvo.delete_cancel': 'Löschung abbrechen',
|
||||
'dsgvo.cookie_consent': 'Wir verwenden Cookies, um Ihnen die bestmögliche Erfahrung zu bieten.',
|
||||
'dsgvo.cookie_essential': 'Essenzielle Cookies',
|
||||
'dsgvo.cookie_essential_desc': 'Diese Cookies sind für die Funktionalität der Plattform erforderlich.',
|
||||
'dsgvo.cookie_analytics': 'Analyse-Cookies',
|
||||
'dsgvo.cookie_analytics_desc': 'Diese Cookies helfen uns, die Plattform zu verbessern.',
|
||||
'dsgvo.cookie_accept': 'Alle akzeptieren',
|
||||
'dsgvo.cookie_reject': 'Nur essenzielle',
|
||||
'dsgvo.cookie_settings': 'Cookie-Einstellungen',
|
||||
'dsgvo.jugendschutz': 'Jugendschutz',
|
||||
'dsgvo.jugendschutz_notice': 'Diese Plattform ist für Schülerinnen und Schüler unter 16 Jahren nur mit Zustimmung der Erziehungsberechtigten nutzbar.',
|
||||
'dsgvo.parental_consent': 'Einverständnis der Erziehungsberechtigten',
|
||||
'dsgvo.no_advertising': 'Keine Werbung für Schüler',
|
||||
'dsgvo.no_tracking': 'Kein Tracking von Schülern unter 16 Jahren',
|
||||
'dsgvo.dpa': 'Auftragsverarbeitungsvertrag (AVV)',
|
||||
'dsgvo.dpa_desc': 'Vertrag zur Auftragsdatenverarbeitung gemäß Art. 28 DSGVO',
|
||||
'dsgvo.data_register': 'Verzeichnis von Verarbeitungstätigkeiten',
|
||||
'dsgvo.data_register_desc': 'Übersicht aller Verarbeitungstätigkeiten gemäß Art. 30 DSGVO',
|
||||
'dsgvo.privacy_policy_text': 'Die nachfolgende Datenschutzerklärung gilt für die Nutzung der Plattform CompetenceTrack. Verantwortlich für die Datenverarbeitung ist die jeweilige Schule.',
|
||||
'dsgvo.data_collected_list': 'Name, E-Mail-Adresse, Passwort (verschlüsselt), Rollenzuordnung, Klassen- und Fächerverknüpfungen, Lernfortschritte, Bewertungen, Anwesenheitsdaten, Notizen und Zeugnisse.',
|
||||
'dsgvo.data_purpose_list': 'Bereitstellung und Betrieb der Plattform, Verwaltung von Schülern und Klassen, Dokumentation von Lernfortschritten, Erstellung von Zeugnissen und Berichten, Kommunikation mit Eltern.',
|
||||
'dsgvo.data_retention_list': 'Nutzerdaten werden für die Dauer der Nutzung gespeichert. Nach Löschung des Kontos werden die Daten nach Ablauf der 30-tägigen Widerspruchsfrist unwiderruflich gelöscht. Auswertungsdaten werden nach 3 Jahren anonymisiert.',
|
||||
'dsgvo.data_third_party_list': 'Es erfolgt keine Weitergabe personenbezogener Daten an Dritte, soweit keine gesetzliche Verpflichtung besteht oder die Datenverarbeitung im Auftrag gemäß Art. 28 DSGVO erfolgt.',
|
||||
'dsgvo.dpa_template_text': 'Der Auftragsverarbeitungsvertrag (AVV) regelt die Datenverarbeitung im Auftrag gemäß Art. 28 DSGVO. Die Schule als Verantwortliche schließt diesen Vertrag mit dem Betreiber der Plattform ab.',
|
||||
'dsgvo.dpa_status_signed': 'Unterzeichnet',
|
||||
'dsgvo.dpa_status_pending': 'Ausstehend',
|
||||
'dsgvo.dpa_sign': 'AVV unterzeichnen',
|
||||
'dsgvo.jugendschutz_login_notice': 'Für Schüler unter 16 Jahren ist die Nutzung nur mit Zustimmung der Erziehungsberechtigten erlaubt.',
|
||||
'dsgvo.jugendschutz_rewards': 'Wettbewerbspreise entsprechen den Anforderungen des Jugendschutzgesetzes.',
|
||||
'dsgvo.tab_privacy': 'Datenschutz',
|
||||
'dsgvo.rights_title': 'Ihre Rechte nach DSGVO',
|
||||
'dsgvo.rights_desc': 'Sie haben gegenüber dem Verantwortlichen folgende Rechte:',
|
||||
'dsgvo.right_access_desc': 'Sie haben das Recht, Auskunft darüber zu verlangen, welche personenbezogenen Daten über Sie verarbeitet werden.',
|
||||
'dsgvo.right_rectification_desc': 'Sie haben das Recht, die Berichtigung unrichtiger personenbezogener Daten zu verlangen.',
|
||||
'dsgvo.right_erasure_desc': 'Sie haben das Recht, die Löschung Ihrer personenbezogenen Daten zu verlangen.',
|
||||
'dsgvo.right_portability_desc': 'Sie haben das Recht, Ihre Daten in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten.',
|
||||
'dsgvo.right_restriction_desc': 'Sie haben das Recht, die Einschränkung der Verarbeitung Ihrer Daten zu verlangen.',
|
||||
'dsgvo.right_objection_desc': 'Sie haben das Recht, aus Gründen der besonderen Situation Widerspruch gegen die Verarbeitung einzulegen.',
|
||||
'dsgvo.dpa_download': 'AVV-Vorlage herunterladen',
|
||||
'dsgvo.data_register_download': 'Verzeichnis herunterladen',
|
||||
};
|
||||
|
||||
const en: TranslationDict = {
|
||||
@@ -6831,6 +6903,78 @@ const en: TranslationDict = {
|
||||
'grading.ausreichend': 'Ausreichend',
|
||||
'grading.mangelhaft': 'Mangelhaft',
|
||||
'grading.ungenuegend': 'Ungenügend',
|
||||
|
||||
// DSGVO / Privacy
|
||||
'dsgvo.title': 'Privacy',
|
||||
'dsgvo.privacy_policy': 'Privacy Policy',
|
||||
'dsgvo.data_processing': 'Data Processing',
|
||||
'dsgvo.data_collected': 'Data Collected',
|
||||
'dsgvo.data_purpose': 'Purpose of Processing',
|
||||
'dsgvo.data_retention': 'Retention Period',
|
||||
'dsgvo.data_third_party': 'Third-Party Data Sharing',
|
||||
'dsgvo.data_portability': 'Data Portability',
|
||||
'dsgvo.right_access': 'Right of Access',
|
||||
'dsgvo.right_rectification': 'Right to Rectification',
|
||||
'dsgvo.right_erasure': 'Right to Erasure',
|
||||
'dsgvo.right_portability': 'Right to Data Portability',
|
||||
'dsgvo.right_restriction': 'Right to Restriction of Processing',
|
||||
'dsgvo.right_objection': 'Right to Object',
|
||||
'dsgvo.contact_dpo': 'Data Protection Officer',
|
||||
'dsgvo.export_data': 'Export My Data',
|
||||
'dsgvo.export_data_desc': 'Export all your personal data in JSON format (GDPR Art. 20)',
|
||||
'dsgvo.export_data_success': 'Your data has been successfully exported.',
|
||||
'dsgvo.export_data_error': 'Error exporting data.',
|
||||
'dsgvo.delete_account': 'Delete Account',
|
||||
'dsgvo.delete_account_desc': 'Delete your account and all associated data (GDPR Art. 17)',
|
||||
'dsgvo.delete_warning': 'This action cannot be undone. All your data will be permanently deleted after 30 days.',
|
||||
'dsgvo.delete_confirm_title': 'Really delete account?',
|
||||
'dsgvo.delete_confirm_desc': 'Enter your password to confirm deletion. Your account will be permanently deleted in 30 days.',
|
||||
'dsgvo.delete_password_label': 'Password to confirm',
|
||||
'dsgvo.delete_success': 'Your account has been scheduled for deletion. You have 30 days to cancel.',
|
||||
'dsgvo.delete_error': 'Error deleting account.',
|
||||
'dsgvo.delete_wrong_password': 'Wrong password.',
|
||||
'dsgvo.delete_cancel_success': 'Deletion has been cancelled.',
|
||||
'dsgvo.delete_scheduled': 'Deletion scheduled on',
|
||||
'dsgvo.delete_cancel': 'Cancel deletion',
|
||||
'dsgvo.cookie_consent': 'We use cookies to provide you with the best possible experience.',
|
||||
'dsgvo.cookie_essential': 'Essential Cookies',
|
||||
'dsgvo.cookie_essential_desc': 'These cookies are required for the platform to function.',
|
||||
'dsgvo.cookie_analytics': 'Analytics Cookies',
|
||||
'dsgvo.cookie_analytics_desc': 'These cookies help us improve the platform.',
|
||||
'dsgvo.cookie_accept': 'Accept All',
|
||||
'dsgvo.cookie_reject': 'Essential Only',
|
||||
'dsgvo.cookie_settings': 'Cookie Settings',
|
||||
'dsgvo.jugendschutz': 'Youth Protection',
|
||||
'dsgvo.jugendschutz_notice': 'This platform may only be used by students under 16 with the consent of their legal guardians.',
|
||||
'dsgvo.parental_consent': 'Parental Consent',
|
||||
'dsgvo.no_advertising': 'No advertising for students',
|
||||
'dsgvo.no_tracking': 'No tracking of students under 16',
|
||||
'dsgvo.dpa': 'Data Processing Agreement (DPA)',
|
||||
'dsgvo.dpa_desc': 'Data processing agreement pursuant to Art. 28 GDPR',
|
||||
'dsgvo.data_register': 'Register of Processing Activities',
|
||||
'dsgvo.data_register_desc': 'Overview of all processing activities pursuant to Art. 30 GDPR',
|
||||
'dsgvo.privacy_policy_text': 'The following privacy policy applies to the use of the CompetenceTrack platform. The respective school is responsible for data processing.',
|
||||
'dsgvo.data_collected_list': 'Name, email address, password (encrypted), role assignment, class and subject associations, learning progress, assessments, attendance data, notes and reports.',
|
||||
'dsgvo.data_purpose_list': 'Provision and operation of the platform, management of students and classes, documentation of learning progress, creation of reports, communication with parents.',
|
||||
'dsgvo.data_retention_list': 'User data is stored for the duration of use. After account deletion, data is permanently deleted after the 30-day objection period. Analytics data is anonymized after 3 years.',
|
||||
'dsgvo.data_third_party_list': 'Personal data is not shared with third parties unless required by law or processed on behalf pursuant to Art. 28 GDPR.',
|
||||
'dsgvo.dpa_template_text': 'The Data Processing Agreement (DPA) regulates data processing on behalf pursuant to Art. 28 GDPR. The school as controller concludes this agreement with the platform operator.',
|
||||
'dsgvo.dpa_status_signed': 'Signed',
|
||||
'dsgvo.dpa_status_pending': 'Pending',
|
||||
'dsgvo.dpa_sign': 'Sign DPA',
|
||||
'dsgvo.jugendschutz_login_notice': 'Students under 16 may only use this platform with the consent of their legal guardians.',
|
||||
'dsgvo.jugendschutz_rewards': 'Competition prizes comply with the Youth Protection Act requirements.',
|
||||
'dsgvo.tab_privacy': 'Privacy',
|
||||
'dsgvo.rights_title': 'Your Rights under GDPR',
|
||||
'dsgvo.rights_desc': 'You have the following rights vis-à-vis the controller:',
|
||||
'dsgvo.right_access_desc': 'You have the right to request information about which personal data is processed about you.',
|
||||
'dsgvo.right_rectification_desc': 'You have the right to request the correction of inaccurate personal data.',
|
||||
'dsgvo.right_erasure_desc': 'You have the right to request the deletion of your personal data.',
|
||||
'dsgvo.right_portability_desc': 'You have the right to receive your data in a structured, commonly used, and machine-readable format.',
|
||||
'dsgvo.right_restriction_desc': 'You have the right to request the restriction of processing of your data.',
|
||||
'dsgvo.right_objection_desc': 'You have the right to object to processing on grounds relating to your particular situation.',
|
||||
'dsgvo.dpa_download': 'Download DPA template',
|
||||
'dsgvo.data_register_download': 'Download register',
|
||||
};
|
||||
|
||||
const translations: Record<string, TranslationDict> = { de, en };
|
||||
|
||||
Reference in new issue
Block a user