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');