254 lines
8.0 KiB
TypeScript
254 lines
8.0 KiB
TypeScript
'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 { appFetch } from '@/lib/client-fetch';
|
|
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 appFetch('/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>
|
|
);
|
|
} |