Enhance ChallengeManagement and EventManagement components: Refactor layout for better readability, implement event registration viewing with score editing functionality, and improve user feedback handling in modals. Update EventRegistrationService to fetch event registrations with user details, ensuring a more interactive admin experience.
This commit is contained in:
31
app/api/admin/events/[id]/registrations/route.ts
Normal file
31
app/api/admin/events/[id]/registrations/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id: eventId } = await params;
|
||||
const registrations = await eventRegistrationService.getEventRegistrations(
|
||||
eventId
|
||||
);
|
||||
|
||||
return NextResponse.json(registrations);
|
||||
} catch (error) {
|
||||
console.error("Error fetching event registrations:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération des inscrits" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,15 @@ import {
|
||||
adminCancelChallenge,
|
||||
reactivateChallenge,
|
||||
} from "@/actions/admin/challenges";
|
||||
import { Button, Card, Input, Textarea, Alert, Modal, CloseButton } from "@/components/ui";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Textarea,
|
||||
Alert,
|
||||
Modal,
|
||||
CloseButton,
|
||||
} from "@/components/ui";
|
||||
import { Avatar } from "@/components/ui";
|
||||
|
||||
interface Challenge {
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
Badge,
|
||||
Modal,
|
||||
CloseButton,
|
||||
Avatar,
|
||||
} from "@/components/ui";
|
||||
import { updateUser } from "@/actions/admin/users";
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
@@ -28,6 +30,24 @@ interface Event {
|
||||
registrationsCount?: number;
|
||||
}
|
||||
|
||||
interface EventRegistration {
|
||||
id: string;
|
||||
userId: string;
|
||||
eventId: string;
|
||||
createdAt: string;
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
score: number;
|
||||
level: number;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
xp: number;
|
||||
maxXp: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface EventFormData {
|
||||
date: string;
|
||||
name: string;
|
||||
@@ -78,6 +98,14 @@ export default function EventManagement() {
|
||||
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [viewingRegistrations, setViewingRegistrations] =
|
||||
useState<Event | null>(null);
|
||||
const [registrations, setRegistrations] = useState<EventRegistration[]>([]);
|
||||
const [loadingRegistrations, setLoadingRegistrations] = useState(false);
|
||||
const [editingScores, setEditingScores] = useState<Record<string, number>>(
|
||||
{}
|
||||
);
|
||||
const [savingScore, setSavingScore] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<EventFormData>({
|
||||
date: "",
|
||||
name: "",
|
||||
@@ -207,6 +235,75 @@ export default function EventManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewRegistrations = async (event: Event) => {
|
||||
setViewingRegistrations(event);
|
||||
setLoadingRegistrations(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/admin/events/${event.id}/registrations`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setRegistrations(data);
|
||||
// Initialiser les scores d'édition avec les scores actuels
|
||||
const scoresMap: Record<string, number> = {};
|
||||
data.forEach((reg: EventRegistration) => {
|
||||
scoresMap[reg.user.id] = reg.user.score;
|
||||
});
|
||||
setEditingScores(scoresMap);
|
||||
} else {
|
||||
alert("Erreur lors de la récupération des inscrits");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching registrations:", error);
|
||||
alert("Erreur lors de la récupération des inscrits");
|
||||
} finally {
|
||||
setLoadingRegistrations(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseRegistrations = () => {
|
||||
setViewingRegistrations(null);
|
||||
setRegistrations([]);
|
||||
setEditingScores({});
|
||||
};
|
||||
|
||||
const handleScoreChange = (userId: string, newScore: number) => {
|
||||
setEditingScores({
|
||||
...editingScores,
|
||||
[userId]: newScore,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveScore = async (userId: string) => {
|
||||
const newScore = editingScores[userId];
|
||||
if (newScore === undefined) return;
|
||||
|
||||
setSavingScore(userId);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await updateUser(userId, { score: newScore });
|
||||
if (result.success) {
|
||||
// Mettre à jour le score dans la liste locale
|
||||
setRegistrations((prev) =>
|
||||
prev.map((reg) =>
|
||||
reg.user.id === userId
|
||||
? { ...reg, user: { ...reg.user, score: newScore } }
|
||||
: reg
|
||||
)
|
||||
);
|
||||
} else {
|
||||
alert(result.error || "Erreur lors de la mise à jour du score");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating score:", error);
|
||||
alert("Erreur lors de la mise à jour du score");
|
||||
} finally {
|
||||
setSavingScore(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
||||
}
|
||||
@@ -410,7 +507,15 @@ export default function EventManagement() {
|
||||
</div>
|
||||
</div>
|
||||
{!isCreating && !editingEvent && (
|
||||
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
|
||||
<div className="flex gap-2 sm:ml-4 flex-shrink-0 flex-wrap">
|
||||
<Button
|
||||
onClick={() => handleViewRegistrations(event)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Inscrits ({event.registrationsCount || 0})
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleEdit(event)}
|
||||
variant="primary"
|
||||
@@ -435,6 +540,116 @@ export default function EventManagement() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal des inscrits */}
|
||||
{viewingRegistrations && (
|
||||
<Modal
|
||||
isOpen={!!viewingRegistrations}
|
||||
onClose={handleCloseRegistrations}
|
||||
size="lg"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
Inscrits à "{viewingRegistrations.name}"
|
||||
</h4>
|
||||
<CloseButton onClick={handleCloseRegistrations} size="lg" />
|
||||
</div>
|
||||
|
||||
{loadingRegistrations ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Chargement...
|
||||
</div>
|
||||
) : registrations.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Aucun inscrit pour cet événement
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto">
|
||||
{registrations.map((registration) => {
|
||||
const user = registration.user;
|
||||
const currentScore = editingScores[user.id] ?? user.score;
|
||||
const isSaving = savingScore === user.id;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={registration.id}
|
||||
variant="default"
|
||||
className="p-3 sm:p-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<Avatar
|
||||
src={user.avatar}
|
||||
username={user.username}
|
||||
size="md"
|
||||
borderClassName="border-2 border-pixel-gold/50"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h5 className="text-pixel-gold font-bold text-sm sm:text-base break-words">
|
||||
{user.username}
|
||||
</h5>
|
||||
<p className="text-gray-400 text-xs sm:text-sm">
|
||||
Niveau {user.level} • HP: {user.hp}/{user.maxHp} •
|
||||
XP: {user.xp}/{user.maxXp}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-3 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs sm:text-sm text-gray-300 whitespace-nowrap">
|
||||
Score:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={currentScore}
|
||||
onChange={(e) =>
|
||||
handleScoreChange(
|
||||
user.id,
|
||||
parseInt(e.target.value) || 0
|
||||
)
|
||||
}
|
||||
disabled={isSaving}
|
||||
className="w-24 px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm text-center disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 sm:gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleScoreChange(user.id, currentScore - 100)
|
||||
}
|
||||
disabled={isSaving}
|
||||
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0 disabled:opacity-50"
|
||||
>
|
||||
-100
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleScoreChange(user.id, currentScore + 100)
|
||||
}
|
||||
disabled={isSaving}
|
||||
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0 disabled:opacity-50"
|
||||
>
|
||||
+100
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSaveScore(user.id)}
|
||||
disabled={isSaving || currentScore === user.score}
|
||||
className="px-2 sm:px-3 py-1 border border-pixel-gold/50 bg-pixel-gold/20 text-pixel-gold text-[10px] sm:text-xs rounded hover:bg-pixel-gold/30 transition flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSaving ? "..." : "Sauver"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ export class EventRegistrationService {
|
||||
}
|
||||
|
||||
// Récupérer les points à retirer depuis les préférences du site
|
||||
const sitePreferences = await sitePreferencesService.getOrCreateSitePreferences();
|
||||
const sitePreferences =
|
||||
await sitePreferencesService.getOrCreateSitePreferences();
|
||||
const pointsToRemove = sitePreferences.eventRegistrationPoints || 100;
|
||||
|
||||
// Supprimer l'inscription et retirer les points en parallèle
|
||||
@@ -118,6 +119,35 @@ export class EventRegistrationService {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les inscrits d'un événement avec leurs informations
|
||||
*/
|
||||
async getEventRegistrations(eventId: string) {
|
||||
return prisma.eventRegistration.findMany({
|
||||
where: {
|
||||
eventId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
score: true,
|
||||
level: true,
|
||||
hp: true,
|
||||
maxHp: true,
|
||||
xp: true,
|
||||
maxXp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide et inscrit un utilisateur à un événement avec toutes les règles métier
|
||||
*/
|
||||
@@ -146,7 +176,8 @@ export class EventRegistrationService {
|
||||
}
|
||||
|
||||
// Récupérer les points à attribuer depuis les préférences du site
|
||||
const sitePreferences = await sitePreferencesService.getOrCreateSitePreferences();
|
||||
const sitePreferences =
|
||||
await sitePreferencesService.getOrCreateSitePreferences();
|
||||
const pointsToAward = sitePreferences.eventRegistrationPoints || 100;
|
||||
|
||||
// Créer l'inscription et attribuer les points en parallèle
|
||||
|
||||
Reference in New Issue
Block a user