Initial commit: Kalendartool (Next.js, Prisma, Docker)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kühn
2026-10-08 14:38:04 +02:00
commit bc49c3074e
94 changed files with 12812 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
/**
* Admin: Kalenderverwaltung (plan.md Abschnitt 13: /admin/calendars/[id]).
* Bearbeiten, deaktivieren, Invite-Links erstellen, Benutzer einsehen.
*/
import { redirect } from 'next/navigation';
import { prisma } from '@/lib/db/client';
import { requireCalendarContext } from '@/lib/api/context';
import { canManageCalendar } from '@/lib/permissions/permissions';
import { displayName } from '@/lib/privacy/display';
import { Card } from '@/components/ui/ui';
import { EditCalendarForm } from '@/components/admin/EditCalendarForm';
import { InviteManager } from '@/components/admin/InviteManager';
import { AppShell } from '@/components/layout/AppShell';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function AdminCalendarPage({ params }: PageProps) {
const { id } = await params;
const context = await requireCalendarContext(id).catch(() => null);
if (!context) {
redirect('/login');
}
if (!canManageCalendar(context.user, context.calendar)) {
redirect('/dashboard');
}
const [members, invites, reservations] = await Promise.all([
prisma.calendarMember.findMany({
where: { calendarId: id },
select: { user: { select: { email: true, username: true, createdAt: true } } },
}),
prisma.invite.findMany({
where: { calendarId: id },
orderBy: { createdAt: 'desc' },
select: { id: true, expiresAt: true, usedAt: true, createdAt: true },
}),
prisma.reservation.findMany({
where: { calendarId: id, status: 'CONFIRMED' },
orderBy: { startAt: 'desc' },
take: 50,
select: {
id: true,
title: true,
startAt: true,
endAt: true,
user: { select: { email: true, username: true } },
},
}),
]);
return (
<AppShell role={context.user.role} userEmail={context.user.email} username={context.user.username}>
<h1 className="text-2xl font-bold">{context.calendar.title} verwalten</h1>
<EditCalendarForm
calendarId={id}
initialTitle={context.calendar.title}
initialDescription={context.calendar.description ?? ''}
initialTimezone={context.calendar.timezone}
initialIsActive={context.calendar.isActive}
/>
<InviteManager calendarId={id} invites={invites} />
<Card>
<h2 className="mb-3 text-lg font-semibold">Mitglieder ({members.length})</h2>
{members.length === 0 ? (
<p className="text-sm text-slate-500">
Noch keine Mitglieder. Erstelle einen Invite-Link.
</p>
) : (
<ul className="space-y-1 text-sm">
{members.map((member) => (
<li key={member.user.email}>
{displayName(member.user.username, member.user.email)}
{member.user.username && (
<span className="ml-2 text-xs text-slate-500">
{member.user.email}
</span>
)}
</li>
))}
</ul>
)}
</Card>
<Card>
<h2 className="mb-3 text-lg font-semibold">Letzte Reservierungen</h2>
{reservations.length === 0 ? (
<p className="text-sm text-slate-500">Noch keine Reservierungen.</p>
) : (
<ul className="space-y-2 text-sm">
{reservations.map((reservation) => (
<li key={reservation.id} className="rounded bg-slate-100 px-3 py-2">
<span className="font-medium">
{reservation.startAt.toLocaleString('de-DE')} –{' '}
{reservation.endAt.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
})}
</span>{' '}
{reservation.title}
<span className="ml-2 text-xs text-slate-500">
{displayName(reservation.user.username, reservation.user.email)}
</span>
</li>
))}
</ul>
)}
</Card>
</AppShell>
);
}

View File

@@ -0,0 +1,9 @@
/**
* Legacy-Route: Das Erstellen und Verwalten von Kalendern befindet
* sich jetzt unter /admin/calendars (Cards-Ansicht).
*/
import { redirect } from 'next/navigation';
export default function NewCalendarPage() {
redirect('/admin/calendars');
}

View File

@@ -0,0 +1,58 @@
/**
* Admin: Kalenderverwaltung (Cards-Ansicht).
* Zeigt alle Kalender als Karten - eigene und die anderer Benutzer.
* Verwaltung direkt im Bearbeiten-Modal oder auf der Detailseite
* /admin/calendars/[id] (Invites, Mitglieder, Reservierungen).
*/
import { redirect } from 'next/navigation';
import { prisma } from '@/lib/db/client';
import { requireDbUser } from '@/lib/api/context';
import { AppShell } from '@/components/layout/AppShell';
import { CalendarManager } from '@/components/admin/CalendarManager';
export default async function ManageCalendarsPage() {
const user = await requireDbUser().catch(() => null);
if (!user || user.role !== 'ADMIN') {
redirect('/dashboard');
}
const calendars = await prisma.calendar.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true,
title: true,
description: true,
timezone: true,
isActive: true,
maxReservationMinutes: true,
showTitleToMembers: true,
showNotesToMembers: true,
ownerId: true,
owner: { select: { email: true, username: true } },
_count: { select: { members: true, reservations: true } },
},
});
return (
<AppShell role={user.role} userEmail={user.email} username={user.username}>
<CalendarManager
currentUserId={user.id}
isAdmin={user.role === 'ADMIN'}
initialCalendars={calendars.map((calendar) => ({
id: calendar.id,
title: calendar.title,
description: calendar.description,
timezone: calendar.timezone,
isActive: calendar.isActive,
ownerEmail: calendar.owner.username ?? calendar.owner.email,
isOwn: calendar.ownerId === user.id,
memberCount: calendar._count.members,
reservationCount: calendar._count.reservations,
maxReservationMinutes: calendar.maxReservationMinutes,
showTitleToMembers: calendar.showTitleToMembers,
showNotesToMembers: calendar.showNotesToMembers,
}))}
/>
</AppShell>
);
}