diff --git a/app/admin/challenges/page.tsx b/app/admin/challenges/page.tsx new file mode 100644 index 0000000..1f0bb8e --- /dev/null +++ b/app/admin/challenges/page.tsx @@ -0,0 +1,26 @@ +import ChallengeManagement from "@/components/admin/ChallengeManagement"; +import { Card } from "@/components/ui"; +import { challengeService } from "@/services/challenges/challenge.service"; + +export const dynamic = "force-dynamic"; + +export default async function AdminChallengesPage() { + const challenges = await challengeService.getAllChallenges(); + + // Sérialiser les dates pour le client + const serializedChallenges = challenges.map((challenge) => ({ + ...challenge, + createdAt: challenge.createdAt.toISOString(), + acceptedAt: challenge.acceptedAt?.toISOString() ?? null, + completedAt: challenge.completedAt?.toISOString() ?? null, + })); + + return ( + +

+ Gestion des Défis +

+ +
+ ); +} diff --git a/app/admin/events/page.tsx b/app/admin/events/page.tsx new file mode 100644 index 0000000..3f99f2f --- /dev/null +++ b/app/admin/events/page.tsx @@ -0,0 +1,34 @@ +import EventManagement from "@/components/admin/EventManagement"; +import { Card } from "@/components/ui"; +import { eventService } from "@/services/events/event.service"; + +export const dynamic = "force-dynamic"; + +export default async function AdminEventsPage() { + const events = await eventService.getEventsWithStatus(); + + // Transformer les données pour la sérialisation + const serializedEvents = events.map((event) => ({ + id: event.id, + date: event.date.toISOString(), + name: event.name, + description: event.description, + type: event.type, + status: event.status, + room: event.room, + time: event.time, + maxPlaces: event.maxPlaces, + createdAt: event.createdAt.toISOString(), + updatedAt: event.updatedAt.toISOString(), + registrationsCount: event.registrationsCount, + })); + + return ( + +

+ Gestion des Événements +

+ +
+ ); +} diff --git a/app/admin/feedbacks/page.tsx b/app/admin/feedbacks/page.tsx new file mode 100644 index 0000000..5bfe6c3 --- /dev/null +++ b/app/admin/feedbacks/page.tsx @@ -0,0 +1,77 @@ +import FeedbackManagement from "@/components/admin/FeedbackManagement"; +import { Card } from "@/components/ui"; +import { eventFeedbackService } from "@/services/events/event-feedback.service"; + +export const dynamic = "force-dynamic"; + +export default async function AdminFeedbacksPage() { + const [feedbacksRaw, statistics] = await Promise.all([ + eventFeedbackService.getAllFeedbacks(), + eventFeedbackService.getFeedbackStatistics(), + ]); + + // Type assertion car getAllFeedbacks inclut event et user par défaut + const feedbacks = feedbacksRaw as unknown as Array<{ + id: string; + rating: number; + comment: string | null; + isRead: boolean; + createdAt: Date; + event: { + id: string; + name: string; + date: Date; + type: string; + }; + user: { + id: string; + username: string; + email: string; + avatar: string | null; + score: number; + }; + }>; + + // Sérialiser les dates pour le client + const serializedFeedbacks = feedbacks.map((feedback) => ({ + id: feedback.id, + rating: feedback.rating, + comment: feedback.comment, + isRead: feedback.isRead, + createdAt: feedback.createdAt.toISOString(), + event: { + id: feedback.event.id, + name: feedback.event.name, + date: feedback.event.date.toISOString(), + type: feedback.event.type, + }, + user: { + id: feedback.user.id, + username: feedback.user.username, + email: feedback.user.email, + avatar: feedback.user.avatar, + score: feedback.user.score, + }, + })); + + const serializedStatistics = statistics.map((stat) => ({ + eventId: stat.eventId, + eventName: stat.eventName, + eventDate: stat.eventDate?.toISOString() ?? null, + eventType: stat.eventType, + averageRating: stat.averageRating, + feedbackCount: stat.feedbackCount, + })); + + return ( + +

+ Gestion des Feedbacks +

+ +
+ ); +} diff --git a/app/admin/houses/page.tsx b/app/admin/houses/page.tsx new file mode 100644 index 0000000..d70a99a --- /dev/null +++ b/app/admin/houses/page.tsx @@ -0,0 +1,90 @@ +import HouseManagement from "@/components/admin/HouseManagement"; +import { Card } from "@/components/ui"; +import { houseService } from "@/services/houses/house.service"; +import { Prisma } from "@/prisma/generated/prisma/client"; + +export const dynamic = "force-dynamic"; + +export default async function AdminHousesPage() { + type HouseWithIncludes = Prisma.HouseGetPayload<{ + include: { + creator: { + select: { + id: true; + username: true; + avatar: true; + }; + }; + memberships: { + include: { + user: { + select: { + id: true; + username: true; + avatar: true; + score: true; + level: true; + }; + }; + }; + }; + }; + }>; + + const houses = (await houseService.getAllHouses({ + include: { + creator: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + memberships: { + include: { + user: { + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }, + }, + orderBy: [{ role: "asc" }, { joinedAt: "asc" }], + }, + }, + orderBy: { + createdAt: "desc", + }, + })) as unknown as HouseWithIncludes[]; + + // Transformer les données pour la sérialisation + const serializedHouses = houses.map((house) => ({ + id: house.id, + name: house.name, + description: house.description, + creatorId: house.creatorId, + creator: house.creator, + createdAt: house.createdAt.toISOString(), + updatedAt: house.updatedAt.toISOString(), + membersCount: house.memberships?.length || 0, + memberships: + house.memberships?.map((membership) => ({ + id: membership.id, + role: membership.role, + joinedAt: membership.joinedAt.toISOString(), + user: membership.user, + })) || [], + })); + + return ( + +

+ Gestion des Maisons +

+ +
+ ); +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..e481931 --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,52 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { Role } from "@/prisma/generated/prisma/client"; +import NavigationWrapper from "@/components/navigation/NavigationWrapper"; +import AdminNavigation from "@/components/admin/AdminNavigation"; +import { SectionTitle } from "@/components/ui"; + +export const dynamic = "force-dynamic"; + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + + if (!session?.user) { + redirect("/login"); + } + + if (session.user.role !== Role.ADMIN) { + redirect("/"); + } + + return ( +
+ {/* Background Image */} +
+ {/* Dark overlay for readability */} +
+
+ +
+
+ + ADMIN + + + + + {children} +
+
+
+ ); +} + diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 83d27d4..b81e586 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,41 +1,7 @@ import { redirect } from "next/navigation"; -import { auth } from "@/lib/auth"; -import { sitePreferencesService } from "@/services/preferences/site-preferences.service"; -import { Role } from "@/prisma/generated/prisma/client"; -import NavigationWrapper from "@/components/navigation/NavigationWrapper"; -import AdminPanel from "@/components/admin/AdminPanel"; export const dynamic = "force-dynamic"; export default async function AdminPage() { - const session = await auth(); - - if (!session?.user) { - redirect("/login"); - } - - if (session.user.role !== Role.ADMIN) { - redirect("/"); - } - - // Récupérer les préférences globales du site (ou créer si elles n'existent pas) - const sitePreferences = - await sitePreferencesService.getOrCreateSitePreferences(); - - return ( -
- {/* Background Image */} -
- {/* Dark overlay for readability */} -
-
- - -
- ); + redirect("/admin/preferences"); } diff --git a/app/admin/preferences/page.tsx b/app/admin/preferences/page.tsx new file mode 100644 index 0000000..a88be75 --- /dev/null +++ b/app/admin/preferences/page.tsx @@ -0,0 +1,30 @@ +import { sitePreferencesService } from "@/services/preferences/site-preferences.service"; +import BackgroundPreferences from "@/components/admin/BackgroundPreferences"; +import EventPointsPreferences from "@/components/admin/EventPointsPreferences"; +import EventFeedbackPointsPreferences from "@/components/admin/EventFeedbackPointsPreferences"; +import HousePointsPreferences from "@/components/admin/HousePointsPreferences"; +import { Card } from "@/components/ui"; + +export const dynamic = "force-dynamic"; + +export default async function AdminPreferencesPage() { + const sitePreferences = + await sitePreferencesService.getOrCreateSitePreferences(); + + return ( + +
+

+ Préférences UI Globales +

+
+
+ + + + +
+
+ ); +} + diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx new file mode 100644 index 0000000..9a42154 --- /dev/null +++ b/app/admin/users/page.tsx @@ -0,0 +1,42 @@ +import UserManagement from "@/components/admin/UserManagement"; +import { Card } from "@/components/ui"; +import { userService } from "@/services/users/user.service"; + +export const dynamic = "force-dynamic"; + +export default async function AdminUsersPage() { + const users = await userService.getAllUsers({ + orderBy: { + score: "desc", + }, + select: { + id: true, + username: true, + email: true, + role: true, + score: true, + level: true, + hp: true, + maxHp: true, + xp: true, + maxXp: true, + avatar: true, + createdAt: true, + }, + }); + + // Sérialiser les dates pour le client + const serializedUsers = users.map((user) => ({ + ...user, + createdAt: user.createdAt.toISOString(), + })); + + return ( + +

+ Gestion des Utilisateurs +

+ +
+ ); +} diff --git a/components/admin/AdminNavigation.tsx b/components/admin/AdminNavigation.tsx new file mode 100644 index 0000000..b53bb31 --- /dev/null +++ b/components/admin/AdminNavigation.tsx @@ -0,0 +1,41 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Button } from "@/components/ui"; + +const adminSections = [ + { id: "preferences", label: "Préférences UI", path: "/admin/preferences" }, + { id: "users", label: "Utilisateurs", path: "/admin/users" }, + { id: "events", label: "Événements", path: "/admin/events" }, + { id: "feedbacks", label: "Feedbacks", path: "/admin/feedbacks" }, + { id: "challenges", label: "Défis", path: "/admin/challenges" }, + { id: "houses", label: "Maisons", path: "/admin/houses" }, +]; + +export default function AdminNavigation() { + const pathname = usePathname(); + + return ( +
+ {adminSections.map((section) => { + const isActive = pathname === section.path || + (section.path === "/admin/preferences" && pathname === "/admin"); + + return ( + + ); + })} +
+ ); +} + diff --git a/components/admin/AdminPanel.tsx b/components/admin/AdminPanel.tsx deleted file mode 100644 index eb5ca4b..0000000 --- a/components/admin/AdminPanel.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client"; - -import { useState } from "react"; -import UserManagement from "@/components/admin/UserManagement"; -import EventManagement from "@/components/admin/EventManagement"; -import FeedbackManagement from "@/components/admin/FeedbackManagement"; -import ChallengeManagement from "@/components/admin/ChallengeManagement"; -import HouseManagement from "@/components/admin/HouseManagement"; -import BackgroundPreferences from "@/components/admin/BackgroundPreferences"; -import EventPointsPreferences from "@/components/admin/EventPointsPreferences"; -import EventFeedbackPointsPreferences from "@/components/admin/EventFeedbackPointsPreferences"; -import HousePointsPreferences from "@/components/admin/HousePointsPreferences"; -import { Button, Card, SectionTitle } from "@/components/ui"; - -interface SitePreferences { - id: string; - homeBackground: string | null; - eventsBackground: string | null; - leaderboardBackground: string | null; - challengesBackground: string | null; - eventRegistrationPoints: number; - eventFeedbackPoints: number; - houseJoinPoints: number; - houseLeavePoints: number; - houseCreatePoints: number; -} - -interface AdminPanelProps { - initialPreferences: SitePreferences; -} - -type AdminSection = - | "preferences" - | "users" - | "events" - | "feedbacks" - | "challenges" - | "houses"; - -export default function AdminPanel({ initialPreferences }: AdminPanelProps) { - const [activeSection, setActiveSection] = - useState("preferences"); - - return ( -
-
- - ADMIN - - - {/* Navigation Tabs */} -
- - - - - - -
- - {activeSection === "preferences" && ( - -
-

- Préférences UI Globales -

-
-
- - - - -
-
- )} - - {activeSection === "users" && ( - -

- Gestion des Utilisateurs -

- -
- )} - - {activeSection === "events" && ( - -

- Gestion des Événements -

- -
- )} - - {activeSection === "feedbacks" && ( - -

- Gestion des Feedbacks -

- -
- )} - - {activeSection === "challenges" && ( - -

- Gestion des Défis -

- -
- )} - - {activeSection === "houses" && ( - -

- Gestion des Maisons -

- -
- )} -
-
- ); -} diff --git a/components/admin/ChallengeManagement.tsx b/components/admin/ChallengeManagement.tsx index c02f7ec..b758cc3 100644 --- a/components/admin/ChallengeManagement.tsx +++ b/components/admin/ChallengeManagement.tsx @@ -42,9 +42,13 @@ interface Challenge { acceptedAt: string | null; } -export default function ChallengeManagement() { - const [challenges, setChallenges] = useState([]); - const [loading, setLoading] = useState(true); +interface ChallengeManagementProps { + initialChallenges: Challenge[]; +} + +export default function ChallengeManagement({ initialChallenges }: ChallengeManagementProps) { + const [challenges, setChallenges] = useState(initialChallenges); + const [loading, setLoading] = useState(false); const [selectedChallenge, setSelectedChallenge] = useState( null ); @@ -60,10 +64,6 @@ export default function ChallengeManagement() { const [successMessage, setSuccessMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); - useEffect(() => { - fetchChallenges(); - }, []); - const fetchChallenges = async () => { try { const response = await fetch("/api/admin/challenges"); @@ -73,8 +73,6 @@ export default function ChallengeManagement() { } } catch (error) { console.error("Error fetching challenges:", error); - } finally { - setLoading(false); } }; diff --git a/components/admin/EventManagement.tsx b/components/admin/EventManagement.tsx index 2794ef5..1ba9889 100644 --- a/components/admin/EventManagement.tsx +++ b/components/admin/EventManagement.tsx @@ -92,9 +92,13 @@ const getStatusLabel = (status: Event["status"]) => { } }; -export default function EventManagement() { - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); +interface EventManagementProps { + initialEvents: Event[]; +} + +export default function EventManagement({ initialEvents }: EventManagementProps) { + const [events, setEvents] = useState(initialEvents); + const [loading, setLoading] = useState(false); const [editingEvent, setEditingEvent] = useState(null); const [isCreating, setIsCreating] = useState(false); const [saving, setSaving] = useState(false); @@ -116,10 +120,6 @@ export default function EventManagement() { maxPlaces: undefined, }); - useEffect(() => { - fetchEvents(); - }, []); - const fetchEvents = async () => { try { const response = await fetch("/api/admin/events"); @@ -129,8 +129,6 @@ export default function EventManagement() { } } catch (error) { console.error("Error fetching events:", error); - } finally { - setLoading(false); } }; diff --git a/components/admin/FeedbackManagement.tsx b/components/admin/FeedbackManagement.tsx index 1c3728c..1be909f 100644 --- a/components/admin/FeedbackManagement.tsx +++ b/components/admin/FeedbackManagement.tsx @@ -38,10 +38,18 @@ interface EventStatistics { feedbackCount: number; } -export default function FeedbackManagement() { - const [feedbacks, setFeedbacks] = useState([]); - const [statistics, setStatistics] = useState([]); - const [loading, setLoading] = useState(true); +interface FeedbackManagementProps { + initialFeedbacks: Feedback[]; + initialStatistics: EventStatistics[]; +} + +export default function FeedbackManagement({ + initialFeedbacks, + initialStatistics, +}: FeedbackManagementProps) { + const [feedbacks, setFeedbacks] = useState(initialFeedbacks); + const [statistics, setStatistics] = useState(initialStatistics); + const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [selectedEvent, setSelectedEvent] = useState(null); const [addingPoints, setAddingPoints] = useState>( @@ -49,10 +57,6 @@ export default function FeedbackManagement() { ); const [markingRead, setMarkingRead] = useState>({}); - useEffect(() => { - fetchFeedbacks(); - }, []); - const fetchFeedbacks = async () => { try { const response = await fetch("/api/admin/feedback"); @@ -65,8 +69,6 @@ export default function FeedbackManagement() { setStatistics(data.statistics || []); } catch { setError("Erreur lors du chargement des feedbacks"); - } finally { - setLoading(false); } }; diff --git a/components/admin/HouseManagement.tsx b/components/admin/HouseManagement.tsx index d398e88..c0ad810 100644 --- a/components/admin/HouseManagement.tsx +++ b/components/admin/HouseManagement.tsx @@ -71,9 +71,13 @@ const getRoleColor = (role: string) => { } }; -export default function HouseManagement() { - const [houses, setHouses] = useState([]); - const [loading, setLoading] = useState(true); +interface HouseManagementProps { + initialHouses: House[]; +} + +export default function HouseManagement({ initialHouses }: HouseManagementProps) { + const [houses, setHouses] = useState(initialHouses); + const [loading, setLoading] = useState(false); const [editingHouse, setEditingHouse] = useState(null); const [saving, setSaving] = useState(false); const [deletingHouseId, setDeletingHouseId] = useState(null); @@ -85,10 +89,6 @@ export default function HouseManagement() { }); const [, startTransition] = useTransition(); - useEffect(() => { - fetchHouses(); - }, []); - const fetchHouses = async () => { try { const response = await fetch("/api/admin/houses"); @@ -98,8 +98,6 @@ export default function HouseManagement() { } } catch (error) { console.error("Error fetching houses:", error); - } finally { - setLoading(false); } }; diff --git a/components/admin/UserManagement.tsx b/components/admin/UserManagement.tsx index 00a9061..612ad26 100644 --- a/components/admin/UserManagement.tsx +++ b/components/admin/UserManagement.tsx @@ -37,19 +37,19 @@ interface EditingUser { role: string | null; } -export default function UserManagement() { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); +interface UserManagementProps { + initialUsers: User[]; +} + +export default function UserManagement({ initialUsers }: UserManagementProps) { + const [users, setUsers] = useState(initialUsers); + const [loading, setLoading] = useState(false); const [editingUser, setEditingUser] = useState(null); const [saving, setSaving] = useState(false); const [deletingUserId, setDeletingUserId] = useState(null); const [, startTransition] = useTransition(); const [uploadingAvatar, setUploadingAvatar] = useState(null); - useEffect(() => { - fetchUsers(); - }, []); - const fetchUsers = async () => { try { const response = await fetch("/api/admin/users"); @@ -59,8 +59,6 @@ export default function UserManagement() { } } catch (error) { console.error("Error fetching users:", error); - } finally { - setLoading(false); } }; diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index 492d416..70269b3 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -1,13 +1,17 @@ "use client"; -import { ButtonHTMLAttributes, ReactNode, ElementType } from "react"; +import { ButtonHTMLAttributes, ReactNode, ElementType, AnchorHTMLAttributes } from "react"; +import Link from "next/link"; -interface ButtonProps extends ButtonHTMLAttributes { +type ButtonProps = ButtonHTMLAttributes & { variant?: "primary" | "secondary" | "success" | "danger" | "ghost"; size?: "sm" | "md" | "lg"; children: ReactNode; as?: ElementType; -} +} & ( + | { as?: Exclude } + | { as: typeof Link; href: string } +); const variantClasses = { primary: "btn-primary border transition-colors",