diff --git a/admin/src/app/actions/auth.ts b/admin/src/app/actions/auth.ts new file mode 100644 index 0000000..ea4f840 --- /dev/null +++ b/admin/src/app/actions/auth.ts @@ -0,0 +1,52 @@ +"use server"; + +import { loginAdmin, logoutAdmin, getCurrentAdminUser } from "@/lib/auth"; +import { revalidatePath } from "next/cache"; + +export interface LoginActionInput { + identifier?: string; + email?: string; + password?: string; + rememberMe?: boolean; +} + +export async function loginAdminAction(input: LoginActionInput | FormData) { + let identifier = ""; + let password = ""; + let rememberMe = true; + + if (input instanceof FormData) { + identifier = (input.get("identifier") as string) || (input.get("email") as string) || ""; + password = (input.get("password") as string) || ""; + rememberMe = input.get("rememberMe") === "true" || input.get("rememberMe") === "on"; + } else if (input && typeof input === "object") { + identifier = input.identifier || input.email || ""; + password = input.password || ""; + rememberMe = input.rememberMe !== false; + } + + if (!identifier || !password) { + return { + success: false, + error: "Veuillez renseigner votre identifiant et votre mot de passe.", + }; + } + + const result = await loginAdmin(identifier, password, rememberMe); + + if (result.success) { + revalidatePath("/", "layout"); + } + + return result; +} + +export async function logoutAdminAction() { + const result = await logoutAdmin(); + revalidatePath("/", "layout"); + return result; +} + +export async function getCurrentAdminAction() { + return await getCurrentAdminUser(); +} diff --git a/admin/src/app/actions/team.ts b/admin/src/app/actions/team.ts new file mode 100644 index 0000000..c0a335e --- /dev/null +++ b/admin/src/app/actions/team.ts @@ -0,0 +1,326 @@ +"use server"; + +import prisma from "@/lib/prisma"; +import bcrypt from "bcryptjs"; +import { requireSuperAdminSession } from "@/lib/auth"; +import { revalidatePath } from "next/cache"; +import { ADMIN_ROLES, type AdminRoleType, ROLE_DEFAULT_PERMISSIONS } from "@/lib/permissions"; + +export interface CreateTeamMemberInput { + name: string; + email: string; + password: string; + adminRole: AdminRoleType; + permissions?: string[]; +} + +export interface UpdateTeamMemberInput { + id: string; + name?: string; + adminRole?: AdminRoleType; + permissions?: string[]; + password?: string; +} + +/** + * Lists all staff / administration members. + * Strictly restricted to Super Admin. + */ +export async function getTeamMembersAction() { + try { + await requireSuperAdminSession(); + + const members = await prisma.user.findMany({ + where: { + role: 'ADMIN', + }, + select: { + id: true, + name: true, + email: true, + role: true, + adminRole: true, + adminPermissions: true, + isSuspended: true, + suspensionReason: true, + createdAt: true, + updatedAt: true, + avatar: true, + }, + orderBy: [ + { adminRole: 'asc' }, + { createdAt: 'desc' }, + ], + }); + + return { + success: true, + members: members.map(m => ({ + ...m, + adminRole: m.adminRole || (m.email === 'admin@afrohub.com' ? 'SUPER_ADMIN' : 'ADMIN'), + adminPermissions: m.adminPermissions || [], + })), + }; + } catch (error: any) { + console.error("Error in getTeamMembersAction:", error); + return { + success: false, + error: error?.message || "Erreur lors de la récupération des membres de l'équipe", + members: [], + }; + } +} + +/** + * Creates a new administration staff account with designated role and tab permissions. + */ +export async function createTeamMemberAction(input: CreateTeamMemberInput) { + try { + await requireSuperAdminSession(); + + const name = input.name?.trim(); + const email = input.email?.toLowerCase().trim(); + const password = input.password; + const adminRole = input.adminRole || 'ADMIN'; + const permissions = input.permissions || []; + + if (!name || !email || !password) { + return { success: false, error: "Nom, email et mot de passe sont obligatoires." }; + } + + if (password.length < 6) { + return { success: false, error: "Le mot de passe doit comporter au moins 6 caractères." }; + } + + if (!ADMIN_ROLES[adminRole]) { + return { success: false, error: "Rôle administrateur invalide." }; + } + + // Check if user already exists + const existing = await prisma.user.findUnique({ + where: { email }, + }); + + if (existing) { + return { success: false, error: "Cette adresse email est déjà associée à un compte." }; + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const newUser = await prisma.user.create({ + data: { + name, + email, + password: hashedPassword, + role: 'ADMIN', + adminRole, + adminPermissions: permissions, + emailVerified: true, + }, + select: { + id: true, + name: true, + email: true, + adminRole: true, + adminPermissions: true, + }, + }); + + revalidatePath("/team"); + return { success: true, user: newUser }; + } catch (error: any) { + console.error("Error in createTeamMemberAction:", error); + return { + success: false, + error: error?.message || "Erreur lors de la création du compte administrateur.", + }; + } +} + +/** + * Updates a staff member's profile, role, custom tab permissions, or password. + */ +export async function updateTeamMemberAction(input: UpdateTeamMemberInput) { + try { + const currentSession = await requireSuperAdminSession(); + + const targetUser = await prisma.user.findUnique({ + where: { id: input.id }, + }); + + if (!targetUser) { + return { success: false, error: "Compte introuvable." }; + } + + // Safety: Prevent removing the last Super Admin + if ( + targetUser.adminRole === 'SUPER_ADMIN' && + input.adminRole && + input.adminRole !== 'SUPER_ADMIN' + ) { + const superAdminCount = await prisma.user.count({ + where: { + role: 'ADMIN', + adminRole: 'SUPER_ADMIN', + isSuspended: false, + }, + }); + + if (superAdminCount <= 1) { + return { + success: false, + error: "Impossible de modifier ce compte : la plateforme doit conserver au moins un Super Administrateur actif.", + }; + } + } + + const updateData: any = {}; + + if (input.name && input.name.trim()) { + updateData.name = input.name.trim(); + } + + if (input.adminRole && ADMIN_ROLES[input.adminRole]) { + updateData.adminRole = input.adminRole; + } + + if (Array.isArray(input.permissions)) { + updateData.adminPermissions = input.permissions; + } + + if (input.password && input.password.trim()) { + if (input.password.trim().length < 6) { + return { success: false, error: "Le nouveau mot de passe doit comporter au moins 6 caractères." }; + } + updateData.password = await bcrypt.hash(input.password.trim(), 10); + } + + await prisma.user.update({ + where: { id: input.id }, + data: updateData, + }); + + revalidatePath("/team"); + revalidatePath("/", "layout"); + return { success: true }; + } catch (error: any) { + console.error("Error in updateTeamMemberAction:", error); + return { + success: false, + error: error?.message || "Erreur lors de la mise à jour du collaborateur.", + }; + } +} + +/** + * Suspends or reactivates a staff member's account. + */ +export async function toggleTeamMemberSuspensionAction(id: string, reason?: string) { + try { + const currentSession = await requireSuperAdminSession(); + + if (id === currentSession.userId) { + return { + success: false, + error: "Vous ne pouvez pas suspendre votre propre compte Super Administrateur.", + }; + } + + const targetUser = await prisma.user.findUnique({ + where: { id }, + }); + + if (!targetUser) { + return { success: false, error: "Compte introuvable." }; + } + + if (targetUser.adminRole === 'SUPER_ADMIN' && !targetUser.isSuspended) { + const superAdminCount = await prisma.user.count({ + where: { + role: 'ADMIN', + adminRole: 'SUPER_ADMIN', + isSuspended: false, + }, + }); + + if (superAdminCount <= 1) { + return { + success: false, + error: "Impossible de suspendre le dernier Super Administrateur actif.", + }; + } + } + + const newSuspended = !targetUser.isSuspended; + + await prisma.user.update({ + where: { id }, + data: { + isSuspended: newSuspended, + suspensionReason: newSuspended ? (reason || "Suspension administrative") : null, + }, + }); + + revalidatePath("/team"); + return { success: true, isSuspended: newSuspended }; + } catch (error: any) { + console.error("Error in toggleTeamMemberSuspensionAction:", error); + return { + success: false, + error: error?.message || "Erreur lors de la suspension/réactivation.", + }; + } +} + +/** + * Deletes a staff member account. + */ +export async function deleteTeamMemberAction(id: string) { + try { + const currentSession = await requireSuperAdminSession(); + + if (id === currentSession.userId) { + return { + success: false, + error: "Vous ne pouvez pas supprimer votre propre compte Super Administrateur.", + }; + } + + const targetUser = await prisma.user.findUnique({ + where: { id }, + }); + + if (!targetUser) { + return { success: false, error: "Compte introuvable." }; + } + + if (targetUser.adminRole === 'SUPER_ADMIN') { + const superAdminCount = await prisma.user.count({ + where: { + role: 'ADMIN', + adminRole: 'SUPER_ADMIN', + }, + }); + + if (superAdminCount <= 1) { + return { + success: false, + error: "Impossible de supprimer le dernier Super Administrateur.", + }; + } + } + + await prisma.user.delete({ + where: { id }, + }); + + revalidatePath("/team"); + return { success: true }; + } catch (error: any) { + console.error("Error in deleteTeamMemberAction:", error); + return { + success: false, + error: error?.message || "Erreur lors de la suppression du collaborateur.", + }; + } +} diff --git a/admin/src/app/dashboard/DashboardClient.tsx b/admin/src/app/dashboard/DashboardClient.tsx index 920077e..f9138b1 100644 --- a/admin/src/app/dashboard/DashboardClient.tsx +++ b/admin/src/app/dashboard/DashboardClient.tsx @@ -45,8 +45,25 @@ interface DashboardData { }>; } +import { useEffect } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import toast from 'react-hot-toast'; + export default function DashboardClient({ initialData }: { initialData: DashboardData }) { const { stats, latestBusinesses, activities } = initialData; + const searchParams = useSearchParams(); + const router = useRouter(); + + useEffect(() => { + const unauthorized = searchParams.get('unauthorized'); + if (unauthorized) { + toast.error(`Accès restreint : vous n'avez pas les autorisations pour accéder à ce module (${unauthorized}).`, { + duration: 5000, + }); + // Clean query param + router.replace('/dashboard'); + } + }, [searchParams, router]); const statCards = [ { label: 'Utilisateurs', value: stats.usersCount, icon: Users, color: 'text-blue-400' }, @@ -117,9 +134,9 @@ export default function DashboardClient({ initialData }: { initialData: Dashboar {b.plan} {b.verified ? ( - + ) : ( - + )} diff --git a/admin/src/app/layout.tsx b/admin/src/app/layout.tsx index 310aa58..ef283b4 100644 --- a/admin/src/app/layout.tsx +++ b/admin/src/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata } from "next"; export const dynamic = 'force-dynamic'; import { Inter } from "next/font/google"; import "./globals.css"; -import Sidebar from "@/components/Sidebar"; +import AdminLayoutWrapper from "@/components/AdminLayoutWrapper"; import { Toaster } from 'react-hot-toast'; const inter = Inter({ subsets: ["latin"] }); @@ -20,14 +20,12 @@ export default function RootLayout({ return ( -
- -
- - {children} -
-
+ + + {children} + ); } + diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx new file mode 100644 index 0000000..c2758b6 --- /dev/null +++ b/admin/src/app/login/page.tsx @@ -0,0 +1,203 @@ +"use client"; + +import React, { useState, useTransition, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { ShieldCheck, Lock, Mail, Eye, EyeOff, ArrowRight, Loader2, AlertCircle } from "lucide-react"; +import toast from "react-hot-toast"; +import { loginAdminAction } from "@/app/actions/auth"; + +function LoginForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const from = searchParams.get("from") || "/dashboard"; + + const [identifier, setIdentifier] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [rememberMe, setRememberMe] = useState(true); + const [errorMessage, setErrorMessage] = useState(null); + const [isPending, startTransition] = useTransition(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setErrorMessage(null); + + if (!identifier.trim() || !password) { + setErrorMessage("Veuillez remplir tous les champs."); + return; + } + + startTransition(async () => { + try { + const res = await loginAdminAction({ + identifier: identifier.trim(), + password, + rememberMe, + }); + + if (!res.success) { + setErrorMessage(res.error || "Échec de l'authentification."); + toast.error(res.error || "Erreur lors de la connexion"); + return; + } + + toast.success(`Bienvenue, ${res.user?.name || "Administrateur"} !`); + // Full page redirect ensures middleware & server cookies are freshly synchronized + window.location.href = from.startsWith("/") ? from : "/dashboard"; + } catch (err) { + console.error("Login client error:", err); + setErrorMessage("Une erreur imprévue est survenue. Veuillez réessayer."); + toast.error("Erreur serveur"); + } + }); + }; + + return ( +
+ {/* Background ambient lighting */} +
+
+ +
+ {/* Card Header & Brand */} +
+
+ +
+

AfroAdmin

+

+ Portail d'administration sécurisé Afrohub +

+
+ + {/* Card Form */} +
+ {errorMessage && ( +
+ +
{errorMessage}
+
+ )} + +
+ {/* Identifier (Email / Username) */} +
+ +
+
+ +
+ setIdentifier(e.target.value)} + disabled={isPending} + className="w-full pl-11 pr-4 py-3 bg-slate-800/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 text-sm focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-all disabled:opacity-50" + /> +
+
+ + {/* Password */} +
+
+ +
+
+
+ +
+ setPassword(e.target.value)} + disabled={isPending} + className="w-full pl-11 pr-11 py-3 bg-slate-800/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 text-sm focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-all disabled:opacity-50" + /> + +
+
+ + {/* Remember Me */} +
+ +
+ + {/* Submit Button */} + +
+ + {/* Security footnote */} +
+
+ + Chiffrement SHA-256 de session • Accès strictement restreint +
+
+
+ + {/* Brand copyright */} +

+ © {new Date().getFullYear()} Afrohub. Tous droits réservés. +

+
+
+ ); +} + +export default function LoginPage() { + return ( + + +
+ } + > + + + ); +} diff --git a/admin/src/app/team/TeamClient.tsx b/admin/src/app/team/TeamClient.tsx new file mode 100644 index 0000000..a34f8e5 --- /dev/null +++ b/admin/src/app/team/TeamClient.tsx @@ -0,0 +1,1029 @@ +"use client"; + +import React, { useState, useTransition } from "react"; +import { + ShieldAlert, + ShieldCheck, + Users, + Plus, + Search, + Edit3, + Trash2, + UserX, + UserCheck, + KeyRound, + CheckCircle2, + AlertTriangle, + X, + Loader2, + Lock, + Mail, + User as UserIcon, + Crown +} from "lucide-react"; +import toast from "react-hot-toast"; +import { + ADMIN_ROLES, + ADMIN_TABS, + type AdminRoleType, + hasTabPermission +} from "@/lib/permissions"; +import { + createTeamMemberAction, + updateTeamMemberAction, + toggleTeamMemberSuspensionAction, + deleteTeamMemberAction +} from "@/app/actions/team"; + +export interface TeamMember { + id: string; + name: string; + email: string; + role: string; + adminRole: string; + adminPermissions: string[]; + isSuspended: boolean; + suspensionReason?: string | null; + createdAt: Date | string; + updatedAt: Date | string; + avatar?: string | null; +} + +interface TeamClientProps { + initialMembers: TeamMember[]; + currentUserId: string; +} + +export default function TeamClient({ initialMembers, currentUserId }: TeamClientProps) { + const [members, setMembers] = useState(initialMembers); + const [searchQuery, setSearchQuery] = useState(""); + const [roleFilter, setRoleFilter] = useState("ALL"); + const [statusFilter, setStatusFilter] = useState("ALL"); + + // Modal states + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [editingMember, setEditingMember] = useState(null); + const [passwordModalMember, setPasswordModalMember] = useState(null); + const [deletingMember, setDeletingMember] = useState(null); + const [suspendingMember, setSuspendingMember] = useState(null); + + // Form states for Create/Edit + const [formName, setFormName] = useState(""); + const [formEmail, setFormEmail] = useState(""); + const [formPassword, setFormPassword] = useState(""); + const [formRole, setFormRole] = useState("ADMIN"); + const [formPermissions, setFormPermissions] = useState([]); + const [isPending, startTransition] = useTransition(); + + // Reset password state + const [newPassword, setNewPassword] = useState(""); + const [suspensionReason, setSuspensionReason] = useState(""); + + // Counts + const superAdminCount = members.filter(m => m.adminRole === 'SUPER_ADMIN').length; + const adminCount = members.filter(m => m.adminRole === 'ADMIN').length; + const modCount = members.filter(m => m.adminRole === 'MODERATOR').length; + const editorCount = members.filter(m => m.adminRole === 'EDITOR').length; + + // Filtered members + const filteredMembers = members.filter((member) => { + const matchesSearch = + member.name.toLowerCase().includes(searchQuery.toLowerCase()) || + member.email.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesRole = roleFilter === "ALL" || member.adminRole === roleFilter; + const matchesStatus = + statusFilter === "ALL" || + (statusFilter === "ACTIVE" && !member.isSuspended) || + (statusFilter === "SUSPENDED" && member.isSuspended); + + return matchesSearch && matchesRole && matchesStatus; + }); + + const openCreateModal = () => { + setFormName(""); + setFormEmail(""); + setFormPassword(""); + setFormRole("ADMIN"); + setFormPermissions(ADMIN_ROLES.ADMIN.defaultTabs); + setIsCreateOpen(true); + }; + + const openEditModal = (member: TeamMember) => { + setEditingMember(member); + setFormName(member.name); + setFormEmail(member.email); + setFormPassword(""); + const role = (member.adminRole as AdminRoleType) || "ADMIN"; + setFormRole(role); + setFormPermissions( + member.adminPermissions && member.adminPermissions.length > 0 + ? member.adminPermissions + : ADMIN_ROLES[role]?.defaultTabs || [] + ); + }; + + const handleRoleChange = (newRole: AdminRoleType) => { + setFormRole(newRole); + // When changing role, auto-select default tabs for this role + if (newRole === 'SUPER_ADMIN') { + setFormPermissions(Object.keys(ADMIN_TABS)); + } else { + setFormPermissions(ADMIN_ROLES[newRole]?.defaultTabs || []); + } + }; + + const togglePermission = (tabId: string) => { + if (formRole === 'SUPER_ADMIN') return; // Super admin always has all + if (tabId === 'team') return; // Only super admin can access team + + setFormPermissions(prev => + prev.includes(tabId) ? prev.filter(p => p !== tabId) : [...prev, tabId] + ); + }; + + const handleCreateSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!formName.trim() || !formEmail.trim() || !formPassword.trim()) { + toast.error("Veuillez renseigner tous les champs obligatoires."); + return; + } + + startTransition(async () => { + const res = await createTeamMemberAction({ + name: formName.trim(), + email: formEmail.trim(), + password: formPassword.trim(), + adminRole: formRole, + permissions: formRole === 'SUPER_ADMIN' ? Object.keys(ADMIN_TABS) : formPermissions, + }); + + if (!res.success) { + toast.error(res.error || "Erreur lors de la création."); + return; + } + + toast.success("Collaborateur créé avec succès !"); + setIsCreateOpen(false); + // Reload or refresh state + window.location.reload(); + }); + }; + + const handleEditSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!editingMember) return; + + startTransition(async () => { + const res = await updateTeamMemberAction({ + id: editingMember.id, + name: formName.trim(), + adminRole: formRole, + permissions: formRole === 'SUPER_ADMIN' ? Object.keys(ADMIN_TABS) : formPermissions, + password: formPassword.trim() ? formPassword.trim() : undefined, + }); + + if (!res.success) { + toast.error(res.error || "Erreur lors de la modification."); + return; + } + + toast.success("Informations mises à jour avec succès !"); + setEditingMember(null); + window.location.reload(); + }); + }; + + const handleToggleSuspension = (member: TeamMember) => { + setSuspendingMember(member); + setSuspensionReason(""); + }; + + const confirmToggleSuspension = () => { + if (!suspendingMember) return; + + startTransition(async () => { + const res = await toggleTeamMemberSuspensionAction( + suspendingMember.id, + suspensionReason.trim() || undefined + ); + + if (!res.success) { + toast.error(res.error || "Erreur lors de la modification du statut."); + return; + } + + toast.success(res.isSuspended ? "Compte suspendu." : "Compte réactivé avec succès."); + setSuspendingMember(null); + setMembers(prev => prev.map(m => m.id === suspendingMember.id ? { ...m, isSuspended: res.isSuspended! } : m)); + }); + }; + + const handleDeleteMember = () => { + if (!deletingMember) return; + + startTransition(async () => { + const res = await deleteTeamMemberAction(deletingMember.id); + if (!res.success) { + toast.error(res.error || "Erreur lors de la suppression."); + return; + } + + toast.success("Compte collaborateur supprimé."); + setMembers(prev => prev.filter(m => m.id !== deletingMember.id)); + setDeletingMember(null); + }); + }; + + const handleResetPassword = (e: React.FormEvent) => { + e.preventDefault(); + if (!passwordModalMember || !newPassword.trim()) return; + + startTransition(async () => { + const res = await updateTeamMemberAction({ + id: passwordModalMember.id, + password: newPassword.trim(), + }); + + if (!res.success) { + toast.error(res.error || "Erreur lors de la réinitialisation."); + return; + } + + toast.success("Mot de passe réinitialisé avec succès !"); + setPasswordModalMember(null); + setNewPassword(""); + }); + }; + + return ( +
+ {/* Page Header */} +
+
+
+ + + +

+ Gestion de l'Équipe & Rôles +

+
+

+ Administrez les différents comptes staff (Super Admin, Admin, Modérateurs, Contributeurs) et configurez les onglets accessibles à chacun. +

+
+ + +
+ + {/* Role Summary Stats Grid */} +
+ {/* Super Admins */} +
+
+ +
+
+

Super Admins

+

{superAdminCount}

+

Droits illimités & gestion accès

+
+
+ + {/* Admins */} +
+
+ +
+
+

Administrateurs

+

{adminCount}

+

Gestion générale opérationnelle

+
+
+ + {/* Modérateurs */} +
+
+ +
+
+

Modérateurs

+

{modCount}

+

Commentaires, signalements & avis

+
+
+ + {/* Contributeurs */} +
+
+ +
+
+

Contributeurs

+

{editorCount}

+

Blog, interviews & événements

+
+
+
+ + {/* Filter & Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-slate-800/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-400 text-sm focus:outline-none focus:border-indigo-500 transition-colors" + /> +
+ +
+ {/* Role filter */} + + + {/* Status filter */} + +
+
+ + {/* Table of Members */} +
+
+ + + + + + + + + + + + {filteredMembers.length === 0 ? ( + + + + ) : ( + filteredMembers.map((member) => { + const roleConfig = ADMIN_ROLES[member.adminRole as AdminRoleType] || ADMIN_ROLES.ADMIN; + const isCurrent = member.id === currentUserId; + const effectiveTabs = member.adminRole === 'SUPER_ADMIN' + ? Object.keys(ADMIN_TABS) + : (member.adminPermissions && member.adminPermissions.length > 0 + ? member.adminPermissions + : roleConfig.defaultTabs); + + return ( + + {/* Name & Email */} + + + {/* Role Badge */} + + + {/* Allowed Tabs */} + + + {/* Status */} + + + {/* Actions */} + + + ); + }) + )} + +
CollaborateurRôle attribuéOnglets autorisésStatutActions
+ Aucun collaborateur ne correspond aux critères de recherche. +
+
+
+ {member.name ? member.name.charAt(0).toUpperCase() : 'A'} +
+
+
+ {member.name} + {isCurrent && ( + + Vous + + )} +
+ {member.email} +
+
+
+ + {member.adminRole === 'SUPER_ADMIN' && } + {member.adminRole === 'ADMIN' && } + {member.adminRole === 'MODERATOR' && } + {member.adminRole === 'EDITOR' && } + {roleConfig.label} + + + {member.adminRole === 'SUPER_ADMIN' ? ( + + Tous les modules (13/13) + + ) : ( +
+ {effectiveTabs.slice(0, 4).map((tabKey) => { + const tabDef = ADMIN_TABS[tabKey]; + return ( + + {tabDef?.name || tabKey} + + ); + })} + {effectiveTabs.length > 4 && ( + + +{effectiveTabs.length - 4} autres + + )} +
+ )} +
+ {member.isSuspended ? ( + + + Suspendu + + ) : ( + + + Actif + + )} + +
+ {/* Edit Role & Tabs */} + + + {/* Reset Password */} + + + {/* Suspend / Reactivate (if not self) */} + {!isCurrent && ( + + )} + + {/* Delete (if not self) */} + {!isCurrent && ( + + )} +
+
+
+
+ + {/* CREATE COLLABORATOR MODAL */} + {isCreateOpen && ( +
+
+
+
+ + + +
+

Nouveau Collaborateur

+

Créez un compte staff et attribuez ses droits d'accès

+
+
+ +
+ +
+ {/* Name & Email */} +
+
+ + setFormName(e.target.value)} + className="w-full px-3.5 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ +
+ + setFormEmail(e.target.value)} + className="w-full px-3.5 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+
+ + {/* Initial Password */} +
+ + setFormPassword(e.target.value)} + className="w-full px-3.5 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ + {/* Role Selection */} +
+ +
+ {(Object.keys(ADMIN_ROLES) as AdminRoleType[]).map((rKey) => { + const r = ADMIN_ROLES[rKey]; + const isSelected = formRole === rKey; + return ( +
handleRoleChange(rKey)} + className={`p-3.5 rounded-xl border cursor-pointer transition-all ${ + isSelected + ? 'bg-indigo-950/40 border-indigo-500 shadow-md shadow-indigo-500/10' + : 'bg-slate-800/60 border-slate-700/80 hover:border-slate-600' + }`} + > +
+ + {r.label} + + handleRoleChange(rKey)} + className="text-indigo-600" + /> +
+

+ {r.description} +

+
+ ); + })} +
+
+ + {/* Granular Tab Permissions Checklist */} +
+
+ + + {formRole === 'SUPER_ADMIN' + ? 'Accès total à tous les modules' + : `${formPermissions.length} onglet(s) sélectionné(s)`} + +
+ +
+ {Object.entries(ADMIN_TABS).map(([tabKey, tab]) => { + const isChecked = formRole === 'SUPER_ADMIN' || formPermissions.includes(tabKey); + const isDisabled = formRole === 'SUPER_ADMIN' || tabKey === 'team'; + + return ( + + ); + })} +
+
+ + {/* Form Buttons */} +
+ + +
+
+
+
+ )} + + {/* EDIT COLLABORATOR MODAL */} + {editingMember && ( +
+
+
+
+ + + +
+

Modifier les Accès & Rôle

+

{editingMember.email}

+
+
+ +
+ +
+ {/* Name */} +
+ + setFormName(e.target.value)} + className="w-full px-3.5 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ + {/* Optional New Password */} +
+ + setFormPassword(e.target.value)} + className="w-full px-3.5 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ + {/* Role Selection */} +
+ +
+ {(Object.keys(ADMIN_ROLES) as AdminRoleType[]).map((rKey) => { + const r = ADMIN_ROLES[rKey]; + const isSelected = formRole === rKey; + return ( +
handleRoleChange(rKey)} + className={`p-3.5 rounded-xl border cursor-pointer transition-all ${ + isSelected + ? 'bg-indigo-950/40 border-indigo-500 shadow-md shadow-indigo-500/10' + : 'bg-slate-800/60 border-slate-700/80 hover:border-slate-600' + }`} + > +
+ + {r.label} + + handleRoleChange(rKey)} + className="text-indigo-600" + /> +
+

+ {r.description} +

+
+ ); + })} +
+
+ + {/* Granular Tab Permissions Checklist */} +
+
+ + + {formRole === 'SUPER_ADMIN' + ? 'Accès total à tous les modules' + : `${formPermissions.length} onglet(s) sélectionné(s)`} + +
+ +
+ {Object.entries(ADMIN_TABS).map(([tabKey, tab]) => { + const isChecked = formRole === 'SUPER_ADMIN' || formPermissions.includes(tabKey); + const isDisabled = formRole === 'SUPER_ADMIN' || tabKey === 'team'; + + return ( + + ); + })} +
+
+ + {/* Form Buttons */} +
+ + +
+
+
+
+ )} + + {/* RESET PASSWORD MODAL */} + {passwordModalMember && ( +
+
+
+
+ +

Réinitialiser le mot de passe

+
+ +
+ +

+ Définir un nouveau mot de passe pour {passwordModalMember.name} ({passwordModalMember.email}) : +

+ +
+
+ + setNewPassword(e.target.value)} + className="w-full px-3.5 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ +
+ + +
+
+
+
+ )} + + {/* SUSPENSION MODAL */} + {suspendingMember && ( +
+
+
+ +

+ {suspendingMember.isSuspended ? "Réactiver le collaborateur" : "Suspendre le collaborateur"} +

+
+ +

+ {suspendingMember.isSuspended ? ( + <>Êtes-vous sûr de vouloir réactiver le compte de {suspendingMember.name} ? Il pourra à nouveau se connecter au backoffice. + ) : ( + <>Êtes-vous sûr de vouloir suspendre le compte de {suspendingMember.name} ? Tous ses accès au panneau d'administration seront révoqués. + )} +

+ + {!suspendingMember.isSuspended && ( +
+ +