refactor: revew all design of services, clients, deadcode, ...
This commit is contained in:
@@ -1,15 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
|
||||||
|
|
||||||
export default async function ManageAdminPage() {
|
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
|
// Rediriger vers la page skills par défaut
|
||||||
redirect("/admin/manage/skills");
|
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 { AdminService } from "@/services/admin-service";
|
||||||
import { SkillsManagementPage } from "@/components/admin/skills";
|
import { SkillsManagementPage } from "@/components/admin/skills";
|
||||||
|
|
||||||
export default async function SkillsPage() {
|
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
|
// Charger les données côté serveur
|
||||||
try {
|
try {
|
||||||
const adminData = await AdminService.getAdminData();
|
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 { AdminService } from "@/services/admin-service";
|
||||||
import { TeamsManagementPage } from "@/components/admin/teams";
|
import { TeamsManagementPage } from "@/components/admin/teams";
|
||||||
|
|
||||||
export default async function TeamsPage() {
|
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
|
// Charger les données côté serveur
|
||||||
try {
|
try {
|
||||||
const adminData = await AdminService.getAdminData();
|
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 { AdminService } from "@/services/admin-service";
|
||||||
import { UsersManagementPage } from "@/components/admin/users";
|
import { UsersManagementPage } from "@/components/admin/users";
|
||||||
|
|
||||||
export default async function UsersPage() {
|
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
|
// Charger les données côté serveur
|
||||||
try {
|
try {
|
||||||
const adminData = await AdminService.getAdminData();
|
const adminData = await AdminService.getAdminData();
|
||||||
|
|
||||||
return (
|
return <UsersManagementPage teams={adminData.teams} />;
|
||||||
<UsersManagementPage
|
|
||||||
teams={adminData.teams}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load admin data:", error);
|
console.error("Failed to load admin data:", error);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
|
||||||
import { AdminService } from "@/services/admin-service";
|
import { AdminService } from "@/services/admin-service";
|
||||||
import { AdminClientWrapper } from "@/components/admin";
|
import { AdminClientWrapper } from "@/components/admin";
|
||||||
|
|
||||||
export default async function AdminPage() {
|
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
|
// Charger les données côté serveur
|
||||||
try {
|
try {
|
||||||
const adminData = await AdminService.getAdminData();
|
const adminData = await AdminService.getAdminData();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
import { AdminService } from "@/services/admin-service";
|
||||||
import { AdminService, TeamStats } from "@/services/admin-service";
|
|
||||||
import { TeamDetailClientWrapper } from "@/components/admin";
|
import { TeamDetailClientWrapper } from "@/components/admin";
|
||||||
|
|
||||||
interface TeamDetailPageProps {
|
interface TeamDetailPageProps {
|
||||||
@@ -13,14 +12,6 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
|
|||||||
// Await params before using
|
// Await params before using
|
||||||
const { teamId } = await params;
|
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 {
|
try {
|
||||||
// Charger les données côté serveur
|
// Charger les données côté serveur
|
||||||
const allTeamsStats = await AdminService.getTeamsStats();
|
const allTeamsStats = await AdminService.getTeamsStats();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getPool } from "@/services/database";
|
import { getPool } from "@/services/database";
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
|
||||||
|
|
||||||
// Configuration pour éviter la génération statique
|
// Configuration pour éviter la génération statique
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -8,12 +7,6 @@ export const dynamic = "force-dynamic";
|
|||||||
// GET - Récupérer toutes les skills
|
// GET - Récupérer toutes les skills
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -56,12 +49,6 @@ export async function GET() {
|
|||||||
// POST - Créer une nouvelle skill
|
// POST - Créer une nouvelle skill
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
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();
|
const { name, categoryId, description, icon } = await request.json();
|
||||||
|
|
||||||
if (!name || !categoryId) {
|
if (!name || !categoryId) {
|
||||||
@@ -125,12 +112,6 @@ export async function POST(request: NextRequest) {
|
|||||||
// PUT - Mettre à jour une skill
|
// PUT - Mettre à jour une skill
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
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();
|
const { id, name, categoryId, description, icon } = await request.json();
|
||||||
|
|
||||||
if (!id || !name || !categoryId) {
|
if (!id || !name || !categoryId) {
|
||||||
@@ -204,12 +185,6 @@ export async function PUT(request: NextRequest) {
|
|||||||
// DELETE - Supprimer une skill
|
// DELETE - Supprimer une skill
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url);
|
||||||
const id = searchParams.get("id");
|
const id = searchParams.get("id");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getPool } from "@/services/database";
|
import { getPool } from "@/services/database";
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
|
||||||
|
|
||||||
// GET - Récupérer les membres d'une équipe
|
// GET - Récupérer les membres d'une équipe
|
||||||
export async function GET(
|
export async function GET(
|
||||||
@@ -8,12 +7,6 @@ export async function GET(
|
|||||||
{ params }: { params: Promise<{ teamId: string }> }
|
{ params }: { params: Promise<{ teamId: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { teamId } = await params;
|
const { teamId } = await params;
|
||||||
|
|
||||||
if (!teamId) {
|
if (!teamId) {
|
||||||
@@ -61,12 +54,6 @@ export async function DELETE(
|
|||||||
{ params }: { params: Promise<{ teamId: string }> }
|
{ params }: { params: Promise<{ teamId: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { teamId } = await params;
|
const { teamId } = await params;
|
||||||
const { memberId } = await request.json();
|
const { memberId } = await request.json();
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getPool } from "@/services/database";
|
import { getPool } from "@/services/database";
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
|
||||||
|
|
||||||
// GET - Récupérer toutes les teams
|
// GET - Récupérer toutes les teams
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -46,12 +39,6 @@ export async function GET() {
|
|||||||
// POST - Créer une nouvelle team
|
// POST - Créer une nouvelle team
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
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();
|
const { name, direction } = await request.json();
|
||||||
|
|
||||||
if (!name || !direction) {
|
if (!name || !direction) {
|
||||||
@@ -106,12 +93,6 @@ export async function POST(request: NextRequest) {
|
|||||||
// PUT - Mettre à jour une team
|
// PUT - Mettre à jour une team
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
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();
|
const { id, name, direction } = await request.json();
|
||||||
|
|
||||||
if (!id || !name || !direction) {
|
if (!id || !name || !direction) {
|
||||||
@@ -187,12 +168,6 @@ export async function PUT(request: NextRequest) {
|
|||||||
// DELETE - Supprimer une team ou une direction
|
// DELETE - Supprimer une team ou une direction
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url);
|
||||||
const id = searchParams.get("id");
|
const id = searchParams.get("id");
|
||||||
const direction = searchParams.get("direction");
|
const direction = searchParams.get("direction");
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getPool } from "@/services/database";
|
import { getPool } from "@/services/database";
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
|
||||||
|
|
||||||
// DELETE - Supprimer complètement un utilisateur
|
// DELETE - Supprimer complètement un utilisateur
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
@@ -8,12 +7,6 @@ export async function DELETE(
|
|||||||
{ params }: { params: Promise<{ userId: string }> }
|
{ params }: { params: Promise<{ userId: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { userId } = await params;
|
const { userId } = await params;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getPool } from "@/services/database";
|
import { getPool } from "@/services/database";
|
||||||
import { isUserAuthenticated } from "@/lib/server-auth";
|
|
||||||
|
|
||||||
// GET - Récupérer la liste des utilisateurs
|
// GET - Récupérer la liste des utilisateurs
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
// Récupérer tous les utilisateurs avec leurs informations d'équipe et d'évaluations
|
// 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 { evaluationService } from "@/services/evaluation-service";
|
||||||
import { userService } from "@/services/user-service";
|
import { userService } from "@/services/user-service";
|
||||||
import { UserEvaluation, UserProfile } from "@/lib/types";
|
import { UserEvaluation, UserProfile } from "@/lib/types";
|
||||||
import { COOKIE_NAME } from "@/lib/auth-utils";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const cookieStore = await cookies();
|
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é)
|
// Support pour l'ancien mode avec paramètres (pour la compatibilité)
|
||||||
if (!userUuid) {
|
if (!userUuid) {
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import {
|
import { AuthService } from "@/services";
|
||||||
isUserAuthenticated,
|
import { SkillsService, TeamsService } from "@/services";
|
||||||
getServerUserEvaluation,
|
import { evaluationService } from "@/services/evaluation-service";
|
||||||
getServerSkillCategories,
|
|
||||||
getServerTeams,
|
|
||||||
} from "@/lib/server-auth";
|
|
||||||
import { EvaluationClientWrapper } from "@/components/evaluation";
|
import { EvaluationClientWrapper } from "@/components/evaluation";
|
||||||
import { SkillEvaluation } from "@/components/skill-evaluation";
|
import { SkillEvaluation } from "@/components/skill-evaluation";
|
||||||
|
|
||||||
export default async function EvaluationPage() {
|
export default async function EvaluationPage() {
|
||||||
// Vérifier l'authentification
|
// Charger les données côté serveur
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
const userUuid = await AuthService.getUserUuidFromCookie();
|
||||||
|
|
||||||
// Si pas de cookie d'authentification, rediriger vers login
|
if (!userUuid) {
|
||||||
if (!isAuthenticated) {
|
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger les données côté serveur
|
|
||||||
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
||||||
getServerUserEvaluation(),
|
evaluationService.getServerUserEvaluation(userUuid!),
|
||||||
getServerSkillCategories(),
|
SkillsService.getSkillCategories(),
|
||||||
getServerTeams(),
|
TeamsService.getTeams(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,265 +1,46 @@
|
|||||||
"use client";
|
import { redirect } from "next/navigation";
|
||||||
|
import { TeamsService, userService } from "@/services";
|
||||||
import { useEffect, useState } from "react";
|
import { AuthService } from "@/services";
|
||||||
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 {
|
import {
|
||||||
Card,
|
LoginLayout,
|
||||||
CardContent,
|
LoginFormWrapper,
|
||||||
CardDescription,
|
LoginLoading,
|
||||||
CardHeader,
|
} from "@/components/login";
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
interface LoginPageProps {}
|
export default async function LoginPage() {
|
||||||
|
try {
|
||||||
|
// Charger les équipes côté serveur
|
||||||
|
const teams = await TeamsService.getTeams();
|
||||||
|
|
||||||
export default function LoginPage({}: LoginPageProps) {
|
// Vérifier si l'utilisateur est déjà connecté
|
||||||
const [teams, setTeams] = useState<Team[]>([]);
|
const userUuid = await AuthService.getUserUuidFromCookie();
|
||||||
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(() => {
|
if (userUuid) {
|
||||||
async function initialize() {
|
// Si l'utilisateur est connecté, récupérer son profil côté serveur
|
||||||
try {
|
const userProfile = await userService.getUserByUuid(userUuid);
|
||||||
// Vérifier si l'utilisateur est déjà connecté
|
|
||||||
const user = await AuthService.getCurrentUser();
|
|
||||||
setCurrentUser(user);
|
|
||||||
|
|
||||||
// Charger les équipes
|
if (userProfile) {
|
||||||
const teamsResponse = await fetch("/api/teams");
|
// Passer le profil utilisateur pour permettre la modification
|
||||||
if (teamsResponse.ok) {
|
return (
|
||||||
const teamsData = await teamsResponse.json();
|
<LoginLayout>
|
||||||
setTeams(teamsData);
|
<LoginFormWrapper teams={teams} initialUser={userProfile} />
|
||||||
}
|
</LoginLayout>
|
||||||
} catch (error) {
|
);
|
||||||
console.error("Error initializing login page:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize();
|
// Si l'utilisateur n'est pas connecté, afficher le formulaire de connexion
|
||||||
}, []);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
|
<LoginLayout>
|
||||||
<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" />
|
<LoginFormWrapper teams={teams} />
|
||||||
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
|
</LoginLayout>
|
||||||
|
);
|
||||||
<div className="relative z-10 container mx-auto py-16 px-6">
|
} catch (error) {
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
console.error("Error loading login page:", error);
|
||||||
<div className="text-center">
|
return (
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400 mx-auto mb-4"></div>
|
<LoginLayout>
|
||||||
<p className="text-white">Chargement...</p>
|
<LoginLoading />
|
||||||
</div>
|
</LoginLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si l'utilisateur est connecté et qu'on ne modifie pas
|
|
||||||
if (currentUser && !isEditing) {
|
|
||||||
const currentTeam = teams.find((t) => t.id === currentUser.teamId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
|
|
||||||
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
|
|
||||||
|
|
||||||
<div className="relative z-10 container mx-auto py-16 px-6">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
|
|
||||||
<Code2 className="h-4 w-4 text-blue-400" />
|
|
||||||
<span className="text-sm font-medium text-slate-200">
|
|
||||||
PeakSkills
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold mb-4 text-white">
|
|
||||||
Vous êtes connecté
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-slate-400 mb-8">
|
|
||||||
Gérez votre profil ou retournez à l'application
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-2xl mx-auto space-y-6">
|
|
||||||
{/* Informations utilisateur */}
|
|
||||||
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-white">Vos informations</CardTitle>
|
|
||||||
<CardDescription className="text-slate-400">
|
|
||||||
Profil actuellement connecté
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-300">
|
|
||||||
Prénom
|
|
||||||
</label>
|
|
||||||
<p className="text-white">{currentUser.firstName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-300">
|
|
||||||
Nom
|
|
||||||
</label>
|
|
||||||
<p className="text-white">{currentUser.lastName}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-slate-300">
|
|
||||||
Équipe
|
|
||||||
</label>
|
|
||||||
<p className="text-white">
|
|
||||||
{currentTeam?.name || "Équipe non trouvée"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex gap-4 justify-center">
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="bg-blue-500 hover:bg-blue-600 text-white"
|
|
||||||
>
|
|
||||||
<Home className="w-4 h-4 mr-2" />
|
|
||||||
Retour à l'accueil
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleEdit}
|
|
||||||
variant="outline"
|
|
||||||
className="border-white/20 text-white hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4 mr-2" />
|
|
||||||
Modifier le profil
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleLogout}
|
|
||||||
variant="outline"
|
|
||||||
className="border-red-500/50 text-red-400 hover:bg-red-500/10"
|
|
||||||
>
|
|
||||||
<LogOut className="w-4 h-4 mr-2" />
|
|
||||||
Se déconnecter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sinon, afficher le formulaire (nouvel utilisateur ou modification)
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
|
|
||||||
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
|
|
||||||
|
|
||||||
<div className="relative z-10 container mx-auto py-16 px-6">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
|
|
||||||
<Code2 className="h-4 w-4 text-blue-400" />
|
|
||||||
<span className="text-sm font-medium text-slate-200">
|
|
||||||
PeakSkills
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold mb-4 text-white">
|
|
||||||
{isEditing
|
|
||||||
? "Modifier vos informations"
|
|
||||||
: "Bienvenue sur PeakSkills"}
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-slate-400 mb-8">
|
|
||||||
{isEditing
|
|
||||||
? "Mettez à jour vos informations personnelles"
|
|
||||||
: "Évaluez vos compétences techniques et suivez votre progression"}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{isEditing && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCancelEdit}
|
|
||||||
variant="outline"
|
|
||||||
className="mb-8 border-white/20 text-white hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4 mr-2" />
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-2xl mx-auto">
|
|
||||||
<div className="relative">
|
|
||||||
{authenticating && (
|
|
||||||
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm z-10 flex items-center justify-center rounded-lg">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-400 mx-auto mb-2"></div>
|
|
||||||
<p className="text-white text-sm">
|
|
||||||
{isEditing
|
|
||||||
? "Mise à jour en cours..."
|
|
||||||
: "Connexion en cours..."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ProfileForm
|
|
||||||
teams={teams}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
initialProfile={
|
|
||||||
isEditing && currentUser ? currentUser : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/page.tsx
22
app/page.tsx
@@ -1,10 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import {
|
import { AuthService } from "@/services";
|
||||||
isUserAuthenticated,
|
import { evaluationService, SkillsService, TeamsService } from "@/services";
|
||||||
getServerUserEvaluation,
|
|
||||||
getServerSkillCategories,
|
|
||||||
getServerTeams,
|
|
||||||
} from "@/lib/server-auth";
|
|
||||||
import { generateRadarData } from "@/lib/evaluation-utils";
|
import { generateRadarData } from "@/lib/evaluation-utils";
|
||||||
import {
|
import {
|
||||||
WelcomeHeader,
|
WelcomeHeader,
|
||||||
@@ -16,19 +12,17 @@ import {
|
|||||||
} from "@/components/home";
|
} from "@/components/home";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
// Vérifier l'authentification
|
// Charger les données côté serveur
|
||||||
const isAuthenticated = await isUserAuthenticated();
|
const userUuid = await AuthService.getUserUuidFromCookie();
|
||||||
|
|
||||||
// Si pas de cookie d'authentification, rediriger vers login
|
if (!userUuid) {
|
||||||
if (!isAuthenticated) {
|
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charger les données côté serveur
|
|
||||||
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
||||||
getServerUserEvaluation(),
|
evaluationService.getServerUserEvaluation(userUuid!),
|
||||||
getServerSkillCategories(),
|
SkillsService.getSkillCategories(),
|
||||||
getServerTeams(),
|
TeamsService.getTeams(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Si pas d'évaluation, afficher l'écran d'accueil
|
// 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { TeamMember } from "@/services/admin-management-service";
|
import { TeamMember } from "@/clients/domains/admin-client";
|
||||||
import { AdminManagementService } from "@/services/admin-management-service";
|
import { adminClient } from "@/clients";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface TeamMembersModalProps {
|
interface TeamMembersModalProps {
|
||||||
@@ -41,7 +41,7 @@ export function TeamMembersModal({
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const membersData = await AdminManagementService.getTeamMembers(teamId);
|
const membersData = await adminClient.getTeamMembers(teamId);
|
||||||
setMembers(membersData);
|
setMembers(membersData);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || "Erreur lors du chargement des membres");
|
setError(err.message || "Erreur lors du chargement des membres");
|
||||||
@@ -59,7 +59,7 @@ export function TeamMembersModal({
|
|||||||
|
|
||||||
setDeletingMemberId(memberId);
|
setDeletingMemberId(memberId);
|
||||||
try {
|
try {
|
||||||
await AdminManagementService.removeTeamMember(teamId, memberId);
|
await adminClient.removeTeamMember(teamId, memberId);
|
||||||
|
|
||||||
// Mettre à jour la liste locale
|
// Mettre à jour la liste locale
|
||||||
setMembers((prev) => prev.filter((member) => member.id !== memberId));
|
setMembers((prev) => prev.filter((member) => member.id !== memberId));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Team, SkillCategory } from "@/lib/types";
|
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 { TeamDetailModal } from "../team-detail/team-detail-modal";
|
||||||
import { AdminHeader } from "../utils/admin-header";
|
import { AdminHeader } from "../utils/admin-header";
|
||||||
import { AdminOverviewCards } from "./admin-overview-cards";
|
import { AdminOverviewCards } from "./admin-overview-cards";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Users, Building2 } from "lucide-react";
|
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";
|
import { DirectionOverview, TeamStatsCard } from "@/components/admin";
|
||||||
|
|
||||||
interface AdminContentTabsProps {
|
interface AdminContentTabsProps {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Users, Target, Building2, UserCheck } from "lucide-react";
|
import { Users, Target, Building2, UserCheck } from "lucide-react";
|
||||||
import { Team, SkillCategory } from "@/lib/types";
|
import { Team, SkillCategory } from "@/lib/types";
|
||||||
import { TeamStats, DirectionStats } from "@/services/admin-service";
|
import { TeamStats, DirectionStats } from "@/lib/admin-types";
|
||||||
|
|
||||||
interface AdminOverviewCardsProps {
|
interface AdminOverviewCardsProps {
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TreeCategoryHeader, TreeItemRow } from "@/components/admin";
|
import { TreeCategoryHeader, TreeItemRow } from "@/components/admin";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
import { Skill } from "@/services/admin-management-service";
|
import { Skill } from "@/clients/domains/admin-client";
|
||||||
|
|
||||||
interface SkillsListProps {
|
interface SkillsListProps {
|
||||||
filteredSkillsByCategory: Record<string, Skill[]>;
|
filteredSkillsByCategory: Record<string, Skill[]>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
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 { TeamDetailHeader } from "./team-detail-header";
|
||||||
import { TeamMetricsCards } from "./team-metrics-cards";
|
import { TeamMetricsCards } from "./team-metrics-cards";
|
||||||
import { TeamDetailTabs } from "./team-detail-tabs";
|
import { TeamDetailTabs } from "./team-detail-tabs";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Users, ExternalLink, Download, Eye } from "lucide-react";
|
import { Users, ExternalLink, Download, Eye } from "lucide-react";
|
||||||
|
|
||||||
import { TeamMember } from "@/services/admin-service";
|
import { TeamMember } from "@/lib/admin-types";
|
||||||
|
|
||||||
interface TeamDetailModalProps {
|
interface TeamDetailModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { TeamOverviewTab } from "./team-overview-tab";
|
|||||||
import { TeamSkillsTab } from "./team-skills-tab";
|
import { TeamSkillsTab } from "./team-skills-tab";
|
||||||
import { TeamMembersTab } from "./team-members-tab";
|
import { TeamMembersTab } from "./team-members-tab";
|
||||||
import { TeamInsightsTab } from "./team-insights-tab";
|
import { TeamInsightsTab } from "./team-insights-tab";
|
||||||
import { TeamStats, TeamMember } from "@/services/admin-service";
|
import { TeamStats, TeamMember } from "@/lib/admin-types";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { User, Award, BookOpen, X } from "lucide-react";
|
import { User, Award, BookOpen, X } from "lucide-react";
|
||||||
import { TeamMember } from "@/services/admin-service";
|
import { TeamMember } from "@/lib/admin-types";
|
||||||
|
|
||||||
interface TeamMemberModalProps {
|
interface TeamMemberModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { User, Award, BookOpen } from "lucide-react";
|
import { User, Award, BookOpen } from "lucide-react";
|
||||||
import { TeamMember } from "@/services/admin-service";
|
import { TeamMember } from "@/lib/admin-types";
|
||||||
|
|
||||||
interface TeamMembersTabProps {
|
interface TeamMembersTabProps {
|
||||||
members: TeamMember[];
|
members: TeamMember[];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BarChart3, Target, Star } from "lucide-react";
|
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";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Users, Building2 } from "lucide-react";
|
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 { Team as TeamType } from "@/lib/types";
|
||||||
import { TeamStats } from "@/services/admin-service";
|
import { TeamStats } from "@/lib/admin-types";
|
||||||
|
|
||||||
interface TeamsListProps {
|
interface TeamsListProps {
|
||||||
filteredTeamsByDirection: Record<string, TeamType[]>;
|
filteredTeamsByDirection: Record<string, TeamType[]>;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|||||||
import { Plus, Building2 } from "lucide-react";
|
import { Plus, Building2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SkillCategory, Team as TeamType } from "@/lib/types";
|
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 { TreeViewPage } from "../management/tree-view-page";
|
||||||
import { useTreeView } from "@/hooks/use-tree-view";
|
import { useTreeView } from "@/hooks/use-tree-view";
|
||||||
import { useFormDialog } from "@/hooks/use-form-dialog";
|
import { useFormDialog } from "@/hooks/use-form-dialog";
|
||||||
@@ -28,7 +28,14 @@ export function TeamsManagementPage({
|
|||||||
const [isMembersModalOpen, setIsMembersModalOpen] = useState(false);
|
const [isMembersModalOpen, setIsMembersModalOpen] = useState(false);
|
||||||
const [selectedTeam, setSelectedTeam] = useState<TeamType | null>(null);
|
const [selectedTeam, setSelectedTeam] = useState<TeamType | null>(null);
|
||||||
|
|
||||||
const { isCreateDialogOpen, isEditDialogOpen, openCreateDialog, closeCreateDialog, openEditDialog, closeEditDialog } = useFormDialog();
|
const {
|
||||||
|
isCreateDialogOpen,
|
||||||
|
isEditDialogOpen,
|
||||||
|
openCreateDialog,
|
||||||
|
closeCreateDialog,
|
||||||
|
openEditDialog,
|
||||||
|
closeEditDialog,
|
||||||
|
} = useFormDialog();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
teams: localTeams,
|
teams: localTeams,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Team } from "@/services/admin-management-service";
|
import { Team } from "@/clients/domains/admin-client";
|
||||||
|
|
||||||
interface UserFormData {
|
interface UserFormData {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -98,7 +98,11 @@ export function UserFormDialog({
|
|||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onSubmit} disabled={isSubmitting}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Users, Building2 } from "lucide-react";
|
import { Users, Building2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { TreeViewPage } from "../management/tree-view-page";
|
||||||
import { useTreeView } from "@/hooks/use-tree-view";
|
import { useTreeView } from "@/hooks/use-tree-view";
|
||||||
import { useFormDialog } from "@/hooks/use-form-dialog";
|
import { useFormDialog } from "@/hooks/use-form-dialog";
|
||||||
@@ -18,7 +18,8 @@ interface UsersManagementPageProps {
|
|||||||
export function UsersManagementPage({ teams }: UsersManagementPageProps) {
|
export function UsersManagementPage({ teams }: UsersManagementPageProps) {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
const { isCreateDialogOpen, openCreateDialog, closeCreateDialog } = useFormDialog();
|
const { isCreateDialogOpen, openCreateDialog, closeCreateDialog } =
|
||||||
|
useFormDialog();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
users,
|
users,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Users, Target, Building2, Filter } from "lucide-react";
|
import { Users, Target, Building2, Filter } from "lucide-react";
|
||||||
import { Team } from "@/lib/types";
|
import { Team } from "@/lib/types";
|
||||||
import { TeamStats } from "@/services/admin-service";
|
import { TeamStats } from "@/lib/admin-types";
|
||||||
import { MultiSelectFilter } from "./multi-select-filter";
|
import { MultiSelectFilter } from "./multi-select-filter";
|
||||||
|
|
||||||
interface AdminFiltersProps {
|
interface AdminFiltersProps {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Plus, X, Link as LinkIcon, Loader2 } from "lucide-react";
|
import { Plus, X, Link as LinkIcon, Loader2 } from "lucide-react";
|
||||||
import { apiClient } from "@/services/client";
|
import { skillsClient } from "@/clients";
|
||||||
|
|
||||||
interface CreateSkillFormProps {
|
interface CreateSkillFormProps {
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
@@ -29,31 +29,31 @@ export function CreateSkillForm({
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const addLink = () => {
|
const addLink = () => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
links: [...prev.links, ""]
|
links: [...prev.links, ""],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLink = (index: number) => {
|
const removeLink = (index: number) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
links: prev.links.filter((_, i) => i !== index)
|
links: prev.links.filter((_, i) => i !== index),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateLink = (index: number, value: string) => {
|
const updateLink = (index: number, value: string) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...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) => {
|
const generateSkillId = (name: string) => {
|
||||||
return name
|
return name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9\s]/g, '')
|
.replace(/[^a-z0-9\s]/g, "")
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, "-")
|
||||||
.trim();
|
.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export function CreateSkillForm({
|
|||||||
const categoryId = getCategoryId(categoryName);
|
const categoryId = getCategoryId(categoryName);
|
||||||
|
|
||||||
// Filtrer les liens vides
|
// Filtrer les liens vides
|
||||||
const validLinks = formData.links.filter(link => link.trim());
|
const validLinks = formData.links.filter((link) => link.trim());
|
||||||
|
|
||||||
const skillData = {
|
const skillData = {
|
||||||
id: skillId,
|
id: skillId,
|
||||||
@@ -90,7 +90,7 @@ export function CreateSkillForm({
|
|||||||
links: validLinks,
|
links: validLinks,
|
||||||
};
|
};
|
||||||
|
|
||||||
const success = await apiClient.createSkill(categoryId, skillData);
|
const success = await skillsClient.createSkill(categoryId, skillData);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
onSuccess(skillId);
|
onSuccess(skillId);
|
||||||
@@ -118,7 +118,9 @@ export function CreateSkillForm({
|
|||||||
id="skill-name"
|
id="skill-name"
|
||||||
placeholder="ex: Next.js, Docker, Figma..."
|
placeholder="ex: Next.js, Docker, Figma..."
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
||||||
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +131,9 @@ export function CreateSkillForm({
|
|||||||
id="skill-description"
|
id="skill-description"
|
||||||
placeholder="Décrivez brièvement cette compétence..."
|
placeholder="Décrivez brièvement cette compétence..."
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, description: e.target.value }))
|
||||||
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
@@ -141,11 +145,14 @@ export function CreateSkillForm({
|
|||||||
id="skill-icon"
|
id="skill-icon"
|
||||||
placeholder="ex: fab-react, fas-database..."
|
placeholder="ex: fab-react, fas-database..."
|
||||||
value={formData.icon}
|
value={formData.icon}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, icon: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, icon: e.target.value }))
|
||||||
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -205,7 +212,12 @@ export function CreateSkillForm({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isLoading}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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 { useState, useEffect } from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { SkillCategory } from "@/lib/types";
|
import { SkillCategory } from "@/lib/types";
|
||||||
import {
|
import { adminClient } from "@/clients";
|
||||||
AdminManagementService,
|
import { Skill } from "@/clients/domains/admin-client";
|
||||||
Skill,
|
|
||||||
} from "@/services/admin-management-service";
|
|
||||||
|
|
||||||
interface SkillFormData {
|
interface SkillFormData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -30,7 +28,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) {
|
|||||||
const fetchSkills = async () => {
|
const fetchSkills = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const skillsData = await AdminManagementService.getSkills();
|
const skillsData = await adminClient.getSkills();
|
||||||
setSkills(skillsData);
|
setSkills(skillsData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching skills:", error);
|
console.error("Error fetching skills:", error);
|
||||||
@@ -74,7 +72,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) {
|
|||||||
category: category.category,
|
category: category.category,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newSkill = await AdminManagementService.createSkill(skillData);
|
const newSkill = await adminClient.createSkill(skillData);
|
||||||
setSkills([...skills, newSkill]);
|
setSkills([...skills, newSkill]);
|
||||||
resetForm();
|
resetForm();
|
||||||
|
|
||||||
@@ -130,7 +128,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) {
|
|||||||
usageCount: editingSkill.usageCount,
|
usageCount: editingSkill.usageCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedSkill = await AdminManagementService.updateSkill(skillData);
|
const updatedSkill = await adminClient.updateSkill(skillData);
|
||||||
|
|
||||||
const updatedSkills = skills.map((skill) =>
|
const updatedSkills = skills.map((skill) =>
|
||||||
skill.id === editingSkill.id ? updatedSkill : skill
|
skill.id === editingSkill.id ? updatedSkill : skill
|
||||||
@@ -167,7 +165,7 @@ export function useSkillsManagement(skillCategories: SkillCategory[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AdminManagementService.deleteSkill(skillId);
|
await adminClient.deleteSkill(skillId);
|
||||||
setSkills(skills.filter((s) => s.id !== skillId));
|
setSkills(skills.filter((s) => s.id !== skillId));
|
||||||
toast({
|
toast({
|
||||||
title: "Succès",
|
title: "Succès",
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Team as TeamType } from "@/lib/types";
|
import { Team as TeamType } from "@/lib/types";
|
||||||
import { TeamStats } from "@/services/admin-service";
|
import { TeamStats } from "@/lib/admin-types";
|
||||||
import {
|
import { adminClient } from "@/clients";
|
||||||
AdminManagementService,
|
|
||||||
Team,
|
|
||||||
} from "@/services/admin-management-service";
|
|
||||||
|
|
||||||
interface TeamFormData {
|
interface TeamFormData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,7 +26,7 @@ export function useTeamsManagement(
|
|||||||
// Charger les teams depuis l'API
|
// Charger les teams depuis l'API
|
||||||
const fetchTeams = async () => {
|
const fetchTeams = async () => {
|
||||||
try {
|
try {
|
||||||
const teamsData = await AdminManagementService.getTeams();
|
const teamsData = await adminClient.getTeams();
|
||||||
// Note: on garde les teams existantes pour la compatibilité
|
// Note: on garde les teams existantes pour la compatibilité
|
||||||
// Les nouvelles teams créées via l'API seront visibles après rafraîchissement
|
// Les nouvelles teams créées via l'API seront visibles après rafraîchissement
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -68,7 +65,7 @@ export function useTeamsManagement(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
const newTeam = await AdminManagementService.createTeam(teamFormData);
|
const newTeam = await adminClient.createTeam(teamFormData);
|
||||||
toast({
|
toast({
|
||||||
title: "Succès",
|
title: "Succès",
|
||||||
description: "Équipe créée avec succès",
|
description: "Équipe créée avec succès",
|
||||||
@@ -129,10 +126,10 @@ export function useTeamsManagement(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
await AdminManagementService.updateTeam({
|
await adminClient.updateTeam({
|
||||||
id: editingTeam.id,
|
id: editingTeam.id,
|
||||||
...teamFormData,
|
...teamFormData,
|
||||||
memberCount: editingTeam.memberCount || 0,
|
memberCount: (editingTeam as any).memberCount || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -175,7 +172,7 @@ export function useTeamsManagement(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await AdminManagementService.deleteTeam(teamId);
|
await adminClient.deleteTeam(teamId);
|
||||||
toast({
|
toast({
|
||||||
title: "Succès",
|
title: "Succès",
|
||||||
description: "Équipe supprimée avec 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
|
// Mettre à jour l'état local au lieu de recharger la page
|
||||||
setTeams((prev) => prev.filter((t) => t.id !== teamId));
|
setTeams((prev) => prev.filter((t) => t.id !== teamId));
|
||||||
setTeamStats((prev) =>
|
setTeamStats((prev) => prev.filter((stats) => stats.teamId !== teamId));
|
||||||
prev.filter((stats) => stats.teamId !== teamId)
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur",
|
title: "Erreur",
|
||||||
@@ -224,7 +219,7 @@ export function useTeamsManagement(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await AdminManagementService.deleteDirection(direction);
|
await adminClient.deleteDirection(direction);
|
||||||
toast({
|
toast({
|
||||||
title: "Succès",
|
title: "Succès",
|
||||||
description: `Direction "${direction}" et toutes ses équipes supprimées avec 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
|
// Mettre à jour l'état local au lieu de recharger la page
|
||||||
setTeams((prev) =>
|
setTeams((prev) => prev.filter((team) => team.direction !== direction));
|
||||||
prev.filter((team) => team.direction !== direction)
|
|
||||||
);
|
|
||||||
setTeamStats((prev) =>
|
setTeamStats((prev) =>
|
||||||
prev.filter((stats) => stats.direction !== direction)
|
prev.filter((stats) => stats.direction !== direction)
|
||||||
);
|
);
|
||||||
@@ -250,9 +243,7 @@ export function useTeamsManagement(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Extraire les directions uniques pour les formulaires
|
// Extraire les directions uniques pour les formulaires
|
||||||
const directions = Array.from(
|
const directions = Array.from(new Set(teams.map((team) => team.direction)));
|
||||||
new Set(teams.map((team) => team.direction))
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams,
|
teams,
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export function useTreeView<T>({
|
|||||||
onSearchChange,
|
onSearchChange,
|
||||||
}: UseTreeViewOptions<T>) {
|
}: UseTreeViewOptions<T>) {
|
||||||
// État pour les catégories ouvertes/fermées
|
// É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
|
// Grouper les données par catégorie et filtrer en fonction de la recherche
|
||||||
const filteredDataByCategory = useMemo(() => {
|
const filteredDataByCategory = useMemo(() => {
|
||||||
@@ -31,23 +33,26 @@ export function useTreeView<T>({
|
|||||||
}, {} as Record<string, T[]>);
|
}, {} as Record<string, T[]>);
|
||||||
|
|
||||||
// Filtrer les données en fonction de la recherche
|
// Filtrer les données en fonction de la recherche
|
||||||
return Object.entries(dataByCategory).reduce((acc, [category, categoryItems]) => {
|
return Object.entries(dataByCategory).reduce(
|
||||||
const filteredItems = categoryItems.filter((item) => {
|
(acc, [category, categoryItems]) => {
|
||||||
const matchesSearch = searchFields.some((field) => {
|
const filteredItems = categoryItems.filter((item) => {
|
||||||
const value = item[field];
|
const matchesSearch = searchFields.some((field) => {
|
||||||
if (typeof value === 'string') {
|
const value = item[field];
|
||||||
return value.toLowerCase().includes(searchTerm.toLowerCase());
|
if (typeof value === "string") {
|
||||||
}
|
return value.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
return false;
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return matchesSearch;
|
||||||
});
|
});
|
||||||
return matchesSearch;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filteredItems.length > 0) {
|
if (filteredItems.length > 0) {
|
||||||
acc[category] = filteredItems;
|
acc[category] = filteredItems;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, T[]>);
|
},
|
||||||
|
{} as Record<string, T[]>
|
||||||
|
);
|
||||||
}, [data, searchFields, groupBy, searchTerm]);
|
}, [data, searchFields, groupBy, searchTerm]);
|
||||||
|
|
||||||
// Fonctions pour gérer l'expansion des catégories
|
// Fonctions pour gérer l'expansion des catégories
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Team } from "@/services/admin-management-service";
|
import { Team } from "@/clients/domains/admin-client";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -113,7 +113,7 @@ export function useUsersManagement(teams: Team[]) {
|
|||||||
setDeletingUserId(user.uuid);
|
setDeletingUserId(user.uuid);
|
||||||
try {
|
try {
|
||||||
// TODO: Implémenter la suppression d'utilisateur
|
// TODO: Implémenter la suppression d'utilisateur
|
||||||
// await AdminManagementService.deleteUser(user.uuid);
|
// await adminClient.deleteUser(user.uuid);
|
||||||
|
|
||||||
// Mettre à jour la liste locale
|
// Mettre à jour la liste locale
|
||||||
setUsers((prev) => prev.filter((u) => u.uuid !== user.uuid));
|
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 {
|
try {
|
||||||
// Simplement créer le profil via l'auth, pas besoin de créer une évaluation vide
|
// 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
|
// Le backend créera automatiquement l'évaluation lors du premier accès
|
||||||
const { AuthService } = await import("@/lib/auth-utils");
|
const { authClient } = await import("@/clients");
|
||||||
await AuthService.login(profile);
|
await authClient.login(profile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize evaluation:", error);
|
console.error("Failed to initialize evaluation:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
UserEvaluation,
|
UserEvaluation,
|
||||||
SkillCategory,
|
SkillCategory,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { apiClient } from "../services/api-client";
|
import { evaluationClient } from "../clients";
|
||||||
|
|
||||||
export function calculateCategoryScore(
|
export function calculateCategoryScore(
|
||||||
categoryEvaluation: CategoryEvaluation
|
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(
|
export function createEmptyEvaluation(
|
||||||
categories: SkillCategory[]
|
categories: SkillCategory[]
|
||||||
): CategoryEvaluation[] {
|
): 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;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
|
uuid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkillEvaluation {
|
export interface SkillEvaluation {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
// Vérifier le cookie d'authentification (maintenant un UUID)
|
// Vérifier le cookie d'authentification (maintenant un UUID)
|
||||||
const userUuid = request.cookies.get(COOKIE_NAME)?.value;
|
const userUuid = request.cookies.get(COOKIE_NAME)?.value;
|
||||||
|
|
||||||
if (!userUuid) {
|
if (!userUuid) {
|
||||||
// Rediriger vers la page de login si pas authentifié
|
// Rediriger vers la page de login si pas authentifié
|
||||||
const loginUrl = new URL("/login", request.url);
|
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 { getPool } from "./database";
|
||||||
import { Team, SkillCategory } from "@/lib/types";
|
import { Team, SkillCategory } from "@/lib/types";
|
||||||
|
import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-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 }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AdminService {
|
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(
|
async loadUserEvaluationByUuid(
|
||||||
userUuid: string
|
userUuid: string
|
||||||
): Promise<UserEvaluation | null> {
|
): Promise<UserEvaluation | null> {
|
||||||
|
if (!userUuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
|
|
||||||
@@ -677,6 +680,23 @@ export class EvaluationService {
|
|||||||
client.release();
|
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
|
// Instance singleton
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export { SkillsService } from "./skills-service";
|
|||||||
// Admin services (server-only)
|
// Admin services (server-only)
|
||||||
export { AdminService } from "./admin-service";
|
export { AdminService } from "./admin-service";
|
||||||
|
|
||||||
// Admin management services (client-side compatible)
|
// Admin types (can be imported anywhere)
|
||||||
export { AdminManagementService } from "./admin-management-service";
|
export type { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
||||||
|
|
||||||
// API client (can be used client-side)
|
// Server auth service (server-side only)
|
||||||
export { ApiClient, apiClient } from "./api-client";
|
export { AuthService, COOKIE_NAME, COOKIE_MAX_AGE } from "./auth-service";
|
||||||
|
|||||||
Reference in New Issue
Block a user