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,
|
adminCancelChallenge,
|
||||||
reactivateChallenge,
|
reactivateChallenge,
|
||||||
} from "@/actions/admin/challenges";
|
} 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";
|
import { Avatar } from "@/components/ui";
|
||||||
|
|
||||||
interface Challenge {
|
interface Challenge {
|
||||||
@@ -441,115 +449,115 @@ export default function ChallengeManagement() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-lg font-bold text-gray-300 mb-2">
|
<h3 className="text-lg font-bold text-gray-300 mb-2">
|
||||||
{selectedChallenge.title}
|
{selectedChallenge.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 mb-4">
|
<p className="text-gray-400 mb-4">
|
||||||
{selectedChallenge.description}
|
{selectedChallenge.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={selectedChallenge.challenger.avatar}
|
src={selectedChallenge.challenger.avatar}
|
||||||
username={selectedChallenge.challenger.username}
|
username={selectedChallenge.challenger.username}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-300">
|
<span className="text-gray-300">
|
||||||
{selectedChallenge.challenger.username}
|
{selectedChallenge.challenger.username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-500">VS</span>
|
<span className="text-gray-500">VS</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={selectedChallenge.challenged.avatar}
|
src={selectedChallenge.challenged.avatar}
|
||||||
username={selectedChallenge.challenged.username}
|
username={selectedChallenge.challenged.username}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-300">
|
<span className="text-gray-300">
|
||||||
{selectedChallenge.challenged.username}
|
{selectedChallenge.challenged.username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-bold text-pixel-gold mb-2">
|
<label className="block text-sm font-bold text-pixel-gold mb-2">
|
||||||
Sélectionner le gagnant
|
Sélectionner le gagnant
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-4">
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="winner"
|
|
||||||
value={selectedChallenge.challenger.id}
|
|
||||||
checked={winnerId === selectedChallenge.challenger.id}
|
|
||||||
onChange={(e) => setWinnerId(e.target.value)}
|
|
||||||
className="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<span className="text-gray-300">
|
|
||||||
{selectedChallenge.challenger.username}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="winner"
|
|
||||||
value={selectedChallenge.challenged.id}
|
|
||||||
checked={winnerId === selectedChallenge.challenged.id}
|
|
||||||
onChange={(e) => setWinnerId(e.target.value)}
|
|
||||||
className="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<span className="text-gray-300">
|
|
||||||
{selectedChallenge.challenged.username}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-sm font-bold text-pixel-gold mb-2">
|
|
||||||
Commentaire (optionnel)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={adminComment}
|
|
||||||
onChange={(e) => setAdminComment(e.target.value)}
|
|
||||||
className="w-full p-2 bg-black/60 border border-pixel-gold/30 rounded text-gray-300"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Commentaire pour les joueurs..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
onClick={handleValidate}
|
<input
|
||||||
variant="primary"
|
type="radio"
|
||||||
disabled={!winnerId || isPending}
|
name="winner"
|
||||||
className="flex-1"
|
value={selectedChallenge.challenger.id}
|
||||||
>
|
checked={winnerId === selectedChallenge.challenger.id}
|
||||||
{isPending ? "Enregistrement..." : "Confirmer le gagnant"}
|
onChange={(e) => setWinnerId(e.target.value)}
|
||||||
</Button>
|
className="w-4 h-4"
|
||||||
<Button
|
/>
|
||||||
onClick={handleReject}
|
<span className="text-gray-300">
|
||||||
variant="secondary"
|
{selectedChallenge.challenger.username}
|
||||||
disabled={isPending}
|
</span>
|
||||||
className="flex-1"
|
</label>
|
||||||
>
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
{isPending ? "Rejet..." : "Rejeter le défi"}
|
<input
|
||||||
</Button>
|
type="radio"
|
||||||
<Button
|
name="winner"
|
||||||
onClick={() => {
|
value={selectedChallenge.challenged.id}
|
||||||
setSelectedChallenge(null);
|
checked={winnerId === selectedChallenge.challenged.id}
|
||||||
setWinnerId("");
|
onChange={(e) => setWinnerId(e.target.value)}
|
||||||
setAdminComment("");
|
className="w-4 h-4"
|
||||||
}}
|
/>
|
||||||
variant="secondary"
|
<span className="text-gray-300">
|
||||||
disabled={isPending}
|
{selectedChallenge.challenged.username}
|
||||||
>
|
</span>
|
||||||
Annuler
|
</label>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-bold text-pixel-gold mb-2">
|
||||||
|
Commentaire (optionnel)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={adminComment}
|
||||||
|
onChange={(e) => setAdminComment(e.target.value)}
|
||||||
|
className="w-full p-2 bg-black/60 border border-pixel-gold/30 rounded text-gray-300"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Commentaire pour les joueurs..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={handleValidate}
|
||||||
|
variant="primary"
|
||||||
|
disabled={!winnerId || isPending}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{isPending ? "Enregistrement..." : "Confirmer le gagnant"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleReject}
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isPending}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{isPending ? "Rejet..." : "Rejeter le défi"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedChallenge(null);
|
||||||
|
setWinnerId("");
|
||||||
|
setAdminComment("");
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
@@ -582,67 +590,67 @@ export default function ChallengeManagement() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
id="edit-title"
|
id="edit-title"
|
||||||
label="Titre"
|
label="Titre"
|
||||||
value={editTitle}
|
value={editTitle}
|
||||||
onChange={(e) => setEditTitle(e.target.value)}
|
onChange={(e) => setEditTitle(e.target.value)}
|
||||||
required
|
required
|
||||||
placeholder="Titre du défi"
|
placeholder="Titre du défi"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
id="edit-description"
|
id="edit-description"
|
||||||
label="Description"
|
label="Description"
|
||||||
value={editDescription}
|
value={editDescription}
|
||||||
onChange={(e) => setEditDescription(e.target.value)}
|
onChange={(e) => setEditDescription(e.target.value)}
|
||||||
required
|
required
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="Description du défi"
|
placeholder="Description du défi"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
id="edit-points"
|
id="edit-points"
|
||||||
label="Récompense (points)"
|
label="Récompense (points)"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
value={editPointsReward}
|
value={editPointsReward}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditPointsReward(parseInt(e.target.value) || 0)
|
setEditPointsReward(parseInt(e.target.value) || 0)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
placeholder="100"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-4 pt-4">
|
||||||
|
<Button
|
||||||
|
onClick={handleUpdate}
|
||||||
|
variant="primary"
|
||||||
|
disabled={
|
||||||
|
isPending ||
|
||||||
|
!editTitle ||
|
||||||
|
!editDescription ||
|
||||||
|
editPointsReward <= 0
|
||||||
}
|
}
|
||||||
required
|
className="flex-1"
|
||||||
placeholder="100"
|
>
|
||||||
/>
|
{isPending ? "Mise à jour..." : "Enregistrer"}
|
||||||
|
</Button>
|
||||||
<div className="flex gap-4 pt-4">
|
<Button
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={handleUpdate}
|
setEditingChallenge(null);
|
||||||
variant="primary"
|
setEditTitle("");
|
||||||
disabled={
|
setEditDescription("");
|
||||||
isPending ||
|
setEditPointsReward(0);
|
||||||
!editTitle ||
|
}}
|
||||||
!editDescription ||
|
variant="secondary"
|
||||||
editPointsReward <= 0
|
disabled={isPending}
|
||||||
}
|
>
|
||||||
className="flex-1"
|
Annuler
|
||||||
>
|
</Button>
|
||||||
{isPending ? "Mise à jour..." : "Enregistrer"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setEditingChallenge(null);
|
|
||||||
setEditTitle("");
|
|
||||||
setEditDescription("");
|
|
||||||
setEditPointsReward(0);
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Modal,
|
Modal,
|
||||||
CloseButton,
|
CloseButton,
|
||||||
|
Avatar,
|
||||||
} from "@/components/ui";
|
} from "@/components/ui";
|
||||||
|
import { updateUser } from "@/actions/admin/users";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,6 +30,24 @@ interface Event {
|
|||||||
registrationsCount?: number;
|
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 {
|
interface EventFormData {
|
||||||
date: string;
|
date: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -78,6 +98,14 @@ export default function EventManagement() {
|
|||||||
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [saving, setSaving] = 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>({
|
const [formData, setFormData] = useState<EventFormData>({
|
||||||
date: "",
|
date: "",
|
||||||
name: "",
|
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) {
|
if (loading) {
|
||||||
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
||||||
}
|
}
|
||||||
@@ -410,7 +507,15 @@ export default function EventManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isCreating && !editingEvent && (
|
{!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
|
<Button
|
||||||
onClick={() => handleEdit(event)}
|
onClick={() => handleEdit(event)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -435,6 +540,116 @@ export default function EventManagement() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ export class EventRegistrationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les points à retirer depuis les préférences du site
|
// 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;
|
const pointsToRemove = sitePreferences.eventRegistrationPoints || 100;
|
||||||
|
|
||||||
// Supprimer l'inscription et retirer les points en parallèle
|
// Supprimer l'inscription et retirer les points en parallèle
|
||||||
@@ -118,6 +119,35 @@ export class EventRegistrationService {
|
|||||||
return count;
|
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
|
* 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
|
// 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;
|
const pointsToAward = sitePreferences.eventRegistrationPoints || 100;
|
||||||
|
|
||||||
// Créer l'inscription et attribuer les points en parallèle
|
// Créer l'inscription et attribuer les points en parallèle
|
||||||
|
|||||||
Reference in New Issue
Block a user