diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 8affaf5..2121dcc 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -3,11 +3,12 @@ import Sidebar from "@/components/admin/Sidebar"; import { useAuth } from "@/contexts/AuthContext"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Loading } from "@/components/ui/Loading"; import toast from "react-hot-toast"; -import { LogOut } from "lucide-react"; +import { LogOut, User, ChevronDown } from "lucide-react"; import Logo from "@/components/Logo"; +import Link from "next/link"; export default function AdminLayout({ children, @@ -16,6 +17,7 @@ export default function AdminLayout({ }) { const { user, isAuthenticated, isLoading, logout } = useAuth(); const router = useRouter(); + const [dropdownOpen, setDropdownOpen] = useState(false); useEffect(() => { if (!isLoading && !isAuthenticated) { @@ -30,6 +32,24 @@ export default function AdminLayout({ } }, [isLoading, isAuthenticated, user, router]); + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.closest('.user-dropdown')) { + setDropdownOpen(false); + } + }; + + if (dropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [dropdownOpen]); + const handleLogout = async () => { await logout(); router.push("/login"); @@ -58,18 +78,51 @@ export default function AdminLayout({

Thé Tip Top - Administration

-

- Connecté en tant que {user?.firstName} {user?.lastName} -

- + + {/* User Dropdown */} +
+ + + {/* Dropdown Menu */} + {dropdownOpen && ( +
+ setDropdownOpen(false)} + className="flex items-center gap-3 px-4 py-3 hover:bg-gray-100 transition-colors" + > + + Profil + +
+ +
+ )} +
diff --git a/app/admin/profil/page.tsx b/app/admin/profil/page.tsx new file mode 100644 index 0000000..de479f8 --- /dev/null +++ b/app/admin/profil/page.tsx @@ -0,0 +1,235 @@ +"use client"; +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useAuth } from "@/contexts/AuthContext"; +import { profileUpdateSchema, ProfileUpdateFormData } from "@/lib/validations"; +import { Input } from "@/components/ui/Input"; +import Button from "@/components/Button"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/Card"; +import { Badge } from "@/components/ui/Badge"; +import { userService } from "@/services/user.service"; +import toast from "react-hot-toast"; +import { useRouter } from "next/navigation"; +import { ROUTES } from "@/utils/constants"; +import { formatDate } from "@/utils/helpers"; + +export const dynamic = 'force-dynamic'; + +export default function AdminProfilePage() { + const { user, isAuthenticated, refreshUser } = useAuth(); + const router = useRouter(); + const [isEditing, setIsEditing] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(profileUpdateSchema), + defaultValues: { + firstName: user?.firstName || "", + lastName: user?.lastName || "", + phone: user?.phone || "", + }, + }); + + if (!user) { + return null; + } + + const onSubmit = async (data: ProfileUpdateFormData) => { + setIsSubmitting(true); + try { + await userService.updateProfile(data); + await refreshUser(); + toast.success("Profil mis à jour avec succès"); + setIsEditing(false); + } catch (error: any) { + toast.error(error.message || "Erreur lors de la mise à jour du profil"); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + reset({ + firstName: user?.firstName || "", + lastName: user?.lastName || "", + phone: user?.phone || "", + }); + setIsEditing(false); + }; + + return ( +
+
+

Mon profil

+ +
+ {/* Profile Info Card */} +
+
+
+

Informations personnelles

+
+
+ {!isEditing ? ( +
+
+ +

{user.firstName}

+
+
+ +

{user.lastName}

+
+
+ +

{user.email}

+
+
+ +

+ {user.phone || "Non renseigné"} +

+
+
+ +
+ Administrateur +
+
+
+ +
+
+ ) : ( +
+ + + + +
+ + +
+
+ )} +
+
+
+ + {/* Account Status Card */} +
+
+
+

Statut du compte

+
+
+
+ +
+ {user.isVerified ? ( + Vérifié ✓ + ) : ( + Non vérifié + )} +
+
+
+ +

+ {formatDate(user.createdAt)} +

+
+
+
+ + {/* Quick Actions Card */} +
+
+

Actions rapides

+
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/app/employe/dashboard/page.tsx b/app/employe/dashboard/page.tsx index 8ee0b76..3aaacc1 100644 --- a/app/employe/dashboard/page.tsx +++ b/app/employe/dashboard/page.tsx @@ -23,8 +23,7 @@ interface PendingTicket { } export default function EmployeDashboardPage() { - const { user, isAuthenticated, isLoading: authLoading } = useAuth(); - const router = useRouter(); + const { user } = useAuth(); const [stats, setStats] = useState({ pendingTickets: 0, claimedToday: 0, @@ -34,22 +33,9 @@ export default function EmployeDashboardPage() { const [loadingTickets, setLoadingTickets] = useState(true); useEffect(() => { - if (!authLoading && !isAuthenticated) { - router.push(ROUTES.LOGIN); - return; - } - - if (isAuthenticated && user?.role !== 'EMPLOYEE') { - router.push(ROUTES.HOME); - toast.error('Accès refusé : rôle employé requis'); - return; - } - - if (isAuthenticated) { - loadPendingTickets(); - loadEmployeeStats(); - } - }, [authLoading, isAuthenticated, user, router]); + loadPendingTickets(); + loadEmployeeStats(); + }, []); const loadPendingTickets = async () => { try { @@ -106,15 +92,7 @@ export default function EmployeDashboardPage() { } }; - if (authLoading) { - return ( -
- -
- ); - } - - if (!isAuthenticated || user?.role !== 'EMPLOYEE') { + if (!user) { return null; } diff --git a/app/employe/layout.tsx b/app/employe/layout.tsx index 052fff9..8e91de4 100644 --- a/app/employe/layout.tsx +++ b/app/employe/layout.tsx @@ -2,10 +2,10 @@ import { useAuth } from "@/contexts/AuthContext"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Loading } from "@/components/ui/Loading"; import toast from "react-hot-toast"; -import { LogOut, Ticket, BarChart3 } from "lucide-react"; +import { LogOut, Ticket, BarChart3, User, ChevronDown } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import Logo from "@/components/Logo"; @@ -18,6 +18,7 @@ export default function EmployeLayout({ const { user, isAuthenticated, isLoading, logout } = useAuth(); const router = useRouter(); const pathname = usePathname(); + const [dropdownOpen, setDropdownOpen] = useState(false); useEffect(() => { if (!isLoading && !isAuthenticated) { @@ -32,6 +33,24 @@ export default function EmployeLayout({ } }, [isLoading, isAuthenticated, user, router]); + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.closest('.user-dropdown')) { + setDropdownOpen(false); + } + }; + + if (dropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [dropdownOpen]); + const handleLogout = async () => { await logout(); router.push("/login"); @@ -107,21 +126,51 @@ export default function EmployeLayout({

Thé Tip Top - Espace Employé

-

- Connecté en tant que{" "} - - {user?.firstName} {user?.lastName} - -

- + + {/* User Dropdown */} +
+ + + {/* Dropdown Menu */} + {dropdownOpen && ( +
+ setDropdownOpen(false)} + className="flex items-center gap-3 px-4 py-3 hover:bg-gray-100 transition-colors" + > + + Profil + +
+ +
+ )} +
diff --git a/app/employe/profil/page.tsx b/app/employe/profil/page.tsx new file mode 100644 index 0000000..bd82d45 --- /dev/null +++ b/app/employe/profil/page.tsx @@ -0,0 +1,233 @@ +"use client"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useAuth } from "@/contexts/AuthContext"; +import { profileUpdateSchema, ProfileUpdateFormData } from "@/lib/validations"; +import { Input } from "@/components/ui/Input"; +import { Badge } from "@/components/ui/Badge"; +import { userService } from "@/services/user.service"; +import toast from "react-hot-toast"; +import { useRouter } from "next/navigation"; +import { ROUTES } from "@/utils/constants"; +import { formatDate } from "@/utils/helpers"; + +export const dynamic = 'force-dynamic'; + +export default function EmployeProfilePage() { + const { user, refreshUser } = useAuth(); + const router = useRouter(); + const [isEditing, setIsEditing] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(profileUpdateSchema), + defaultValues: { + firstName: user?.firstName || "", + lastName: user?.lastName || "", + phone: user?.phone || "", + }, + }); + + if (!user) { + return null; + } + + const onSubmit = async (data: ProfileUpdateFormData) => { + setIsSubmitting(true); + try { + await userService.updateProfile(data); + await refreshUser(); + toast.success("Profil mis à jour avec succès"); + setIsEditing(false); + } catch (error: any) { + toast.error(error.message || "Erreur lors de la mise à jour du profil"); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + reset({ + firstName: user?.firstName || "", + lastName: user?.lastName || "", + phone: user?.phone || "", + }); + setIsEditing(false); + }; + + return ( +
+
+

Mon profil

+ +
+ {/* Profile Info Card */} +
+
+
+

Informations personnelles

+
+
+ {!isEditing ? ( +
+
+ +

{user.firstName}

+
+
+ +

{user.lastName}

+
+
+ +

{user.email}

+
+
+ +

+ {user.phone || "Non renseigné"} +

+
+
+ +
+ Employé +
+
+
+ +
+
+ ) : ( +
+ + + + +
+ + +
+
+ )} +
+
+
+ + {/* Account Status Card */} +
+
+
+

Statut du compte

+
+
+
+ +
+ {user.isVerified ? ( + Vérifié ✓ + ) : ( + Non vérifié + )} +
+
+
+ +

+ {formatDate(user.createdAt)} +

+
+
+
+ + {/* Quick Actions Card */} +
+
+

Actions rapides

+
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/app/layout-client.tsx b/app/layout-client.tsx index d94851b..8118d30 100644 --- a/app/layout-client.tsx +++ b/app/layout-client.tsx @@ -1,19 +1,29 @@ "use client"; import { usePathname } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import CookieConsent from "@/components/CookieConsent"; export default function LayoutClient({ children }: { children: React.ReactNode }) { const pathname = usePathname(); + const { user } = useAuth(); // Ne pas afficher Header/Footer dans l'espace admin et employé const isAdminRoute = pathname?.startsWith("/admin"); const isEmployeRoute = pathname?.startsWith("/employe"); const isHomePage = pathname === "/"; + const isLoginPage = pathname === "/login" || pathname === "/register"; - if (isAdminRoute || isEmployeRoute) { + // Vérifier si l'utilisateur est admin ou employé + const userRole = user?.role?.toUpperCase(); + const isAdminOrEmployee = userRole === "ADMIN" || userRole === "EMPLOYEE"; + + // Ne pas afficher le header si: + // 1. On est sur une route admin/employé + // 2. On est sur login/register ET l'utilisateur est admin/employé (en cours de redirection) + if (isAdminRoute || isEmployeRoute || (isLoginPage && isAdminOrEmployee)) { return <>{children}; } diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx index c441305..d3d5e87 100644 --- a/contexts/AuthContext.tsx +++ b/contexts/AuthContext.tsx @@ -72,13 +72,14 @@ export const AuthProvider: React.FC = ({ children }) => { toast.success('Connexion réussie !'); // Redirect based on role (support both uppercase and lowercase) + // Use replace() to remove login page from history const role = response.user.role.toUpperCase(); if (role === 'ADMIN') { - router.push(ROUTES.ADMIN_DASHBOARD); + router.replace(ROUTES.ADMIN_DASHBOARD); } else if (role === 'EMPLOYEE') { - router.push(ROUTES.EMPLOYEE_DASHBOARD); + router.replace(ROUTES.EMPLOYEE_DASHBOARD); } else { - router.push(ROUTES.CLIENT_DASHBOARD); + router.replace(ROUTES.CLIENT_DASHBOARD); } } catch (error: any) { toast.error(error.message || 'Erreur lors de la connexion'); @@ -123,13 +124,14 @@ export const AuthProvider: React.FC = ({ children }) => { toast.success('Connexion avec Google réussie !'); // Redirect based on role (support both uppercase and lowercase) + // Use replace() to remove login page from history const role = response.user.role.toUpperCase(); if (role === 'ADMIN') { - router.push(ROUTES.ADMIN_DASHBOARD); + router.replace(ROUTES.ADMIN_DASHBOARD); } else if (role === 'EMPLOYEE') { - router.push(ROUTES.EMPLOYEE_DASHBOARD); + router.replace(ROUTES.EMPLOYEE_DASHBOARD); } else { - router.push(ROUTES.CLIENT_DASHBOARD); + router.replace(ROUTES.CLIENT_DASHBOARD); } } catch (error: any) { toast.error(error.message || 'Erreur lors de la connexion avec Google'); @@ -146,13 +148,14 @@ export const AuthProvider: React.FC = ({ children }) => { toast.success('Connexion avec Facebook réussie !'); // Redirect based on role (support both uppercase and lowercase) + // Use replace() to remove login page from history const role = response.user.role.toUpperCase(); if (role === 'ADMIN') { - router.push(ROUTES.ADMIN_DASHBOARD); + router.replace(ROUTES.ADMIN_DASHBOARD); } else if (role === 'EMPLOYEE') { - router.push(ROUTES.EMPLOYEE_DASHBOARD); + router.replace(ROUTES.EMPLOYEE_DASHBOARD); } else { - router.push(ROUTES.CLIENT_DASHBOARD); + router.replace(ROUTES.CLIENT_DASHBOARD); } } catch (error: any) { toast.error(error.message || 'Erreur lors de la connexion avec Facebook');