423 lines
13 KiB
TypeScript
423 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useTransition } from "react";
|
|
import { calculateEventStatus } from "@/lib/eventStatus";
|
|
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
|
|
import { Input, Textarea, Button, Card, Badge } from "@/components/ui";
|
|
|
|
interface Event {
|
|
id: string;
|
|
date: string;
|
|
name: string;
|
|
description: string;
|
|
type: "ATELIER" | "KATA" | "PRESENTATION" | "LEARNING_HOUR";
|
|
status: "UPCOMING" | "LIVE" | "PAST";
|
|
room?: string | null;
|
|
time?: string | null;
|
|
maxPlaces?: number | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
registrationsCount?: number;
|
|
}
|
|
|
|
interface EventFormData {
|
|
date: string;
|
|
name: string;
|
|
description: string;
|
|
type: "ATELIER" | "KATA" | "PRESENTATION" | "LEARNING_HOUR";
|
|
room?: string;
|
|
time?: string;
|
|
maxPlaces?: number;
|
|
}
|
|
|
|
const eventTypes: Event["type"][] = [
|
|
"ATELIER",
|
|
"KATA",
|
|
"PRESENTATION",
|
|
"LEARNING_HOUR",
|
|
];
|
|
const getEventTypeLabel = (type: Event["type"]) => {
|
|
switch (type) {
|
|
case "ATELIER":
|
|
return "Atelier";
|
|
case "KATA":
|
|
return "Kata";
|
|
case "PRESENTATION":
|
|
return "Présentation";
|
|
case "LEARNING_HOUR":
|
|
return "Learning Hour";
|
|
default:
|
|
return type;
|
|
}
|
|
};
|
|
|
|
const getStatusLabel = (status: Event["status"]) => {
|
|
switch (status) {
|
|
case "UPCOMING":
|
|
return "À venir";
|
|
case "LIVE":
|
|
return "En cours";
|
|
case "PAST":
|
|
return "Passé";
|
|
default:
|
|
return status;
|
|
}
|
|
};
|
|
|
|
export default function EventManagement() {
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [formData, setFormData] = useState<EventFormData>({
|
|
date: "",
|
|
name: "",
|
|
description: "",
|
|
type: "ATELIER",
|
|
room: "",
|
|
time: "",
|
|
maxPlaces: undefined,
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchEvents();
|
|
}, []);
|
|
|
|
const fetchEvents = async () => {
|
|
try {
|
|
const response = await fetch("/api/admin/events");
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setEvents(data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching events:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
setIsCreating(true);
|
|
setEditingEvent(null);
|
|
setFormData({
|
|
date: "",
|
|
name: "",
|
|
description: "",
|
|
type: "ATELIER",
|
|
room: "",
|
|
time: "",
|
|
maxPlaces: undefined,
|
|
});
|
|
};
|
|
|
|
const handleEdit = (event: Event) => {
|
|
setEditingEvent(event);
|
|
setIsCreating(false);
|
|
setFormData({
|
|
date: event.date,
|
|
name: event.name,
|
|
description: event.description,
|
|
type: event.type,
|
|
room: event.room || "",
|
|
time: event.time || "",
|
|
maxPlaces: event.maxPlaces || undefined,
|
|
});
|
|
};
|
|
|
|
const [, startTransition] = useTransition();
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
startTransition(async () => {
|
|
try {
|
|
let result;
|
|
if (isCreating) {
|
|
result = await createEvent(formData);
|
|
} else if (editingEvent) {
|
|
result = await updateEvent(editingEvent.id, formData);
|
|
}
|
|
|
|
if (result?.success) {
|
|
await fetchEvents();
|
|
setEditingEvent(null);
|
|
setIsCreating(false);
|
|
setFormData({
|
|
date: "",
|
|
name: "",
|
|
description: "",
|
|
type: "ATELIER",
|
|
room: "",
|
|
time: "",
|
|
maxPlaces: undefined,
|
|
});
|
|
} else {
|
|
alert(result?.error || "Erreur lors de la sauvegarde");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error saving event:", error);
|
|
alert("Erreur lors de la sauvegarde");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleDelete = async (eventId: string) => {
|
|
if (!confirm("Êtes-vous sûr de vouloir supprimer cet événement ?")) {
|
|
return;
|
|
}
|
|
|
|
startTransition(async () => {
|
|
try {
|
|
const result = await deleteEvent(eventId);
|
|
|
|
if (result.success) {
|
|
await fetchEvents();
|
|
} else {
|
|
alert(result.error || "Erreur lors de la suppression");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting event:", error);
|
|
alert("Erreur lors de la suppression");
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setEditingEvent(null);
|
|
setIsCreating(false);
|
|
setFormData({
|
|
date: "",
|
|
name: "",
|
|
description: "",
|
|
type: "ATELIER",
|
|
room: "",
|
|
time: "",
|
|
maxPlaces: undefined,
|
|
});
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-4">
|
|
<h3 className="text-lg sm:text-xl font-gaming font-bold text-pixel-gold break-words">
|
|
Événements ({events.length})
|
|
</h3>
|
|
{!isCreating && !editingEvent && (
|
|
<Button
|
|
onClick={handleCreate}
|
|
variant="success"
|
|
size="sm"
|
|
className="whitespace-nowrap flex-shrink-0"
|
|
>
|
|
+ Nouvel événement
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{(isCreating || editingEvent) && (
|
|
<Card variant="default" className="p-3 sm:p-4 mb-4">
|
|
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
|
|
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
|
</h4>
|
|
<div className="space-y-4">
|
|
<Input
|
|
type="date"
|
|
label="Date"
|
|
value={formData.date}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, date: e.target.value })
|
|
}
|
|
className="text-xs sm:text-sm px-3 py-2"
|
|
/>
|
|
<Input
|
|
type="text"
|
|
label="Nom"
|
|
value={formData.name}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, name: e.target.value })
|
|
}
|
|
placeholder="Nom de l'événement"
|
|
className="text-xs sm:text-sm px-3 py-2"
|
|
/>
|
|
<Textarea
|
|
label="Description"
|
|
value={formData.description}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, description: e.target.value })
|
|
}
|
|
placeholder="Description de l'événement"
|
|
rows={4}
|
|
className="text-xs sm:text-sm px-3 py-2"
|
|
/>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
|
Type
|
|
</label>
|
|
<select
|
|
value={formData.type}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
type: e.target.value as Event["type"],
|
|
})
|
|
}
|
|
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
|
>
|
|
{eventTypes.map((type) => (
|
|
<option key={type} value={type}>
|
|
{getEventTypeLabel(type)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<Input
|
|
type="text"
|
|
label="Salle"
|
|
value={formData.room || ""}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, room: e.target.value })
|
|
}
|
|
placeholder="Ex: Nautilus"
|
|
className="text-xs sm:text-sm px-3 py-2"
|
|
/>
|
|
<Input
|
|
type="text"
|
|
label="Heure"
|
|
value={formData.time || ""}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, time: e.target.value })
|
|
}
|
|
placeholder="Ex: 11h-12h"
|
|
className="text-xs sm:text-sm px-3 py-2"
|
|
/>
|
|
<Input
|
|
type="number"
|
|
label="Places max"
|
|
value={formData.maxPlaces || ""}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
maxPlaces: e.target.value
|
|
? parseInt(e.target.value)
|
|
: undefined,
|
|
})
|
|
}
|
|
placeholder="Ex: 25"
|
|
className="text-xs sm:text-sm px-3 py-2"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-2">
|
|
<Button
|
|
onClick={handleSave}
|
|
variant="success"
|
|
size="md"
|
|
disabled={saving}
|
|
>
|
|
{saving ? "Enregistrement..." : "Enregistrer"}
|
|
</Button>
|
|
<Button onClick={handleCancel} variant="secondary" size="md">
|
|
Annuler
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{events.length === 0 ? (
|
|
<div className="text-center text-gray-400 py-8">
|
|
Aucun événement trouvé
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{events.map((event) => {
|
|
const status = calculateEventStatus(event.date);
|
|
const statusVariant =
|
|
status === "UPCOMING"
|
|
? "success"
|
|
: status === "LIVE"
|
|
? "warning"
|
|
: "default";
|
|
|
|
return (
|
|
<Card key={event.id} variant="default" className="p-3 sm:p-4">
|
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
|
|
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
|
{event.name}
|
|
</h4>
|
|
<Badge variant="default" size="sm">
|
|
{getEventTypeLabel(event.type)}
|
|
</Badge>
|
|
<Badge variant={statusVariant} size="sm">
|
|
{getStatusLabel(status)}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
|
|
{event.description}
|
|
</p>
|
|
<div className="flex flex-wrap items-center gap-2 sm:gap-4 mt-2">
|
|
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
|
Date: {new Date(event.date).toLocaleDateString("fr-FR")}
|
|
</p>
|
|
{event.room && (
|
|
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
|
📍 Salle: {event.room}
|
|
</p>
|
|
)}
|
|
{event.time && (
|
|
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
|
🕐 Heure: {event.time}
|
|
</p>
|
|
)}
|
|
{event.maxPlaces && (
|
|
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
|
👥 Places: {event.maxPlaces}
|
|
</p>
|
|
)}
|
|
<Badge variant="info" size="sm">
|
|
{event.registrationsCount || 0} inscrit
|
|
{event.registrationsCount !== 1 ? "s" : ""}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
{!isCreating && !editingEvent && (
|
|
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
|
|
<Button
|
|
onClick={() => handleEdit(event)}
|
|
variant="primary"
|
|
size="sm"
|
|
className="whitespace-nowrap"
|
|
>
|
|
Modifier
|
|
</Button>
|
|
<Button
|
|
onClick={() => handleDelete(event.id)}
|
|
variant="danger"
|
|
size="sm"
|
|
className="whitespace-nowrap"
|
|
>
|
|
Supprimer
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|