refactor: revew all design of services, clients, deadcode, ...
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 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();
|
||||
|
||||
useEffect(() => {
|
||||
async function initialize() {
|
||||
export default async function LoginPage() {
|
||||
try {
|
||||
// Charger les équipes côté serveur
|
||||
const teams = await TeamsService.getTeams();
|
||||
|
||||
// Vérifier si l'utilisateur est déjà connecté
|
||||
const user = await AuthService.getCurrentUser();
|
||||
setCurrentUser(user);
|
||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
||||
|
||||
// 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 (userUuid) {
|
||||
// Si l'utilisateur est connecté, récupérer son profil côté serveur
|
||||
const userProfile = await userService.getUserByUuid(userUuid);
|
||||
|
||||
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) {
|
||||
if (userProfile) {
|
||||
// Passer le profil utilisateur pour permettre la 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="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} initialUser={userProfile} />
|
||||
</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)
|
||||
// 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="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>
|
||||
<LoginLayout>
|
||||
<LoginFormWrapper teams={teams} />
|
||||
</LoginLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error loading login page:", error);
|
||||
return (
|
||||
<LoginLayout>
|
||||
<LoginLoading />
|
||||
</LoginLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
22
app/page.tsx
22
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
|
||||
|
||||
115
clients/README.md
Normal file
115
clients/README.md
Normal 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
|
||||
69
clients/base/http-client.ts
Normal file
69
clients/base/http-client.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
86
clients/domains/admin-client.ts
Normal file
86
clients/domains/admin-client.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
33
clients/domains/auth-client.ts
Normal file
33
clients/domains/auth-client.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
26
clients/domains/skills-client.ts
Normal file
26
clients/domains/skills-client.ts
Normal 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
14
clients/index.ts
Normal 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();
|
||||
@@ -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));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
components/login/index.ts
Normal file
3
components/login/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { LoginFormWrapper } from "./login-form-wrapper";
|
||||
export { LoginLoading } from "./login-loading";
|
||||
export { LoginLayout } from "./login-layout";
|
||||
213
components/login/login-form-wrapper.tsx
Normal file
213
components/login/login-form-wrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
18
components/login/login-layout.tsx
Normal file
18
components/login/login-layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
components/login/login-loading.tsx
Normal file
17
components/login/login-loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,11 +33,12 @@ 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]) => {
|
||||
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') {
|
||||
if (typeof value === "string") {
|
||||
return value.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
@@ -47,7 +50,9 @@ export function useTreeView<T>({
|
||||
acc[category] = filteredItems;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, T[]>);
|
||||
},
|
||||
{} as Record<string, T[]>
|
||||
);
|
||||
}, [data, searchFields, groupBy, searchTerm]);
|
||||
|
||||
// Fonctions pour gérer l'expansion des catégories
|
||||
|
||||
@@ -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
113
lib/README.md
Normal 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
35
lib/admin-types.ts
Normal 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 }>;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export interface UserProfile {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
teamId: string;
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
export interface SkillEvaluation {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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
32
services/auth-service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Client-side services only
|
||||
// Safe to import in React components and hooks
|
||||
|
||||
export { ApiClient, apiClient } from "./api-client";
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user