115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
} |