From 6fba622003874913ba11ccde462dcc3d22898af5 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 24 Aug 2025 22:03:15 +0200 Subject: [PATCH] refactor: revew all design of services, clients, deadcode, ... --- app/admin/manage/page.tsx | 9 - app/admin/manage/skills/page.tsx | 10 - app/admin/manage/teams/page.tsx | 10 - app/admin/manage/users/page.tsx | 16 +- app/admin/page.tsx | 10 - app/admin/team/[teamId]/page.tsx | 11 +- app/api/admin/skills/route.ts | 25 - app/api/admin/teams/[teamId]/members/route.ts | 13 - app/api/admin/teams/route.ts | 25 - app/api/admin/users/[userId]/route.ts | 7 - app/api/admin/users/route.ts | 7 - app/api/evaluations/route.ts | 3 +- app/evaluation/page.tsx | 23 +- app/login/page.tsx | 287 ++--------- app/page.tsx | 22 +- clients/README.md | 115 +++++ clients/base/http-client.ts | 69 +++ clients/domains/admin-client.ts | 86 ++++ clients/domains/auth-client.ts | 33 ++ clients/domains/skills-client.ts | 26 + clients/index.ts | 14 + .../admin/management/team-members-modal.tsx | 8 +- .../admin/overview/admin-client-wrapper.tsx | 2 +- .../admin/overview/admin-content-tabs.tsx | 2 +- .../admin/overview/admin-overview-cards.tsx | 2 +- components/admin/skills/skills-list.tsx | 2 +- .../team-detail-client-wrapper.tsx | 2 +- .../admin/team-detail/team-detail-modal.tsx | 2 +- .../admin/team-detail/team-detail-tabs.tsx | 2 +- .../admin/team-detail/team-member-modal.tsx | 2 +- .../admin/team-detail/team-members-tab.tsx | 2 +- .../admin/team-detail/team-overview-tab.tsx | 2 +- components/admin/teams/teams-list.tsx | 8 +- .../admin/teams/teams-management-page.tsx | 15 +- components/admin/users/user-form-dialog.tsx | 8 +- .../admin/users/users-management-page.tsx | 9 +- components/admin/utils/admin-filters.tsx | 2 +- components/create-skill-form.tsx | 44 +- components/login/index.ts | 3 + components/login/login-form-wrapper.tsx | 213 +++++++++ components/login/login-layout.tsx | 18 + components/login/login-loading.tsx | 17 + hooks/use-evaluation.ts | 429 ----------------- hooks/use-skills-management.ts | 14 +- hooks/use-teams-management.ts | 33 +- hooks/use-tree-view.ts | 37 +- hooks/use-users-management.ts | 4 +- lib/README.md | 113 +++++ lib/admin-types.ts | 35 ++ lib/auth-utils.ts | 71 --- lib/data-loader.ts | 36 -- lib/evaluation-actions.ts | 4 +- lib/evaluation-utils.ts | 24 +- lib/server-auth.ts | 91 ---- lib/types.ts | 1 + middleware.ts | 1 - services/admin-management-service.ts | 193 -------- services/admin-service.ts | 35 +- services/api-client.ts | 444 ------------------ services/auth-service.ts | 32 ++ services/client.ts | 4 - services/evaluation-service.ts | 20 + services/index.ts | 8 +- 63 files changed, 969 insertions(+), 1846 deletions(-) create mode 100644 clients/README.md create mode 100644 clients/base/http-client.ts create mode 100644 clients/domains/admin-client.ts create mode 100644 clients/domains/auth-client.ts create mode 100644 clients/domains/skills-client.ts create mode 100644 clients/index.ts create mode 100644 components/login/index.ts create mode 100644 components/login/login-form-wrapper.tsx create mode 100644 components/login/login-layout.tsx create mode 100644 components/login/login-loading.tsx delete mode 100644 hooks/use-evaluation.ts create mode 100644 lib/README.md create mode 100644 lib/admin-types.ts delete mode 100644 lib/auth-utils.ts delete mode 100644 lib/data-loader.ts delete mode 100644 lib/server-auth.ts delete mode 100644 services/admin-management-service.ts delete mode 100644 services/api-client.ts create mode 100644 services/auth-service.ts delete mode 100644 services/client.ts diff --git a/app/admin/manage/page.tsx b/app/admin/manage/page.tsx index 316948b..a2abb25 100644 --- a/app/admin/manage/page.tsx +++ b/app/admin/manage/page.tsx @@ -1,15 +1,6 @@ import { redirect } from "next/navigation"; -import { isUserAuthenticated } from "@/lib/server-auth"; export default async function ManageAdminPage() { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { - redirect("/login"); - } - // Rediriger vers la page skills par défaut redirect("/admin/manage/skills"); } diff --git a/app/admin/manage/skills/page.tsx b/app/admin/manage/skills/page.tsx index dff7fdf..1b40676 100644 --- a/app/admin/manage/skills/page.tsx +++ b/app/admin/manage/skills/page.tsx @@ -1,17 +1,7 @@ -import { redirect } from "next/navigation"; -import { isUserAuthenticated } from "@/lib/server-auth"; import { AdminService } from "@/services/admin-service"; import { SkillsManagementPage } from "@/components/admin/skills"; export default async function SkillsPage() { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { - redirect("/login"); - } - // Charger les données côté serveur try { const adminData = await AdminService.getAdminData(); diff --git a/app/admin/manage/teams/page.tsx b/app/admin/manage/teams/page.tsx index 20caf95..831a471 100644 --- a/app/admin/manage/teams/page.tsx +++ b/app/admin/manage/teams/page.tsx @@ -1,17 +1,7 @@ -import { redirect } from "next/navigation"; -import { isUserAuthenticated } from "@/lib/server-auth"; import { AdminService } from "@/services/admin-service"; import { TeamsManagementPage } from "@/components/admin/teams"; export default async function TeamsPage() { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { - redirect("/login"); - } - // Charger les données côté serveur try { const adminData = await AdminService.getAdminData(); diff --git a/app/admin/manage/users/page.tsx b/app/admin/manage/users/page.tsx index b7980b6..fbb0020 100644 --- a/app/admin/manage/users/page.tsx +++ b/app/admin/manage/users/page.tsx @@ -1,26 +1,12 @@ -import { redirect } from "next/navigation"; -import { isUserAuthenticated } from "@/lib/server-auth"; import { AdminService } from "@/services/admin-service"; import { UsersManagementPage } from "@/components/admin/users"; export default async function UsersPage() { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { - redirect("/login"); - } - // Charger les données côté serveur try { const adminData = await AdminService.getAdminData(); - return ( - - ); + return ; } catch (error) { console.error("Failed to load admin data:", error); return ( diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 09ac96e..8aef21d 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,17 +1,7 @@ -import { redirect } from "next/navigation"; -import { isUserAuthenticated } from "@/lib/server-auth"; import { AdminService } from "@/services/admin-service"; import { AdminClientWrapper } from "@/components/admin"; export default async function AdminPage() { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { - redirect("/login"); - } - // Charger les données côté serveur try { const adminData = await AdminService.getAdminData(); diff --git a/app/admin/team/[teamId]/page.tsx b/app/admin/team/[teamId]/page.tsx index 96b4aff..9782e3b 100644 --- a/app/admin/team/[teamId]/page.tsx +++ b/app/admin/team/[teamId]/page.tsx @@ -1,6 +1,5 @@ import { redirect } from "next/navigation"; -import { isUserAuthenticated } from "@/lib/server-auth"; -import { AdminService, TeamStats } from "@/services/admin-service"; +import { AdminService } from "@/services/admin-service"; import { TeamDetailClientWrapper } from "@/components/admin"; interface TeamDetailPageProps { @@ -13,14 +12,6 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) { // Await params before using const { teamId } = await params; - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { - redirect("/login"); - } - try { // Charger les données côté serveur const allTeamsStats = await AdminService.getTeamsStats(); diff --git a/app/api/admin/skills/route.ts b/app/api/admin/skills/route.ts index 1541583..d530908 100644 --- a/app/api/admin/skills/route.ts +++ b/app/api/admin/skills/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; import { getPool } from "@/services/database"; -import { isUserAuthenticated } from "@/lib/server-auth"; // Configuration pour éviter la génération statique export const dynamic = "force-dynamic"; @@ -8,12 +7,6 @@ export const dynamic = "force-dynamic"; // GET - Récupérer toutes les skills export async function GET() { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const pool = getPool(); const query = ` SELECT @@ -56,12 +49,6 @@ export async function GET() { // POST - Créer une nouvelle skill export async function POST(request: NextRequest) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { name, categoryId, description, icon } = await request.json(); if (!name || !categoryId) { @@ -125,12 +112,6 @@ export async function POST(request: NextRequest) { // PUT - Mettre à jour une skill export async function PUT(request: NextRequest) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { id, name, categoryId, description, icon } = await request.json(); if (!id || !name || !categoryId) { @@ -204,12 +185,6 @@ export async function PUT(request: NextRequest) { // DELETE - Supprimer une skill export async function DELETE(request: NextRequest) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { searchParams } = new URL(request.url); const id = searchParams.get("id"); diff --git a/app/api/admin/teams/[teamId]/members/route.ts b/app/api/admin/teams/[teamId]/members/route.ts index 9887400..70b26ec 100644 --- a/app/api/admin/teams/[teamId]/members/route.ts +++ b/app/api/admin/teams/[teamId]/members/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; import { getPool } from "@/services/database"; -import { isUserAuthenticated } from "@/lib/server-auth"; // GET - Récupérer les membres d'une équipe export async function GET( @@ -8,12 +7,6 @@ export async function GET( { params }: { params: Promise<{ teamId: string }> } ) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { teamId } = await params; if (!teamId) { @@ -61,12 +54,6 @@ export async function DELETE( { params }: { params: Promise<{ teamId: string }> } ) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { teamId } = await params; const { memberId } = await request.json(); diff --git a/app/api/admin/teams/route.ts b/app/api/admin/teams/route.ts index 8170710..3c444cc 100644 --- a/app/api/admin/teams/route.ts +++ b/app/api/admin/teams/route.ts @@ -1,16 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { getPool } from "@/services/database"; -import { isUserAuthenticated } from "@/lib/server-auth"; // GET - Récupérer toutes les teams export async function GET() { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const pool = getPool(); const query = ` SELECT @@ -46,12 +39,6 @@ export async function GET() { // POST - Créer une nouvelle team export async function POST(request: NextRequest) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { name, direction } = await request.json(); if (!name || !direction) { @@ -106,12 +93,6 @@ export async function POST(request: NextRequest) { // PUT - Mettre à jour une team export async function PUT(request: NextRequest) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { id, name, direction } = await request.json(); if (!id || !name || !direction) { @@ -187,12 +168,6 @@ export async function PUT(request: NextRequest) { // DELETE - Supprimer une team ou une direction export async function DELETE(request: NextRequest) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { searchParams } = new URL(request.url); const id = searchParams.get("id"); const direction = searchParams.get("direction"); diff --git a/app/api/admin/users/[userId]/route.ts b/app/api/admin/users/[userId]/route.ts index e09449e..d5be15a 100644 --- a/app/api/admin/users/[userId]/route.ts +++ b/app/api/admin/users/[userId]/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; import { getPool } from "@/services/database"; -import { isUserAuthenticated } from "@/lib/server-auth"; // DELETE - Supprimer complètement un utilisateur export async function DELETE( @@ -8,12 +7,6 @@ export async function DELETE( { params }: { params: Promise<{ userId: string }> } ) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const { userId } = await params; if (!userId) { diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts index cfdb5b1..dab0f08 100644 --- a/app/api/admin/users/route.ts +++ b/app/api/admin/users/route.ts @@ -1,16 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { getPool } from "@/services/database"; -import { isUserAuthenticated } from "@/lib/server-auth"; // GET - Récupérer la liste des utilisateurs export async function GET(request: NextRequest) { try { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); - if (!isAuthenticated) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - const pool = getPool(); // Récupérer tous les utilisateurs avec leurs informations d'équipe et d'évaluations diff --git a/app/api/evaluations/route.ts b/app/api/evaluations/route.ts index 5c60eb6..d733193 100644 --- a/app/api/evaluations/route.ts +++ b/app/api/evaluations/route.ts @@ -3,12 +3,11 @@ import { cookies } from "next/headers"; import { evaluationService } from "@/services/evaluation-service"; import { userService } from "@/services/user-service"; import { UserEvaluation, UserProfile } from "@/lib/types"; -import { COOKIE_NAME } from "@/lib/auth-utils"; export async function GET(request: NextRequest) { try { const cookieStore = await cookies(); - const userUuid = cookieStore.get(COOKIE_NAME)?.value; + const userUuid = cookieStore.get("peakSkills_userId")?.value; // Support pour l'ancien mode avec paramètres (pour la compatibilité) if (!userUuid) { diff --git a/app/evaluation/page.tsx b/app/evaluation/page.tsx index 31baec3..effb22f 100644 --- a/app/evaluation/page.tsx +++ b/app/evaluation/page.tsx @@ -1,27 +1,22 @@ import { redirect } from "next/navigation"; -import { - isUserAuthenticated, - getServerUserEvaluation, - getServerSkillCategories, - getServerTeams, -} from "@/lib/server-auth"; +import { AuthService } from "@/services"; +import { SkillsService, TeamsService } from "@/services"; +import { evaluationService } from "@/services/evaluation-service"; import { EvaluationClientWrapper } from "@/components/evaluation"; import { SkillEvaluation } from "@/components/skill-evaluation"; export default async function EvaluationPage() { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); + // Charger les données côté serveur + const userUuid = await AuthService.getUserUuidFromCookie(); - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { + if (!userUuid) { redirect("/login"); } - // Charger les données côté serveur const [userEvaluation, skillCategories, teams] = await Promise.all([ - getServerUserEvaluation(), - getServerSkillCategories(), - getServerTeams(), + evaluationService.getServerUserEvaluation(userUuid!), + SkillsService.getSkillCategories(), + TeamsService.getTeams(), ]); return ( diff --git a/app/login/page.tsx b/app/login/page.tsx index da96d1f..a8dff13 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,265 +1,46 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { ProfileForm } from "@/components/profile-form"; -import { AuthService } from "@/lib/auth-utils"; -import { UserProfile, Team } from "@/lib/types"; -import { Code2, LogOut, Edit, X, Home } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { redirect } from "next/navigation"; +import { TeamsService, userService } from "@/services"; +import { AuthService } from "@/services"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import Link from "next/link"; + LoginLayout, + LoginFormWrapper, + LoginLoading, +} from "@/components/login"; -interface LoginPageProps {} +export default async function LoginPage() { + try { + // Charger les équipes côté serveur + const teams = await TeamsService.getTeams(); -export default function LoginPage({}: LoginPageProps) { - const [teams, setTeams] = useState([]); - const [loading, setLoading] = useState(true); - const [authenticating, setAuthenticating] = useState(false); - const [currentUser, setCurrentUser] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const router = useRouter(); + // Vérifier si l'utilisateur est déjà connecté + const userUuid = await AuthService.getUserUuidFromCookie(); - useEffect(() => { - async function initialize() { - try { - // Vérifier si l'utilisateur est déjà connecté - const user = await AuthService.getCurrentUser(); - setCurrentUser(user); + if (userUuid) { + // Si l'utilisateur est connecté, récupérer son profil côté serveur + const userProfile = await userService.getUserByUuid(userUuid); - // Charger les équipes - const teamsResponse = await fetch("/api/teams"); - if (teamsResponse.ok) { - const teamsData = await teamsResponse.json(); - setTeams(teamsData); - } - } catch (error) { - console.error("Error initializing login page:", error); - } finally { - setLoading(false); + if (userProfile) { + // Passer le profil utilisateur pour permettre la modification + return ( + + + + ); } } - initialize(); - }, []); - - const handleSubmit = async (profile: UserProfile) => { - setAuthenticating(true); - try { - await AuthService.login(profile); - if (isEditing) { - // Si on modifie le profil existant, mettre à jour l'état - setCurrentUser(profile); - setIsEditing(false); - } else { - // Si c'est un nouveau login, rediriger - router.push("/"); - } - } catch (error) { - console.error("Login failed:", error); - // Vous pouvez ajouter une notification d'erreur ici - } finally { - setAuthenticating(false); - } - }; - - const handleLogout = async () => { - try { - await AuthService.logout(); - setCurrentUser(null); - setIsEditing(false); - } catch (error) { - console.error("Logout failed:", error); - } - }; - - const handleEdit = () => { - setIsEditing(true); - }; - - const handleCancelEdit = () => { - setIsEditing(false); - }; - - if (loading) { + // Si l'utilisateur n'est pas connecté, afficher le formulaire de connexion return ( -
-
-
- -
-
-
-
-

Chargement...

-
-
-
-
+ + + + ); + } catch (error) { + console.error("Error loading login page:", error); + return ( + + + ); } - - // Si l'utilisateur est connecté et qu'on ne modifie pas - if (currentUser && !isEditing) { - const currentTeam = teams.find((t) => t.id === currentUser.teamId); - - return ( -
-
-
- -
-
-
- - - PeakSkills - -
- -

- Vous êtes connecté -

-

- Gérez votre profil ou retournez à l'application -

-
- -
- {/* Informations utilisateur */} - - - Vos informations - - Profil actuellement connecté - - - -
-
- -

{currentUser.firstName}

-
-
- -

{currentUser.lastName}

-
-
-
- -

- {currentTeam?.name || "Équipe non trouvée"} -

-
-
-
- - {/* Actions */} -
- - - -
-
-
-
- ); - } - - // Sinon, afficher le formulaire (nouvel utilisateur ou modification) - return ( -
-
-
- -
-
-
- - - PeakSkills - -
- -

- {isEditing - ? "Modifier vos informations" - : "Bienvenue sur PeakSkills"} -

-

- {isEditing - ? "Mettez à jour vos informations personnelles" - : "Évaluez vos compétences techniques et suivez votre progression"} -

- - {isEditing && ( - - )} -
- -
-
- {authenticating && ( -
-
-
-

- {isEditing - ? "Mise à jour en cours..." - : "Connexion en cours..."} -

-
-
- )} - -
-
-
-
- ); } diff --git a/app/page.tsx b/app/page.tsx index 68cfe4a..558633b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,6 @@ import { redirect } from "next/navigation"; -import { - isUserAuthenticated, - getServerUserEvaluation, - getServerSkillCategories, - getServerTeams, -} from "@/lib/server-auth"; +import { AuthService } from "@/services"; +import { evaluationService, SkillsService, TeamsService } from "@/services"; import { generateRadarData } from "@/lib/evaluation-utils"; import { WelcomeHeader, @@ -16,19 +12,17 @@ import { } from "@/components/home"; export default async function HomePage() { - // Vérifier l'authentification - const isAuthenticated = await isUserAuthenticated(); + // Charger les données côté serveur + const userUuid = await AuthService.getUserUuidFromCookie(); - // Si pas de cookie d'authentification, rediriger vers login - if (!isAuthenticated) { + if (!userUuid) { redirect("/login"); } - // Charger les données côté serveur const [userEvaluation, skillCategories, teams] = await Promise.all([ - getServerUserEvaluation(), - getServerSkillCategories(), - getServerTeams(), + evaluationService.getServerUserEvaluation(userUuid!), + SkillsService.getSkillCategories(), + TeamsService.getTeams(), ]); // Si pas d'évaluation, afficher l'écran d'accueil diff --git a/clients/README.md b/clients/README.md new file mode 100644 index 0000000..afe9f30 --- /dev/null +++ b/clients/README.md @@ -0,0 +1,115 @@ +# API Clients Architecture + +Cette architecture respecte les principes SOLID en séparant les responsabilités par domaine métier et en évitant le code mort. + +## Structure + +``` +clients/ +├── base/ +│ └── http-client.ts # Classe de base avec logique HTTP commune +├── domains/ +│ ├── evaluation-client.ts # Client pour les évaluations (lecture + modification) +│ ├── teams-client.ts # Client pour la gestion des équipes (lecture + CRUD) +│ ├── skills-client.ts # Client pour les compétences (lecture + création) +│ ├── auth-client.ts # Client pour l'authentification (login, logout, getCurrentUser) +│ └── admin-client.ts # Client pour la gestion admin (skills, teams, users) +├── index.ts # Exports publics de tous les clients +├── client.ts # Wrapper client-side sécurisé +└── README.md # Ce fichier +``` + +## Services associés + +- **`services/auth-service.ts`** - Service d'authentification côté client (renommé depuis auth-utils) +- **`services/evaluation-service.ts`** - Service d'évaluation côté serveur +- **`services/teams-service.ts`** - Service des équipes côté serveur +- **`services/skills-service.ts`** - Service des compétences côté serveur + +## Principes + +- **Single Responsibility** : Chaque client gère un seul domaine métier +- **Open/Closed** : Facile d'étendre sans modifier le code existant +- **Liskov Substitution** : Tous les clients héritent de BaseHttpClient +- **Interface Segregation** : Chaque client expose uniquement ses méthodes +- **Dependency Inversion** : Dépend de l'abstraction BaseHttpClient + +## Clients par responsabilité + +### EvaluationClient + +- **`loadUserEvaluation()`** - Chargement d'une évaluation utilisateur +- **`saveUserEvaluation()`** - Sauvegarde d'une évaluation +- **`updateSkillLevel()`** - Mise à jour du niveau d'une skill +- **`updateSkillMentorStatus()`** - Mise à jour du statut mentor +- **`updateSkillLearningStatus()`** - Mise à jour du statut d'apprentissage +- **`addSkillToEvaluation()`** - Ajout d'une skill à l'évaluation +- **`removeSkillFromEvaluation()`** - Suppression d'une skill + +### SkillsClient + +- **`loadSkillCategories()`** - Chargement des catégories de skills +- **`createSkill()`** - Création d'une nouvelle skill + +### TeamsClient + +- **`loadTeams()`** - Chargement des équipes +- **`createTeam()`** - Création d'une équipe +- **`updateTeam()`** - Mise à jour d'une équipe +- **`deleteTeam()`** - Suppression d'une équipe + +### AuthClient + +- **`login()`** - Authentification d'un utilisateur +- **`getCurrentUser()`** - Récupération de l'utilisateur actuel +- **`logout()`** - Déconnexion d'un utilisateur + +### AdminClient + +- **`getSkills()`** - Récupération de toutes les skills +- **`createSkill()`** - Création d'une nouvelle skill +- **`updateSkill()`** - Mise à jour d'une skill +- **`deleteSkill()`** - Suppression d'une skill +- **`getTeams()`** - Récupération de toutes les équipes +- **`createTeam()`** - Création d'une nouvelle équipe +- **`updateTeam()`** - Mise à jour d'une équipe +- **`deleteTeam()`** - Suppression d'une équipe +- **`deleteDirection()`** - Suppression d'une direction +- **`getTeamMembers()`** - Récupération des membres d'une équipe +- **`removeTeamMember()`** - Suppression d'un membre d'équipe +- **`deleteUser()`** - Suppression d'un utilisateur + +## Utilisation + +### Import direct + +```typescript +import { + evaluationClient, + teamsClient, + skillsClient, + authClient, + adminClient, +} from "@/clients"; +``` + +### Import client-side sécurisé + +```typescript +import { + evaluationClient, + teamsClient, + skillsClient, + authClient, + adminClient, +} from "@/services/client"; +``` + +## Avantages + +- **Code mort supprimé** : Plus de méthodes dupliquées +- **Architecture simple** : Chaque client gère son domaine complet +- **Performance** : Seules les méthodes nécessaires sont importées +- **Maintenabilité** : Architecture claire et logique +- **Testabilité** : Chaque client peut être testé indépendamment +- **Séparation claire** : Client HTTP vs services métier diff --git a/clients/base/http-client.ts b/clients/base/http-client.ts new file mode 100644 index 0000000..1cb4e4e --- /dev/null +++ b/clients/base/http-client.ts @@ -0,0 +1,69 @@ +export abstract class BaseHttpClient { + protected baseUrl: string; + + constructor() { + this.baseUrl = process.env.NEXT_PUBLIC_API_URL || "/api/"; + } + + protected async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const defaultOptions: RequestInit = { + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + ...options, + }; + + try { + const response = await fetch(url, defaultOptions); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + // Handle empty responses + if ( + response.status === 204 || + response.headers.get("content-length") === "0" + ) { + return {} as T; + } + + return await response.json(); + } catch (error) { + console.error(`Request failed for ${endpoint}:`, error); + throw error; + } + } + + protected async get(endpoint: string): Promise { + return this.request(endpoint); + } + + protected async post(endpoint: string, data?: any): Promise { + return this.request(endpoint, { + method: "POST", + body: data ? JSON.stringify(data) : undefined, + }); + } + + protected async put(endpoint: string, data?: any): Promise { + return this.request(endpoint, { + method: "PUT", + body: data ? JSON.stringify(data) : undefined, + }); + } + + protected async delete(endpoint: string, data?: any): Promise { + return this.request(endpoint, { + method: "DELETE", + body: data ? JSON.stringify(data) : undefined, + }); + } +} diff --git a/clients/domains/admin-client.ts b/clients/domains/admin-client.ts new file mode 100644 index 0000000..6bbff22 --- /dev/null +++ b/clients/domains/admin-client.ts @@ -0,0 +1,86 @@ +import { BaseHttpClient } from "../base/http-client"; + +export interface Skill { + id: string; + name: string; + description: string; + icon: string; + categoryId: string; + category: string; + usageCount: number; +} + +export interface Team { + id: string; + name: string; + direction: string; + memberCount: number; +} + +export interface TeamMember { + id: string; + firstName: string; + lastName: string; + fullName: string; + joinedAt: string; +} + +export class AdminClient extends BaseHttpClient { + // Skills Management + async getSkills(): Promise { + return await this.get(`/admin/skills`); + } + + async createSkill( + skillData: Omit + ): Promise { + return await this.post(`/admin/skills`, skillData); + } + + async updateSkill(skillData: Skill): Promise { + return await this.put(`/admin/skills`, skillData); + } + + async deleteSkill(skillId: string): Promise { + await this.delete(`/admin/skills?id=${skillId}`); + } + + // Teams Management + async getTeams(): Promise { + return await this.get(`/admin/teams`); + } + + async createTeam(teamData: Omit): Promise { + return await this.post(`/admin/teams`, teamData); + } + + async updateTeam(teamData: Team): Promise { + return await this.put(`/admin/teams`, teamData); + } + + async deleteTeam(teamId: string): Promise { + await this.delete(`/admin/teams?id=${teamId}`); + } + + async deleteDirection(direction: string): Promise { + await this.delete( + `/admin/teams?direction=${encodeURIComponent(direction)}` + ); + } + + // Team Members + async getTeamMembers(teamId: string): Promise { + return await this.get(`/admin/teams/${teamId}/members`); + } + + async removeTeamMember(teamId: string, memberId: string): Promise { + await this.delete(`/admin/teams/${teamId}/members`, { + memberId, + }); + } + + // User Management + async deleteUser(userId: string): Promise { + await this.delete(`/admin/users/${userId}`); + } +} diff --git a/clients/domains/auth-client.ts b/clients/domains/auth-client.ts new file mode 100644 index 0000000..cdaaace --- /dev/null +++ b/clients/domains/auth-client.ts @@ -0,0 +1,33 @@ +import { BaseHttpClient } from "../base/http-client"; +import { UserProfile } from "../../lib/types"; + +export class AuthClient extends BaseHttpClient { + /** + * Authentifie un utilisateur et créé le cookie + */ + async login( + profile: UserProfile + ): Promise<{ user: UserProfile & { uuid: string }; userUuid: string }> { + return await this.post("/auth", profile); + } + + /** + * Récupère l'utilisateur actuel depuis le cookie + */ + async getCurrentUser(): Promise { + try { + const response = await this.get<{ user: UserProfile }>("/auth"); + return response.user; + } catch (error) { + console.error("Failed to get current user:", error); + return null; + } + } + + /** + * Déconnecte l'utilisateur (supprime le cookie) + */ + async logout(): Promise { + await this.delete("/auth"); + } +} diff --git a/clients/domains/skills-client.ts b/clients/domains/skills-client.ts new file mode 100644 index 0000000..5f033e1 --- /dev/null +++ b/clients/domains/skills-client.ts @@ -0,0 +1,26 @@ +import { BaseHttpClient } from "../base/http-client"; +import { SkillCategory } from "../../lib/types"; + +export class SkillsClient extends BaseHttpClient { + /** + * Crée une nouvelle skill + */ + async createSkill( + categoryId: string, + skill: { + id: string; + name: string; + description: string; + icon?: string; + links: string[]; + } + ): Promise { + try { + await this.post(`/skills/${categoryId}`, skill); + return true; + } catch (error) { + console.error("Erreur lors de la création de la skill:", error); + return false; + } + } +} diff --git a/clients/index.ts b/clients/index.ts new file mode 100644 index 0000000..1fbcb9b --- /dev/null +++ b/clients/index.ts @@ -0,0 +1,14 @@ +// Import all client classes first +import { SkillsClient } from "./domains/skills-client"; +import { AuthClient } from "./domains/auth-client"; +import { AdminClient } from "./domains/admin-client"; + +// Export all client classes +export { SkillsClient } from "./domains/skills-client"; +export { AuthClient } from "./domains/auth-client"; +export { AdminClient } from "./domains/admin-client"; + +// Create and export client instances +export const skillsClient = new SkillsClient(); +export const authClient = new AuthClient(); +export const adminClient = new AdminClient(); diff --git a/components/admin/management/team-members-modal.tsx b/components/admin/management/team-members-modal.tsx index e76442a..d9ea0c6 100644 --- a/components/admin/management/team-members-modal.tsx +++ b/components/admin/management/team-members-modal.tsx @@ -6,8 +6,8 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; -import { TeamMember } from "@/services/admin-management-service"; -import { AdminManagementService } from "@/services/admin-management-service"; +import { TeamMember } from "@/clients/domains/admin-client"; +import { adminClient } from "@/clients"; import { useToast } from "@/hooks/use-toast"; interface TeamMembersModalProps { @@ -41,7 +41,7 @@ export function TeamMembersModal({ setIsLoading(true); setError(null); try { - const membersData = await AdminManagementService.getTeamMembers(teamId); + const membersData = await adminClient.getTeamMembers(teamId); setMembers(membersData); } catch (err: any) { setError(err.message || "Erreur lors du chargement des membres"); @@ -59,7 +59,7 @@ export function TeamMembersModal({ setDeletingMemberId(memberId); try { - await AdminManagementService.removeTeamMember(teamId, memberId); + await adminClient.removeTeamMember(teamId, memberId); // Mettre à jour la liste locale setMembers((prev) => prev.filter((member) => member.id !== memberId)); diff --git a/components/admin/overview/admin-client-wrapper.tsx b/components/admin/overview/admin-client-wrapper.tsx index e391913..0b6f274 100644 --- a/components/admin/overview/admin-client-wrapper.tsx +++ b/components/admin/overview/admin-client-wrapper.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Team, SkillCategory } from "@/lib/types"; -import { TeamStats, DirectionStats } from "@/services/admin-service"; +import { TeamStats, DirectionStats } from "@/lib/admin-types"; import { TeamDetailModal } from "../team-detail/team-detail-modal"; import { AdminHeader } from "../utils/admin-header"; import { AdminOverviewCards } from "./admin-overview-cards"; diff --git a/components/admin/overview/admin-content-tabs.tsx b/components/admin/overview/admin-content-tabs.tsx index b726b57..15dbe02 100644 --- a/components/admin/overview/admin-content-tabs.tsx +++ b/components/admin/overview/admin-content-tabs.tsx @@ -2,7 +2,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Users, Building2 } from "lucide-react"; -import { TeamStats, DirectionStats } from "@/services/admin-service"; +import { TeamStats, DirectionStats } from "@/lib/admin-types"; import { DirectionOverview, TeamStatsCard } from "@/components/admin"; interface AdminContentTabsProps { diff --git a/components/admin/overview/admin-overview-cards.tsx b/components/admin/overview/admin-overview-cards.tsx index e059981..453be99 100644 --- a/components/admin/overview/admin-overview-cards.tsx +++ b/components/admin/overview/admin-overview-cards.tsx @@ -2,7 +2,7 @@ import { Users, Target, Building2, UserCheck } from "lucide-react"; import { Team, SkillCategory } from "@/lib/types"; -import { TeamStats, DirectionStats } from "@/services/admin-service"; +import { TeamStats, DirectionStats } from "@/lib/admin-types"; interface AdminOverviewCardsProps { teams: Team[]; diff --git a/components/admin/skills/skills-list.tsx b/components/admin/skills/skills-list.tsx index addd937..6b4a5b7 100644 --- a/components/admin/skills/skills-list.tsx +++ b/components/admin/skills/skills-list.tsx @@ -11,7 +11,7 @@ import { } from "lucide-react"; import { TreeCategoryHeader, TreeItemRow } from "@/components/admin"; import { TechIcon } from "@/components/icons/tech-icon"; -import { Skill } from "@/services/admin-management-service"; +import { Skill } from "@/clients/domains/admin-client"; interface SkillsListProps { filteredSkillsByCategory: Record; diff --git a/components/admin/team-detail/team-detail-client-wrapper.tsx b/components/admin/team-detail/team-detail-client-wrapper.tsx index 8d5185f..a2f4201 100644 --- a/components/admin/team-detail/team-detail-client-wrapper.tsx +++ b/components/admin/team-detail/team-detail-client-wrapper.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect } from "react"; -import { TeamStats, TeamMember } from "@/services/admin-service"; +import { TeamStats, TeamMember } from "@/lib/admin-types"; import { TeamDetailHeader } from "./team-detail-header"; import { TeamMetricsCards } from "./team-metrics-cards"; import { TeamDetailTabs } from "./team-detail-tabs"; diff --git a/components/admin/team-detail/team-detail-modal.tsx b/components/admin/team-detail/team-detail-modal.tsx index bf03a48..d49dca1 100644 --- a/components/admin/team-detail/team-detail-modal.tsx +++ b/components/admin/team-detail/team-detail-modal.tsx @@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Users, ExternalLink, Download, Eye } from "lucide-react"; -import { TeamMember } from "@/services/admin-service"; +import { TeamMember } from "@/lib/admin-types"; interface TeamDetailModalProps { isOpen: boolean; diff --git a/components/admin/team-detail/team-detail-tabs.tsx b/components/admin/team-detail/team-detail-tabs.tsx index 03ea865..3dcab23 100644 --- a/components/admin/team-detail/team-detail-tabs.tsx +++ b/components/admin/team-detail/team-detail-tabs.tsx @@ -5,7 +5,7 @@ import { TeamOverviewTab } from "./team-overview-tab"; import { TeamSkillsTab } from "./team-skills-tab"; import { TeamMembersTab } from "./team-members-tab"; import { TeamInsightsTab } from "./team-insights-tab"; -import { TeamStats, TeamMember } from "@/services/admin-service"; +import { TeamStats, TeamMember } from "@/lib/admin-types"; interface SkillAnalysis { skillName: string; diff --git a/components/admin/team-detail/team-member-modal.tsx b/components/admin/team-detail/team-member-modal.tsx index b68f6ba..ef006a1 100644 --- a/components/admin/team-detail/team-member-modal.tsx +++ b/components/admin/team-detail/team-member-modal.tsx @@ -9,7 +9,7 @@ import { import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { User, Award, BookOpen, X } from "lucide-react"; -import { TeamMember } from "@/services/admin-service"; +import { TeamMember } from "@/lib/admin-types"; interface TeamMemberModalProps { isOpen: boolean; diff --git a/components/admin/team-detail/team-members-tab.tsx b/components/admin/team-detail/team-members-tab.tsx index 44c79b8..08530ce 100644 --- a/components/admin/team-detail/team-members-tab.tsx +++ b/components/admin/team-detail/team-members-tab.tsx @@ -1,7 +1,7 @@ "use client"; import { User, Award, BookOpen } from "lucide-react"; -import { TeamMember } from "@/services/admin-service"; +import { TeamMember } from "@/lib/admin-types"; interface TeamMembersTabProps { members: TeamMember[]; diff --git a/components/admin/team-detail/team-overview-tab.tsx b/components/admin/team-detail/team-overview-tab.tsx index 1a87375..f82a154 100644 --- a/components/admin/team-detail/team-overview-tab.tsx +++ b/components/admin/team-detail/team-overview-tab.tsx @@ -1,7 +1,7 @@ "use client"; import { BarChart3, Target, Star } from "lucide-react"; -import { TeamStats } from "@/services/admin-service"; +import { TeamStats } from "@/lib/admin-types"; import { TechIcon } from "@/components/icons/tech-icon"; interface SkillAnalysis { diff --git a/components/admin/teams/teams-list.tsx b/components/admin/teams/teams-list.tsx index 47114b2..41057e7 100644 --- a/components/admin/teams/teams-list.tsx +++ b/components/admin/teams/teams-list.tsx @@ -1,9 +1,13 @@ "use client"; import { Users, Building2 } from "lucide-react"; -import { TreeCategoryHeader, TreeItemRow, TeamMetrics } from "@/components/admin"; +import { + TreeCategoryHeader, + TreeItemRow, + TeamMetrics, +} from "@/components/admin"; import { Team as TeamType } from "@/lib/types"; -import { TeamStats } from "@/services/admin-service"; +import { TeamStats } from "@/lib/admin-types"; interface TeamsListProps { filteredTeamsByDirection: Record; diff --git a/components/admin/teams/teams-management-page.tsx b/components/admin/teams/teams-management-page.tsx index 0632fed..1d19b99 100644 --- a/components/admin/teams/teams-management-page.tsx +++ b/components/admin/teams/teams-management-page.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Plus, Building2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SkillCategory, Team as TeamType } from "@/lib/types"; -import { TeamStats } from "@/services/admin-service"; +import { TeamStats } from "@/lib/admin-types"; import { TreeViewPage } from "../management/tree-view-page"; import { useTreeView } from "@/hooks/use-tree-view"; import { useFormDialog } from "@/hooks/use-form-dialog"; @@ -27,9 +27,16 @@ export function TeamsManagementPage({ const [searchTerm, setSearchTerm] = useState(""); const [isMembersModalOpen, setIsMembersModalOpen] = useState(false); const [selectedTeam, setSelectedTeam] = useState(null); - - const { isCreateDialogOpen, isEditDialogOpen, openCreateDialog, closeCreateDialog, openEditDialog, closeEditDialog } = useFormDialog(); - + + const { + isCreateDialogOpen, + isEditDialogOpen, + openCreateDialog, + closeCreateDialog, + openEditDialog, + closeEditDialog, + } = useFormDialog(); + const { teams: localTeams, teamStats: localTeamStats, diff --git a/components/admin/users/user-form-dialog.tsx b/components/admin/users/user-form-dialog.tsx index 907aa7e..6d48f65 100644 --- a/components/admin/users/user-form-dialog.tsx +++ b/components/admin/users/user-form-dialog.tsx @@ -16,7 +16,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Team } from "@/services/admin-management-service"; +import { Team } from "@/clients/domains/admin-client"; interface UserFormData { firstName: string; @@ -98,7 +98,11 @@ export function UserFormDialog({ Annuler
diff --git a/components/admin/users/users-management-page.tsx b/components/admin/users/users-management-page.tsx index e8e10f7..ba58743 100644 --- a/components/admin/users/users-management-page.tsx +++ b/components/admin/users/users-management-page.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { Users, Building2 } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Team } from "@/services/admin-management-service"; +import { Team } from "@/clients/domains/admin-client"; import { TreeViewPage } from "../management/tree-view-page"; import { useTreeView } from "@/hooks/use-tree-view"; import { useFormDialog } from "@/hooks/use-form-dialog"; @@ -17,9 +17,10 @@ interface UsersManagementPageProps { export function UsersManagementPage({ teams }: UsersManagementPageProps) { const [searchTerm, setSearchTerm] = useState(""); - - const { isCreateDialogOpen, openCreateDialog, closeCreateDialog } = useFormDialog(); - + + const { isCreateDialogOpen, openCreateDialog, closeCreateDialog } = + useFormDialog(); + const { users, isLoading, diff --git a/components/admin/utils/admin-filters.tsx b/components/admin/utils/admin-filters.tsx index d4fdd3e..db72442 100644 --- a/components/admin/utils/admin-filters.tsx +++ b/components/admin/utils/admin-filters.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Users, Target, Building2, Filter } from "lucide-react"; import { Team } from "@/lib/types"; -import { TeamStats } from "@/services/admin-service"; +import { TeamStats } from "@/lib/admin-types"; import { MultiSelectFilter } from "./multi-select-filter"; interface AdminFiltersProps { diff --git a/components/create-skill-form.tsx b/components/create-skill-form.tsx index 3af5bef..908d517 100644 --- a/components/create-skill-form.tsx +++ b/components/create-skill-form.tsx @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Plus, X, Link as LinkIcon, Loader2 } from "lucide-react"; -import { apiClient } from "@/services/client"; +import { skillsClient } from "@/clients"; interface CreateSkillFormProps { categoryName: string; @@ -29,31 +29,31 @@ export function CreateSkillForm({ const [error, setError] = useState(""); const addLink = () => { - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - links: [...prev.links, ""] + links: [...prev.links, ""], })); }; const removeLink = (index: number) => { - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - links: prev.links.filter((_, i) => i !== index) + links: prev.links.filter((_, i) => i !== index), })); }; const updateLink = (index: number, value: string) => { - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - links: prev.links.map((link, i) => i === index ? value : link) + links: prev.links.map((link, i) => (i === index ? value : link)), })); }; const generateSkillId = (name: string) => { return name .toLowerCase() - .replace(/[^a-z0-9\s]/g, '') - .replace(/\s+/g, '-') + .replace(/[^a-z0-9\s]/g, "") + .replace(/\s+/g, "-") .trim(); }; @@ -80,7 +80,7 @@ export function CreateSkillForm({ const categoryId = getCategoryId(categoryName); // Filtrer les liens vides - const validLinks = formData.links.filter(link => link.trim()); + const validLinks = formData.links.filter((link) => link.trim()); const skillData = { id: skillId, @@ -90,7 +90,7 @@ export function CreateSkillForm({ links: validLinks, }; - const success = await apiClient.createSkill(categoryId, skillData); + const success = await skillsClient.createSkill(categoryId, skillData); if (success) { onSuccess(skillId); @@ -118,7 +118,9 @@ export function CreateSkillForm({ id="skill-name" placeholder="ex: Next.js, Docker, Figma..." value={formData.name} - onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} + onChange={(e) => + setFormData((prev) => ({ ...prev, name: e.target.value })) + } disabled={isLoading} />
@@ -129,7 +131,9 @@ export function CreateSkillForm({ id="skill-description" placeholder="Décrivez brièvement cette compétence..." value={formData.description} - onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} + onChange={(e) => + setFormData((prev) => ({ ...prev, description: e.target.value })) + } disabled={isLoading} rows={3} /> @@ -141,11 +145,14 @@ export function CreateSkillForm({ id="skill-icon" placeholder="ex: fab-react, fas-database..." value={formData.icon} - onChange={(e) => setFormData(prev => ({ ...prev, icon: e.target.value }))} + onChange={(e) => + setFormData((prev) => ({ ...prev, icon: e.target.value })) + } disabled={isLoading} />

- Utilisez les classes FontAwesome (fab-, fas-, far-) ou laissez vide pour l'icône par défaut + Utilisez les classes FontAwesome (fab-, fas-, far-) ou laissez vide + pour l'icône par défaut

@@ -205,7 +212,12 @@ export function CreateSkillForm({ )} -
diff --git a/components/login/index.ts b/components/login/index.ts new file mode 100644 index 0000000..a2e48cd --- /dev/null +++ b/components/login/index.ts @@ -0,0 +1,3 @@ +export { LoginFormWrapper } from "./login-form-wrapper"; +export { LoginLoading } from "./login-loading"; +export { LoginLayout } from "./login-layout"; diff --git a/components/login/login-form-wrapper.tsx b/components/login/login-form-wrapper.tsx new file mode 100644 index 0000000..55e25c2 --- /dev/null +++ b/components/login/login-form-wrapper.tsx @@ -0,0 +1,213 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { ProfileForm } from "@/components/profile-form"; +import { authClient } from "@/clients"; +import { UserProfile, Team } from "@/lib/types"; +import { Code2, LogOut, Edit, X, Home } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +interface LoginFormWrapperProps { + teams: Team[]; + initialUser?: UserProfile | null; +} + +export function LoginFormWrapper({ + teams, + initialUser, +}: LoginFormWrapperProps) { + const [authenticating, setAuthenticating] = useState(false); + const [currentUser, setCurrentUser] = useState( + initialUser || null + ); + const [isEditing, setIsEditing] = useState(false); + const router = useRouter(); + + useEffect(() => { + if (initialUser) { + setCurrentUser(initialUser); + } + }, [initialUser]); + + const handleSubmit = async (profile: UserProfile) => { + setAuthenticating(true); + try { + await authClient.login(profile); + if (isEditing) { + // Si on modifie le profil existant, mettre à jour l'état + setCurrentUser(profile); + setIsEditing(false); + } else { + // Si c'est un nouveau login, rediriger + router.push("/"); + } + } catch (error) { + console.error("Login failed:", error); + // Vous pouvez ajouter une notification d'erreur ici + } finally { + setAuthenticating(false); + } + }; + + const handleLogout = async () => { + try { + await authClient.logout(); + setCurrentUser(null); + setIsEditing(false); + } catch (error) { + console.error("Logout failed:", error); + } + }; + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleCancelEdit = () => { + setIsEditing(false); + }; + + // Si l'utilisateur est connecté et qu'on ne modifie pas + if (currentUser && !isEditing) { + const currentTeam = teams.find((t) => t.id === currentUser.teamId); + + return ( +
+
+ + PeakSkills +
+ +

+ Vous êtes connecté +

+

+ Gérez votre profil ou retournez à l'application +

+ +
+ {/* Informations utilisateur */} + + + Vos informations + + Profil actuellement connecté + + + +
+
+ +

{currentUser.firstName}

+
+
+ +

{currentUser.lastName}

+
+
+
+ +

+ {currentTeam?.name || "Équipe non trouvée"} +

+
+
+
+ + {/* Actions */} +
+ + + +
+
+
+ ); + } + + // Sinon, afficher le formulaire (nouvel utilisateur ou modification) + return ( +
+
+ + PeakSkills +
+ +

+ {isEditing ? "Modifier vos informations" : "Bienvenue sur PeakSkills"} +

+

+ {isEditing + ? "Mettez à jour vos informations personnelles" + : "Évaluez vos compétences techniques et suivez votre progression"} +

+ + {isEditing && ( + + )} + +
+
+ {authenticating && ( +
+
+
+

+ {isEditing + ? "Mise à jour en cours..." + : "Connexion en cours..."} +

+
+
+ )} + +
+
+
+ ); +} diff --git a/components/login/login-layout.tsx b/components/login/login-layout.tsx new file mode 100644 index 0000000..e80292c --- /dev/null +++ b/components/login/login-layout.tsx @@ -0,0 +1,18 @@ +import { Code2 } from "lucide-react"; + +interface LoginLayoutProps { + children: React.ReactNode; +} + +export function LoginLayout({ children }: LoginLayoutProps) { + return ( +
+
+
+ +
+ {children} +
+
+ ); +} diff --git a/components/login/login-loading.tsx b/components/login/login-loading.tsx new file mode 100644 index 0000000..3f91e31 --- /dev/null +++ b/components/login/login-loading.tsx @@ -0,0 +1,17 @@ +export function LoginLoading() { + return ( +
+
+
+ +
+
+
+
+

Chargement...

+
+
+
+
+ ); +} diff --git a/hooks/use-evaluation.ts b/hooks/use-evaluation.ts deleted file mode 100644 index 0fbe617..0000000 --- a/hooks/use-evaluation.ts +++ /dev/null @@ -1,429 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { - UserEvaluation, - SkillCategory, - Team, - CategoryEvaluation, - UserProfile, - SkillLevel, -} from "@/lib/types"; -import { - loadUserEvaluation, - saveUserEvaluation, - createEmptyEvaluation, -} from "@/lib/evaluation-utils"; -import { apiClient } from "@/services/api-client"; -import { loadSkillCategories, loadTeams } from "@/lib/data-loader"; -import { AuthService } from "@/lib/auth-utils"; - -// Fonction pour migrer une évaluation existante avec de nouvelles catégories -function migrateEvaluation( - evaluation: UserEvaluation, - allCategories: SkillCategory[] -): UserEvaluation { - const existingCategoryNames = evaluation.evaluations.map((e) => e.category); - const missingCategories = allCategories.filter( - (cat) => !existingCategoryNames.includes(cat.category) - ); - - if (missingCategories.length === 0) { - return evaluation; // Pas de migration nécessaire - } - - console.log( - "🔄 Migrating evaluation with new categories:", - missingCategories.map((c) => c.category) - ); - - const newCategoryEvaluations: CategoryEvaluation[] = missingCategories.map( - (category) => ({ - category: category.category, - skills: [], - selectedSkillIds: [], - }) - ); - - return { - ...evaluation, - evaluations: [...evaluation.evaluations, ...newCategoryEvaluations], - lastUpdated: new Date().toISOString(), - }; -} - -export function useEvaluation() { - const [userEvaluation, setUserEvaluation] = useState( - null - ); - const [skillCategories, setSkillCategories] = useState([]); - const [teams, setTeams] = useState([]); - const [loading, setLoading] = useState(true); - - // Load initial data - useEffect(() => { - async function initializeData() { - try { - const [categories, teamsData] = await Promise.all([ - loadSkillCategories(), - loadTeams(), - ]); - - setSkillCategories(categories); - setTeams(teamsData); - - // Try to load user profile from cookie and then load evaluation from API - try { - const profile = await AuthService.getCurrentUser(); - if (profile) { - const saved = await loadUserEvaluation(profile); - if (saved) { - // Migrate evaluation to include new categories if needed - const migratedEvaluation = migrateEvaluation(saved, categories); - setUserEvaluation(migratedEvaluation); - if (migratedEvaluation !== saved) { - await saveUserEvaluation(migratedEvaluation); // Save the migrated version - } - } - } - } catch (profileError) { - console.error("Failed to load user profile:", profileError); - } - } catch (error) { - console.error("Failed to initialize data:", error); - } finally { - setLoading(false); - } - } - - initializeData(); - }, []); - - const loadEvaluationForProfile = async (profile: UserProfile) => { - try { - setLoading(true); - const saved = await loadUserEvaluation(profile); - if (saved) { - // Migrate evaluation to include new categories if needed - const migratedEvaluation = migrateEvaluation(saved, skillCategories); - setUserEvaluation(migratedEvaluation); - if (migratedEvaluation !== saved) { - await saveUserEvaluation(migratedEvaluation); // Save the migrated version - } - } else { - // Create new evaluation - const evaluations = createEmptyEvaluation(skillCategories); - const newEvaluation: UserEvaluation = { - profile, - evaluations, - lastUpdated: new Date().toISOString(), - }; - setUserEvaluation(newEvaluation); - } - } catch (error) { - console.error("Failed to load evaluation for profile:", error); - } finally { - setLoading(false); - } - }; - - const reloadSkillCategories = async () => { - try { - const categories = await loadSkillCategories(); - setSkillCategories(categories); - - // Si on a une évaluation en cours, la migrer avec les nouvelles catégories - if (userEvaluation) { - const migratedEvaluation = migrateEvaluation( - userEvaluation, - categories - ); - if (migratedEvaluation !== userEvaluation) { - setUserEvaluation(migratedEvaluation); - await saveUserEvaluation(migratedEvaluation); - } - } - } catch (error) { - console.error("Failed to reload skill categories:", error); - } - }; - - const updateProfile = async (profile: UserProfile) => { - const evaluations = - userEvaluation?.evaluations || createEmptyEvaluation(skillCategories); - const newEvaluation: UserEvaluation = { - profile, - evaluations, - lastUpdated: new Date().toISOString(), - }; - - // Authenticate user and create cookie - await AuthService.login(profile); - - setUserEvaluation(newEvaluation); - await saveUserEvaluation(newEvaluation); - }; - - const updateSkillLevel = async ( - category: string, - skillId: string, - level: SkillLevel - ) => { - if (!userEvaluation) return; - - try { - // Optimistic update - const updatedEvaluations = userEvaluation.evaluations.map((catEval) => { - if (catEval.category === category) { - const existingSkill = catEval.skills.find( - (s) => s.skillId === skillId - ); - const updatedSkills = existingSkill - ? catEval.skills.map((skill) => - skill.skillId === skillId ? { ...skill, level } : skill - ) - : [...catEval.skills, { skillId, level }]; - - return { - ...catEval, - skills: updatedSkills, - }; - } - return catEval; - }); - - const newEvaluation: UserEvaluation = { - ...userEvaluation, - evaluations: updatedEvaluations, - lastUpdated: new Date().toISOString(), - }; - - setUserEvaluation(newEvaluation); - - // Update via API - await apiClient.updateSkillLevel( - userEvaluation.profile, - category, - skillId, - level - ); - } catch (error) { - console.error("Failed to update skill level:", error); - // Revert optimistic update if needed - } - }; - - const updateSkillMentorStatus = async ( - category: string, - skillId: string, - canMentor: boolean - ) => { - if (!userEvaluation) return; - - try { - const updatedEvaluations = userEvaluation.evaluations.map((catEval) => { - if (catEval.category === category) { - const updatedSkills = catEval.skills.map((skill) => - skill.skillId === skillId ? { ...skill, canMentor } : skill - ); - - return { - ...catEval, - skills: updatedSkills, - }; - } - return catEval; - }); - - const newEvaluation: UserEvaluation = { - ...userEvaluation, - evaluations: updatedEvaluations, - lastUpdated: new Date().toISOString(), - }; - - setUserEvaluation(newEvaluation); - await apiClient.updateSkillMentorStatus( - userEvaluation.profile, - category, - skillId, - canMentor - ); - } catch (error) { - console.error("Failed to update skill mentor status:", error); - } - }; - - const updateSkillLearningStatus = async ( - category: string, - skillId: string, - wantsToLearn: boolean - ) => { - if (!userEvaluation) return; - - try { - const updatedEvaluations = userEvaluation.evaluations.map((catEval) => { - if (catEval.category === category) { - const updatedSkills = catEval.skills.map((skill) => - skill.skillId === skillId ? { ...skill, wantsToLearn } : skill - ); - - return { - ...catEval, - skills: updatedSkills, - }; - } - return catEval; - }); - - const newEvaluation: UserEvaluation = { - ...userEvaluation, - evaluations: updatedEvaluations, - lastUpdated: new Date().toISOString(), - }; - - setUserEvaluation(newEvaluation); - await apiClient.updateSkillLearningStatus( - userEvaluation.profile, - category, - skillId, - wantsToLearn - ); - } catch (error) { - console.error("Failed to update skill learning status:", error); - } - }; - - const addSkillToEvaluation = async (category: string, skillId: string) => { - if (!userEvaluation) return; - - // Sauvegarder l'état actuel pour le rollback - const previousEvaluation = userEvaluation; - - try { - // Optimistic UI update - mettre à jour immédiatement l'interface - const updatedEvaluations = userEvaluation.evaluations.map((catEval) => { - if (catEval.category === category) { - if (!catEval.selectedSkillIds.includes(skillId)) { - return { - ...catEval, - selectedSkillIds: [...catEval.selectedSkillIds, skillId], - skills: [ - ...catEval.skills, - { skillId, level: null, canMentor: false, wantsToLearn: false }, - ], - }; - } - } - return catEval; - }); - - const newEvaluation: UserEvaluation = { - ...userEvaluation, - evaluations: updatedEvaluations, - lastUpdated: new Date().toISOString(), - }; - - setUserEvaluation(newEvaluation); - - // Appel API en arrière-plan - await apiClient.addSkillToEvaluation( - userEvaluation.profile, - category, - skillId - ); - } catch (error) { - console.error("Failed to add skill to evaluation:", error); - // Rollback optimiste en cas d'erreur - setUserEvaluation(previousEvaluation); - // Optionnel: afficher une notification d'erreur à l'utilisateur - } - }; - - const removeSkillFromEvaluation = async ( - category: string, - skillId: string - ) => { - if (!userEvaluation) return; - - // Sauvegarder l'état actuel pour le rollback - const previousEvaluation = userEvaluation; - - try { - // Optimistic UI update - mettre à jour immédiatement l'interface - const updatedEvaluations = userEvaluation.evaluations.map((catEval) => { - if (catEval.category === category) { - return { - ...catEval, - selectedSkillIds: catEval.selectedSkillIds.filter( - (id) => id !== skillId - ), - skills: catEval.skills.filter((skill) => skill.skillId !== skillId), - }; - } - return catEval; - }); - - const newEvaluation: UserEvaluation = { - ...userEvaluation, - evaluations: updatedEvaluations, - lastUpdated: new Date().toISOString(), - }; - - setUserEvaluation(newEvaluation); - - // Appel API en arrière-plan - await apiClient.removeSkillFromEvaluation( - userEvaluation.profile, - category, - skillId - ); - } catch (error) { - console.error("Failed to remove skill from evaluation:", error); - // Rollback optimiste en cas d'erreur - setUserEvaluation(previousEvaluation); - // Optionnel: afficher une notification d'erreur à l'utilisateur - } - }; - - const initializeEmptyEvaluation = async (profile: UserProfile) => { - const evaluations = createEmptyEvaluation(skillCategories); - const newEvaluation: UserEvaluation = { - profile, - evaluations, - lastUpdated: new Date().toISOString(), - }; - - // Authenticate user and create cookie - await AuthService.login(profile); - - setUserEvaluation(newEvaluation); - await saveUserEvaluation(newEvaluation); - }; - - const clearUserProfile = async () => { - try { - await AuthService.logout(); - setUserEvaluation(null); - } catch (error) { - console.error("Failed to logout:", error); - setUserEvaluation(null); - } - }; - - return { - userEvaluation, - skillCategories, - teams, - loading, - loadEvaluationForProfile, - updateProfile, - updateSkillLevel, - updateSkillMentorStatus, - updateSkillLearningStatus, - addSkillToEvaluation, - removeSkillFromEvaluation, - initializeEmptyEvaluation, - clearUserProfile, - reloadSkillCategories, - }; -} diff --git a/hooks/use-skills-management.ts b/hooks/use-skills-management.ts index 19113c6..2821394 100644 --- a/hooks/use-skills-management.ts +++ b/hooks/use-skills-management.ts @@ -1,10 +1,8 @@ import { useState, useEffect } from "react"; import { useToast } from "@/hooks/use-toast"; import { SkillCategory } from "@/lib/types"; -import { - AdminManagementService, - Skill, -} from "@/services/admin-management-service"; +import { adminClient } from "@/clients"; +import { Skill } from "@/clients/domains/admin-client"; interface SkillFormData { name: string; @@ -30,7 +28,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) { const fetchSkills = async () => { try { setIsLoading(true); - const skillsData = await AdminManagementService.getSkills(); + const skillsData = await adminClient.getSkills(); setSkills(skillsData); } catch (error) { console.error("Error fetching skills:", error); @@ -74,7 +72,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) { category: category.category, }; - const newSkill = await AdminManagementService.createSkill(skillData); + const newSkill = await adminClient.createSkill(skillData); setSkills([...skills, newSkill]); resetForm(); @@ -130,7 +128,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) { usageCount: editingSkill.usageCount, }; - const updatedSkill = await AdminManagementService.updateSkill(skillData); + const updatedSkill = await adminClient.updateSkill(skillData); const updatedSkills = skills.map((skill) => skill.id === editingSkill.id ? updatedSkill : skill @@ -167,7 +165,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) { } try { - await AdminManagementService.deleteSkill(skillId); + await adminClient.deleteSkill(skillId); setSkills(skills.filter((s) => s.id !== skillId)); toast({ title: "Succès", diff --git a/hooks/use-teams-management.ts b/hooks/use-teams-management.ts index 2c58eb2..4af7e0b 100644 --- a/hooks/use-teams-management.ts +++ b/hooks/use-teams-management.ts @@ -1,11 +1,8 @@ import { useState, useEffect } from "react"; import { useToast } from "@/hooks/use-toast"; import { Team as TeamType } from "@/lib/types"; -import { TeamStats } from "@/services/admin-service"; -import { - AdminManagementService, - Team, -} from "@/services/admin-management-service"; +import { TeamStats } from "@/lib/admin-types"; +import { adminClient } from "@/clients"; interface TeamFormData { name: string; @@ -29,7 +26,7 @@ export function useTeamsManagement( // Charger les teams depuis l'API const fetchTeams = async () => { try { - const teamsData = await AdminManagementService.getTeams(); + const teamsData = await adminClient.getTeams(); // Note: on garde les teams existantes pour la compatibilité // Les nouvelles teams créées via l'API seront visibles après rafraîchissement } catch (error) { @@ -68,7 +65,7 @@ export function useTeamsManagement( try { setIsSubmitting(true); - const newTeam = await AdminManagementService.createTeam(teamFormData); + const newTeam = await adminClient.createTeam(teamFormData); toast({ title: "Succès", description: "Équipe créée avec succès", @@ -129,10 +126,10 @@ export function useTeamsManagement( try { setIsSubmitting(true); - await AdminManagementService.updateTeam({ + await adminClient.updateTeam({ id: editingTeam.id, ...teamFormData, - memberCount: editingTeam.memberCount || 0, + memberCount: (editingTeam as any).memberCount || 0, }); toast({ @@ -175,7 +172,7 @@ export function useTeamsManagement( ) ) { try { - await AdminManagementService.deleteTeam(teamId); + await adminClient.deleteTeam(teamId); toast({ title: "Succès", description: "Équipe supprimée avec succès", @@ -183,9 +180,7 @@ export function useTeamsManagement( // Mettre à jour l'état local au lieu de recharger la page setTeams((prev) => prev.filter((t) => t.id !== teamId)); - setTeamStats((prev) => - prev.filter((stats) => stats.teamId !== teamId) - ); + setTeamStats((prev) => prev.filter((stats) => stats.teamId !== teamId)); } catch (error: any) { toast({ title: "Erreur", @@ -224,17 +219,15 @@ export function useTeamsManagement( ) ) { try { - await AdminManagementService.deleteDirection(direction); + await adminClient.deleteDirection(direction); toast({ title: "Succès", description: `Direction "${direction}" et toutes ses équipes supprimées avec succès`, variant: "default", }); - + // Mettre à jour l'état local au lieu de recharger la page - setTeams((prev) => - prev.filter((team) => team.direction !== direction) - ); + setTeams((prev) => prev.filter((team) => team.direction !== direction)); setTeamStats((prev) => prev.filter((stats) => stats.direction !== direction) ); @@ -250,9 +243,7 @@ export function useTeamsManagement( }; // Extraire les directions uniques pour les formulaires - const directions = Array.from( - new Set(teams.map((team) => team.direction)) - ); + const directions = Array.from(new Set(teams.map((team) => team.direction))); return { teams, diff --git a/hooks/use-tree-view.ts b/hooks/use-tree-view.ts index 23038fc..7b71213 100644 --- a/hooks/use-tree-view.ts +++ b/hooks/use-tree-view.ts @@ -16,7 +16,9 @@ export function useTreeView({ onSearchChange, }: UseTreeViewOptions) { // État pour les catégories ouvertes/fermées - const [expandedCategories, setExpandedCategories] = useState>(new Set()); + const [expandedCategories, setExpandedCategories] = useState>( + new Set() + ); // Grouper les données par catégorie et filtrer en fonction de la recherche const filteredDataByCategory = useMemo(() => { @@ -31,23 +33,26 @@ export function useTreeView({ }, {} as Record); // Filtrer les données en fonction de la recherche - return Object.entries(dataByCategory).reduce((acc, [category, categoryItems]) => { - const filteredItems = categoryItems.filter((item) => { - const matchesSearch = searchFields.some((field) => { - const value = item[field]; - if (typeof value === 'string') { - return value.toLowerCase().includes(searchTerm.toLowerCase()); - } - return false; + return Object.entries(dataByCategory).reduce( + (acc, [category, categoryItems]) => { + const filteredItems = categoryItems.filter((item) => { + const matchesSearch = searchFields.some((field) => { + const value = item[field]; + if (typeof value === "string") { + return value.toLowerCase().includes(searchTerm.toLowerCase()); + } + return false; + }); + return matchesSearch; }); - return matchesSearch; - }); - if (filteredItems.length > 0) { - acc[category] = filteredItems; - } - return acc; - }, {} as Record); + if (filteredItems.length > 0) { + acc[category] = filteredItems; + } + return acc; + }, + {} as Record + ); }, [data, searchFields, groupBy, searchTerm]); // Fonctions pour gérer l'expansion des catégories diff --git a/hooks/use-users-management.ts b/hooks/use-users-management.ts index 3689e61..b7fc7d4 100644 --- a/hooks/use-users-management.ts +++ b/hooks/use-users-management.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { useToast } from "@/hooks/use-toast"; -import { Team } from "@/services/admin-management-service"; +import { Team } from "@/clients/domains/admin-client"; interface User { uuid: string; @@ -113,7 +113,7 @@ export function useUsersManagement(teams: Team[]) { setDeletingUserId(user.uuid); try { // TODO: Implémenter la suppression d'utilisateur - // await AdminManagementService.deleteUser(user.uuid); + // await adminClient.deleteUser(user.uuid); // Mettre à jour la liste locale setUsers((prev) => prev.filter((u) => u.uuid !== user.uuid)); diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..28b2e16 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,113 @@ +# Server-Side Utilities Architecture + +Cette architecture respecte les principes SOLID en séparant clairement les responsabilités côté serveur et en évitant le code mort. + +## Structure + +``` +lib/ +├── evaluation-utils.ts # Utilitaires pour les évaluations +├── evaluation-actions.ts # Actions d'évaluation +├── score-utils.ts # Utilitaires pour les scores +├── skill-file-loader.ts # Chargement des fichiers de skills +├── types.ts # Types TypeScript généraux +├── admin-types.ts # Types pour l'administration +├── utils.ts # Utilitaires généraux +├── category-icons.ts # Icônes des catégories +├── tech-colors.ts # Couleurs des technologies +├── pattern-colors.ts # Couleurs des patterns +└── README.md # Ce fichier +``` + +## Responsabilités + +### evaluation-utils.ts + +- **Utilitaires** pour les évaluations côté client +- **Génération de données** pour les graphiques radar + +### evaluation-actions.ts + +- **Actions d'évaluation** côté serveur +- **Gestion des profils** utilisateur + +### score-utils.ts + +- **Calcul des scores** et niveaux de compétences +- **Logique métier** pour les évaluations + +### skill-file-loader.ts + +- **Chargement des fichiers** de compétences depuis le système de fichiers +- **Parsing des données** JSON + +### types.ts + +- **Types TypeScript** généraux de l'application +- **Interfaces** pour les entités principales + +### admin-types.ts + +- **Types pour l'administration** et les statistiques +- **Interfaces** pour TeamMember, TeamStats, DirectionStats + +## Services d'authentification + +### AuthClient (client-side uniquement) + +- **`login()`** - Authentification côté client +- **`getCurrentUser()`** - Récupération utilisateur côté client +- **`logout()`** - Déconnexion côté client + +### AuthService (server-side uniquement) + +- **`getUserUuidFromCookie()`** - Récupère l'UUID depuis le cookie côté serveur +- **`isUserAuthenticated()`** - Vérifie l'authentification côté serveur + +## Séparation client/serveur + +- **`clients/domains/auth-client.ts`** - Côté client uniquement (React components, hooks) +- **`services/auth-service.ts`** - Côté serveur uniquement (API routes, pages) +- **Pas de duplication** entre les deux services + +## Utilisation + +### Import direct des services + +```typescript +import { AuthService } from "@/services"; +import { SkillsService, TeamsService } from "@/services"; +import { evaluationService } from "@/services/evaluation-service"; +``` + +### Utilisation dans les pages + +```typescript +export default async function HomePage() { + const userUuid = await AuthService.getUserUuidFromCookie(); + + if (!userUuid) { + redirect("/login"); + } + + const [userEvaluation, skillCategories, teams] = await Promise.all([ + evaluationService.getServerUserEvaluation(userUuid!), + SkillsService.getSkillCategories(), + TeamsService.getTeams(), + ]); +} +``` + +## Avantages + +- **Séparation claire** : Chaque fichier a une seule responsabilité +- **Pas de code mort** : Utilisation directe des services existants +- **Maintenabilité** : Code organisé et facile à comprendre +- **Réutilisabilité** : Fonctions modulaires et indépendantes +- **Testabilité** : Chaque module peut être testé séparément +- **Évolutivité** : Facile d'ajouter de nouvelles fonctionnalités +- **Architecture logique** : L'authentification est dans les services d'auth +- **Séparation client/serveur** : Pas de confusion entre les deux environnements +- **Noms cohérents** : AuthService pour le serveur, AuthClient pour le client +- **Imports directs** : Plus de wrapper inutile, appel direct des services +- **Simplicité** : Architecture claire et directe diff --git a/lib/admin-types.ts b/lib/admin-types.ts new file mode 100644 index 0000000..3b09f05 --- /dev/null +++ b/lib/admin-types.ts @@ -0,0 +1,35 @@ +// Types pour l'administration et les statistiques + +export interface TeamMember { + uuid: string; + firstName: string; + lastName: string; + skills: Array<{ + skillId: string; + skillName: string; + category: string; + level: number; + canMentor: boolean; + wantsToLearn: boolean; + }>; + joinDate: string; +} + +export interface TeamStats { + teamId: string; + teamName: string; + direction: string; + totalMembers: number; + averageSkillLevel: number; + topSkills: Array<{ skillName: string; averageLevel: number; icon?: string }>; + skillCoverage: number; // Percentage of skills evaluated + members: TeamMember[]; +} + +export interface DirectionStats { + direction: string; + teams: TeamStats[]; + totalMembers: number; + averageSkillLevel: number; + topCategories: Array<{ category: string; averageLevel: number }>; +} diff --git a/lib/auth-utils.ts b/lib/auth-utils.ts deleted file mode 100644 index 1639124..0000000 --- a/lib/auth-utils.ts +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import { UserProfile } from "./types"; - -/** - * Service d'authentification côté client - */ -export class AuthService { - /** - * Authentifie un utilisateur et créé le cookie - */ - static async login( - profile: UserProfile - ): Promise<{ user: UserProfile & { uuid: string }; userUuid: string }> { - const response = await fetch("/api/auth", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(profile), - }); - - if (!response.ok) { - throw new Error("Failed to authenticate user"); - } - - return response.json(); - } - - /** - * Récupère l'utilisateur actuel depuis le cookie - */ - static async getCurrentUser(): Promise { - try { - const response = await fetch("/api/auth", { - method: "GET", - credentials: "same-origin", - }); - - if (!response.ok) { - return null; - } - - const data = await response.json(); - return data.user; - } catch (error) { - console.error("Failed to get current user:", error); - return null; - } - } - - /** - * Déconnecte l'utilisateur (supprime le cookie) - */ - static async logout(): Promise { - const response = await fetch("/api/auth", { - method: "DELETE", - credentials: "same-origin", - }); - - if (!response.ok) { - throw new Error("Failed to logout"); - } - } -} - -/** - * Constantes pour les cookies - */ -export const COOKIE_NAME = "peakSkills_userId"; -export const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 jours diff --git a/lib/data-loader.ts b/lib/data-loader.ts deleted file mode 100644 index 8edd323..0000000 --- a/lib/data-loader.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SkillCategory, Team } from "./types"; - -export async function loadSkillCategories(): Promise { - try { - const response = await fetch("/api/skills"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error("Failed to load skill categories:", error); - return []; - } -} - -/** - * Load skill categories from local files (fallback or development mode) - * This is a client-side safe alternative that still uses the API - * For server-side file loading, use loadSkillCategoriesFromFiles from skill-file-loader - */ -export async function loadSkillCategoriesFromAPI(): Promise { - return loadSkillCategories(); -} - -export async function loadTeams(): Promise { - try { - const response = await fetch("/api/teams"); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error("Failed to load teams:", error); - return []; - } -} diff --git a/lib/evaluation-actions.ts b/lib/evaluation-actions.ts index b5472b6..468918e 100644 --- a/lib/evaluation-actions.ts +++ b/lib/evaluation-actions.ts @@ -160,8 +160,8 @@ export async function initializeEmptyEvaluation( try { // Simplement créer le profil via l'auth, pas besoin de créer une évaluation vide // Le backend créera automatiquement l'évaluation lors du premier accès - const { AuthService } = await import("@/lib/auth-utils"); - await AuthService.login(profile); + const { authClient } = await import("@/clients"); + await authClient.login(profile); } catch (error) { console.error("Failed to initialize evaluation:", error); throw error; diff --git a/lib/evaluation-utils.ts b/lib/evaluation-utils.ts index c2b5f3e..accd733 100644 --- a/lib/evaluation-utils.ts +++ b/lib/evaluation-utils.ts @@ -6,7 +6,7 @@ import { UserEvaluation, SkillCategory, } from "./types"; -import { apiClient } from "../services/api-client"; +import { evaluationClient } from "../clients"; export function calculateCategoryScore( categoryEvaluation: CategoryEvaluation @@ -45,28 +45,6 @@ export function generateRadarData( }); } -export async function saveUserEvaluation( - evaluation: UserEvaluation -): Promise { - try { - await apiClient.saveUserEvaluation(evaluation); - } catch (error) { - console.error("Failed to save user evaluation:", error); - throw error; - } -} - -export async function loadUserEvaluation( - profile: UserEvaluation["profile"] -): Promise { - try { - return await apiClient.loadUserEvaluation(profile); - } catch (error) { - console.error("Failed to load user evaluation:", error); - return null; - } -} - export function createEmptyEvaluation( categories: SkillCategory[] ): CategoryEvaluation[] { diff --git a/lib/server-auth.ts b/lib/server-auth.ts deleted file mode 100644 index ee5222b..0000000 --- a/lib/server-auth.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { cookies } from "next/headers"; -import { COOKIE_NAME } from "./auth-utils"; -import { evaluationService } from "@/services/evaluation-service"; -import { TeamsService } from "@/services/teams-service"; -import { SkillsService } from "@/services/skills-service"; -import { SkillCategory, Team } from "./types"; - -/** - * Récupère l'UUID utilisateur depuis le cookie côté serveur - */ -export async function getUserUuidFromCookie(): Promise { - const cookieStore = await cookies(); - const userUuidCookie = cookieStore.get("peakSkills_userId"); - - if (!userUuidCookie?.value) { - return null; - } - - return userUuidCookie.value; -} - -/** - * Récupère l'ID utilisateur depuis le cookie côté serveur (legacy) - */ -export async function getUserIdFromCookie(): Promise { - const cookieStore = await cookies(); - const userIdCookie = cookieStore.get("peakSkills_userId"); - - if (!userIdCookie?.value) { - return null; - } - - // Essayer de parser comme number pour backward compatibility - const userId = parseInt(userIdCookie.value); - return isNaN(userId) ? null : userId; -} - -/** - * Récupère l'évaluation complète de l'utilisateur côté serveur - */ -export async function getServerUserEvaluation() { - const userUuid = await getUserUuidFromCookie(); - - if (!userUuid) { - return null; - } - - try { - // Charger directement l'évaluation par UUID - const userEvaluation = await evaluationService.loadUserEvaluationByUuid( - userUuid - ); - - return userEvaluation; - } catch (error) { - console.error("Failed to get user evaluation:", error); - return null; - } -} - -/** - * Charge les catégories de compétences côté serveur depuis PostgreSQL - */ -export async function getServerSkillCategories(): Promise { - try { - return await SkillsService.getSkillCategories(); - } catch (error) { - console.error("Failed to load skill categories:", error); - return []; - } -} - -/** - * Charge les équipes côté serveur depuis PostgreSQL - */ -export async function getServerTeams(): Promise { - try { - return await TeamsService.getTeams(); - } catch (error) { - console.error("Failed to load teams:", error); - return []; - } -} - -/** - * Vérifie simplement si l'utilisateur est authentifié via le cookie - */ -export async function isUserAuthenticated(): Promise { - const userUuid = await getUserUuidFromCookie(); - return !!userUuid; -} diff --git a/lib/types.ts b/lib/types.ts index b5365fb..afe15d4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -43,6 +43,7 @@ export interface UserProfile { firstName: string; lastName: string; teamId: string; + uuid?: string; } export interface SkillEvaluation { diff --git a/middleware.ts b/middleware.ts index 9df67eb..632f4ad 100644 --- a/middleware.ts +++ b/middleware.ts @@ -30,7 +30,6 @@ export function middleware(request: NextRequest) { // Vérifier le cookie d'authentification (maintenant un UUID) const userUuid = request.cookies.get(COOKIE_NAME)?.value; - if (!userUuid) { // Rediriger vers la page de login si pas authentifié const loginUrl = new URL("/login", request.url); diff --git a/services/admin-management-service.ts b/services/admin-management-service.ts deleted file mode 100644 index f1121a7..0000000 --- a/services/admin-management-service.ts +++ /dev/null @@ -1,193 +0,0 @@ -export interface Skill { - id: string; - name: string; - description: string; - icon: string; - categoryId: string; - category: string; - usageCount: number; -} - -export interface Team { - id: string; - name: string; - direction: string; - memberCount: number; -} - -export interface TeamMember { - id: string; - firstName: string; - lastName: string; - fullName: string; - joinedAt: string; -} - -export class AdminManagementService { - private static baseUrl = "/api/admin"; - - // Skills Management - static async getSkills(): Promise { - const response = await fetch(`${this.baseUrl}/skills`); - if (!response.ok) { - throw new Error("Failed to fetch skills"); - } - return response.json(); - } - - static async createSkill( - skillData: Omit - ): Promise { - const response = await fetch(`${this.baseUrl}/skills`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(skillData), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to create skill"); - } - - return response.json(); - } - - static async updateSkill(skillData: Skill): Promise { - const response = await fetch(`${this.baseUrl}/skills`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(skillData), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to update skill"); - } - - return response.json(); - } - - static async deleteSkill(skillId: string): Promise { - const response = await fetch(`${this.baseUrl}/skills?id=${skillId}`, { - method: "DELETE", - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to delete skill"); - } - } - - // Teams Management - static async getTeams(): Promise { - const response = await fetch(`${this.baseUrl}/teams`); - if (!response.ok) { - throw new Error("Failed to fetch teams"); - } - return response.json(); - } - - static async createTeam( - teamData: Omit - ): Promise { - const response = await fetch(`${this.baseUrl}/teams`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(teamData), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to create team"); - } - - return response.json(); - } - - static async updateTeam(teamData: Team): Promise { - const response = await fetch(`${this.baseUrl}/teams`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(teamData), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to update team"); - } - - return response.json(); - } - - static async deleteTeam(teamId: string): Promise { - const response = await fetch(`${this.baseUrl}/teams?id=${teamId}`, { - method: "DELETE", - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to delete team"); - } - } - - static async deleteDirection(direction: string): Promise { - const response = await fetch( - `${this.baseUrl}/teams?direction=${encodeURIComponent(direction)}`, - { - method: "DELETE", - } - ); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to delete direction"); - } - } - - // Team Members - static async getTeamMembers(teamId: string): Promise { - const response = await fetch(`${this.baseUrl}/teams/${teamId}/members`); - if (!response.ok) { - throw new Error("Failed to fetch team members"); - } - return response.json(); - } - - static async removeTeamMember( - teamId: string, - memberId: string - ): Promise { - const response = await fetch(`${this.baseUrl}/teams/${teamId}/members`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ memberId }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to remove team member"); - } - } - - // User Management - static async deleteUser(userId: string): Promise { - const response = await fetch(`${this.baseUrl}/users/${userId}`, { - method: "DELETE", - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to delete user"); - } - } -} diff --git a/services/admin-service.ts b/services/admin-service.ts index ff2413e..5ffbd9c 100644 --- a/services/admin-service.ts +++ b/services/admin-service.ts @@ -1,39 +1,6 @@ import { getPool } from "./database"; import { Team, SkillCategory } from "@/lib/types"; - -export interface TeamMember { - uuid: string; - firstName: string; - lastName: string; - skills: Array<{ - skillId: string; - skillName: string; - category: string; - level: number; - canMentor: boolean; - wantsToLearn: boolean; - }>; - joinDate: string; -} - -export interface TeamStats { - teamId: string; - teamName: string; - direction: string; - totalMembers: number; - averageSkillLevel: number; - topSkills: Array<{ skillName: string; averageLevel: number; icon?: string }>; - skillCoverage: number; // Percentage of skills evaluated - members: TeamMember[]; -} - -export interface DirectionStats { - direction: string; - teams: TeamStats[]; - totalMembers: number; - averageSkillLevel: number; - topCategories: Array<{ category: string; averageLevel: number }>; -} +import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types"; export class AdminService { /** diff --git a/services/api-client.ts b/services/api-client.ts deleted file mode 100644 index b29e795..0000000 --- a/services/api-client.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { - UserEvaluation, - UserProfile, - SkillLevel, - Team, - SkillCategory, - Skill, -} from "../lib/types"; - -export class ApiClient { - private baseUrl: string; - - constructor() { - this.baseUrl = process.env.NEXT_PUBLIC_API_URL || ""; - } - - /** - * Charge une évaluation utilisateur depuis l'API - * Si profile est fourni, utilise les paramètres (mode compatibilité) - * Sinon, utilise l'authentification par cookie - */ - async loadUserEvaluation( - profile?: UserProfile - ): Promise { - try { - let url = `${this.baseUrl}/api/evaluations`; - - // Mode compatibilité avec profile en paramètres - if (profile) { - const params = new URLSearchParams({ - firstName: profile.firstName, - lastName: profile.lastName, - teamId: profile.teamId, - }); - url += `?${params}`; - } - - const response = await fetch(url, { - credentials: "same-origin", // Pour inclure les cookies - }); - - if (!response.ok) { - throw new Error("Erreur lors du chargement de l'évaluation"); - } - - const data = await response.json(); - return data.evaluation; - } catch (error) { - console.error("Erreur lors du chargement de l'évaluation:", error); - return null; - } - } - - /** - * Sauvegarde une évaluation utilisateur via l'API - */ - async saveUserEvaluation(evaluation: UserEvaluation): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/evaluations`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ evaluation }), - credentials: "same-origin", - }); - - if (!response.ok) { - throw new Error("Erreur lors de la sauvegarde de l'évaluation"); - } - } catch (error) { - console.error("Erreur lors de la sauvegarde de l'évaluation:", error); - throw error; - } - } - - /** - * Met à jour le niveau d'une skill - */ - async updateSkillLevel( - profile: UserProfile, - category: string, - skillId: string, - level: SkillLevel - ): Promise { - await this.updateSkill(profile, category, skillId, { - action: "updateLevel", - level, - }); - } - - /** - * Met à jour le statut de mentorat d'une skill - */ - async updateSkillMentorStatus( - profile: UserProfile, - category: string, - skillId: string, - canMentor: boolean - ): Promise { - await this.updateSkill(profile, category, skillId, { - action: "updateMentorStatus", - canMentor, - }); - } - - /** - * Met à jour le statut d'apprentissage d'une skill - */ - async updateSkillLearningStatus( - profile: UserProfile, - category: string, - skillId: string, - wantsToLearn: boolean - ): Promise { - await this.updateSkill(profile, category, skillId, { - action: "updateLearningStatus", - wantsToLearn, - }); - } - - /** - * Ajoute une skill à l'évaluation - */ - async addSkillToEvaluation( - profile: UserProfile, - category: string, - skillId: string - ): Promise { - await this.updateSkill(profile, category, skillId, { - action: "addSkill", - }); - } - - /** - * Supprime une skill de l'évaluation - */ - async removeSkillFromEvaluation( - profile: UserProfile, - category: string, - skillId: string - ): Promise { - await this.updateSkill(profile, category, skillId, { - action: "removeSkill", - }); - } - - /** - * Méthode utilitaire pour mettre à jour une skill - */ - private async updateSkill( - profile: UserProfile, - category: string, - skillId: string, - options: { - action: - | "updateLevel" - | "updateMentorStatus" - | "updateLearningStatus" - | "addSkill" - | "removeSkill"; - level?: SkillLevel; - canMentor?: boolean; - wantsToLearn?: boolean; - } - ): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/evaluations/skills`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - profile, - category, - skillId, - ...options, - }), - credentials: "same-origin", - }); - - if (!response.ok) { - throw new Error("Erreur lors de la mise à jour de la skill"); - } - } catch (error) { - console.error("Erreur lors de la mise à jour de la skill:", error); - throw error; - } - } - - /** - * Charge toutes les équipes - */ - async loadTeams(): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/teams`); - - if (!response.ok) { - throw new Error("Erreur lors du chargement des équipes"); - } - - return await response.json(); - } catch (error) { - console.error("Erreur lors du chargement des équipes:", error); - return []; - } - } - - /** - * Charge une équipe par ID - */ - async loadTeamById(teamId: string): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/teams/${teamId}`); - - if (!response.ok) { - if (response.status === 404) { - return null; - } - throw new Error("Erreur lors du chargement de l'équipe"); - } - - return await response.json(); - } catch (error) { - console.error("Erreur lors du chargement de l'équipe:", error); - return null; - } - } - - /** - * Charge les équipes par direction - */ - async loadTeamsByDirection(direction: string): Promise { - try { - const response = await fetch( - `${this.baseUrl}/api/teams/direction/${direction}` - ); - - if (!response.ok) { - throw new Error("Erreur lors du chargement des équipes par direction"); - } - - return await response.json(); - } catch (error) { - console.error( - "Erreur lors du chargement des équipes par direction:", - error - ); - return []; - } - } - - /** - * Charge toutes les directions - */ - async loadDirections(): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/teams/directions`); - - if (!response.ok) { - throw new Error("Erreur lors du chargement des directions"); - } - - return await response.json(); - } catch (error) { - console.error("Erreur lors du chargement des directions:", error); - return []; - } - } - - /** - * Crée une nouvelle équipe - */ - async createTeam( - team: Omit - ): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/teams`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(team), - }); - - if (!response.ok) { - throw new Error("Erreur lors de la création de l'équipe"); - } - - return await response.json(); - } catch (error) { - console.error("Erreur lors de la création de l'équipe:", error); - return null; - } - } - - /** - * Met à jour une équipe - */ - async updateTeam( - teamId: string, - updates: Partial> - ): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/teams/${teamId}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(updates), - }); - - if (!response.ok) { - throw new Error("Erreur lors de la mise à jour de l'équipe"); - } - - return await response.json(); - } catch (error) { - console.error("Erreur lors de la mise à jour de l'équipe:", error); - return null; - } - } - - /** - * Supprime une équipe - */ - async deleteTeam(teamId: string): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/teams/${teamId}`, { - method: "DELETE", - }); - - if (!response.ok) { - throw new Error("Erreur lors de la suppression de l'équipe"); - } - - return true; - } catch (error) { - console.error("Erreur lors de la suppression de l'équipe:", error); - return false; - } - } - - /** - * Charge toutes les catégories de skills - */ - async loadSkillCategories(): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/skills`); - - if (!response.ok) { - throw new Error("Erreur lors du chargement des catégories de skills"); - } - - return await response.json(); - } catch (error) { - console.error( - "Erreur lors du chargement des catégories de skills:", - error - ); - return []; - } - } - - /** - * Charge les skills d'une catégorie - */ - async loadSkillsByCategory(categoryId: string): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/skills/${categoryId}`); - - if (!response.ok) { - throw new Error("Erreur lors du chargement des skills par catégorie"); - } - - return await response.json(); - } catch (error) { - console.error( - "Erreur lors du chargement des skills par catégorie:", - error - ); - return []; - } - } - - /** - * Crée une nouvelle catégorie de skill - */ - async createSkillCategory(category: { - id: string; - name: string; - icon: string; - }): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/skills`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(category), - }); - - return response.ok; - } catch (error) { - console.error( - "Erreur lors de la création de la catégorie de skill:", - error - ); - return false; - } - } - - /** - * Crée une nouvelle skill - */ - async createSkill( - categoryId: string, - skill: { - id: string; - name: string; - description: string; - icon?: string; - links: string[]; - } - ): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/skills/${categoryId}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(skill), - }); - - return response.ok; - } catch (error) { - console.error("Erreur lors de la création de la skill:", error); - return false; - } - } -} - -// Instance singleton -export const apiClient = new ApiClient(); diff --git a/services/auth-service.ts b/services/auth-service.ts new file mode 100644 index 0000000..f3e7751 --- /dev/null +++ b/services/auth-service.ts @@ -0,0 +1,32 @@ +import { cookies } from "next/headers"; +// Constantes pour les cookies (définies ici car auth-service.ts a été supprimé) +export const COOKIE_NAME = "peakSkills_userId"; +export const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 jours + +/** + * Service d'authentification côté serveur + * Implémente les méthodes qui nécessitent next/headers + */ +export class AuthService { + /** + * Récupère l'UUID utilisateur depuis le cookie côté serveur + */ + static async getUserUuidFromCookie(): Promise { + const cookieStore = await cookies(); + const userUuidCookie = cookieStore.get(COOKIE_NAME); + + if (!userUuidCookie?.value) { + return null; + } + + return userUuidCookie.value; + } + + /** + * Vérifie si l'utilisateur est authentifié côté serveur + */ + static async isUserAuthenticated(): Promise { + const userUuid = await this.getUserUuidFromCookie(); + return !!userUuid; + } +} diff --git a/services/client.ts b/services/client.ts deleted file mode 100644 index 601a0f1..0000000 --- a/services/client.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Client-side services only -// Safe to import in React components and hooks - -export { ApiClient, apiClient } from "./api-client"; diff --git a/services/evaluation-service.ts b/services/evaluation-service.ts index b7e95ff..ce563d3 100644 --- a/services/evaluation-service.ts +++ b/services/evaluation-service.ts @@ -15,6 +15,9 @@ export class EvaluationService { async loadUserEvaluationByUuid( userUuid: string ): Promise { + if (!userUuid) { + return null; + } const pool = getPool(); const client = await pool.connect(); @@ -677,6 +680,23 @@ export class EvaluationService { client.release(); } } + + /** + * Récupère l'évaluation complète de l'utilisateur côté serveur + * Combine la récupération du cookie et le chargement de l'évaluation + */ + async getServerUserEvaluation(userUuid: string) { + if (!userUuid) { + return null; + } + + try { + return await this.loadUserEvaluationByUuid(userUuid); + } catch (error) { + console.error("Failed to get user evaluation:", error); + return null; + } + } } // Instance singleton diff --git a/services/index.ts b/services/index.ts index c3ead87..a1988ec 100644 --- a/services/index.ts +++ b/services/index.ts @@ -20,8 +20,8 @@ export { SkillsService } from "./skills-service"; // Admin services (server-only) export { AdminService } from "./admin-service"; -// Admin management services (client-side compatible) -export { AdminManagementService } from "./admin-management-service"; +// Admin types (can be imported anywhere) +export type { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types"; -// API client (can be used client-side) -export { ApiClient, apiClient } from "./api-client"; +// Server auth service (server-side only) +export { AuthService, COOKIE_NAME, COOKIE_MAX_AGE } from "./auth-service";