Initial commit: Kalendartool (Next.js, Prisma, Docker)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
115
app/admin/calendars/[id]/page.tsx
Normal file
115
app/admin/calendars/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
app/admin/calendars/new/page.tsx
Normal file
9
app/admin/calendars/new/page.tsx
Normal 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');
|
||||
}
|
||||
58
app/admin/calendars/page.tsx
Normal file
58
app/admin/calendars/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
app/admin/users/page.tsx
Normal file
25
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Admin: Benutzerverwaltung (Option B).
|
||||
* Nur ADMIN-Rolle hat Zugriff; USER werden zurueck zum Dashboard geleitet.
|
||||
*/
|
||||
import { redirect } from 'next/navigation';
|
||||
import { requireDbUser } from '@/lib/api/context';
|
||||
import { UserManagement } from '@/components/admin/UserManagement';
|
||||
import { AppShell } from '@/components/layout/AppShell';
|
||||
|
||||
export default async function AdminUsersPage() {
|
||||
const user = await requireDbUser().catch(() => null);
|
||||
if (!user) {
|
||||
redirect('/login');
|
||||
}
|
||||
if (user.role !== 'ADMIN') {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell role={user.role} userEmail={user.email} username={user.username}>
|
||||
<h1 className="mb-6 text-2xl font-bold">Benutzerverwaltung</h1>
|
||||
<UserManagement currentUserId={user.id} />
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user