From ff9f5e849cff691d7276206b949ba79595bc97d3 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:55:49 +0200 Subject: [PATCH] refactor(admin): split academy page.tsx into colocated components Split 1257-LOC client page into _types.ts plus nine components under _components/ (TabNavigation/StatCard/FilterBar in shared, CourseCard, EnrollmentCard, CertificatesTab, EnrollmentEditModal, CourseEditModal, SettingsTab, and PageSections for header actions and empty states). Behavior preserved exactly; page.tsx is now a thin wiring shell. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../academy/_components/CertificatesTab.tsx | 139 +++ .../sdk/academy/_components/CourseCard.tsx | 85 ++ .../academy/_components/CourseEditModal.tsx | 129 +++ .../academy/_components/EnrollmentCard.tsx | 133 +++ .../_components/EnrollmentEditModal.tsx | 70 ++ .../sdk/academy/_components/PageSections.tsx | 224 ++++ .../sdk/academy/_components/SettingsTab.tsx | 110 ++ .../app/sdk/academy/_components/shared.tsx | 168 +++ admin-compliance/app/sdk/academy/_types.ts | 12 + admin-compliance/app/sdk/academy/page.tsx | 982 +----------------- 10 files changed, 1102 insertions(+), 950 deletions(-) create mode 100644 admin-compliance/app/sdk/academy/_components/CertificatesTab.tsx create mode 100644 admin-compliance/app/sdk/academy/_components/CourseCard.tsx create mode 100644 admin-compliance/app/sdk/academy/_components/CourseEditModal.tsx create mode 100644 admin-compliance/app/sdk/academy/_components/EnrollmentCard.tsx create mode 100644 admin-compliance/app/sdk/academy/_components/EnrollmentEditModal.tsx create mode 100644 admin-compliance/app/sdk/academy/_components/PageSections.tsx create mode 100644 admin-compliance/app/sdk/academy/_components/SettingsTab.tsx create mode 100644 admin-compliance/app/sdk/academy/_components/shared.tsx create mode 100644 admin-compliance/app/sdk/academy/_types.ts diff --git a/admin-compliance/app/sdk/academy/_components/CertificatesTab.tsx b/admin-compliance/app/sdk/academy/_components/CertificatesTab.tsx new file mode 100644 index 0000000..ba462f1 --- /dev/null +++ b/admin-compliance/app/sdk/academy/_components/CertificatesTab.tsx @@ -0,0 +1,139 @@ +'use client' + +import React from 'react' +import type { Certificate } from '@/lib/sdk/academy/types' + +// ============================================================================= +// CERTIFICATE ROW +// ============================================================================= + +function CertificateRow({ cert }: { cert: Certificate }) { + const now = new Date() + const validUntil = new Date(cert.validUntil) + const daysLeft = Math.ceil((validUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) + const isExpired = daysLeft <= 0 + const isExpiringSoon = daysLeft > 0 && daysLeft <= 30 + + return ( + + {cert.userName} + {cert.courseName} + {new Date(cert.issuedAt).toLocaleDateString('de-DE')} + {validUntil.toLocaleDateString('de-DE')} + + {isExpired ? ( + Abgelaufen + ) : isExpiringSoon ? ( + Laeuft bald ab + ) : ( + Gueltig + )} + + + {cert.pdfUrl ? ( + + PDF Download + + ) : ( + + Nicht verfuegbar + + )} + + + ) +} + +// ============================================================================= +// CERTIFICATES TAB +// ============================================================================= + +export function CertificatesTab({ + certificates, + certSearch, + onSearchChange +}: { + certificates: Certificate[] + certSearch: string + onSearchChange: (s: string) => void +}) { + const now = new Date() + const total = certificates.length + const valid = certificates.filter(c => new Date(c.validUntil) > now).length + const expired = certificates.filter(c => new Date(c.validUntil) <= now).length + + return ( +
+ {/* Stats */} +
+
+
{total}
+
Gesamt
+
+
+
{valid}
+
Gueltig
+
+
+
{expired}
+
Abgelaufen
+
+
+ + {/* Search */} +
+ onSearchChange(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + /> +
+ + {/* Table */} + {certificates.length === 0 ? ( +
+
+ + + +
+

Noch keine Zertifikate ausgestellt

+

Zertifikate werden automatisch nach Kursabschluss generiert.

+
+ ) : ( +
+ + + + + + + + + + + + + {certificates + .filter(c => + !certSearch || + c.userName.toLowerCase().includes(certSearch.toLowerCase()) || + c.courseName.toLowerCase().includes(certSearch.toLowerCase()) + ) + .map(cert => ) + } + +
MitarbeiterKursAusgestellt amGueltig bisStatusAktionen
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/academy/_components/CourseCard.tsx b/admin-compliance/app/sdk/academy/_components/CourseCard.tsx new file mode 100644 index 0000000..ac28a77 --- /dev/null +++ b/admin-compliance/app/sdk/academy/_components/CourseCard.tsx @@ -0,0 +1,85 @@ +'use client' + +import React from 'react' +import Link from 'next/link' +import { Course, COURSE_CATEGORY_INFO } from '@/lib/sdk/academy/types' + +export function CourseCard({ course, enrollmentCount, onEdit }: { course: Course; enrollmentCount: number; onEdit?: (course: Course) => void }) { + const categoryInfo = COURSE_CATEGORY_INFO[course.category] || COURSE_CATEGORY_INFO['custom'] + + return ( +
+ +
+
+
+
+ + {categoryInfo.label} + + {course.status === 'published' && ( + Veroeffentlicht + )} +
+

{course.title}

+

{course.description}

+
+ + + + + {course.lessons.length} Lektionen + + + + + + {course.durationMinutes} Min. + + + + + + {enrollmentCount} Teilnehmer + + + + + + Bestehensgrenze: {course.passingScore}% + +
+
+
+
+ {course.requiredForRoles.includes('all') ? 'Pflicht fuer alle' : `${course.requiredForRoles.length} Rollen`} +
+
+ {new Date(course.updatedAt).toLocaleDateString('de-DE')} +
+
+
+
+
+ Erstellt: {new Date(course.createdAt).toLocaleDateString('de-DE')} +
+ + Details + +
+
+ + {onEdit && ( + + )} +
+ ) +} diff --git a/admin-compliance/app/sdk/academy/_components/CourseEditModal.tsx b/admin-compliance/app/sdk/academy/_components/CourseEditModal.tsx new file mode 100644 index 0000000..e3a5e3e --- /dev/null +++ b/admin-compliance/app/sdk/academy/_components/CourseEditModal.tsx @@ -0,0 +1,129 @@ +'use client' + +import React, { useState } from 'react' +import { Course, CourseCategory, COURSE_CATEGORY_INFO } from '@/lib/sdk/academy/types' +import { updateCourse } from '@/lib/sdk/academy/api' + +export function CourseEditModal({ course, onClose, onSaved }: { + course: Course + onClose: () => void + onSaved: () => void +}) { + const [title, setTitle] = useState(course.title) + const [description, setDescription] = useState(course.description) + const [category, setCategory] = useState(course.category) + const [durationMinutes, setDurationMinutes] = useState(course.durationMinutes) + const [passingScore, setPassingScore] = useState(course.passingScore ?? 70) + const [status, setStatus] = useState<'draft' | 'published'>(course.status ?? 'draft') + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + const handleSave = async () => { + setSaving(true) + setError(null) + try { + await updateCourse(course.id, { title, description, category, durationMinutes, passingScore, status }) + onSaved() + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Speichern') + } finally { + setSaving(false) + } + } + + return ( +
+
+
+

Kurs bearbeiten

+ +
+
+
+ + setTitle(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + /> +
+
+ +