refactor: revew all design of services, clients, deadcode, ...

This commit is contained in:
Julien Froidefond
2025-08-24 22:03:15 +02:00
parent f4dcc89c11
commit 6fba622003
63 changed files with 969 additions and 1846 deletions

View File

@@ -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");
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 (
<UsersManagementPage
teams={adminData.teams}
/>
);
return <UsersManagementPage teams={adminData.teams} />;
} catch (error) {
console.error("Failed to load admin data:", error);
return (

View File

@@ -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();

View File

@@ -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();

View File

@@ -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");

View File

@@ -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();

View File

@@ -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");

View File

@@ -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) {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 (

View File

@@ -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<Team[]>([]);
const [loading, setLoading] = useState(true);
const [authenticating, setAuthenticating] = useState(false);
const [currentUser, setCurrentUser] = useState<UserProfile | null>(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 (
<LoginLayout>
<LoginFormWrapper teams={teams} initialUser={userProfile} />
</LoginLayout>
);
}
}
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 (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
<div className="relative z-10 container mx-auto py-16 px-6">
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400 mx-auto mb-4"></div>
<p className="text-white">Chargement...</p>
</div>
</div>
</div>
</div>
<LoginLayout>
<LoginFormWrapper teams={teams} />
</LoginLayout>
);
} catch (error) {
console.error("Error loading login page:", error);
return (
<LoginLayout>
<LoginLoading />
</LoginLayout>
);
}
// Si l'utilisateur est connecté et qu'on ne modifie pas
if (currentUser && !isEditing) {
const currentTeam = teams.find((t) => t.id === currentUser.teamId);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
<div className="relative z-10 container mx-auto py-16 px-6">
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
<Code2 className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">
PeakSkills
</span>
</div>
<h1 className="text-4xl font-bold mb-4 text-white">
Vous êtes connecté
</h1>
<p className="text-lg text-slate-400 mb-8">
Gérez votre profil ou retournez à l'application
</p>
</div>
<div className="max-w-2xl mx-auto space-y-6">
{/* Informations utilisateur */}
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-white">Vos informations</CardTitle>
<CardDescription className="text-slate-400">
Profil actuellement connecté
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-300">
Prénom
</label>
<p className="text-white">{currentUser.firstName}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-300">
Nom
</label>
<p className="text-white">{currentUser.lastName}</p>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-300">
Équipe
</label>
<p className="text-white">
{currentTeam?.name || "Équipe non trouvée"}
</p>
</div>
</CardContent>
</Card>
{/* Actions */}
<div className="flex gap-4 justify-center">
<Button
onClick={() => router.push("/")}
className="bg-blue-500 hover:bg-blue-600 text-white"
>
<Home className="w-4 h-4 mr-2" />
Retour à l'accueil
</Button>
<Button
onClick={handleEdit}
variant="outline"
className="border-white/20 text-white hover:bg-white/10"
>
<Edit className="w-4 h-4 mr-2" />
Modifier le profil
</Button>
<Button
onClick={handleLogout}
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/10"
>
<LogOut className="w-4 h-4 mr-2" />
Se déconnecter
</Button>
</div>
</div>
</div>
</div>
);
}
// Sinon, afficher le formulaire (nouvel utilisateur ou modification)
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
<div className="relative z-10 container mx-auto py-16 px-6">
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
<Code2 className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">
PeakSkills
</span>
</div>
<h1 className="text-4xl font-bold mb-4 text-white">
{isEditing
? "Modifier vos informations"
: "Bienvenue sur PeakSkills"}
</h1>
<p className="text-lg text-slate-400 mb-8">
{isEditing
? "Mettez à jour vos informations personnelles"
: "Évaluez vos compétences techniques et suivez votre progression"}
</p>
{isEditing && (
<Button
onClick={handleCancelEdit}
variant="outline"
className="mb-8 border-white/20 text-white hover:bg-white/10"
>
<X className="w-4 h-4 mr-2" />
Annuler
</Button>
)}
</div>
<div className="max-w-2xl mx-auto">
<div className="relative">
{authenticating && (
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm z-10 flex items-center justify-center rounded-lg">
<div className="text-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-400 mx-auto mb-2"></div>
<p className="text-white text-sm">
{isEditing
? "Mise à jour en cours..."
: "Connexion en cours..."}
</p>
</div>
</div>
)}
<ProfileForm
teams={teams}
onSubmit={handleSubmit}
initialProfile={
isEditing && currentUser ? currentUser : undefined
}
/>
</div>
</div>
</div>
</div>
);
}

View File

@@ -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

115
clients/README.md Normal file
View File

@@ -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

View File

@@ -0,0 +1,69 @@
export abstract class BaseHttpClient {
protected baseUrl: string;
constructor() {
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || "/api/";
}
protected async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
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<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint);
}
protected async post<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: "POST",
body: data ? JSON.stringify(data) : undefined,
});
}
protected async put<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: "PUT",
body: data ? JSON.stringify(data) : undefined,
});
}
protected async delete<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: "DELETE",
body: data ? JSON.stringify(data) : undefined,
});
}
}

View File

@@ -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<Skill[]> {
return await this.get<Skill[]>(`/admin/skills`);
}
async createSkill(
skillData: Omit<Skill, "id" | "usageCount">
): Promise<Skill> {
return await this.post<Skill>(`/admin/skills`, skillData);
}
async updateSkill(skillData: Skill): Promise<Skill> {
return await this.put<Skill>(`/admin/skills`, skillData);
}
async deleteSkill(skillId: string): Promise<void> {
await this.delete(`/admin/skills?id=${skillId}`);
}
// Teams Management
async getTeams(): Promise<Team[]> {
return await this.get<Team[]>(`/admin/teams`);
}
async createTeam(teamData: Omit<Team, "id" | "memberCount">): Promise<Team> {
return await this.post<Team>(`/admin/teams`, teamData);
}
async updateTeam(teamData: Team): Promise<Team> {
return await this.put<Team>(`/admin/teams`, teamData);
}
async deleteTeam(teamId: string): Promise<void> {
await this.delete(`/admin/teams?id=${teamId}`);
}
async deleteDirection(direction: string): Promise<void> {
await this.delete(
`/admin/teams?direction=${encodeURIComponent(direction)}`
);
}
// Team Members
async getTeamMembers(teamId: string): Promise<TeamMember[]> {
return await this.get<TeamMember[]>(`/admin/teams/${teamId}/members`);
}
async removeTeamMember(teamId: string, memberId: string): Promise<void> {
await this.delete(`/admin/teams/${teamId}/members`, {
memberId,
});
}
// User Management
async deleteUser(userId: string): Promise<void> {
await this.delete(`/admin/users/${userId}`);
}
}

View File

@@ -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<UserProfile | null> {
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<void> {
await this.delete("/auth");
}
}

View File

@@ -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<boolean> {
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;
}
}
}

14
clients/index.ts Normal file
View File

@@ -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();

View File

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

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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[];

View File

@@ -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<string, Skill[]>;

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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[];

View File

@@ -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 {

View File

@@ -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<string, TeamType[]>;

View File

@@ -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";
@@ -28,7 +28,14 @@ export function TeamsManagementPage({
const [isMembersModalOpen, setIsMembersModalOpen] = useState(false);
const [selectedTeam, setSelectedTeam] = useState<TeamType | null>(null);
const { isCreateDialogOpen, isEditDialogOpen, openCreateDialog, closeCreateDialog, openEditDialog, closeEditDialog } = useFormDialog();
const {
isCreateDialogOpen,
isEditDialogOpen,
openCreateDialog,
closeCreateDialog,
openEditDialog,
closeEditDialog,
} = useFormDialog();
const {
teams: localTeams,

View File

@@ -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
</Button>
<Button onClick={onSubmit} disabled={isSubmitting}>
{isSubmitting ? "En cours..." : title.includes("Créer") ? "Créer" : "Mettre à jour"}
{isSubmitting
? "En cours..."
: title.includes("Créer")
? "Créer"
: "Mettre à jour"}
</Button>
</div>
</div>

View File

@@ -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";
@@ -18,7 +18,8 @@ interface UsersManagementPageProps {
export function UsersManagementPage({ teams }: UsersManagementPageProps) {
const [searchTerm, setSearchTerm] = useState("");
const { isCreateDialogOpen, openCreateDialog, closeCreateDialog } = useFormDialog();
const { isCreateDialogOpen, openCreateDialog, closeCreateDialog } =
useFormDialog();
const {
users,

View File

@@ -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 {

View File

@@ -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}
/>
</div>
@@ -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}
/>
<p className="text-xs text-muted-foreground">
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
</p>
</div>
@@ -205,7 +212,12 @@ export function CreateSkillForm({
</>
)}
</Button>
<Button type="button" variant="outline" onClick={onCancel} disabled={isLoading}>
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isLoading}
>
Annuler
</Button>
</div>

View File

@@ -0,0 +1,3 @@
export { LoginFormWrapper } from "./login-form-wrapper";
export { LoginLoading } from "./login-loading";
export { LoginLayout } from "./login-layout";

View File

@@ -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<UserProfile | null>(
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 (
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
<Code2 className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">PeakSkills</span>
</div>
<h1 className="text-4xl font-bold mb-4 text-white">
Vous êtes connecté
</h1>
<p className="text-lg text-slate-400 mb-8">
Gérez votre profil ou retournez à l'application
</p>
<div className="max-w-2xl mx-auto space-y-6">
{/* Informations utilisateur */}
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-white">Vos informations</CardTitle>
<CardDescription className="text-slate-400">
Profil actuellement connecté
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-slate-300">
Prénom
</label>
<p className="text-white">{currentUser.firstName}</p>
</div>
<div>
<label className="text-sm font-medium text-slate-300">
Nom
</label>
<p className="text-white">{currentUser.lastName}</p>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-300">
Équipe
</label>
<p className="text-white">
{currentTeam?.name || "Équipe non trouvée"}
</p>
</div>
</CardContent>
</Card>
{/* Actions */}
<div className="flex gap-4 justify-center">
<Button
onClick={() => router.push("/")}
className="bg-blue-500 hover:bg-blue-600 text-white"
>
<Home className="w-4 h-4 mr-2" />
Retour à l'accueil
</Button>
<Button
onClick={handleEdit}
variant="outline"
className="border-white/20 text-white hover:bg-white/10"
>
<Edit className="w-4 h-4 mr-2" />
Modifier le profil
</Button>
<Button
onClick={handleLogout}
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/10"
>
<LogOut className="w-4 h-4 mr-2" />
Se déconnecter
</Button>
</div>
</div>
</div>
);
}
// Sinon, afficher le formulaire (nouvel utilisateur ou modification)
return (
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
<Code2 className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">PeakSkills</span>
</div>
<h1 className="text-4xl font-bold mb-4 text-white">
{isEditing ? "Modifier vos informations" : "Bienvenue sur PeakSkills"}
</h1>
<p className="text-lg text-slate-400 mb-8">
{isEditing
? "Mettez à jour vos informations personnelles"
: "Évaluez vos compétences techniques et suivez votre progression"}
</p>
{isEditing && (
<Button
onClick={handleCancelEdit}
variant="outline"
className="mb-8 border-white/20 text-white hover:bg-white/10"
>
<X className="w-4 h-4 mr-2" />
Annuler
</Button>
)}
<div className="max-w-2xl mx-auto">
<div className="relative">
{authenticating && (
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm z-10 flex items-center justify-center rounded-lg">
<div className="text-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-400 mx-auto mb-2"></div>
<p className="text-white text-sm">
{isEditing
? "Mise à jour en cours..."
: "Connexion en cours..."}
</p>
</div>
</div>
)}
<ProfileForm
teams={teams}
onSubmit={handleSubmit}
initialProfile={isEditing && currentUser ? currentUser : undefined}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import { Code2 } from "lucide-react";
interface LoginLayoutProps {
children: React.ReactNode;
}
export function LoginLayout({ children }: LoginLayoutProps) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
<div className="relative z-10 container mx-auto py-16 px-6">
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
export function LoginLoading() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
<div className="relative z-10 container mx-auto py-16 px-6">
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400 mx-auto mb-4"></div>
<p className="text-white">Chargement...</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -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<UserEvaluation | null>(
null
);
const [skillCategories, setSkillCategories] = useState<SkillCategory[]>([]);
const [teams, setTeams] = useState<Team[]>([]);
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,
};
}

View File

@@ -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",

View File

@@ -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,7 +219,7 @@ 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`,
@@ -232,9 +227,7 @@ export function useTeamsManagement(
});
// 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,

View File

@@ -16,7 +16,9 @@ export function useTreeView<T>({
onSearchChange,
}: UseTreeViewOptions<T>) {
// État pour les catégories ouvertes/fermées
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
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<T>({
}, {} as Record<string, T[]>);
// 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<string, T[]>);
if (filteredItems.length > 0) {
acc[category] = filteredItems;
}
return acc;
},
{} as Record<string, T[]>
);
}, [data, searchFields, groupBy, searchTerm]);
// Fonctions pour gérer l'expansion des catégories

View File

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

113
lib/README.md Normal file
View File

@@ -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

35
lib/admin-types.ts Normal file
View File

@@ -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 }>;
}

View File

@@ -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<UserProfile | null> {
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<void> {
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

View File

@@ -1,36 +0,0 @@
import { SkillCategory, Team } from "./types";
export async function loadSkillCategories(): Promise<SkillCategory[]> {
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<SkillCategory[]> {
return loadSkillCategories();
}
export async function loadTeams(): Promise<Team[]> {
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 [];
}
}

View File

@@ -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;

View File

@@ -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<void> {
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<UserEvaluation | null> {
try {
return await apiClient.loadUserEvaluation(profile);
} catch (error) {
console.error("Failed to load user evaluation:", error);
return null;
}
}
export function createEmptyEvaluation(
categories: SkillCategory[]
): CategoryEvaluation[] {

View File

@@ -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<string | null> {
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<number | null> {
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<SkillCategory[]> {
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<Team[]> {
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<boolean> {
const userUuid = await getUserUuidFromCookie();
return !!userUuid;
}

View File

@@ -43,6 +43,7 @@ export interface UserProfile {
firstName: string;
lastName: string;
teamId: string;
uuid?: string;
}
export interface SkillEvaluation {

View File

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

View File

@@ -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<Skill[]> {
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<Skill, "id" | "usageCount">
): Promise<Skill> {
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<Skill> {
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<void> {
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<Team[]> {
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<Team, "id" | "memberCount">
): Promise<Team> {
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<Team> {
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<void> {
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<void> {
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<TeamMember[]> {
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<void> {
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<void> {
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");
}
}
}

View File

@@ -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 {
/**

View File

@@ -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<UserEvaluation | null> {
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<void> {
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<void> {
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<void> {
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<void> {
await this.updateSkill(profile, category, skillId, {
action: "updateLearningStatus",
wantsToLearn,
});
}
/**
* Ajoute une skill à l'évaluation
*/
async addSkillToEvaluation(
profile: UserProfile,
category: string,
skillId: string
): Promise<void> {
await this.updateSkill(profile, category, skillId, {
action: "addSkill",
});
}
/**
* Supprime une skill de l'évaluation
*/
async removeSkillFromEvaluation(
profile: UserProfile,
category: string,
skillId: string
): Promise<void> {
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<void> {
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<Team[]> {
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<Team | null> {
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<Team[]> {
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<string[]> {
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<Team, "created_at" | "updated_at">
): Promise<Team | null> {
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<Omit<Team, "id">>
): Promise<Team | null> {
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<boolean> {
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<SkillCategory[]> {
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<Skill[]> {
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<boolean> {
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<boolean> {
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();

32
services/auth-service.ts Normal file
View File

@@ -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<string | null> {
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<boolean> {
const userUuid = await this.getUserUuidFromCookie();
return !!userUuid;
}
}

View File

@@ -1,4 +0,0 @@
// Client-side services only
// Safe to import in React components and hooks
export { ApiClient, apiClient } from "./api-client";

View File

@@ -15,6 +15,9 @@ export class EvaluationService {
async loadUserEvaluationByUuid(
userUuid: string
): Promise<UserEvaluation | null> {
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

View File

@@ -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";