diff --git a/app/account/page.tsx b/app/account/page.tsx new file mode 100644 index 0000000..fc90953 --- /dev/null +++ b/app/account/page.tsx @@ -0,0 +1,36 @@ +import { redirect } from "next/navigation"; +import { AuthService, userService, TeamsService } from "@/services"; +import { AccountForm } from "@/components/account/account-form"; + +export default async function AccountPage() { + try { + // Vérifier si l'utilisateur est connecté + const userUuid = await AuthService.getUserUuidFromCookie(); + + if (!userUuid) { + redirect("/login"); + } + + // Récupérer le profil utilisateur + const userProfile = await userService.getUserByUuid(userUuid); + + if (!userProfile) { + redirect("/login"); + } + + // Charger les équipes pour la sélection + const teams = await TeamsService.getTeams(); + + return ( +
+
+

Mon compte

+ +
+
+ ); + } catch (error) { + console.error("Error loading account page:", error); + redirect("/login"); + } +} diff --git a/app/api/auth/profile/route.ts b/app/api/auth/profile/route.ts new file mode 100644 index 0000000..6a49fa9 --- /dev/null +++ b/app/api/auth/profile/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AuthService, userService } from "@/services"; + +export async function PUT(request: NextRequest) { + try { + // Vérifier si l'utilisateur est connecté + const userUuid = await AuthService.getUserUuidFromCookie(); + + if (!userUuid) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + // Récupérer les données de mise à jour + const { firstName, lastName, teamId } = await request.json(); + + // Validation des données + if (!firstName || !lastName || !teamId) { + return NextResponse.json( + { error: "Tous les champs sont requis" }, + { status: 400 } + ); + } + + // Mettre à jour l'utilisateur + await userService.updateUserByUuid(userUuid, { + firstName, + lastName, + teamId, + }); + + return NextResponse.json({ + message: "Profil mis à jour avec succès", + user: { + firstName, + lastName, + teamId, + }, + }); + } catch (error: any) { + console.error("Profile update error:", error); + return NextResponse.json( + { error: error.message || "Erreur lors de la mise à jour du profil" }, + { status: 500 } + ); + } +} diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts index 02fe90a..bfa4e59 100644 --- a/app/api/auth/route.ts +++ b/app/api/auth/route.ts @@ -1,35 +1,52 @@ import { NextRequest, NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { userService } from "@/services/user-service"; -import { AuthService, COOKIE_NAME } from "@/services/auth-service"; -import { UserProfile } from "@/lib/types"; +import { AuthService, userService, TeamsService } from "@/services"; -/** - * GET /api/auth - Récupère l'utilisateur actuel depuis le cookie - */ -export async function GET() { +export async function GET(request: NextRequest) { try { - const cookieStore = await cookies(); - const userUuid = cookieStore.get(COOKIE_NAME)?.value; + // Récupérer l'UUID utilisateur depuis le cookie + const userUuid = await AuthService.getUserUuidFromCookie(); if (!userUuid) { - return NextResponse.json({ user: null }, { status: 200 }); + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); } + // Récupérer le profil utilisateur const userProfile = await userService.getUserByUuid(userUuid); if (!userProfile) { - // Cookie invalide, le supprimer - const response = NextResponse.json({ user: null }, { status: 200 }); - response.cookies.set(COOKIE_NAME, "", { maxAge: 0 }); - return response; + return NextResponse.json( + { error: "Utilisateur non trouvé" }, + { status: 404 } + ); } - return NextResponse.json({ user: userProfile }, { status: 200 }); + // Récupérer le nom de l'équipe + let teamName = "Équipe non définie"; + if (userProfile.teamId) { + try { + const team = await TeamsService.getTeamById(userProfile.teamId); + if (team) { + teamName = team.name; + } + } catch (error) { + console.error("Failed to fetch team name:", error); + } + } + + // Retourner les informations complètes de l'utilisateur + return NextResponse.json({ + user: { + firstName: userProfile.firstName, + lastName: userProfile.lastName, + teamId: userProfile.teamId, + teamName: teamName, + uuid: userUuid, + }, + }); } catch (error) { - console.error("Error getting current user:", error); + console.error("Auth GET error:", error); return NextResponse.json( - { error: "Failed to get current user" }, + { error: "Erreur interne du serveur" }, { status: 500 } ); } diff --git a/clients/domains/auth-client.ts b/clients/domains/auth-client.ts index a4fea7b..ae4a925 100644 --- a/clients/domains/auth-client.ts +++ b/clients/domains/auth-client.ts @@ -51,9 +51,23 @@ export class AuthClient extends BaseHttpClient { /** * Récupère l'utilisateur actuel depuis le cookie */ - async getCurrentUser(): Promise { + async getCurrentUser(): Promise<{ + firstName: string; + lastName: string; + teamId: string; + teamName: string; + uuid: string; + } | null> { try { - const response = await this.get<{ user: UserProfile }>("/auth"); + const response = await this.get<{ + user: { + firstName: string; + lastName: string; + teamId: string; + teamName: string; + uuid: string; + }; + }>("/auth"); return response.user; } catch (error) { console.error("Failed to get current user:", error); diff --git a/components/account/account-form.tsx b/components/account/account-form.tsx new file mode 100644 index 0000000..709ec80 --- /dev/null +++ b/components/account/account-form.tsx @@ -0,0 +1,300 @@ +"use client"; + +import { useState, useMemo, useRef, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { UserProfile, Team } from "@/lib/types"; +import { Search, Building2, ChevronDown, Check, Save, X } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; + +interface AccountFormProps { + initialProfile: UserProfile; + teams: Team[]; +} + +export function AccountForm({ initialProfile, teams }: AccountFormProps) { + const [firstName, setFirstName] = useState(initialProfile.firstName); + const [lastName, setLastName] = useState(initialProfile.lastName); + const [teamId, setTeamId] = useState(initialProfile.teamId); + const [searchTerm, setSearchTerm] = useState(""); + const [isTeamDropdownOpen, setIsTeamDropdownOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [loading, setLoading] = useState(false); + const teamDropdownRef = useRef(null); + const [dropdownPosition, setDropdownPosition] = useState<"below" | "above">( + "below" + ); + const { toast } = useToast(); + + const hasChanges = + firstName !== initialProfile.firstName || + lastName !== initialProfile.lastName || + teamId !== initialProfile.teamId; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!hasChanges) return; + + setLoading(true); + try { + const response = await fetch("/api/auth/profile", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + firstName, + lastName, + teamId, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Erreur lors de la mise à jour"); + } + + const result = await response.json(); + + toast({ + title: "Profil mis à jour", + description: + result.message || "Vos informations ont été modifiées avec succès.", + }); + + setIsEditing(false); + } catch (error: any) { + console.error("Update failed:", error); + toast({ + title: "Erreur de mise à jour", + description: error.message || "Erreur lors de la mise à jour du profil", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + setFirstName(initialProfile.firstName); + setLastName(initialProfile.lastName); + setTeamId(initialProfile.teamId); + setIsEditing(false); + }; + + // Group teams by direction and filter by search term + const teamsByDirection = useMemo(() => { + const filteredTeams = teams.filter( + (team) => + team.name.toLowerCase().includes(searchTerm.toLowerCase()) || + team.direction.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return filteredTeams.reduce((acc, team) => { + if (!acc[team.direction]) { + acc[team.direction] = []; + } + acc[team.direction].push(team); + return acc; + }, {} as Record); + }, [teams, searchTerm]); + + // Calculate dropdown position when opening + const handleDropdownToggle = () => { + if (!isTeamDropdownOpen && teamDropdownRef.current) { + const rect = teamDropdownRef.current.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const spaceBelow = viewportHeight - rect.bottom; + const spaceAbove = rect.top; + const dropdownHeight = 300; + + setDropdownPosition( + spaceBelow >= dropdownHeight || spaceBelow > spaceAbove + ? "below" + : "above" + ); + } + setIsTeamDropdownOpen(!isTeamDropdownOpen); + }; + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + teamDropdownRef.current && + !teamDropdownRef.current.contains(event.target as Node) + ) { + setIsTeamDropdownOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const selectedTeam = teams.find((team) => team.id === teamId); + + return ( + + + Informations personnelles + + Modifiez vos informations personnelles. Vos identifiants de connexion + (email et mot de passe) ne peuvent pas être modifiés ici. + + + +
+
+
+ + setFirstName(e.target.value)} + placeholder="Votre prénom" + disabled={!isEditing} + required + /> +
+ +
+ + setLastName(e.target.value)} + placeholder="Votre nom" + disabled={!isEditing} + required + /> +
+
+ +
+ +
+ + + {isTeamDropdownOpen && isEditing && ( +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-8 h-8 text-sm" + autoFocus + /> +
+
+ + {Object.entries(teamsByDirection).map( + ([direction, directionTeams]) => ( +
+
+ + {direction} +
+ {directionTeams.map((team) => ( + + ))} +
+ ) + )} + + {Object.keys(teamsByDirection).length === 0 && ( +
+ Aucune équipe trouvée pour "{searchTerm}" +
+ )} +
+ )} +
+
+ +
+ {!isEditing ? ( + + ) : ( + <> + + + + )} +
+
+
+
+ ); +} diff --git a/components/account/index.ts b/components/account/index.ts new file mode 100644 index 0000000..81e2d40 --- /dev/null +++ b/components/account/index.ts @@ -0,0 +1 @@ +export { AccountForm } from "./account-form"; diff --git a/components/layout/navigation.tsx b/components/layout/navigation.tsx index 127a01a..113894a 100644 --- a/components/layout/navigation.tsx +++ b/components/layout/navigation.tsx @@ -4,7 +4,19 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/layout/theme-toggle"; -import { BarChart3, User, Settings, Building2 } from "lucide-react"; +import { + BarChart3, + User, + Settings, + Building2, + ChevronDown, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; interface NavigationProps { userInfo?: { @@ -67,22 +79,41 @@ export function Navigation({ userInfo }: NavigationProps = {}) {
{userInfo && ( - -
- -
-
-

- {userInfo.firstName} {userInfo.lastName} -

-

- {userInfo.teamName} -

-
- + + + + + + + + + Mon compte + + + + + + Se déconnecter + + + + )}
diff --git a/hooks/use-user-context.tsx b/hooks/use-user-context.tsx index 21aa1e1..1694b8f 100644 --- a/hooks/use-user-context.tsx +++ b/hooks/use-user-context.tsx @@ -1,6 +1,13 @@ "use client"; -import { createContext, useContext, useState, ReactNode } from "react"; +import { + createContext, + useContext, + useState, + useEffect, + ReactNode, +} from "react"; +import { authClient } from "@/clients"; interface UserInfo { firstName: string; @@ -11,15 +18,41 @@ interface UserInfo { interface UserContextType { userInfo: UserInfo | null; setUserInfo: (userInfo: UserInfo | null) => void; + loading: boolean; } const UserContext = createContext(undefined); export function UserProvider({ children }: { children: ReactNode }) { const [userInfo, setUserInfo] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchUserInfo = async () => { + try { + // Récupérer les informations utilisateur depuis l'API + const user = await authClient.getCurrentUser(); + if (user) { + setUserInfo({ + firstName: user.firstName, + lastName: user.lastName, + teamName: user.teamName || "Équipe non définie", + }); + } + } catch (error) { + console.error("Failed to fetch user info:", error); + // En cas d'erreur, on considère que l'utilisateur n'est pas connecté + setUserInfo(null); + } finally { + setLoading(false); + } + }; + + fetchUserInfo(); + }, []); return ( - + {children} );