Initial commit: Kalendartool (Next.js, Prisma, Docker)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
253
components/admin/CalendarManager.tsx
Normal file
253
components/admin/CalendarManager.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Kalenderverwaltung (Admin): alle Kalender als Karten.
|
||||
*
|
||||
* - Klick auf die Karte oeffnet das Kalender-Modal (Tag/Woche/Monat
|
||||
* mit Reservierungen ansehen/anlegen/loeschen).
|
||||
* - Stift-Icon oeffnet ein Bearbeiten-Modal (PATCH /api/calendars/[id]).
|
||||
* - "Kalender erstellen" oeffnet ein Erstellen-Modal (POST /api/calendars).
|
||||
* - Alle Modals sind per X-Icon, ESC und Hintergrund-Klick schliessbar.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Card, ErrorMessage, Input, Label } from '@/components/ui/ui';
|
||||
import { ModalShell } from '@/components/ui/ModalShell';
|
||||
import { CalendarCard, getCardGradient } from '@/components/calendar/CalendarCard';
|
||||
import { CalendarModal } from '@/components/calendar/CalendarModal';
|
||||
import { EditCalendarModal } from '@/components/admin/EditCalendarModal';
|
||||
|
||||
/** Daten eines Kalenders fuer die Karten-Ansicht (serverseitig geladen). */
|
||||
export interface ManagedCalendar {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
timezone: string;
|
||||
isActive: boolean;
|
||||
ownerEmail: string;
|
||||
isOwn: boolean;
|
||||
memberCount: number;
|
||||
reservationCount: number;
|
||||
/** Maximale Reservierungsdauer in Minuten; 0 = unbegrenzt. */
|
||||
maxReservationMinutes: number;
|
||||
/** Duerven Member die Titel fremder Reservierungen sehen? */
|
||||
showTitleToMembers: boolean;
|
||||
/** Duerven Member die Notizen fremder Reservierungen sehen? */
|
||||
showNotesToMembers: boolean;
|
||||
}
|
||||
|
||||
/** Formularwerte zum Erstellen/Bearbeiten eines Kalenders. */
|
||||
interface CalendarFormValues {
|
||||
title: string;
|
||||
description: string | null;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export function CalendarManager({
|
||||
initialCalendars,
|
||||
currentUserId,
|
||||
isAdmin,
|
||||
}: {
|
||||
initialCalendars: ManagedCalendar[];
|
||||
currentUserId: string;
|
||||
isAdmin: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
/** Aktuell im Bearbeiten-Modal geoeffneter Kalender (null = geschlossen). */
|
||||
const [editingCalendar, setEditingCalendar] = useState<ManagedCalendar | null>(null);
|
||||
/** Aktuell im Kalender-Modal geoeffneter Kalender (null = geschlossen). */
|
||||
const [openCalendar, setOpenCalendar] = useState<ManagedCalendar | null>(null);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/** Erstellt einen neuen Kalender (POST /api/calendars). */
|
||||
async function createCalendar(values: CalendarFormValues) {
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch('/api/calendars', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = (await response.json().catch(() => ({}))) as { error?: string };
|
||||
setError(data.error ?? 'Erstellen fehlgeschlagen.');
|
||||
return;
|
||||
}
|
||||
setIsCreating(false);
|
||||
router.refresh();
|
||||
} catch {
|
||||
setError('Netzwerkfehler. Bitte erneut versuchen.');
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert einen Kalender: wird von EditCalendarModal selbst
|
||||
* ausgefuehrt (PATCH inkl. router.refresh()); hier nur Schliessen.
|
||||
*/
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Kalender verwalten</h1>
|
||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
|
||||
{initialCalendars.length} Kalender · eigene und Kalender anderer Benutzer
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setError(null);
|
||||
setIsCreating(true);
|
||||
}}
|
||||
>
|
||||
+ Kalender erstellen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && !isCreating && !editingCalendar && !openCalendar && (
|
||||
<ErrorMessage message={error} />
|
||||
)}
|
||||
|
||||
{initialCalendars.length === 0 ? (
|
||||
<Card>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Noch keine Kalender vorhanden. Erstelle deinen ersten Kalender.
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<ul className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{initialCalendars.map((calendar, index) => (
|
||||
<li key={calendar.id}>
|
||||
<CalendarCard
|
||||
title={calendar.title}
|
||||
subtitle={calendar.isOwn ? 'Mein Kalender' : calendar.ownerEmail}
|
||||
isActive={calendar.isActive}
|
||||
memberCount={calendar.memberCount}
|
||||
href={`/admin/calendars/${calendar.id}`}
|
||||
linkLabel={`Kalender ${calendar.title} verwalten`}
|
||||
gradient={getCardGradient(index)}
|
||||
iconTitle={`${calendar.memberCount} Mitglieder · ${calendar.reservationCount} Reservierungen`}
|
||||
editLabel={`Kalender ${calendar.title} bearbeiten`}
|
||||
onEdit={() => {
|
||||
setError(null);
|
||||
setEditingCalendar(calendar);
|
||||
}}
|
||||
onOpen={() => {
|
||||
setError(null);
|
||||
setOpenCalendar(calendar);
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{openCalendar && (
|
||||
<CalendarModal
|
||||
key={openCalendar.id}
|
||||
calendarId={openCalendar.id}
|
||||
calendarTitle={openCalendar.title}
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
maxReservationMinutes={openCalendar.maxReservationMinutes}
|
||||
onClose={() => setOpenCalendar(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isCreating && (
|
||||
<CreateCalendarModal
|
||||
isPending={isPending}
|
||||
error={error}
|
||||
onClose={() => {
|
||||
setIsCreating(false);
|
||||
setError(null);
|
||||
}}
|
||||
onCreate={createCalendar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingCalendar && (
|
||||
<EditCalendarModal
|
||||
key={editingCalendar.id}
|
||||
calendar={editingCalendar}
|
||||
onClose={() => {
|
||||
setEditingCalendar(null);
|
||||
setError(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Modal: Neuen Kalender erstellen (POST /api/calendars). */
|
||||
function CreateCalendarModal({
|
||||
isPending,
|
||||
error,
|
||||
onClose,
|
||||
onCreate,
|
||||
}: {
|
||||
isPending: boolean;
|
||||
error: string | null;
|
||||
onClose: () => void;
|
||||
onCreate: (values: CalendarFormValues) => void;
|
||||
}) {
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
onCreate({
|
||||
title: String(formData.get('title') ?? '').trim(),
|
||||
description: String(formData.get('description') ?? '').trim() || null,
|
||||
timezone: String(formData.get('timezone') ?? '').trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
title="Kalender erstellen"
|
||||
ariaLabel="Kalender erstellen"
|
||||
isPending={isPending}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<ErrorMessage message={error} />
|
||||
<div>
|
||||
<Label htmlFor="calendar-title">Titel</Label>
|
||||
<Input id="calendar-title" name="title" type="text" required maxLength={120} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="calendar-description">Beschreibung (optional)</Label>
|
||||
<Input
|
||||
id="calendar-description"
|
||||
name="description"
|
||||
type="text"
|
||||
maxLength={2000}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="calendar-timezone">Zeitzone</Label>
|
||||
<Input
|
||||
id="calendar-timezone"
|
||||
name="timezone"
|
||||
type="text"
|
||||
required
|
||||
defaultValue="Europe/Berlin"
|
||||
maxLength={64}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="secondary" disabled={isPending} onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button disabled={isPending}>{isPending ? 'Erstellen …' : 'Erstellen'}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user