Compare commits
10 Commits
a5bcdd34fb
...
3fd3e165e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fd3e165e2 | ||
|
|
a6362b6435 | ||
|
|
6db4921d0f | ||
|
|
ad5d954182 | ||
|
|
7d12a66c12 | ||
|
|
117ac243f5 | ||
|
|
725a368b7e | ||
|
|
84979501fa | ||
|
|
85b0cb0a6b | ||
|
|
a8cad0b2ec |
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next"
|
||||||
|
}
|
||||||
174
NEXTAUTH_MIGRATION.md
Normal file
174
NEXTAUTH_MIGRATION.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Migration vers NextAuth v5 - Résumé
|
||||||
|
|
||||||
|
## Changements effectués
|
||||||
|
|
||||||
|
### 1. Installation
|
||||||
|
- ✅ Installé `next-auth@5.0.0-beta.29`
|
||||||
|
|
||||||
|
### 2. Configuration NextAuth
|
||||||
|
- ✅ Créé `auth.ts` à la racine avec:
|
||||||
|
- Credentials provider utilisant `userService.verifyCredentials()`
|
||||||
|
- Stratégie JWT
|
||||||
|
- Callbacks pour enrichir la session avec `userId`, `teamId`, `firstName`, `lastName`
|
||||||
|
- Pages custom (`/login`)
|
||||||
|
- Middleware callback `authorized` pour protéger les routes
|
||||||
|
|
||||||
|
- ✅ Créé `app/api/auth/[...nextauth]/route.ts` pour les handlers NextAuth
|
||||||
|
|
||||||
|
- ✅ Créé `types/next-auth.d.ts` pour étendre les types Session et User
|
||||||
|
|
||||||
|
### 3. Middleware
|
||||||
|
- ✅ Remplacé le middleware custom par `export { auth as middleware } from "@/auth"`
|
||||||
|
- ✅ Les redirections sont gérées dans le callback `authorized`
|
||||||
|
|
||||||
|
### 4. Routes API
|
||||||
|
- ✅ Supprimé `app/api/auth/login/route.ts` (géré par NextAuth)
|
||||||
|
- ✅ Supprimé `app/api/auth/logout/route.ts` (géré par NextAuth)
|
||||||
|
- ✅ Supprimé `app/api/auth/profile/route.ts` (utiliser `useSession()` côté client)
|
||||||
|
- ✅ Adapté `app/api/auth/register/route.ts` pour retourner les infos sans créer de session
|
||||||
|
- ✅ Mis à jour toutes les routes protégées pour utiliser `auth()` au lieu de `AuthService.requireAuthenticatedUser()`
|
||||||
|
|
||||||
|
### 5. Services
|
||||||
|
- ✅ Supprimé `services/auth-service.ts` (remplacé par NextAuth)
|
||||||
|
- ✅ Mis à jour `services/evaluation-service.ts` pour ne plus utiliser `AuthService`
|
||||||
|
- ✅ Mis à jour `services/index.ts` pour retirer l'export d'AuthService
|
||||||
|
|
||||||
|
### 6. Clients
|
||||||
|
- ✅ Mis à jour `clients/domains/auth-client.ts` pour utiliser:
|
||||||
|
- `signIn("credentials")` pour le login
|
||||||
|
- `signOut()` pour le logout
|
||||||
|
- Auto-login après register avec `signIn()`
|
||||||
|
|
||||||
|
### 7. Frontend
|
||||||
|
- ✅ Créé `components/auth/session-provider.tsx`
|
||||||
|
- ✅ Wrapped l'application avec `<SessionProvider>` dans `app/layout.tsx`
|
||||||
|
- ✅ Mis à jour tous les composants pour utiliser la session NextAuth
|
||||||
|
- ✅ Mis à jour toutes les pages serveur pour utiliser `auth()` au lieu de `AuthService`
|
||||||
|
|
||||||
|
## Configuration requise
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
Créer un fichier `.env.local` avec:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Générer un secret avec: openssl rand -base64 32
|
||||||
|
AUTH_SECRET=your-generated-secret-here
|
||||||
|
|
||||||
|
# Pour la production
|
||||||
|
# AUTH_URL=https://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pour générer AUTH_SECRET
|
||||||
|
```bash
|
||||||
|
openssl rand -base64 32
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
### Côté serveur (Server Components, API Routes)
|
||||||
|
```typescript
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id;
|
||||||
|
const teamId = session.user.teamId;
|
||||||
|
const firstName = session.user.firstName;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Côté client (Client Components)
|
||||||
|
```typescript
|
||||||
|
"use client";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { signIn, signOut } from "next-auth/react";
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
|
if (status === "loading") return <div>Loading...</div>;
|
||||||
|
if (!session) return <div>Not authenticated</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Welcome {session.user.firstName}!</p>
|
||||||
|
<button onClick={() => signOut()}>Logout</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login/Register
|
||||||
|
```typescript
|
||||||
|
// Login
|
||||||
|
const result = await signIn("credentials", {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
redirect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register (via API puis auto-login)
|
||||||
|
const response = await fetch("/api/auth/register", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await signIn("credentials", { email, password, redirect: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
await signOut({ redirect: false });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibilité
|
||||||
|
|
||||||
|
### Utilisateurs existants
|
||||||
|
✅ **Tous les utilisateurs existants sont compatibles** car:
|
||||||
|
- La table `users` contient déjà `email` et `password_hash`
|
||||||
|
- La méthode `userService.verifyCredentials()` vérifie les mots de passe hashés avec bcrypt
|
||||||
|
- Les UUID sont préservés
|
||||||
|
|
||||||
|
### Sessions
|
||||||
|
- ⚠️ Les anciennes sessions (cookie `session_token`) ne sont plus valides
|
||||||
|
- Les utilisateurs devront se reconnecter une fois
|
||||||
|
- NextAuth utilise ses propres cookies de session
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Vérifications à faire
|
||||||
|
1. ✅ Login avec email/password
|
||||||
|
2. ✅ Logout
|
||||||
|
3. ✅ Register puis auto-login
|
||||||
|
4. ✅ Routes protégées redirigent vers /login si non authentifié
|
||||||
|
5. ✅ /login redirige vers / si déjà authentifié
|
||||||
|
6. ✅ Les évaluations fonctionnent correctement
|
||||||
|
7. ✅ Les pages admin fonctionnent correctement
|
||||||
|
|
||||||
|
## Rollback (si nécessaire)
|
||||||
|
|
||||||
|
Si des problèmes surviennent, voici les commits importants:
|
||||||
|
- Installation de NextAuth
|
||||||
|
- Création de auth.ts et configuration
|
||||||
|
- Mise à jour des routes et services
|
||||||
|
- Suppression de auth-service.ts
|
||||||
|
|
||||||
|
Pour rollback: `git revert <commit-hash>` des commits concernés.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- NextAuth v5 utilise des cookies signés et sécurisés automatiquement
|
||||||
|
- Les sessions JWT sont stateless (pas de stockage en base)
|
||||||
|
- La configuration `authorized` dans auth.ts gère toute la logique de protection des routes
|
||||||
|
- Le SessionProvider doit wrapper toute l'application pour que `useSession()` fonctionne
|
||||||
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { AuthService, userService, TeamsService } from "@/services";
|
import { auth } from "@/auth";
|
||||||
|
import { userService, TeamsService } from "@/services";
|
||||||
import { AccountForm } from "@/components/account/account-form";
|
import { AccountForm } from "@/components/account/account-form";
|
||||||
|
|
||||||
export default async function AccountPage() {
|
export default async function AccountPage() {
|
||||||
try {
|
try {
|
||||||
// Vérifier si l'utilisateur est connecté
|
// Vérifier si l'utilisateur est connecté
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
if (!userUuid) {
|
if (!session?.user) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer le profil utilisateur
|
// Récupérer le profil utilisateur
|
||||||
const userProfile = await userService.getUserByUuid(userUuid);
|
const userProfile = await userService.getUserByUuid(session.user.id);
|
||||||
|
|
||||||
if (!userProfile) {
|
if (!userProfile) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default async function SkillsPage() {
|
|||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-lg text-red-500">
|
<div className="text-lg text-red-500">
|
||||||
Erreur lors du chargement des données d'administration
|
Erreur lors du chargement des données d'administration
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default async function TeamsPage() {
|
|||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-lg text-red-500">
|
<div className="text-lg text-red-500">
|
||||||
Erreur lors du chargement des données d'administration
|
Erreur lors du chargement des données d'administration
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function UsersPage() {
|
|||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-lg text-red-500">
|
<div className="text-lg text-red-500">
|
||||||
Erreur lors du chargement des données d'administration
|
Erreur lors du chargement des données d'administration
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default async function AdminPage() {
|
|||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-lg text-red-500">
|
<div className="text-lg text-red-500">
|
||||||
Erreur lors du chargement des données d'administration
|
Erreur lors du chargement des données d'administration
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 flex items-center justify-center">
|
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 flex items-center justify-center">
|
||||||
<div className="text-white text-xl text-red-500">
|
<div className="text-white text-xl text-red-500">
|
||||||
Erreur lors du chargement des détails de l'équipe
|
Erreur lors du chargement des détails de l'équipe
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
3
app/api/auth/[...nextauth]/route.ts
Normal file
3
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { handlers } from "@/auth";
|
||||||
|
|
||||||
|
export const { GET, POST } = handlers;
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { AuthService, UserService } from "@/services";
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { email, password } = await request.json();
|
|
||||||
|
|
||||||
// Validation des données
|
|
||||||
if (!email || !password) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Email et mot de passe requis" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier les identifiants
|
|
||||||
const userService = new UserService();
|
|
||||||
const user = await userService.verifyCredentials(email, password);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Email ou mot de passe incorrect" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Générer un token de session
|
|
||||||
const sessionToken = await AuthService.createSession(user.uuid_id);
|
|
||||||
|
|
||||||
// Créer la réponse avec le cookie de session
|
|
||||||
const response = NextResponse.json(
|
|
||||||
{
|
|
||||||
message: "Connexion réussie",
|
|
||||||
user: {
|
|
||||||
id: user.uuid_id,
|
|
||||||
firstName: user.first_name,
|
|
||||||
lastName: user.last_name,
|
|
||||||
email: user.email,
|
|
||||||
teamId: user.team_id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Définir le cookie de session
|
|
||||||
response.cookies.set("session_token", sessionToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: 60 * 60 * 24 * 7, // 7 jours
|
|
||||||
path: "/",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Erreur interne du serveur" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export async function POST() {
|
|
||||||
try {
|
|
||||||
// Créer la réponse
|
|
||||||
const response = NextResponse.json(
|
|
||||||
{ message: "Déconnexion réussie" },
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Supprimer le cookie de session
|
|
||||||
response.cookies.set("session_token", "", {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: 0, // Expire immédiatement
|
|
||||||
path: "/",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Logout error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Erreur interne du serveur" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { AuthService, userService } from "@/services";
|
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Vérifier si l'utilisateur est connecté
|
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
|
||||||
|
|
||||||
if (!userUuid) {
|
|
||||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer les données de mise à jour
|
|
||||||
const { firstName, lastName, teamId } = await request.json();
|
|
||||||
|
|
||||||
// Validation des données
|
|
||||||
if (!firstName || !lastName || !teamId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Tous les champs sont requis" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour l'utilisateur
|
|
||||||
await userService.updateUserByUuid(userUuid, {
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
message: "Profil mis à jour avec succès",
|
|
||||||
user: {
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
teamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Profile update error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || "Erreur lors de la mise à jour du profil" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { AuthService, userService } from "@/services";
|
import { userService } from "@/services";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -44,11 +44,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Générer un token de session
|
// Retourner les informations de l'utilisateur créé
|
||||||
const sessionToken = await AuthService.createSession(newUser.uuid_id);
|
// Le client devra appeler signIn() pour créer la session
|
||||||
|
return NextResponse.json(
|
||||||
// Créer la réponse avec le cookie de session
|
|
||||||
const response = NextResponse.json(
|
|
||||||
{
|
{
|
||||||
message: "Compte créé avec succès",
|
message: "Compte créé avec succès",
|
||||||
user: {
|
user: {
|
||||||
@@ -61,17 +59,6 @@ export async function POST(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
{ status: 201 }
|
{ status: 201 }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Définir le cookie de session
|
|
||||||
response.cookies.set("session_token", sessionToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: 60 * 60 * 24 * 7, // 7 jours
|
|
||||||
path: "/",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Register error:", error);
|
console.error("Register error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { AuthService } from "@/services/auth-service";
|
import { auth } from "@/auth";
|
||||||
import { evaluationService } from "@/services/evaluation-service";
|
import { evaluationService } from "@/services/evaluation-service";
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Récupérer l'utilisateur depuis le cookie (maintenant un UUID)
|
// Récupérer l'utilisateur depuis la session NextAuth
|
||||||
const { userProfile } = await AuthService.requireAuthenticatedUser();
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Non authentifié" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userProfile = {
|
||||||
|
firstName: session.user.firstName,
|
||||||
|
lastName: session.user.lastName,
|
||||||
|
teamId: session.user.teamId,
|
||||||
|
};
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { category, skillId, level, canMentor, wantsToLearn, action } = body;
|
const { category, skillId, level, canMentor, wantsToLearn, action } = body;
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { TeamReviewService } from "@/services/team-review-service";
|
import { TeamReviewService } from "@/services/team-review-service";
|
||||||
import { AuthService } from "@/services/auth-service";
|
import { auth } from "@/auth";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Vérifier l'authentification
|
// Vérifier l'authentification
|
||||||
const { userProfile } = await AuthService.requireAuthenticatedUser();
|
const session = await auth();
|
||||||
|
|
||||||
const teamId = userProfile.teamId;
|
if (!session?.user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Non authentifié" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamId = session.user.teamId;
|
||||||
const data = await TeamReviewService.getTeamReviewData(teamId);
|
const data = await TeamReviewService.getTeamReviewData(teamId);
|
||||||
|
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { AuthService } from "@/services";
|
import { auth } from "@/auth";
|
||||||
import { SkillsService, TeamsService } from "@/services";
|
import { SkillsService, TeamsService } from "@/services";
|
||||||
import { evaluationService } from "@/services/evaluation-service";
|
import { evaluationService } from "@/services/evaluation-service";
|
||||||
import { EvaluationClientWrapper } from "@/components/evaluation";
|
import { EvaluationClientWrapper } from "@/components/evaluation";
|
||||||
@@ -7,12 +7,14 @@ import { SkillEvaluation } from "@/components/skill-evaluation";
|
|||||||
|
|
||||||
export default async function EvaluationPage() {
|
export default async function EvaluationPage() {
|
||||||
// Charger les données côté serveur
|
// Charger les données côté serveur
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
if (!userUuid) {
|
if (!session?.user) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userUuid = session.user.id;
|
||||||
|
|
||||||
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
||||||
evaluationService.getServerUserEvaluation(userUuid!),
|
evaluationService.getServerUserEvaluation(userUuid!),
|
||||||
SkillsService.getSkillCategories(),
|
SkillsService.getSkillCategories(),
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { ThemeProvider } from "@/components/layout/theme-provider";
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { UserProvider } from "@/hooks/use-user-context";
|
import { UserProvider } from "@/hooks/use-user-context";
|
||||||
import { NavigationWrapper } from "@/components/layout/navigation-wrapper";
|
import { NavigationWrapper } from "@/components/layout/navigation-wrapper";
|
||||||
import { AuthService, TeamsService } from "@/services";
|
import { SessionProvider } from "@/components/auth/session-provider";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
import { TeamsService } from "@/services";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "PeakSkills - Auto-évaluation de compétences",
|
title: "PeakSkills - Auto-évaluation de compétences",
|
||||||
@@ -20,17 +22,17 @@ export default async function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
// Récupérer les infos utilisateur côté serveur
|
// Récupérer les infos utilisateur depuis la session NextAuth
|
||||||
let userInfo = null;
|
let userInfo = null;
|
||||||
try {
|
try {
|
||||||
const { userUuid, userProfile } =
|
const session = await auth();
|
||||||
await AuthService.requireAuthenticatedUser();
|
|
||||||
|
|
||||||
|
if (session?.user) {
|
||||||
// Récupérer le nom de l'équipe
|
// Récupérer le nom de l'équipe
|
||||||
let teamName = "Équipe non définie";
|
let teamName = "Équipe non définie";
|
||||||
if (userProfile.teamId) {
|
if (session.user.teamId) {
|
||||||
try {
|
try {
|
||||||
const team = await TeamsService.getTeamById(userProfile.teamId);
|
const team = await TeamsService.getTeamById(session.user.teamId);
|
||||||
if (team) {
|
if (team) {
|
||||||
teamName = team.name;
|
teamName = team.name;
|
||||||
}
|
}
|
||||||
@@ -40,12 +42,13 @@ export default async function RootLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
userInfo = {
|
userInfo = {
|
||||||
firstName: userProfile.firstName,
|
firstName: session.user.firstName,
|
||||||
lastName: userProfile.lastName,
|
lastName: session.user.lastName,
|
||||||
teamName,
|
teamName,
|
||||||
teamId: userProfile.teamId,
|
teamId: session.user.teamId,
|
||||||
uuid: userUuid,
|
uuid: session.user.id,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Utilisateur non authentifié, userInfo reste null
|
// Utilisateur non authentifié, userInfo reste null
|
||||||
console.log("User not authenticated:", error);
|
console.log("User not authenticated:", error);
|
||||||
@@ -56,6 +59,7 @@ export default async function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
|
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
<SessionProvider>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
@@ -68,6 +72,7 @@ export default async function RootLayout({
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</SessionProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TeamsService, userService } from "@/services";
|
import { TeamsService, userService } from "@/services";
|
||||||
import { AuthService } from "@/services";
|
import { auth } from "@/auth";
|
||||||
import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login";
|
import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login";
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
@@ -8,13 +8,13 @@ export default async function LoginPage() {
|
|||||||
const teams = await TeamsService.getTeams();
|
const teams = await TeamsService.getTeams();
|
||||||
|
|
||||||
// Vérifier si l'utilisateur est déjà connecté
|
// Vérifier si l'utilisateur est déjà connecté
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
let userProfile = null;
|
let userProfile = null;
|
||||||
|
|
||||||
if (userUuid) {
|
if (session?.user) {
|
||||||
// Si l'utilisateur est connecté, récupérer son profil côté serveur
|
// Si l'utilisateur est connecté, récupérer son profil côté serveur
|
||||||
userProfile = await userService.getUserByUuid(userUuid);
|
userProfile = await userService.getUserByUuid(session.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth
|
// Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { AuthService } from "@/services";
|
import { auth } from "@/auth";
|
||||||
import { evaluationService, SkillsService, TeamsService } from "@/services";
|
import { evaluationService, SkillsService, TeamsService } from "@/services";
|
||||||
import { generateRadarData } from "@/lib/evaluation-utils";
|
import { generateRadarData } from "@/lib/evaluation-utils";
|
||||||
import {
|
import {
|
||||||
@@ -14,12 +14,14 @@ import {
|
|||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
// Charger les données côté serveur
|
// Charger les données côté serveur
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const session = await auth();
|
||||||
|
|
||||||
if (!userUuid) {
|
if (!session?.user) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userUuid = session.user.id;
|
||||||
|
|
||||||
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
const [userEvaluation, skillCategories, teams] = await Promise.all([
|
||||||
evaluationService.getServerUserEvaluation(userUuid!),
|
evaluationService.getServerUserEvaluation(userUuid!),
|
||||||
SkillsService.getSkillCategories(),
|
SkillsService.getSkillCategories(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TeamReviewService } from "@/services/team-review-service";
|
import { TeamReviewService } from "@/services/team-review-service";
|
||||||
import { AuthService } from "@/services/auth-service";
|
import { auth } from "@/auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TeamOverview } from "@/components/team-review/team-overview";
|
import { TeamOverview } from "@/components/team-review/team-overview";
|
||||||
import { SkillMatrix } from "@/components/team-review/skill-matrix";
|
import { SkillMatrix } from "@/components/team-review/skill-matrix";
|
||||||
@@ -13,10 +13,14 @@ export const dynamic = "force-dynamic";
|
|||||||
|
|
||||||
async function TeamReviewPage() {
|
async function TeamReviewPage() {
|
||||||
try {
|
try {
|
||||||
const { userProfile } = await AuthService.requireAuthenticatedUser();
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
const teamData = await TeamReviewService.getTeamReviewData(
|
const teamData = await TeamReviewService.getTeamReviewData(
|
||||||
userProfile.teamId
|
session.user.teamId
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +36,7 @@ async function TeamReviewPage() {
|
|||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm">
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm">
|
||||||
<Users className="h-4 w-4 text-blue-400" />
|
<Users className="h-4 w-4 text-blue-400" />
|
||||||
<span className="text-sm font-medium text-slate-200">
|
<span className="text-sm font-medium text-slate-200">
|
||||||
Vue d'équipe
|
Vue d'équipe
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +45,7 @@ async function TeamReviewPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-slate-400 max-w-2xl mx-auto leading-relaxed">
|
<p className="text-slate-400 max-w-2xl mx-auto leading-relaxed">
|
||||||
Vue d'ensemble et analyse des compétences de l'équipe{" "}
|
Vue d'ensemble et analyse des compétences de l'équipe{" "}
|
||||||
{teamData.team.direction}
|
{teamData.team.direction}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
69
auth.ts
Normal file
69
auth.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import NextAuth from "next-auth";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
|
||||||
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
|
providers: [
|
||||||
|
Credentials({
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
if (!credentials?.email || !credentials?.password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import dynamique pour éviter l'erreur Edge Runtime dans le middleware
|
||||||
|
const { userService } = await import("@/services/user-service");
|
||||||
|
|
||||||
|
const user = await userService.verifyCredentials(
|
||||||
|
credentials.email as string,
|
||||||
|
credentials.password as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retourner l'utilisateur au format attendu par NextAuth
|
||||||
|
return {
|
||||||
|
id: user.uuid_id,
|
||||||
|
email: user.email,
|
||||||
|
name: `${user.first_name} ${user.last_name}`,
|
||||||
|
firstName: user.first_name,
|
||||||
|
lastName: user.last_name,
|
||||||
|
teamId: user.team_id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, user }) {
|
||||||
|
// Lors de la première connexion, ajouter les infos custom au token
|
||||||
|
if (user) {
|
||||||
|
token.id = user.id;
|
||||||
|
token.teamId = (user as any).teamId;
|
||||||
|
token.firstName = (user as any).firstName;
|
||||||
|
token.lastName = (user as any).lastName;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
// Ajouter les infos du token à la session
|
||||||
|
if (token && session.user) {
|
||||||
|
session.user.id = token.id as string;
|
||||||
|
session.user.teamId = token.teamId as string;
|
||||||
|
session.user.firstName = token.firstName as string;
|
||||||
|
session.user.lastName = token.lastName as string;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { signIn, signOut } from "next-auth/react";
|
||||||
import { BaseHttpClient } from "../base/http-client";
|
import { BaseHttpClient } from "../base/http-client";
|
||||||
import { UserProfile } from "../../lib/types";
|
|
||||||
|
|
||||||
export interface LoginCredentials {
|
export interface LoginCredentials {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -24,27 +24,54 @@ export interface AuthUser {
|
|||||||
|
|
||||||
export class AuthClient extends BaseHttpClient {
|
export class AuthClient extends BaseHttpClient {
|
||||||
/**
|
/**
|
||||||
* Connecte un utilisateur avec email/password
|
* Connecte un utilisateur avec email/password via NextAuth
|
||||||
*/
|
*/
|
||||||
async login(
|
async login(credentials: LoginCredentials): Promise<{ success: boolean; error?: string }> {
|
||||||
credentials: LoginCredentials
|
const result = await signIn("credentials", {
|
||||||
): Promise<{ user: AuthUser; message: string }> {
|
email: credentials.email,
|
||||||
return await this.post("/auth/login", credentials);
|
password: credentials.password,
|
||||||
|
redirect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
return { success: false, error: "Email ou mot de passe incorrect" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée un nouveau compte utilisateur
|
* Crée un nouveau compte utilisateur puis se connecte automatiquement
|
||||||
*/
|
*/
|
||||||
async register(
|
async register(data: RegisterData): Promise<{ success: boolean; error?: string }> {
|
||||||
data: RegisterData
|
try {
|
||||||
): Promise<{ user: AuthUser; message: string }> {
|
// Créer l'utilisateur via l'API
|
||||||
return await this.post("/auth/register", data);
|
const response = await this.post<{ user: AuthUser; message: string }>("/auth/register", data);
|
||||||
|
|
||||||
|
// Se connecter automatiquement après création
|
||||||
|
const loginResult = await signIn("credentials", {
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
redirect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loginResult?.error) {
|
||||||
|
return { success: false, error: "Compte créé mais erreur de connexion" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || "Erreur lors de la création du compte"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Déconnecte l'utilisateur
|
* Déconnecte l'utilisateur via NextAuth
|
||||||
*/
|
*/
|
||||||
async logout(): Promise<{ message: string }> {
|
async logout(): Promise<void> {
|
||||||
return await this.post("/auth/logout");
|
await signOut({ redirect: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export function AccountForm({ initialProfile, teams }: AccountFormProps) {
|
|||||||
|
|
||||||
{Object.keys(teamsByDirection).length === 0 && (
|
{Object.keys(teamsByDirection).length === 0 && (
|
||||||
<div className="px-3 py-4 text-center text-sm text-muted-foreground">
|
<div className="px-3 py-4 text-center text-sm text-muted-foreground">
|
||||||
Aucune équipe trouvée pour "{searchTerm}"
|
Aucune équipe trouvée pour "{searchTerm}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Users, User, Calendar, X, Trash2 } from "lucide-react";
|
import { Users, User, Calendar, X, Trash2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -31,13 +31,7 @@ export function TeamMembersModal({
|
|||||||
const [deletingMemberId, setDeletingMemberId] = useState<string | null>(null);
|
const [deletingMemberId, setDeletingMemberId] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchMembers = useCallback(async () => {
|
||||||
if (isOpen && teamId) {
|
|
||||||
fetchMembers();
|
|
||||||
}
|
|
||||||
}, [isOpen, teamId]);
|
|
||||||
|
|
||||||
const fetchMembers = async () => {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -48,7 +42,13 @@ export function TeamMembersModal({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [teamId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && teamId) {
|
||||||
|
fetchMembers();
|
||||||
|
}
|
||||||
|
}, [isOpen, teamId, fetchMembers]);
|
||||||
|
|
||||||
const handleRemoveMember = async (memberId: string, memberName: string) => {
|
const handleRemoveMember = async (memberId: string, memberName: string) => {
|
||||||
if (
|
if (
|
||||||
@@ -91,7 +91,7 @@ export function TeamMembersModal({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Users className="w-6 h-6 text-blue-400" />
|
<Users className="w-6 h-6 text-blue-400" />
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Membres de l'équipe</CardTitle>
|
<CardTitle className="text-xl">Membres de l'équipe</CardTitle>
|
||||||
<p className="text-sm text-slate-500 font-normal">{teamName}</p>
|
<p className="text-sm text-slate-500 font-normal">{teamName}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +137,7 @@ export function TeamMembersModal({
|
|||||||
Aucun membre dans cette équipe
|
Aucun membre dans cette équipe
|
||||||
</p>
|
</p>
|
||||||
<p className="text-slate-500 text-sm mt-1">
|
<p className="text-slate-500 text-sm mt-1">
|
||||||
Les membres apparaîtront ici une fois qu'ils seront assignés
|
Les membres apparaîtront ici une fois qu'ils seront assignés
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
getSkillLevelLabel,
|
getSkillLevelLabel,
|
||||||
getSkillLevelColor,
|
getSkillLevelColor,
|
||||||
} from "../team-detail/team-stats-row";
|
} from "../team-detail/team-stats-row";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface DirectionOverviewProps {
|
interface DirectionOverviewProps {
|
||||||
direction: string;
|
direction: string;
|
||||||
@@ -182,7 +186,10 @@ export function DirectionOverview({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
averageCriticalCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
averageCriticalCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -193,7 +200,10 @@ export function DirectionOverview({
|
|||||||
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
||||||
averageCriticalCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
averageCriticalCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "bg-gradient-to-r from-red-500 to-red-400"
|
? "bg-gradient-to-r from-red-500 to-red-400"
|
||||||
: "bg-gradient-to-r from-green-500 to-green-400"
|
: "bg-gradient-to-r from-green-500 to-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -210,7 +220,10 @@ export function DirectionOverview({
|
|||||||
<span className="text-sm text-slate-300">Majeures:</span>
|
<span className="text-sm text-slate-300">Majeures:</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
averageCriticalCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
averageCriticalCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-orange-400"
|
? "text-orange-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -221,7 +234,10 @@ export function DirectionOverview({
|
|||||||
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
className={`h-1.5 rounded-full transition-all shadow-sm ${
|
||||||
averageCriticalCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
averageCriticalCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "bg-gradient-to-r from-orange-500 to-orange-400"
|
? "bg-gradient-to-r from-orange-500 to-orange-400"
|
||||||
: "bg-gradient-to-r from-green-500 to-green-400"
|
: "bg-gradient-to-r from-green-500 to-green-400"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ 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";
|
||||||
import { TeamMemberModal } from "@/components/admin";
|
import { TeamMemberModal } from "@/components/admin";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamDetailClientWrapperProps {
|
interface TeamDetailClientWrapperProps {
|
||||||
team: TeamStats;
|
team: TeamStats;
|
||||||
@@ -185,10 +189,14 @@ export function TeamDetailClientWrapper({
|
|||||||
),
|
),
|
||||||
skillGaps: {
|
skillGaps: {
|
||||||
incontournable: skillAnalysis.filter(
|
incontournable: skillAnalysis.filter(
|
||||||
(s) => s.importance === "incontournable" && s.coverage < 75
|
(s) =>
|
||||||
|
s.importance === "incontournable" &&
|
||||||
|
isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
majeure: skillAnalysis.filter(
|
majeure: skillAnalysis.filter(
|
||||||
(s) => s.importance === "majeure" && s.coverage < 60
|
(s) =>
|
||||||
|
s.importance === "majeure" &&
|
||||||
|
isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
standard: skillAnalysis.filter(
|
standard: skillAnalysis.filter(
|
||||||
(s) => s.importance === "standard" && s.averageLevel < 1.5
|
(s) => s.importance === "standard" && s.averageLevel < 1.5
|
||||||
@@ -196,10 +204,14 @@ export function TeamDetailClientWrapper({
|
|||||||
},
|
},
|
||||||
strongSkills: {
|
strongSkills: {
|
||||||
incontournable: skillAnalysis.filter(
|
incontournable: skillAnalysis.filter(
|
||||||
(s) => s.importance === "incontournable" && s.coverage >= 75
|
(s) =>
|
||||||
|
s.importance === "incontournable" &&
|
||||||
|
!isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
majeure: skillAnalysis.filter(
|
majeure: skillAnalysis.filter(
|
||||||
(s) => s.importance === "majeure" && s.coverage >= 60
|
(s) =>
|
||||||
|
s.importance === "majeure" &&
|
||||||
|
!isCoverageBelowObjective(s.coverage, s.importance)
|
||||||
).length,
|
).length,
|
||||||
standard: skillAnalysis.filter(
|
standard: skillAnalysis.filter(
|
||||||
(s) => s.importance === "standard" && s.averageLevel >= 2.5
|
(s) => s.importance === "standard" && s.averageLevel >= 2.5
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function TeamDetailHeader({
|
|||||||
className="text-slate-400 hover:text-white hover:bg-white/10"
|
className="text-slate-400 hover:text-white hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Retour à l'admin
|
Retour à l'admin
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-slate-400 text-sm">|</div>
|
<div className="text-slate-400 text-sm">|</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ 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 "@/lib/admin-types";
|
import { TeamMember } from "@/lib/admin-types";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamDetailModalProps {
|
interface TeamDetailModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -112,7 +116,7 @@ export function TeamDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-400">
|
<DialogDescription className="text-slate-400">
|
||||||
Aperçu rapide de l'équipe. Cliquez sur "Voir tous les détails" pour
|
Aperçu rapide de l'équipe. Cliquez sur "Voir tous les détails" pour
|
||||||
une vue complète.
|
une vue complète.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -157,7 +161,10 @@ export function TeamDetailModal({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
team.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -168,7 +175,10 @@ export function TeamDetailModal({
|
|||||||
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all ${
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
team.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
@@ -182,7 +192,10 @@ export function TeamDetailModal({
|
|||||||
<span className="text-sm text-slate-300">Majeures</span>
|
<span className="text-sm text-slate-300">Majeures</span>
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
team.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -193,7 +206,10 @@ export function TeamDetailModal({
|
|||||||
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
<div className="w-full bg-slate-700/50 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all ${
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
team.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
team.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
@@ -209,13 +225,11 @@ export function TeamDetailModal({
|
|||||||
<h3 className="font-medium text-white mb-3">Top 3 Compétences</h3>
|
<h3 className="font-medium text-white mb-3">Top 3 Compétences</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{team.topSkills.slice(0, 3).map((skill, idx) => {
|
{team.topSkills.slice(0, 3).map((skill, idx) => {
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable"
|
const isUnderTarget = isCoverageBelowObjective(
|
||||||
? 75
|
skill.coverage,
|
||||||
: skill.importance === "majeure"
|
skill.importance
|
||||||
? 60
|
);
|
||||||
: 0;
|
|
||||||
const isUnderTarget = target > 0 && skill.coverage < target;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function TeamDetailTabs({
|
|||||||
value="overview"
|
value="overview"
|
||||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-slate-400 hover:text-white transition-colors"
|
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-slate-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
Vue d'ensemble
|
Vue d'ensemble
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="skills"
|
value="skills"
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { TrendingUp, MessageSquare, Lightbulb } from "lucide-react";
|
import { TrendingUp, MessageSquare, Lightbulb } from "lucide-react";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
@@ -64,9 +68,7 @@ export function TeamInsightsTab({
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{/* Incontournables */}
|
{/* Incontournables */}
|
||||||
{skillAnalysis
|
{skillAnalysis
|
||||||
.filter(
|
.filter((s) => isCoverageBelowObjective(s.coverage, s.importance))
|
||||||
(s) => s.importance === "incontournable" && s.coverage < 75
|
|
||||||
)
|
|
||||||
.map((skill, idx) => (
|
.map((skill, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -83,7 +85,7 @@ export function TeamInsightsTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-[10px] text-red-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="text-[10px] text-red-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
Objectif: 75%
|
Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-red-400 font-medium">
|
<div className="text-xs text-red-400 font-medium">
|
||||||
{skill.coverage.toFixed(0)}%
|
{skill.coverage.toFixed(0)}%
|
||||||
@@ -95,7 +97,7 @@ export function TeamInsightsTab({
|
|||||||
|
|
||||||
{/* Majeures */}
|
{/* Majeures */}
|
||||||
{skillAnalysis
|
{skillAnalysis
|
||||||
.filter((s) => s.importance === "majeure" && s.coverage < 60)
|
.filter((s) => isCoverageBelowObjective(s.coverage, s.importance))
|
||||||
.map((skill, idx) => (
|
.map((skill, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -112,7 +114,7 @@ export function TeamInsightsTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-[10px] text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="text-[10px] text-blue-300 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
Objectif: 60%
|
Objectif: {COVERAGE_OBJECTIVES[skill.importance]}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-blue-400 font-medium">
|
<div className="text-xs text-blue-400 font-medium">
|
||||||
{skill.coverage.toFixed(0)}%
|
{skill.coverage.toFixed(0)}%
|
||||||
@@ -202,7 +204,7 @@ export function TeamInsightsTab({
|
|||||||
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6">
|
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6">
|
||||||
<h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||||
<Lightbulb className="h-5 w-5 text-yellow-400" />
|
<Lightbulb className="h-5 w-5 text-yellow-400" />
|
||||||
Recommandations pour l'équipe
|
Recommandations pour l'équipe
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl">
|
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl">
|
||||||
@@ -217,7 +219,7 @@ export function TeamInsightsTab({
|
|||||||
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""}{" "}
|
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""}{" "}
|
||||||
incontournable
|
incontournable
|
||||||
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""} sous
|
{teamInsights.skillGaps.incontournable > 1 ? "s" : ""} sous
|
||||||
l'objectif de 75%.
|
l'objectif de {COVERAGE_OBJECTIVES.incontournable}%.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -237,9 +239,9 @@ export function TeamInsightsTab({
|
|||||||
<>
|
<>
|
||||||
Attention: {teamInsights.skillGaps.majeure} compétence
|
Attention: {teamInsights.skillGaps.majeure} compétence
|
||||||
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} majeure
|
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} majeure
|
||||||
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} n'atteigne
|
{teamInsights.skillGaps.majeure > 1 ? "s" : ""} n'atteigne
|
||||||
{teamInsights.skillGaps.majeure > 1 ? "nt" : ""} pas
|
{teamInsights.skillGaps.majeure > 1 ? "nt" : ""} pas
|
||||||
l'objectif de 60%.
|
l'objectif de {COVERAGE_OBJECTIVES.majeure}%.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Users, BarChart3, Award, BookOpen, Target } from "lucide-react";
|
import { Users, BarChart3, Award, BookOpen, Target } from "lucide-react";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamInsights {
|
interface TeamInsights {
|
||||||
averageTeamLevel: number;
|
averageTeamLevel: number;
|
||||||
@@ -81,7 +85,10 @@ export function TeamMetricsCards({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`text-2xl font-bold ${
|
className={`text-2xl font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -102,7 +109,10 @@ export function TeamMetricsCards({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`text-2xl font-bold ${
|
className={`text-2xl font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
import { BarChart3, Target, Star } from "lucide-react";
|
import { BarChart3, Target, Star } from "lucide-react";
|
||||||
import { TeamStats } from "@/lib/admin-types";
|
import { TeamStats } from "@/lib/admin-types";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
category: string;
|
category: string;
|
||||||
importance: "incontournable" | "majeure" | "standard";
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
icon?: string;
|
||||||
experts: Array<{
|
experts: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
level: number;
|
level: number;
|
||||||
@@ -69,7 +74,7 @@ export function TeamOverviewTab({
|
|||||||
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6">
|
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6">
|
||||||
<h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
||||||
<Star className="h-5 w-5 text-yellow-400" />
|
<Star className="h-5 w-5 text-yellow-400" />
|
||||||
Top Compétences de l'équipe
|
Top Compétences de l'équipe
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{team.topSkills.slice(0, 6).map((skill, idx) => (
|
{team.topSkills.slice(0, 6).map((skill, idx) => (
|
||||||
@@ -125,15 +130,14 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={`text-xs ${
|
className={`text-xs ${
|
||||||
skill.importance === "incontournable"
|
skill.importance === "standard"
|
||||||
? skill.coverage < 75
|
? "text-slate-400"
|
||||||
|
: isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
: skill.importance === "majeure"
|
|
||||||
? skill.coverage < 60
|
|
||||||
? "text-red-400"
|
|
||||||
: "text-green-400"
|
|
||||||
: "text-slate-400"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{skill.coverage.toFixed(0)}%
|
{skill.coverage.toFixed(0)}%
|
||||||
@@ -231,7 +235,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
skill.coverage < 75
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -241,7 +248,12 @@ export function TeamOverviewTab({
|
|||||||
<div className="w-16 bg-white/10 rounded-full h-1.5">
|
<div className="w-16 bg-white/10 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full ${
|
className={`h-1.5 rounded-full ${
|
||||||
skill.coverage < 75 ? "bg-red-500" : "bg-green-500"
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
|
? "bg-red-500"
|
||||||
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${skill.coverage}%` }}
|
style={{ width: `${skill.coverage}%` }}
|
||||||
/>
|
/>
|
||||||
@@ -292,7 +304,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
skill.coverage < 60
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -302,7 +317,12 @@ export function TeamOverviewTab({
|
|||||||
<div className="w-16 bg-white/10 rounded-full h-1.5">
|
<div className="w-16 bg-white/10 rounded-full h-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full ${
|
className={`h-1.5 rounded-full ${
|
||||||
skill.coverage < 60 ? "bg-red-500" : "bg-green-500"
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
|
? "bg-red-500"
|
||||||
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${skill.coverage}%` }}
|
style={{ width: `${skill.coverage}%` }}
|
||||||
/>
|
/>
|
||||||
@@ -404,7 +424,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -417,7 +440,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="w-16 bg-white/10 rounded-full h-2">
|
<div className="w-16 bg-white/10 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${
|
className={`h-2 rounded-full ${
|
||||||
teamInsights.criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
@@ -437,7 +463,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
teamInsights.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -447,7 +476,10 @@ export function TeamOverviewTab({
|
|||||||
<div className="w-16 bg-white/10 rounded-full h-2">
|
<div className="w-16 bg-white/10 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${
|
className={`h-2 rounded-full ${
|
||||||
teamInsights.criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
teamInsights.criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "bg-red-500"
|
? "bg-red-500"
|
||||||
: "bg-green-500"
|
: "bg-green-500"
|
||||||
}`}
|
}`}
|
||||||
@@ -473,7 +505,7 @@ export function TeamOverviewTab({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||||
<span className="text-slate-300">Objectifs d'apprentissage</span>
|
<span className="text-slate-300">Objectifs d'apprentissage</span>
|
||||||
<span className="text-white font-bold">
|
<span className="text-white font-bold">
|
||||||
{teamInsights.totalLearners}
|
{teamInsights.totalLearners}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
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 {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface SkillAnalysis {
|
interface SkillAnalysis {
|
||||||
skillName: string;
|
skillName: string;
|
||||||
@@ -124,13 +128,11 @@ export function TeamSkillsTab({ skillAnalysis }: TeamSkillsTabProps) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredSkills.map((skill, idx) => {
|
{filteredSkills.map((skill, idx) => {
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable"
|
const isUnderTarget = isCoverageBelowObjective(
|
||||||
? 75
|
skill.coverage,
|
||||||
: skill.importance === "majeure"
|
skill.importance
|
||||||
? 60
|
);
|
||||||
: 0;
|
|
||||||
const isUnderTarget = target > 0 && skill.coverage < target;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamStatsCardProps {
|
interface TeamStatsCardProps {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -24,6 +28,8 @@ interface TeamStatsCardProps {
|
|||||||
averageLevel: number;
|
averageLevel: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
|
coverage: number;
|
||||||
}>;
|
}>;
|
||||||
skillCoverage: number;
|
skillCoverage: number;
|
||||||
onViewDetails?: () => void;
|
onViewDetails?: () => void;
|
||||||
@@ -52,10 +58,11 @@ export function getSkillLevelBadgeClasses(level: number): string {
|
|||||||
return "bg-green-500/20 border-green-500/30 text-green-300";
|
return "bg-green-500/20 border-green-500/30 text-green-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProgressColor(percentage: number): string {
|
export function getProgressColor(
|
||||||
if (percentage < 30) return "bg-red-500";
|
percentage: number,
|
||||||
if (percentage < 60) return "bg-orange-500";
|
importance: "incontournable" | "majeure" | "standard"
|
||||||
if (percentage < 80) return "bg-blue-500";
|
): string {
|
||||||
|
if (isCoverageBelowObjective(percentage, importance)) return "bg-red-500";
|
||||||
return "bg-green-500";
|
return "bg-green-500";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
import { getImportanceColors } from "@/lib/tech-colors";
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
interface TeamStatsRowProps {
|
interface TeamStatsRowProps {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -70,10 +74,11 @@ export function getSkillLevelBadgeClasses(level: number): string {
|
|||||||
return "bg-green-500/20 border-green-500/30 text-green-300";
|
return "bg-green-500/20 border-green-500/30 text-green-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProgressColor(percentage: number): string {
|
export function getProgressColor(
|
||||||
if (percentage < 30) return "bg-red-500";
|
percentage: number,
|
||||||
if (percentage < 60) return "bg-orange-500";
|
importance: "incontournable" | "majeure" | "standard"
|
||||||
if (percentage < 80) return "bg-blue-500";
|
): string {
|
||||||
|
if (isCoverageBelowObjective(percentage, importance)) return "bg-red-500";
|
||||||
return "bg-green-500";
|
return "bg-green-500";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +95,14 @@ export function TeamStatsRow({
|
|||||||
onViewReport,
|
onViewReport,
|
||||||
}: TeamStatsRowProps) {
|
}: TeamStatsRowProps) {
|
||||||
// Calculer les alertes sur les compétences critiques
|
// Calculer les alertes sur les compétences critiques
|
||||||
const hasIncontournableAlert = criticalSkillsCoverage.incontournable < 75;
|
const hasIncontournableAlert = isCoverageBelowObjective(
|
||||||
const hasMajeureAlert = criticalSkillsCoverage.majeure < 60;
|
criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
);
|
||||||
|
const hasMajeureAlert = isCoverageBelowObjective(
|
||||||
|
criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group bg-gradient-to-r from-slate-800/50 to-slate-700/40 backdrop-blur-sm border border-slate-600/40 rounded-xl p-4 hover:from-slate-700/60 hover:to-slate-600/50 hover:border-slate-500/50 transition-all duration-300 shadow-lg hover:shadow-xl">
|
<div className="group bg-gradient-to-r from-slate-800/50 to-slate-700/40 backdrop-blur-sm border border-slate-600/40 rounded-xl p-4 hover:from-slate-700/60 hover:to-slate-600/50 hover:border-slate-500/50 transition-all duration-300 shadow-lg hover:shadow-xl">
|
||||||
@@ -144,7 +155,10 @@ export function TeamStatsRow({
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
criticalSkillsCoverage.incontournable < 75
|
isCoverageBelowObjective(
|
||||||
|
criticalSkillsCoverage.incontournable,
|
||||||
|
"incontournable"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -158,7 +172,7 @@ export function TeamStatsRow({
|
|||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
Couverture des compétences incontournables
|
Couverture des compétences incontournables
|
||||||
<br />
|
<br />
|
||||||
Objectif : 75%
|
Objectif : {COVERAGE_OBJECTIVES.incontournable}%
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -170,7 +184,10 @@ export function TeamStatsRow({
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className={`text-sm font-bold ${
|
className={`text-sm font-bold ${
|
||||||
criticalSkillsCoverage.majeure < 60
|
isCoverageBelowObjective(
|
||||||
|
criticalSkillsCoverage.majeure,
|
||||||
|
"majeure"
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-green-400"
|
: "text-green-400"
|
||||||
}`}
|
}`}
|
||||||
@@ -184,7 +201,7 @@ export function TeamStatsRow({
|
|||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
Couverture des compétences majeures
|
Couverture des compétences majeures
|
||||||
<br />
|
<br />
|
||||||
Objectif : 60%
|
Objectif : {COVERAGE_OBJECTIVES.majeure}%
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -222,13 +239,11 @@ export function TeamStatsRow({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{topSkills.slice(0, 3).map((skill, idx) => {
|
{topSkills.slice(0, 3).map((skill, idx) => {
|
||||||
const colors = getImportanceColors(skill.importance);
|
const colors = getImportanceColors(skill.importance);
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable"
|
const isUnderTarget = isCoverageBelowObjective(
|
||||||
? 75
|
skill.coverage,
|
||||||
: skill.importance === "majeure"
|
skill.importance
|
||||||
? 60
|
);
|
||||||
: 0;
|
|
||||||
const isUnderTarget = target > 0 && skill.coverage < target;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider key={idx}>
|
<TooltipProvider key={idx}>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function TeamFormDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="team-name">Nom de l'équipe *</Label>
|
<Label htmlFor="team-name">Nom de l'équipe *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="team-name"
|
id="team-name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function AdminHeader() {
|
|||||||
<h1 className="text-4xl font-bold text-white">Dashboard Managérial</h1>
|
<h1 className="text-4xl font-bold text-white">Dashboard Managérial</h1>
|
||||||
|
|
||||||
<p className="text-slate-400 max-w-2xl mx-auto leading-relaxed">
|
<p className="text-slate-400 max-w-2xl mx-auto leading-relaxed">
|
||||||
Vue d'ensemble des compétences par équipe et direction pour pilotage
|
Vue d'ensemble des compétences par équipe et direction pour pilotage
|
||||||
stratégique
|
stratégique
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export function AdminHeader() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Building2 className="w-4 h-4 mr-2" />
|
<Building2 className="w-4 h-4 mr-2" />
|
||||||
Vue d'ensemble
|
Vue d'ensemble
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/admin/manage">
|
<Link href="/admin/manage">
|
||||||
|
|||||||
13
components/auth/session-provider.tsx
Normal file
13
components/auth/session-provider.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface SessionProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SessionProvider({ children }: SessionProviderProps) {
|
||||||
|
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export function ActionSection() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="bg-blue-500 hover:bg-blue-600 text-white px-8 py-3 rounded-xl"
|
className="bg-blue-500 hover:bg-blue-600 text-white px-8 py-3 rounded-xl"
|
||||||
>
|
>
|
||||||
<Link href="/evaluation">Continuer l'évaluation</Link>
|
<Link href="/evaluation">Continuer l'évaluation</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface CategoryBreakdownProps {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
|
import { ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
|
||||||
import { getCategoryIcon } from "@/lib/category-icons";
|
import { getCategoryIcon } from "@/lib/category-icons";
|
||||||
import { getScoreColors } from "@/lib/score-utils";
|
import { getScoreColors } from "@/lib/score-utils";
|
||||||
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
import { SkillProgress } from "./skill-progress";
|
import { SkillProgress } from "./skill-progress";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ interface CategoryCardProps {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -114,18 +116,34 @@ export function CategoryCard({
|
|||||||
<div className="px-3 pb-3 border-t border-white/10 bg-white/5">
|
<div className="px-3 pb-3 border-t border-white/10 bg-white/5">
|
||||||
{categoryEval && skillCategory && skillsCount > 0 ? (
|
{categoryEval && skillCategory && skillsCount > 0 ? (
|
||||||
<div className="pt-3 space-y-2 max-h-60 overflow-y-auto">
|
<div className="pt-3 space-y-2 max-h-60 overflow-y-auto">
|
||||||
{categoryEval.selectedSkillIds.map((skillId) => {
|
{categoryEval.selectedSkillIds
|
||||||
|
.map((skillId) => {
|
||||||
const skill = skillCategory.skills.find(
|
const skill = skillCategory.skills.find(
|
||||||
(s) => s.id === skillId
|
(s) => s.id === skillId
|
||||||
);
|
);
|
||||||
if (!skill) return null;
|
return skill ? { ...skill, id: skillId } : null;
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(skill): skill is NonNullable<typeof skill> => skill !== null
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const importanceOrder = {
|
||||||
|
incontournable: 2,
|
||||||
|
majeure: 1,
|
||||||
|
standard: 0,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
importanceOrder[b.importance] -
|
||||||
|
importanceOrder[a.importance]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((skill) => {
|
||||||
const skillEval = categoryEval.skills.find(
|
const skillEval = categoryEval.skills.find(
|
||||||
(s) => s.skillId === skillId
|
(s) => s.skillId === skill.id
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<SkillProgress
|
<SkillProgress
|
||||||
key={skillId}
|
key={skill.id}
|
||||||
skill={skill}
|
skill={skill}
|
||||||
skillEval={skillEval}
|
skillEval={skillEval}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ export function MentorSection({
|
|||||||
userEvaluation,
|
userEvaluation,
|
||||||
skillCategories,
|
skillCategories,
|
||||||
}: MentorSectionProps) {
|
}: MentorSectionProps) {
|
||||||
|
// Fonction de tri par importance
|
||||||
|
const sortByImportance = (
|
||||||
|
a: { importance: string },
|
||||||
|
b: { importance: string }
|
||||||
|
) => {
|
||||||
|
const importanceOrder = {
|
||||||
|
incontournable: 2,
|
||||||
|
majeure: 1,
|
||||||
|
standard: 0,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
importanceOrder[b.importance as keyof typeof importanceOrder] -
|
||||||
|
importanceOrder[a.importance as keyof typeof importanceOrder]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Récupérer les compétences maîtrisées (expert uniquement)
|
// Récupérer les compétences maîtrisées (expert uniquement)
|
||||||
const masteredSkills = userEvaluation.evaluations.flatMap((cat) => {
|
const masteredSkills = userEvaluation.evaluations.flatMap((cat) => {
|
||||||
const skillCategory = skillCategories.find(
|
const skillCategory = skillCategories.find(
|
||||||
@@ -81,6 +97,40 @@ export function MentorSection({
|
|||||||
className={`bg-white/5 border border-white/10 rounded-xl p-6 backdrop-blur-sm ${className}`}
|
className={`bg-white/5 border border-white/10 rounded-xl p-6 backdrop-blur-sm ${className}`}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Peut mentorer */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||||
|
<span className="w-2 h-2 bg-blue-400 rounded-full"></span>
|
||||||
|
Peut mentorer
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{mentorSkills.length > 0 ? (
|
||||||
|
mentorSkills.sort(sortByImportance).map((tech) => {
|
||||||
|
const colors = getImportanceColors(tech.importance);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tech.id}
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg ${colors.bg} ${colors.border} border transition-all hover:scale-105`}
|
||||||
|
>
|
||||||
|
<TechIcon
|
||||||
|
iconName={tech.icon}
|
||||||
|
className="w-4 h-4"
|
||||||
|
fallbackText={tech.name}
|
||||||
|
/>
|
||||||
|
<span className={`text-sm font-medium ${colors.text}`}>
|
||||||
|
{tech.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-slate-400 text-sm">
|
||||||
|
Aucune compétence mentor configurée
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Technologies maîtrisées */}
|
{/* Technologies maîtrisées */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||||
@@ -89,7 +139,7 @@ export function MentorSection({
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{masteredSkills.length > 0 ? (
|
{masteredSkills.length > 0 ? (
|
||||||
masteredSkills.map((tech) => {
|
masteredSkills.sort(sortByImportance).map((tech) => {
|
||||||
const colors = getImportanceColors(tech.importance);
|
const colors = getImportanceColors(tech.importance);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -116,40 +166,6 @@ export function MentorSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Peut mentorer */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
|
||||||
<span className="w-2 h-2 bg-blue-400 rounded-full"></span>
|
|
||||||
Peut mentorer
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{mentorSkills.length > 0 ? (
|
|
||||||
mentorSkills.map((tech) => {
|
|
||||||
const colors = getImportanceColors(tech.importance);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={tech.id}
|
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg ${colors.bg} ${colors.border} border transition-all hover:scale-105`}
|
|
||||||
>
|
|
||||||
<TechIcon
|
|
||||||
iconName={tech.icon}
|
|
||||||
className="w-4 h-4"
|
|
||||||
fallbackText={tech.name}
|
|
||||||
/>
|
|
||||||
<span className={`text-sm font-medium ${colors.text}`}>
|
|
||||||
{tech.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<p className="text-slate-400 text-sm">
|
|
||||||
Aucune compétence mentor configurée
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Technologies à apprendre */}
|
{/* Technologies à apprendre */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||||
@@ -158,7 +174,7 @@ export function MentorSection({
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{learningSkills.length > 0 ? (
|
{learningSkills.length > 0 ? (
|
||||||
learningSkills.map((tech) => {
|
learningSkills.sort(sortByImportance).map((tech) => {
|
||||||
const colors = getImportanceColors(tech.importance);
|
const colors = getImportanceColors(tech.importance);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -178,7 +194,7 @@ export function MentorSection({
|
|||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<p className="text-slate-400 text-sm">
|
<p className="text-slate-400 text-sm">
|
||||||
Aucun objectif d'apprentissage configuré
|
Aucun objectif d'apprentissage configuré
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function RadarSection({ radarData }: RadarSectionProps) {
|
|||||||
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6">
|
<div className="bg-white/5 backdrop-blur-sm border border-white/10 rounded-2xl p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-xl font-bold text-white mb-2">
|
<h3 className="text-xl font-bold text-white mb-2">
|
||||||
Vue d'ensemble de vos compétences
|
Vue d'ensemble de vos compétences
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-slate-400 text-sm">
|
<p className="text-slate-400 text-sm">
|
||||||
Radar chart représentant votre niveau par catégorie
|
Radar chart représentant votre niveau par catégorie
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
import { getSkillLevelLabel } from "@/lib/score-utils";
|
import { getSkillLevelLabel } from "@/lib/score-utils";
|
||||||
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
|
||||||
interface SkillProgressProps {
|
interface SkillProgressProps {
|
||||||
skill: {
|
skill: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
importance: "incontournable" | "majeure" | "standard";
|
||||||
};
|
};
|
||||||
skillEval?: {
|
skillEval?: {
|
||||||
skillId: string;
|
skillId: string;
|
||||||
@@ -14,15 +16,19 @@ interface SkillProgressProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SkillProgress({ skill, skillEval }: SkillProgressProps) {
|
export function SkillProgress({ skill, skillEval }: SkillProgressProps) {
|
||||||
|
const colors = getImportanceColors(skill.importance);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between p-2 bg-white/5 border border-white/10 rounded-lg">
|
<div
|
||||||
|
className={`flex items-center justify-between p-2 ${colors.bg} border ${colors.border} rounded-lg`}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TechIcon
|
<TechIcon
|
||||||
iconName={skill.icon || ""}
|
iconName={skill.icon || ""}
|
||||||
className="w-4 h-4 text-blue-400"
|
className={`w-4 h-4 ${colors.text}`}
|
||||||
fallbackText={skill.name}
|
fallbackText={skill.name}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-white">{skill.name}</span>
|
<span className={`text-sm ${colors.text}`}>{skill.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{skillEval?.level && (
|
{skillEval?.level && (
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export function WelcomeScreen() {
|
|||||||
Bienvenue ! Commencez votre parcours
|
Bienvenue ! Commencez votre parcours
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-slate-400 mb-12 max-w-2xl mx-auto">
|
<p className="text-lg text-slate-400 mb-12 max-w-2xl mx-auto">
|
||||||
Vous êtes connecté avec succès. Il est temps d'évaluer vos
|
Vous êtes connecté avec succès. Il est temps d'évaluer vos
|
||||||
compétences techniques pour obtenir une vue d'ensemble personnalisée
|
compétences techniques pour obtenir une vue d'ensemble personnalisée
|
||||||
de votre expertise.
|
de votre expertise.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export function WelcomeScreen() {
|
|||||||
Évaluez vos compétences
|
Évaluez vos compétences
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-slate-400 text-sm">
|
<p className="text-slate-400 text-sm">
|
||||||
Sélectionnez vos domaines d'expertise et évaluez votre niveau
|
Sélectionnez vos domaines d'expertise et évaluez votre niveau
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6 backdrop-blur-sm">
|
<div className="bg-white/5 border border-white/10 rounded-xl p-6 backdrop-blur-sm">
|
||||||
@@ -63,7 +63,7 @@ export function WelcomeScreen() {
|
|||||||
href="/evaluation"
|
href="/evaluation"
|
||||||
className="group bg-blue-500 hover:bg-blue-600 text-white px-8 py-4 rounded-xl font-medium transition-all inline-flex items-center gap-2"
|
className="group bg-blue-500 hover:bg-blue-600 text-white px-8 py-4 rounded-xl font-medium transition-all inline-flex items-center gap-2"
|
||||||
>
|
>
|
||||||
Commencer l'évaluation
|
Commencer l'évaluation
|
||||||
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,19 +31,30 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await authClient.login({ email, password });
|
const result = await authClient.login({ email, password });
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setError(result.error || "Erreur de connexion");
|
||||||
|
toast({
|
||||||
|
title: "Erreur de connexion",
|
||||||
|
description: result.error || "Erreur de connexion",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Connexion réussie",
|
title: "Connexion réussie",
|
||||||
description: response.message,
|
description: "Bienvenue !",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rediriger vers l'accueil après connexion
|
// Rediriger vers l'accueil après connexion
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
router.refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login failed:", error);
|
console.error("Login failed:", error);
|
||||||
|
|
||||||
const errorMessage = error.response?.data?.error || "Erreur de connexion";
|
const errorMessage = error.message || "Erreur de connexion";
|
||||||
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
toast({
|
toast({
|
||||||
@@ -66,26 +77,30 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await authClient.register(data);
|
const result = await authClient.register(data);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
setError(result.error || "Erreur lors de la création du compte");
|
||||||
|
toast({
|
||||||
|
title: "Erreur d'inscription",
|
||||||
|
description: result.error || "Erreur lors de la création du compte",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Compte créé",
|
title: "Compte créé",
|
||||||
description: response.message,
|
description: "Connexion automatique...",
|
||||||
});
|
|
||||||
|
|
||||||
// Après inscription réussie, connecter automatiquement l'utilisateur
|
|
||||||
toast({
|
|
||||||
title: "Connexion automatique",
|
|
||||||
description: "Redirection vers l'accueil...",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rediriger vers l'accueil après inscription réussie
|
// Rediriger vers l'accueil après inscription réussie
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
router.refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Register failed:", error);
|
console.error("Register failed:", error);
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage = error.message || "Erreur lors de la création du compte";
|
||||||
error.response?.data?.error || "Erreur lors de la création du compte";
|
|
||||||
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
toast({
|
toast({
|
||||||
@@ -102,11 +117,13 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
|
|||||||
try {
|
try {
|
||||||
await authClient.logout();
|
await authClient.logout();
|
||||||
// Rediriger vers la page de login après déconnexion
|
// Rediriger vers la page de login après déconnexion
|
||||||
window.location.href = "/login";
|
router.push("/login");
|
||||||
|
router.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout failed:", error);
|
console.error("Logout failed:", error);
|
||||||
// En cas d'erreur, forcer le rechargement pour nettoyer l'état
|
// En cas d'erreur, forcer le rechargement pour nettoyer l'état
|
||||||
window.location.reload();
|
router.push("/login");
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,7 +148,7 @@ export function AuthWrapper({ teams, initialUser }: AuthWrapperProps) {
|
|||||||
onClick={() => (window.location.href = "/")}
|
onClick={() => (window.location.href = "/")}
|
||||||
className="px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg"
|
className="px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg"
|
||||||
>
|
>
|
||||||
Aller à l'accueil
|
Aller à l'accueil
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ export function RegisterForm({
|
|||||||
|
|
||||||
{Object.keys(teamsByDirection).length === 0 && (
|
{Object.keys(teamsByDirection).length === 0 && (
|
||||||
<div className="px-3 py-4 text-center text-sm text-slate-400">
|
<div className="px-3 py-4 text-center text-sm text-slate-400">
|
||||||
Aucune équipe trouvée pour "{searchTerm}"
|
Aucune équipe trouvée pour "{searchTerm}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export function SkillEvaluation({
|
|||||||
} else if (categories.length > 0 && !selectedCategory) {
|
} else if (categories.length > 0 && !selectedCategory) {
|
||||||
setSelectedCategory(categories[0].category);
|
setSelectedCategory(categories[0].category);
|
||||||
}
|
}
|
||||||
}, [categoryParam, categories]); // Remove selectedCategory from deps to avoid loop
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [categoryParam, categories]); // selectedCategory intentionally excluded to avoid infinite loop
|
||||||
|
|
||||||
const currentCategory = categories.find(
|
const currentCategory = categories.find(
|
||||||
(cat) => cat.category === selectedCategory
|
(cat) => cat.category === selectedCategory
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export function SkillSelector({
|
|||||||
<div className="text-center py-8 text-muted-foreground border-2 border-dashed border-muted rounded-lg">
|
<div className="text-center py-8 text-muted-foreground border-2 border-dashed border-muted rounded-lg">
|
||||||
<p className="mb-2">Aucune compétence sélectionnée</p>
|
<p className="mb-2">Aucune compétence sélectionnée</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Cliquez sur "Ajouter une compétence" pour commencer
|
Cliquez sur "Ajouter une compétence" pour commencer
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { TeamMemberProfile, SkillGap } from "@/lib/team-review-types";
|
|||||||
import { UserCheck, GraduationCap } from "lucide-react";
|
import { UserCheck, GraduationCap } from "lucide-react";
|
||||||
import { TechIcon } from "@/components/icons/tech-icon";
|
import { TechIcon } from "@/components/icons/tech-icon";
|
||||||
import { getImportanceColors } from "@/lib/tech-colors";
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
import { isCoverageBelowObjective } from "@/lib/evaluation-utils";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
interface SkillMatrixProps {
|
interface SkillMatrixProps {
|
||||||
members: TeamMemberProfile[];
|
members: TeamMemberProfile[];
|
||||||
@@ -26,6 +28,21 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
(skill) => skill.skillId && skill.skillName && skill.category
|
(skill) => skill.skillId && skill.skillName && skill.category
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fonction de tri par importance
|
||||||
|
const sortByImportance = (a: SkillGap, b: SkillGap) => {
|
||||||
|
const importanceOrder = {
|
||||||
|
incontournable: 2,
|
||||||
|
majeure: 1,
|
||||||
|
standard: 0,
|
||||||
|
};
|
||||||
|
const importanceDiff =
|
||||||
|
importanceOrder[b.importance] - importanceOrder[a.importance];
|
||||||
|
if (importanceDiff !== 0) return importanceDiff;
|
||||||
|
|
||||||
|
// Si même importance, trier par couverture (ascendant)
|
||||||
|
return (a.coverage || 0) - (b.coverage || 0);
|
||||||
|
};
|
||||||
|
|
||||||
const skillsByCategory = validSkillGaps.reduce((acc, skill) => {
|
const skillsByCategory = validSkillGaps.reduce((acc, skill) => {
|
||||||
if (!acc[skill.category]) {
|
if (!acc[skill.category]) {
|
||||||
acc[skill.category] = [];
|
acc[skill.category] = [];
|
||||||
@@ -34,6 +51,11 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, SkillGap[]>);
|
}, {} as Record<string, SkillGap[]>);
|
||||||
|
|
||||||
|
// Trier les compétences par importance dans chaque catégorie
|
||||||
|
Object.values(skillsByCategory).forEach((skills) => {
|
||||||
|
skills.sort(sortByImportance);
|
||||||
|
});
|
||||||
|
|
||||||
const getLevelBadge = (level: string | null) => {
|
const getLevelBadge = (level: string | null) => {
|
||||||
const colors = {
|
const colors = {
|
||||||
never: "bg-white/5 text-slate-300",
|
never: "bg-white/5 text-slate-300",
|
||||||
@@ -80,12 +102,165 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-8">
|
<Tabs defaultValue="all" className="w-full">
|
||||||
{Object.entries(skillsByCategory).map(([category, skills]) => (
|
<TabsList className="mb-4 bg-white/5 border-white/10">
|
||||||
<div key={`category-${category}`}>
|
<TabsTrigger
|
||||||
<h3 className="text-lg font-semibold text-slate-200 mb-4">
|
value="all"
|
||||||
|
className="data-[state=active]:bg-white/10"
|
||||||
|
>
|
||||||
|
Toutes
|
||||||
|
</TabsTrigger>
|
||||||
|
{Object.keys(skillsByCategory).map((category) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={category}
|
||||||
|
value={category}
|
||||||
|
className="data-[state=active]:bg-white/10"
|
||||||
|
>
|
||||||
{category}
|
{category}
|
||||||
</h3>
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="all">
|
||||||
|
<div className="rounded-md border border-white/10">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-white/10">
|
||||||
|
<TableHead className="w-[200px] text-slate-300">
|
||||||
|
Compétence
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-[120px] text-slate-300">
|
||||||
|
Catégorie
|
||||||
|
</TableHead>
|
||||||
|
{members.map((member) => (
|
||||||
|
<TableHead
|
||||||
|
key={`header-${member.member.uuid}`}
|
||||||
|
className="text-slate-300"
|
||||||
|
>
|
||||||
|
{member.member.firstName} {member.member.lastName}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
<TableHead className="w-[120px] text-slate-300">
|
||||||
|
Couverture
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{validSkillGaps.sort(sortByImportance).map((skill) => {
|
||||||
|
const colors = getImportanceColors(skill.importance);
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={`skill-row-${skill.skillId}-${skill.category}`}
|
||||||
|
className="border-white/10"
|
||||||
|
>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`w-8 h-8 rounded-lg ${colors.bg} ${colors.border} border flex items-center justify-center`}
|
||||||
|
>
|
||||||
|
<TechIcon
|
||||||
|
iconName={skill.icon || ""}
|
||||||
|
className={`w-4 h-4 ${colors.text}`}
|
||||||
|
fallbackText={skill.skillName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className={`${colors.text} font-medium`}>
|
||||||
|
{skill.skillName}
|
||||||
|
</span>
|
||||||
|
{skill.risk === "high" && (
|
||||||
|
<Badge
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-red-500/20 text-red-200 border-red-500/30 w-fit"
|
||||||
|
>
|
||||||
|
Risque
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-white/5 text-slate-300 border-white/10"
|
||||||
|
>
|
||||||
|
{skill.category}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
{members.map((member) => {
|
||||||
|
const memberSkill = member.skills.find(
|
||||||
|
(s) => s.skillId === skill.skillId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
key={`skill-${skill.skillId}-member-${member.member.uuid}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{getLevelBadge(memberSkill?.level || null)}
|
||||||
|
{memberSkill?.canMentor && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-1 bg-green-500/10 text-green-200 border-green-500/30"
|
||||||
|
>
|
||||||
|
<UserCheck className="h-3 w-3" />
|
||||||
|
<span className="text-xs">Mentor</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{memberSkill?.wantsToLearn && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-1 bg-blue-500/10 text-blue-200 border-blue-500/30"
|
||||||
|
>
|
||||||
|
<GraduationCap className="h-3 w-3" />
|
||||||
|
<span className="text-xs">Apprenant</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-full bg-white/10 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full ${
|
||||||
|
(skill.coverage || 0) >= 75
|
||||||
|
? "bg-green-500/50"
|
||||||
|
: (skill.coverage || 0) >= 50
|
||||||
|
? "bg-yellow-500/50"
|
||||||
|
: "bg-red-500/50"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: `${Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(100, skill.coverage || 0)
|
||||||
|
)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-sm whitespace-nowrap ${
|
||||||
|
(skill.coverage || 0) >= 75
|
||||||
|
? "text-green-400"
|
||||||
|
: (skill.coverage || 0) >= 50
|
||||||
|
? "text-yellow-400"
|
||||||
|
: "text-red-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Math.round(skill.coverage || 0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{Object.entries(skillsByCategory).map(([category, skills]) => (
|
||||||
|
<TabsContent key={category} value={category}>
|
||||||
<div className="rounded-md border border-white/10">
|
<div className="rounded-md border border-white/10">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -176,10 +351,13 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-full bg-white/10 rounded-full h-2">
|
<div className="w-full bg-white/10 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${colors.bg.replace(
|
className={`h-2 rounded-full ${
|
||||||
"/20",
|
(skill.coverage || 0) >= 75
|
||||||
"/50"
|
? "bg-green-500/50"
|
||||||
)}`}
|
: (skill.coverage || 0) >= 50
|
||||||
|
? "bg-yellow-500/50"
|
||||||
|
: "bg-red-500/50"
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.max(
|
width: `${Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -189,7 +367,13 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`text-sm ${colors.text} whitespace-nowrap`}
|
className={`text-sm whitespace-nowrap ${
|
||||||
|
(skill.coverage || 0) >= 75
|
||||||
|
? "text-green-400"
|
||||||
|
: (skill.coverage || 0) >= 50
|
||||||
|
? "text-yellow-400"
|
||||||
|
: "text-red-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{Math.round(skill.coverage || 0)}%
|
{Math.round(skill.coverage || 0)}%
|
||||||
</span>
|
</span>
|
||||||
@@ -201,9 +385,9 @@ export function SkillMatrix({ members, skillGaps }: SkillMatrixProps) {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import { getImportanceColors } from "@/lib/tech-colors";
|
import { getImportanceColors } from "@/lib/tech-colors";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -41,20 +45,20 @@ export function TeamOverview({
|
|||||||
const categoriesNeedingAttention = [...categoryCoverage]
|
const categoriesNeedingAttention = [...categoryCoverage]
|
||||||
.map((cat) => {
|
.map((cat) => {
|
||||||
// Pour chaque catégorie, on identifie :
|
// Pour chaque catégorie, on identifie :
|
||||||
// 1. Les compétences incontournables sous-couvertes (< 75%)
|
// 1. Les compétences incontournables sous-couvertes
|
||||||
const uncoveredIncontournables = skillGaps.filter(
|
const uncoveredIncontournables = skillGaps.filter(
|
||||||
(gap) =>
|
(gap) =>
|
||||||
gap.category === cat.category &&
|
gap.category === cat.category &&
|
||||||
gap.importance === "incontournable" &&
|
gap.importance === "incontournable" &&
|
||||||
gap.coverage < 75
|
isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Les compétences majeures sous-couvertes (< 60%)
|
// 2. Les compétences majeures sous-couvertes
|
||||||
const uncoveredMajeures = skillGaps.filter(
|
const uncoveredMajeures = skillGaps.filter(
|
||||||
(gap) =>
|
(gap) =>
|
||||||
gap.category === cat.category &&
|
gap.category === cat.category &&
|
||||||
gap.importance === "majeure" &&
|
gap.importance === "majeure" &&
|
||||||
gap.coverage < 60
|
isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Une catégorie nécessite de l'attention si :
|
// Une catégorie nécessite de l'attention si :
|
||||||
@@ -81,14 +85,14 @@ export function TeamOverview({
|
|||||||
attentionScore,
|
attentionScore,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter((cat): cat is NonNullable<typeof cat> => cat !== null)
|
||||||
.sort((a, b) => b.attentionScore - a.attentionScore)
|
.sort((a, b) => b.attentionScore - a.attentionScore)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white/5 border-white/10 backdrop-blur">
|
<Card className="bg-white/5 border-white/10 backdrop-blur">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-slate-200">Vue d'ensemble</CardTitle>
|
<CardTitle className="text-slate-200">Vue d'ensemble</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
@@ -190,8 +194,7 @@ export function TeamOverview({
|
|||||||
})
|
})
|
||||||
.map((skill) => {
|
.map((skill) => {
|
||||||
const colors = getImportanceColors(skill.importance);
|
const colors = getImportanceColors(skill.importance);
|
||||||
const target =
|
const target = COVERAGE_OBJECTIVES[skill.importance];
|
||||||
skill.importance === "incontournable" ? 75 : 60;
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={skill.skillId}>
|
<Tooltip key={skill.skillId}>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
@@ -203,7 +206,10 @@ export function TeamOverview({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
skill.coverage < target
|
isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-slate-400"
|
: "text-slate-400"
|
||||||
}
|
}
|
||||||
@@ -224,7 +230,10 @@ export function TeamOverview({
|
|||||||
<br />
|
<br />
|
||||||
Actuel : {skill.coverage.toFixed(0)}%
|
Actuel : {skill.coverage.toFixed(0)}%
|
||||||
<br />
|
<br />
|
||||||
{skill.coverage < target
|
{isCoverageBelowObjective(
|
||||||
|
skill.coverage,
|
||||||
|
skill.importance
|
||||||
|
)
|
||||||
? `Manque ${(
|
? `Manque ${(
|
||||||
target - skill.coverage
|
target - skill.coverage
|
||||||
).toFixed(0)}%`
|
).toFixed(0)}%`
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import {
|
|||||||
Cell,
|
Cell,
|
||||||
Legend,
|
Legend,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
import {
|
import {
|
||||||
Tooltip as UITooltip,
|
Tooltip as UITooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -82,9 +86,11 @@ export function TeamStats({
|
|||||||
// Gaps critiques par catégorie, séparés par importance
|
// Gaps critiques par catégorie, séparés par importance
|
||||||
const criticalGapsByCategory = skillGaps.reduce((acc, gap) => {
|
const criticalGapsByCategory = skillGaps.reduce((acc, gap) => {
|
||||||
const isIncontournableUndercovered =
|
const isIncontournableUndercovered =
|
||||||
gap.importance === "incontournable" && gap.coverage < 75;
|
gap.importance === "incontournable" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance);
|
||||||
const isMajeureUndercovered =
|
const isMajeureUndercovered =
|
||||||
gap.importance === "majeure" && gap.coverage < 60;
|
gap.importance === "majeure" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance);
|
||||||
|
|
||||||
if (isIncontournableUndercovered || isMajeureUndercovered) {
|
if (isIncontournableUndercovered || isMajeureUndercovered) {
|
||||||
if (!acc[gap.category]) {
|
if (!acc[gap.category]) {
|
||||||
@@ -124,7 +130,7 @@ export function TeamStats({
|
|||||||
<Card className="bg-white/5 border-white/10 backdrop-blur">
|
<Card className="bg-white/5 border-white/10 backdrop-blur">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-slate-200">
|
<CardTitle className="text-slate-200">
|
||||||
Statistiques de l'équipe
|
Statistiques de l'équipe
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-8">
|
<CardContent className="space-y-8">
|
||||||
@@ -161,8 +167,13 @@ export function TeamStats({
|
|||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
<p>Compétences critiques sous-couvertes :</p>
|
<p>Compétences critiques sous-couvertes :</p>
|
||||||
<ul className="list-disc list-inside space-y-0.5">
|
<ul className="list-disc list-inside space-y-0.5">
|
||||||
<li>Incontournables : couverture < 75%</li>
|
<li>
|
||||||
<li>Majeures : couverture < 60%</li>
|
Incontournables : couverture <{" "}
|
||||||
|
{COVERAGE_OBJECTIVES.incontournable}%
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Majeures : couverture < {COVERAGE_OBJECTIVES.majeure}%
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -205,7 +216,10 @@ export function TeamStats({
|
|||||||
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
|
<TooltipContent className="bg-slate-900 text-slate-200 border border-slate-700">
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<p>Compétences incontournables</p>
|
<p>Compétences incontournables</p>
|
||||||
<p className="text-slate-400">Objectif : 75% de couverture</p>
|
<p className="text-slate-400">
|
||||||
|
Objectif : {COVERAGE_OBJECTIVES.incontournable}% de
|
||||||
|
couverture
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</UITooltip>
|
</UITooltip>
|
||||||
@@ -343,16 +357,16 @@ export function TeamStats({
|
|||||||
formatter={(value, name) => {
|
formatter={(value, name) => {
|
||||||
const label =
|
const label =
|
||||||
name === "incontournable"
|
name === "incontournable"
|
||||||
? "Incontournables (obj. 75%)"
|
? `Incontournables (obj. ${COVERAGE_OBJECTIVES.incontournable}%)`
|
||||||
: "Majeures (obj. 60%)";
|
: `Majeures (obj. ${COVERAGE_OBJECTIVES.majeure}%)`;
|
||||||
return [value, label];
|
return [value, label];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Legend
|
<Legend
|
||||||
formatter={(value) => {
|
formatter={(value) => {
|
||||||
return value === "incontournable"
|
return value === "incontournable"
|
||||||
? "Incontournables (obj. 75%)"
|
? `Incontournables (obj. ${COVERAGE_OBJECTIVES.incontournable}%)`
|
||||||
: "Majeures (obj. 60%)";
|
: `Majeures (obj. ${COVERAGE_OBJECTIVES.majeure}%)`;
|
||||||
}}
|
}}
|
||||||
wrapperStyle={{
|
wrapperStyle={{
|
||||||
paddingTop: "20px",
|
paddingTop: "20px",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3008:3000"
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DB_HOST: postgres
|
DB_HOST: postgres
|
||||||
@@ -13,6 +13,10 @@ services:
|
|||||||
DB_USER: peakskills_user
|
DB_USER: peakskills_user
|
||||||
DB_PASSWORD: peakskills_password
|
DB_PASSWORD: peakskills_password
|
||||||
NEXT_PUBLIC_API_URL: ""
|
NEXT_PUBLIC_API_URL: ""
|
||||||
|
# Auth.js configuration
|
||||||
|
AUTH_SECRET: "FvhDat3sJK5TI1L4fcugCGFmLsO1BCi+mwSYeLkl8JA="
|
||||||
|
AUTH_TRUST_HOST: "true"
|
||||||
|
AUTH_URL: "http://localhost:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function useTeamsManagement(
|
|||||||
setTeams((prev) => [...prev, newLocalTeam]);
|
setTeams((prev) => [...prev, newLocalTeam]);
|
||||||
|
|
||||||
// Ajouter les stats de la nouvelle équipe (avec les propriétés minimales)
|
// Ajouter les stats de la nouvelle équipe (avec les propriétés minimales)
|
||||||
const newTeamStats = {
|
const newTeamStats: TeamStats = {
|
||||||
teamId: newTeam.id,
|
teamId: newTeam.id,
|
||||||
teamName: newTeam.name,
|
teamName: newTeam.name,
|
||||||
direction: newTeam.direction,
|
direction: newTeam.direction,
|
||||||
@@ -70,7 +70,11 @@ export function useTeamsManagement(
|
|||||||
skillCoverage: 0,
|
skillCoverage: 0,
|
||||||
topSkills: [],
|
topSkills: [],
|
||||||
members: [],
|
members: [],
|
||||||
} as TeamStats;
|
criticalSkillsCoverage: {
|
||||||
|
incontournable: 0,
|
||||||
|
majeure: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
setTeamStats((prev) => [...prev, newTeamStats]);
|
setTeamStats((prev) => [...prev, newTeamStats]);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CategoryEvaluation,
|
CategoryEvaluation,
|
||||||
RadarChartData,
|
RadarChartData,
|
||||||
SkillCategory,
|
SkillCategory,
|
||||||
|
SkillImportance,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export function calculateCategoryScore(
|
export function calculateCategoryScore(
|
||||||
@@ -51,3 +52,47 @@ export function createEmptyEvaluation(
|
|||||||
selectedSkillIds: [],
|
selectedSkillIds: [],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const COVERAGE_OBJECTIVES: Record<SkillImportance, number> = {
|
||||||
|
incontournable: 75,
|
||||||
|
majeure: 60,
|
||||||
|
standard: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isCoverageBelowObjective(
|
||||||
|
coverage: number,
|
||||||
|
importance: SkillImportance
|
||||||
|
): boolean {
|
||||||
|
const objective = COVERAGE_OBJECTIVES[importance];
|
||||||
|
return objective > 0 && coverage < objective;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCoverageObjective(importance: SkillImportance): number {
|
||||||
|
return COVERAGE_OBJECTIVES[importance];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateSkillCoverage(
|
||||||
|
levels: number[],
|
||||||
|
totalMembers: number
|
||||||
|
): number {
|
||||||
|
if (levels.length === 0 || totalMembers === 0) return 0;
|
||||||
|
|
||||||
|
// Compter le nombre de membres autonomes ou experts (niveau >= 2)
|
||||||
|
const expertCount = levels.filter((level) => level >= 2).length;
|
||||||
|
|
||||||
|
// La couverture est le pourcentage de membres autonomes ou experts
|
||||||
|
return (expertCount / totalMembers) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSkillCoverageSQL(levelField: string): string {
|
||||||
|
return `
|
||||||
|
COALESCE(
|
||||||
|
(COUNT(*) FILTER (WHERE ${levelField} >= 2) * 100.0) / NULLIF(COUNT(*), 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExpertLevel(level: number): boolean {
|
||||||
|
return level >= 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ export const SKILL_LEVEL_VALUES: Record<Exclude<SkillLevel, null>, number> = {
|
|||||||
expert: 3,
|
expert: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SkillImportance = "incontournable" | "majeure" | "standard";
|
||||||
|
|
||||||
export interface Skill {
|
export interface Skill {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
links: string[];
|
links: string[];
|
||||||
importance?: string;
|
importance: SkillImportance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkillCategory {
|
export interface SkillCategory {
|
||||||
|
|||||||
@@ -1,44 +1,28 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { auth } from "@/auth";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
const COOKIE_NAME = "session_token";
|
export default auth((req) => {
|
||||||
|
const { pathname } = req.nextUrl;
|
||||||
|
const isLoggedIn = !!req.auth;
|
||||||
|
const isOnLoginPage = pathname === "/login";
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
// Pages publiques (API auth et teams)
|
||||||
const { pathname } = request.nextUrl;
|
if (pathname.startsWith("/api/auth") || pathname.startsWith("/api/teams")) {
|
||||||
|
|
||||||
// Pages qui ne nécessitent pas d'authentification
|
|
||||||
const publicPaths = ["/login"];
|
|
||||||
|
|
||||||
// Pages API qui ne nécessitent pas d'authentification
|
|
||||||
const publicApiPaths = ["/api/teams", "/api/auth"];
|
|
||||||
console.log(pathname);
|
|
||||||
|
|
||||||
// Vérifier si c'est une route publique
|
|
||||||
if (
|
|
||||||
publicPaths.includes(pathname) ||
|
|
||||||
publicApiPaths.some((path) => pathname.startsWith(path))
|
|
||||||
) {
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si c'est un fichier statique
|
// Si connecté et sur login, rediriger vers home
|
||||||
if (
|
if (isLoggedIn && isOnLoginPage) {
|
||||||
pathname.includes("/_next/") ||
|
return NextResponse.redirect(new URL("/", req.url));
|
||||||
pathname.includes("/favicon.ico") ||
|
|
||||||
pathname.includes("/public/")
|
|
||||||
) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier le cookie d'authentification (maintenant un UUID)
|
// Si non connecté et pas sur login, rediriger vers login
|
||||||
const userUuid = request.cookies.get(COOKIE_NAME)?.value;
|
if (!isLoggedIn && !isOnLoginPage) {
|
||||||
if (!userUuid) {
|
return NextResponse.redirect(new URL("/login", req.url));
|
||||||
// Rediriger vers la page de login si pas authentifié
|
|
||||||
const loginUrl = new URL("/login", request.url);
|
|
||||||
return NextResponse.redirect(loginUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
"lint:fix": "next lint --fix",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"generate-test-data": "tsx scripts/generate-test-data.ts",
|
"generate-test-data": "tsx scripts/generate-test-data.ts",
|
||||||
"sync-skills": "tsx scripts/sync-skills.ts",
|
"sync-skills": "tsx scripts/sync-skills.ts",
|
||||||
@@ -60,7 +61,8 @@
|
|||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"next": "15.2.4",
|
"next": "15.5.7",
|
||||||
|
"next-auth": "5.0.0-beta.29",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
|
|||||||
504
pnpm-lock.yaml
generated
504
pnpm-lock.yaml
generated
@@ -139,7 +139,7 @@ importers:
|
|||||||
version: 8.5.1(react@19.1.1)
|
version: 8.5.1(react@19.1.1)
|
||||||
geist:
|
geist:
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.4.2(next@15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))
|
version: 1.4.2(next@15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))
|
||||||
input-otp:
|
input-otp:
|
||||||
specifier: 1.4.1
|
specifier: 1.4.1
|
||||||
version: 1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 1.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
@@ -147,8 +147,11 @@ importers:
|
|||||||
specifier: ^0.454.0
|
specifier: ^0.454.0
|
||||||
version: 0.454.0(react@19.1.1)
|
version: 0.454.0(react@19.1.1)
|
||||||
next:
|
next:
|
||||||
specifier: 15.2.4
|
specifier: 15.5.7
|
||||||
version: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
next-auth:
|
||||||
|
specifier: 5.0.0-beta.29
|
||||||
|
version: 5.0.0-beta.29(next@15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
@@ -223,6 +226,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
'@auth/core@0.40.0':
|
||||||
|
resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@simplewebauthn/browser': ^9.0.1
|
||||||
|
'@simplewebauthn/server': ^9.0.2
|
||||||
|
nodemailer: ^6.8.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@simplewebauthn/browser':
|
||||||
|
optional: true
|
||||||
|
'@simplewebauthn/server':
|
||||||
|
optional: true
|
||||||
|
nodemailer:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@babel/runtime@7.28.3':
|
'@babel/runtime@7.28.3':
|
||||||
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -230,8 +247,8 @@ packages:
|
|||||||
'@date-fns/tz@1.2.0':
|
'@date-fns/tz@1.2.0':
|
||||||
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
|
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
|
||||||
|
|
||||||
'@emnapi/runtime@1.4.5':
|
'@emnapi/runtime@1.7.1':
|
||||||
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
|
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.25.9':
|
'@esbuild/aix-ppc64@0.25.9':
|
||||||
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
||||||
@@ -431,107 +448,139 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react-hook-form: ^7.0.0
|
react-hook-form: ^7.0.0
|
||||||
|
|
||||||
'@img/sharp-darwin-arm64@0.33.5':
|
'@img/colour@1.0.0':
|
||||||
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
|
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@img/sharp-darwin-x64@0.33.5':
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
|
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
|
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@img/sharp-libvips-darwin-x64@1.0.4':
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
|
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm64@1.0.4':
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
|
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.33.5':
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.33.5':
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.33.5':
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
|
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.33.5':
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.33.5':
|
'@img/sharp-wasm32@0.34.5':
|
||||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [wasm32]
|
cpu: [wasm32]
|
||||||
|
|
||||||
'@img/sharp-win32-ia32@0.33.5':
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
|
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.5':
|
||||||
|
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@img/sharp-win32-x64@0.33.5':
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
|
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -556,57 +605,60 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.30':
|
'@jridgewell/trace-mapping@0.3.30':
|
||||||
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
|
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
|
||||||
|
|
||||||
'@next/env@15.2.4':
|
'@next/env@15.5.7':
|
||||||
resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==}
|
resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==}
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@15.2.4':
|
'@next/swc-darwin-arm64@15.5.7':
|
||||||
resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==}
|
resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-darwin-x64@15.2.4':
|
'@next/swc-darwin-x64@15.5.7':
|
||||||
resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==}
|
resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@15.2.4':
|
'@next/swc-linux-arm64-gnu@15.5.7':
|
||||||
resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==}
|
resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@15.2.4':
|
'@next/swc-linux-arm64-musl@15.5.7':
|
||||||
resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==}
|
resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@15.2.4':
|
'@next/swc-linux-x64-gnu@15.5.7':
|
||||||
resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==}
|
resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@15.2.4':
|
'@next/swc-linux-x64-musl@15.5.7':
|
||||||
resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==}
|
resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@15.2.4':
|
'@next/swc-win32-arm64-msvc@15.5.7':
|
||||||
resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==}
|
resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@15.2.4':
|
'@next/swc-win32-x64-msvc@15.5.7':
|
||||||
resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==}
|
resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@panva/hkdf@1.2.1':
|
||||||
|
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.0':
|
'@radix-ui/number@1.1.0':
|
||||||
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
||||||
|
|
||||||
@@ -1263,9 +1315,6 @@ packages:
|
|||||||
'@radix-ui/rect@1.1.0':
|
'@radix-ui/rect@1.1.0':
|
||||||
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
||||||
|
|
||||||
'@swc/counter@0.1.3':
|
|
||||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
@@ -1429,13 +1478,12 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
busboy@1.6.0:
|
|
||||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
|
||||||
engines: {node: '>=10.16.0'}
|
|
||||||
|
|
||||||
caniuse-lite@1.0.30001735:
|
caniuse-lite@1.0.30001735:
|
||||||
resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
|
resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
|
||||||
|
|
||||||
|
caniuse-lite@1.0.30001759:
|
||||||
|
resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==}
|
||||||
|
|
||||||
chownr@3.0.0:
|
chownr@3.0.0:
|
||||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1456,20 +1504,6 @@ packages:
|
|||||||
react: ^18 || ^19 || ^19.0.0-rc
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
color-convert@2.0.1:
|
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
|
||||||
engines: {node: '>=7.0.0'}
|
|
||||||
|
|
||||||
color-name@1.1.4:
|
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
|
||||||
|
|
||||||
color-string@1.9.1:
|
|
||||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
|
||||||
|
|
||||||
color@4.2.3:
|
|
||||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
|
||||||
engines: {node: '>=12.5.0'}
|
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
@@ -1530,6 +1564,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
detect-node-es@1.1.0:
|
detect-node-es@1.1.0:
|
||||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||||
|
|
||||||
@@ -1605,13 +1643,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
is-arrayish@0.3.2:
|
|
||||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
|
||||||
|
|
||||||
jiti@2.5.1:
|
jiti@2.5.1:
|
||||||
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jose@6.1.0:
|
||||||
|
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -1712,19 +1750,35 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
next-auth@5.0.0-beta.29:
|
||||||
|
resolution: {integrity: sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@simplewebauthn/browser': ^9.0.1
|
||||||
|
'@simplewebauthn/server': ^9.0.2
|
||||||
|
next: ^14.0.0-0 || ^15.0.0-0
|
||||||
|
nodemailer: ^6.6.5
|
||||||
|
react: ^18.2.0 || ^19.0.0-0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@simplewebauthn/browser':
|
||||||
|
optional: true
|
||||||
|
'@simplewebauthn/server':
|
||||||
|
optional: true
|
||||||
|
nodemailer:
|
||||||
|
optional: true
|
||||||
|
|
||||||
next-themes@0.4.6:
|
next-themes@0.4.6:
|
||||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
next@15.2.4:
|
next@15.5.7:
|
||||||
resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==}
|
resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==}
|
||||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.1.0
|
'@opentelemetry/api': ^1.1.0
|
||||||
'@playwright/test': ^1.41.2
|
'@playwright/test': ^1.51.1
|
||||||
babel-plugin-react-compiler: '*'
|
babel-plugin-react-compiler: '*'
|
||||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||||
@@ -1754,6 +1808,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
oauth4webapi@3.8.2:
|
||||||
|
resolution: {integrity: sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==}
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1822,6 +1879,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
preact-render-to-string@6.5.11:
|
||||||
|
resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
|
||||||
|
peerDependencies:
|
||||||
|
preact: '>=10'
|
||||||
|
|
||||||
|
preact@10.24.3:
|
||||||
|
resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
@@ -1916,18 +1981,15 @@ packages:
|
|||||||
scheduler@0.26.0:
|
scheduler@0.26.0:
|
||||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||||
|
|
||||||
semver@7.7.2:
|
semver@7.7.3:
|
||||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
sharp@0.33.5:
|
sharp@0.34.5:
|
||||||
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
simple-swizzle@0.2.2:
|
|
||||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
|
||||||
|
|
||||||
sonner@1.7.4:
|
sonner@1.7.4:
|
||||||
resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==}
|
resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1942,10 +2004,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
engines: {node: '>= 10.x'}
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
streamsearch@1.1.0:
|
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
|
|
||||||
styled-jsx@5.1.6:
|
styled-jsx@5.1.6:
|
||||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -2055,11 +2113,19 @@ snapshots:
|
|||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
|
'@auth/core@0.40.0':
|
||||||
|
dependencies:
|
||||||
|
'@panva/hkdf': 1.2.1
|
||||||
|
jose: 6.1.0
|
||||||
|
oauth4webapi: 3.8.2
|
||||||
|
preact: 10.24.3
|
||||||
|
preact-render-to-string: 6.5.11(preact@10.24.3)
|
||||||
|
|
||||||
'@babel/runtime@7.28.3': {}
|
'@babel/runtime@7.28.3': {}
|
||||||
|
|
||||||
'@date-fns/tz@1.2.0': {}
|
'@date-fns/tz@1.2.0': {}
|
||||||
|
|
||||||
'@emnapi/runtime@1.4.5':
|
'@emnapi/runtime@1.7.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
@@ -2183,79 +2249,101 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react-hook-form: 7.62.0(react@19.1.1)
|
react-hook-form: 7.62.0(react@19.1.1)
|
||||||
|
|
||||||
'@img/sharp-darwin-arm64@0.33.5':
|
'@img/colour@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-darwin-x64@0.33.5':
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-darwin-x64': 1.0.4
|
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-darwin-x64@1.0.4':
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm64@1.0.4':
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.33.5':
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-linux-arm64': 1.0.4
|
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.33.5':
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-linux-arm': 1.0.5
|
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.33.5':
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-linux-s390x': 1.0.4
|
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.33.5':
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-linux-x64': 1.0.4
|
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.33.5':
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/runtime': 1.4.5
|
'@emnapi/runtime': 1.7.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-win32-ia32@0.33.5':
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-win32-x64@0.33.5':
|
'@img/sharp-win32-ia32@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@isaacs/fs-minipass@4.0.1':
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
@@ -2281,32 +2369,34 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
'@next/env@15.2.4': {}
|
'@next/env@15.5.7': {}
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@15.2.4':
|
'@next/swc-darwin-arm64@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-darwin-x64@15.2.4':
|
'@next/swc-darwin-x64@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@15.2.4':
|
'@next/swc-linux-arm64-gnu@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@15.2.4':
|
'@next/swc-linux-arm64-musl@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@15.2.4':
|
'@next/swc-linux-x64-gnu@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@15.2.4':
|
'@next/swc-linux-x64-musl@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@15.2.4':
|
'@next/swc-win32-arm64-msvc@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@15.2.4':
|
'@next/swc-win32-x64-msvc@15.5.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@panva/hkdf@1.2.1': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.0': {}
|
'@radix-ui/number@1.1.0': {}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.1': {}
|
'@radix-ui/primitive@1.1.1': {}
|
||||||
@@ -2999,8 +3089,6 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/rect@1.1.0': {}
|
'@radix-ui/rect@1.1.0': {}
|
||||||
|
|
||||||
'@swc/counter@0.1.3': {}
|
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -3155,12 +3243,10 @@ snapshots:
|
|||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.25.3)
|
update-browserslist-db: 1.1.3(browserslist@4.25.3)
|
||||||
|
|
||||||
busboy@1.6.0:
|
|
||||||
dependencies:
|
|
||||||
streamsearch: 1.1.0
|
|
||||||
|
|
||||||
caniuse-lite@1.0.30001735: {}
|
caniuse-lite@1.0.30001735: {}
|
||||||
|
|
||||||
|
caniuse-lite@1.0.30001759: {}
|
||||||
|
|
||||||
chownr@3.0.0: {}
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
@@ -3183,26 +3269,6 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- '@types/react-dom'
|
- '@types/react-dom'
|
||||||
|
|
||||||
color-convert@2.0.1:
|
|
||||||
dependencies:
|
|
||||||
color-name: 1.1.4
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
color-name@1.1.4:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
color-string@1.9.1:
|
|
||||||
dependencies:
|
|
||||||
color-name: 1.1.4
|
|
||||||
simple-swizzle: 0.2.2
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
color@4.2.3:
|
|
||||||
dependencies:
|
|
||||||
color-convert: 2.0.1
|
|
||||||
color-string: 1.9.1
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
d3-array@3.2.4:
|
d3-array@3.2.4:
|
||||||
@@ -3251,6 +3317,9 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.0.4: {}
|
detect-libc@2.0.4: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
|
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
@@ -3317,9 +3386,9 @@ snapshots:
|
|||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
geist@1.4.2(next@15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)):
|
geist@1.4.2(next@15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
next: 15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
next: 15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
|
||||||
get-nonce@1.0.1: {}
|
get-nonce@1.0.1: {}
|
||||||
|
|
||||||
@@ -3336,11 +3405,10 @@ snapshots:
|
|||||||
|
|
||||||
internmap@2.0.3: {}
|
internmap@2.0.3: {}
|
||||||
|
|
||||||
is-arrayish@0.3.2:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
jiti@2.5.1: {}
|
jiti@2.5.1: {}
|
||||||
|
|
||||||
|
jose@6.1.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
@@ -3412,32 +3480,36 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
next-auth@5.0.0-beta.29(next@15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1):
|
||||||
|
dependencies:
|
||||||
|
'@auth/core': 0.40.0
|
||||||
|
next: 15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
react: 19.1.1
|
||||||
|
|
||||||
next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
react-dom: 19.1.1(react@19.1.1)
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
|
|
||||||
next@15.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
next@15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 15.2.4
|
'@next/env': 15.5.7
|
||||||
'@swc/counter': 0.1.3
|
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
busboy: 1.6.0
|
caniuse-lite: 1.0.30001759
|
||||||
caniuse-lite: 1.0.30001735
|
|
||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
react-dom: 19.1.1(react@19.1.1)
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
styled-jsx: 5.1.6(react@19.1.1)
|
styled-jsx: 5.1.6(react@19.1.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 15.2.4
|
'@next/swc-darwin-arm64': 15.5.7
|
||||||
'@next/swc-darwin-x64': 15.2.4
|
'@next/swc-darwin-x64': 15.5.7
|
||||||
'@next/swc-linux-arm64-gnu': 15.2.4
|
'@next/swc-linux-arm64-gnu': 15.5.7
|
||||||
'@next/swc-linux-arm64-musl': 15.2.4
|
'@next/swc-linux-arm64-musl': 15.5.7
|
||||||
'@next/swc-linux-x64-gnu': 15.2.4
|
'@next/swc-linux-x64-gnu': 15.5.7
|
||||||
'@next/swc-linux-x64-musl': 15.2.4
|
'@next/swc-linux-x64-musl': 15.5.7
|
||||||
'@next/swc-win32-arm64-msvc': 15.2.4
|
'@next/swc-win32-arm64-msvc': 15.5.7
|
||||||
'@next/swc-win32-x64-msvc': 15.2.4
|
'@next/swc-win32-x64-msvc': 15.5.7
|
||||||
sharp: 0.33.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
@@ -3450,6 +3522,8 @@ snapshots:
|
|||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
|
||||||
|
oauth4webapi@3.8.2: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
pg-cloudflare@1.2.7:
|
pg-cloudflare@1.2.7:
|
||||||
@@ -3513,6 +3587,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend: 4.0.2
|
xtend: 4.0.2
|
||||||
|
|
||||||
|
preact-render-to-string@6.5.11(preact@10.24.3):
|
||||||
|
dependencies:
|
||||||
|
preact: 10.24.3
|
||||||
|
|
||||||
|
preact@10.24.3: {}
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@@ -3611,39 +3691,39 @@ snapshots:
|
|||||||
|
|
||||||
scheduler@0.26.0: {}
|
scheduler@0.26.0: {}
|
||||||
|
|
||||||
semver@7.7.2:
|
semver@7.7.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
sharp@0.33.5:
|
sharp@0.34.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
color: 4.2.3
|
'@img/colour': 1.0.0
|
||||||
detect-libc: 2.0.4
|
detect-libc: 2.1.2
|
||||||
semver: 7.7.2
|
semver: 7.7.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-darwin-arm64': 0.33.5
|
'@img/sharp-darwin-arm64': 0.34.5
|
||||||
'@img/sharp-darwin-x64': 0.33.5
|
'@img/sharp-darwin-x64': 0.34.5
|
||||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||||
'@img/sharp-libvips-darwin-x64': 1.0.4
|
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||||
'@img/sharp-libvips-linux-arm': 1.0.5
|
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||||
'@img/sharp-libvips-linux-arm64': 1.0.4
|
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||||
'@img/sharp-libvips-linux-s390x': 1.0.4
|
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||||
'@img/sharp-libvips-linux-x64': 1.0.4
|
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||||
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||||
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||||
'@img/sharp-linux-arm': 0.33.5
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||||
'@img/sharp-linux-arm64': 0.33.5
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||||
'@img/sharp-linux-s390x': 0.33.5
|
'@img/sharp-linux-arm': 0.34.5
|
||||||
'@img/sharp-linux-x64': 0.33.5
|
'@img/sharp-linux-arm64': 0.34.5
|
||||||
'@img/sharp-linuxmusl-arm64': 0.33.5
|
'@img/sharp-linux-ppc64': 0.34.5
|
||||||
'@img/sharp-linuxmusl-x64': 0.33.5
|
'@img/sharp-linux-riscv64': 0.34.5
|
||||||
'@img/sharp-wasm32': 0.33.5
|
'@img/sharp-linux-s390x': 0.34.5
|
||||||
'@img/sharp-win32-ia32': 0.33.5
|
'@img/sharp-linux-x64': 0.34.5
|
||||||
'@img/sharp-win32-x64': 0.33.5
|
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||||
optional: true
|
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||||
|
'@img/sharp-wasm32': 0.34.5
|
||||||
simple-swizzle@0.2.2:
|
'@img/sharp-win32-arm64': 0.34.5
|
||||||
dependencies:
|
'@img/sharp-win32-ia32': 0.34.5
|
||||||
is-arrayish: 0.3.2
|
'@img/sharp-win32-x64': 0.34.5
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
sonner@1.7.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
sonner@1.7.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||||
@@ -3655,8 +3735,6 @@ snapshots:
|
|||||||
|
|
||||||
split2@4.2.0: {}
|
split2@4.2.0: {}
|
||||||
|
|
||||||
streamsearch@1.1.0: {}
|
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.1.1):
|
styled-jsx@5.1.6(react@19.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { getPool } from "./database";
|
|||||||
import { Team, SkillCategory } from "@/lib/types";
|
import { Team, SkillCategory } from "@/lib/types";
|
||||||
import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
import { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
||||||
import { SkillsService } from "./skills-service";
|
import { SkillsService } from "./skills-service";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
generateSkillCoverageSQL,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
/**
|
/**
|
||||||
@@ -9,8 +13,11 @@ export class AdminService {
|
|||||||
*/
|
*/
|
||||||
static async getTeamsStats(): Promise<TeamStats[]> {
|
static async getTeamsStats(): Promise<TeamStats[]> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
// Récupérer toutes les équipes avec leurs membres et évaluations
|
// Récupérer toutes les équipes avec leurs membres et évaluations
|
||||||
const query = `
|
const query = `
|
||||||
WITH team_members AS (
|
WITH team_members AS (
|
||||||
@@ -56,14 +63,7 @@ export class AdminService {
|
|||||||
s.icon as skill_icon,
|
s.icon as skill_icon,
|
||||||
s.importance,
|
s.importance,
|
||||||
AVG(ss.level_numeric) as avg_level,
|
AVG(ss.level_numeric) as avg_level,
|
||||||
COALESCE(
|
${generateSkillCoverageSQL("ss.level_numeric")} as coverage
|
||||||
SUM(CASE
|
|
||||||
WHEN ss.level_numeric >= 2 THEN 100.0 -- autonomous ou expert
|
|
||||||
WHEN ss.level_numeric = 1 THEN 50.0 -- not-autonomous
|
|
||||||
ELSE 0.0 -- never
|
|
||||||
END) / NULLIF(COUNT(*), 0),
|
|
||||||
0
|
|
||||||
) as coverage
|
|
||||||
FROM skill_stats ss
|
FROM skill_stats ss
|
||||||
JOIN skills s ON ss.skill_id = s.id
|
JOIN skills s ON ss.skill_id = s.id
|
||||||
WHERE ss.skill_name IS NOT NULL
|
WHERE ss.skill_name IS NOT NULL
|
||||||
@@ -73,11 +73,17 @@ export class AdminService {
|
|||||||
SELECT
|
SELECT
|
||||||
team_id,
|
team_id,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
AVG(CASE WHEN importance = 'incontournable' THEN coverage ELSE NULL END),
|
AVG(CASE
|
||||||
|
WHEN importance = 'incontournable' THEN coverage
|
||||||
|
ELSE NULL
|
||||||
|
END),
|
||||||
0
|
0
|
||||||
) as incontournable_coverage,
|
) as incontournable_coverage,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
AVG(CASE WHEN importance = 'majeure' THEN coverage ELSE NULL END),
|
AVG(CASE
|
||||||
|
WHEN importance = 'majeure' THEN coverage
|
||||||
|
ELSE NULL
|
||||||
|
END),
|
||||||
0
|
0
|
||||||
) as majeure_coverage
|
) as majeure_coverage
|
||||||
FROM team_skill_averages
|
FROM team_skill_averages
|
||||||
@@ -149,7 +155,8 @@ export class AdminService {
|
|||||||
ORDER BY tm.direction, tm.team_name
|
ORDER BY tm.direction, tm.team_name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await pool.query(query);
|
const result = await client.query(query);
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return result.rows.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
teamId: row.team_id,
|
teamId: row.team_id,
|
||||||
@@ -168,8 +175,11 @@ export class AdminService {
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching teams stats:", error);
|
console.error("Error fetching teams stats:", error);
|
||||||
throw new Error("Failed to fetch teams statistics");
|
throw new Error("Failed to fetch teams statistics");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,10 +251,15 @@ export class AdminService {
|
|||||||
skills: any[];
|
skills: any[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [categoriesResult, skills] = await Promise.all([
|
const [categoriesResult, skills] = await Promise.all([
|
||||||
pool.query("SELECT id, name, icon FROM skill_categories ORDER BY name"),
|
client.query(
|
||||||
|
"SELECT id, name, icon FROM skill_categories ORDER BY name"
|
||||||
|
),
|
||||||
SkillsService.getAllSkillsWithUsage(),
|
SkillsService.getAllSkillsWithUsage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -254,13 +269,18 @@ export class AdminService {
|
|||||||
skills: [],
|
skills: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skillCategories,
|
skillCategories,
|
||||||
skills,
|
skills,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching skills page data:", error);
|
console.error("Error fetching skills page data:", error);
|
||||||
throw new Error("Failed to fetch skills page data");
|
throw new Error("Failed to fetch skills page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,13 +328,16 @@ export class AdminService {
|
|||||||
users: any[];
|
users: any[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [teamsResult, usersResult] = await Promise.all([
|
const [teamsResult, usersResult] = await Promise.all([
|
||||||
pool.query(
|
client.query(
|
||||||
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
||||||
),
|
),
|
||||||
pool.query(`
|
client.query(`
|
||||||
SELECT
|
SELECT
|
||||||
u.uuid_id as uuid,
|
u.uuid_id as uuid,
|
||||||
u.first_name as "firstName",
|
u.first_name as "firstName",
|
||||||
@@ -330,13 +353,18 @@ export class AdminService {
|
|||||||
`),
|
`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams: teamsResult.rows,
|
teams: teamsResult.rows,
|
||||||
users: usersResult.rows,
|
users: usersResult.rows,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching users page data:", error);
|
console.error("Error fetching users page data:", error);
|
||||||
throw new Error("Failed to fetch users page data");
|
throw new Error("Failed to fetch users page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,10 +377,13 @@ export class AdminService {
|
|||||||
directionStats: DirectionStats[];
|
directionStats: DirectionStats[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [teamsResult, teamStats] = await Promise.all([
|
const [teamsResult, teamStats] = await Promise.all([
|
||||||
pool.query(
|
client.query(
|
||||||
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
||||||
),
|
),
|
||||||
AdminService.getTeamsStats(),
|
AdminService.getTeamsStats(),
|
||||||
@@ -360,14 +391,19 @@ export class AdminService {
|
|||||||
|
|
||||||
const directionStats = AdminService.generateDirectionStats(teamStats);
|
const directionStats = AdminService.generateDirectionStats(teamStats);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams: teamsResult.rows,
|
teams: teamsResult.rows,
|
||||||
teamStats,
|
teamStats,
|
||||||
directionStats,
|
directionStats,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching teams page data:", error);
|
console.error("Error fetching teams page data:", error);
|
||||||
throw new Error("Failed to fetch teams page data");
|
throw new Error("Failed to fetch teams page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,13 +417,18 @@ export class AdminService {
|
|||||||
directionStats: DirectionStats[];
|
directionStats: DirectionStats[];
|
||||||
}> {
|
}> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const [teamsResult, categoriesResult, teamStats] = await Promise.all([
|
const [teamsResult, categoriesResult, teamStats] = await Promise.all([
|
||||||
pool.query(
|
client.query(
|
||||||
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
"SELECT id, name, direction FROM teams ORDER BY direction, name"
|
||||||
),
|
),
|
||||||
pool.query("SELECT id, name, icon FROM skill_categories ORDER BY name"),
|
client.query(
|
||||||
|
"SELECT id, name, icon FROM skill_categories ORDER BY name"
|
||||||
|
),
|
||||||
AdminService.getTeamsStats(),
|
AdminService.getTeamsStats(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -400,6 +441,8 @@ export class AdminService {
|
|||||||
|
|
||||||
const directionStats = AdminService.generateDirectionStats(teamStats);
|
const directionStats = AdminService.generateDirectionStats(teamStats);
|
||||||
|
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams,
|
teams,
|
||||||
skillCategories,
|
skillCategories,
|
||||||
@@ -407,8 +450,11 @@ export class AdminService {
|
|||||||
directionStats,
|
directionStats,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error fetching overview page data:", error);
|
console.error("Error fetching overview page data:", error);
|
||||||
throw new Error("Failed to fetch overview page data");
|
throw new Error("Failed to fetch overview page data");
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import { cookies } from "next/headers";
|
|
||||||
import { UserProfile } from "@/lib/types";
|
|
||||||
import { userService } from "@/services/user-service";
|
|
||||||
|
|
||||||
// Constantes pour les cookies
|
|
||||||
export const COOKIE_NAME = "session_token";
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie l'authentification et retourne le profil utilisateur
|
|
||||||
* @throws {Error} avec status 401 si non authentifié ou 404 si utilisateur non trouvé
|
|
||||||
*/
|
|
||||||
static async requireAuthenticatedUser(): Promise<{
|
|
||||||
userUuid: string;
|
|
||||||
userProfile: UserProfile;
|
|
||||||
}> {
|
|
||||||
const userUuid = await this.getUserUuidFromCookie();
|
|
||||||
|
|
||||||
if (!userUuid) {
|
|
||||||
const error = new Error("Utilisateur non authentifié");
|
|
||||||
(error as any).status = 401;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userProfile = await userService.getUserByUuid(userUuid);
|
|
||||||
if (!userProfile) {
|
|
||||||
const error = new Error("Utilisateur introuvable");
|
|
||||||
(error as any).status = 404;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { userUuid, userProfile };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentifie un utilisateur et retourne la configuration du cookie
|
|
||||||
* Note: Cette méthode est maintenant obsolète avec le nouveau système d'auth
|
|
||||||
* Utilisez login/register à la place
|
|
||||||
*/
|
|
||||||
static async authenticateUser(profile: UserProfile): Promise<{
|
|
||||||
userUuid: string;
|
|
||||||
cookieConfig: {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
options: {
|
|
||||||
maxAge: number;
|
|
||||||
httpOnly: boolean;
|
|
||||||
secure: boolean;
|
|
||||||
sameSite: "lax" | "strict" | "none";
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
// Vérifier si l'utilisateur existe déjà avec ces informations
|
|
||||||
const existingUser = await userService.findUserByProfile(profile);
|
|
||||||
|
|
||||||
if (!existingUser) {
|
|
||||||
throw new Error(
|
|
||||||
"Utilisateur non trouvé. Veuillez vous connecter avec votre email et mot de passe."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
userUuid: existingUser.uuid,
|
|
||||||
cookieConfig: {
|
|
||||||
name: COOKIE_NAME,
|
|
||||||
value: existingUser.uuid,
|
|
||||||
options: {
|
|
||||||
maxAge: COOKIE_MAX_AGE,
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
path: "/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crée une nouvelle session pour un utilisateur
|
|
||||||
*/
|
|
||||||
static async createSession(userUuid: string): Promise<string> {
|
|
||||||
// Pour l'instant, on utilise l'UUID comme token de session
|
|
||||||
// Plus tard, on pourra implémenter un système de JWT ou de sessions en base
|
|
||||||
return userUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Valide un token de session et retourne l'UUID utilisateur
|
|
||||||
*/
|
|
||||||
static async validateSession(sessionToken: string): Promise<string | null> {
|
|
||||||
// Pour l'instant, on considère que le token est valide s'il correspond à un UUID
|
|
||||||
// Plus tard, on pourra ajouter une validation plus robuste
|
|
||||||
if (sessionToken && sessionToken.length > 0) {
|
|
||||||
return sessionToken;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { getPool } from "./database";
|
import { getPool } from "./database";
|
||||||
import { userService } from "./user-service";
|
import { userService } from "./user-service";
|
||||||
import { AuthService } from "./auth-service";
|
|
||||||
import {
|
import {
|
||||||
UserEvaluation,
|
UserEvaluation,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
@@ -121,8 +120,12 @@ export class EvaluationService {
|
|||||||
try {
|
try {
|
||||||
await client.query("BEGIN");
|
await client.query("BEGIN");
|
||||||
|
|
||||||
// 1. Upsert user avec UUID
|
// 1. Récupérer le userUuid depuis le profile de l'évaluation
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
const existingUser = await userService.findUserByProfile(evaluation.profile);
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new Error("Utilisateur non trouvé");
|
||||||
|
}
|
||||||
|
const userUuid = existingUser.uuid;
|
||||||
|
|
||||||
// 2. Upsert user_evaluation avec user_uuid
|
// 2. Upsert user_evaluation avec user_uuid
|
||||||
const userEvalQuery = `
|
const userEvalQuery = `
|
||||||
@@ -624,7 +627,12 @@ export class EvaluationService {
|
|||||||
try {
|
try {
|
||||||
await client.query("BEGIN");
|
await client.query("BEGIN");
|
||||||
|
|
||||||
const userUuid = await AuthService.getUserUuidFromCookie();
|
// Récupérer le userUuid depuis le profile
|
||||||
|
const existingUser = await userService.findUserByProfile(profile);
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new Error("Utilisateur non trouvé");
|
||||||
|
}
|
||||||
|
const userUuid = existingUser.uuid;
|
||||||
|
|
||||||
// Supprimer directement la skill evaluation
|
// Supprimer directement la skill evaluation
|
||||||
const deleteQuery = `
|
const deleteQuery = `
|
||||||
|
|||||||
@@ -22,6 +22,3 @@ export { AdminService } from "./admin-service";
|
|||||||
|
|
||||||
// Admin types (can be imported anywhere)
|
// Admin types (can be imported anywhere)
|
||||||
export type { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
export type { TeamMember, TeamStats, DirectionStats } from "@/lib/admin-types";
|
||||||
|
|
||||||
// Server auth service (server-side only)
|
|
||||||
export { AuthService, COOKIE_NAME, COOKIE_MAX_AGE } from "./auth-service";
|
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import {
|
|||||||
TeamMember,
|
TeamMember,
|
||||||
TeamMemberSkill,
|
TeamMemberSkill,
|
||||||
} from "@/lib/team-review-types";
|
} from "@/lib/team-review-types";
|
||||||
import { SkillLevel } from "@/lib/types";
|
import { SKILL_LEVEL_VALUES, SkillImportance } from "@/lib/types";
|
||||||
|
import {
|
||||||
|
COVERAGE_OBJECTIVES,
|
||||||
|
isCoverageBelowObjective,
|
||||||
|
calculateSkillCoverage,
|
||||||
|
} from "@/lib/evaluation-utils";
|
||||||
|
|
||||||
export class TeamReviewService {
|
export class TeamReviewService {
|
||||||
static async getTeamReviewData(teamId: string): Promise<TeamReviewData> {
|
static async getTeamReviewData(teamId: string): Promise<TeamReviewData> {
|
||||||
@@ -100,7 +105,11 @@ export class TeamReviewService {
|
|||||||
skillName: row.skill_name,
|
skillName: row.skill_name,
|
||||||
category: row.category,
|
category: row.category,
|
||||||
importance: row.importance || "standard",
|
importance: row.importance || "standard",
|
||||||
level: row.level as SkillLevel,
|
level: row.level as
|
||||||
|
| "never"
|
||||||
|
| "not-autonomous"
|
||||||
|
| "autonomous"
|
||||||
|
| "expert",
|
||||||
canMentor: row.can_mentor || false,
|
canMentor: row.can_mentor || false,
|
||||||
wantsToLearn: row.wants_to_learn || false,
|
wantsToLearn: row.wants_to_learn || false,
|
||||||
};
|
};
|
||||||
@@ -140,16 +149,25 @@ export class TeamReviewService {
|
|||||||
const teamMembers = evaluations.filter((e) => e.level).length;
|
const teamMembers = evaluations.filter((e) => e.level).length;
|
||||||
|
|
||||||
const totalTeamMembers = membersMap.size;
|
const totalTeamMembers = membersMap.size;
|
||||||
const coverage =
|
|
||||||
totalTeamMembers > 0 ? (teamMembers / totalTeamMembers) * 100 : 0;
|
|
||||||
|
|
||||||
// Déterminer le niveau de risque en fonction de l'importance
|
// Calculer la couverture en fonction des niveaux
|
||||||
|
const levels = evaluations.map((e) =>
|
||||||
|
e.level
|
||||||
|
? SKILL_LEVEL_VALUES[e.level as keyof typeof SKILL_LEVEL_VALUES]
|
||||||
|
: 0
|
||||||
|
);
|
||||||
|
const coverage = calculateSkillCoverage(levels, totalTeamMembers);
|
||||||
|
|
||||||
|
// Déterminer le niveau de risque en fonction de l'importance et de la couverture
|
||||||
|
const coverageObjective = COVERAGE_OBJECTIVES[skill.importance as SkillImportance];
|
||||||
const risk =
|
const risk =
|
||||||
skill.importance === "incontournable" && experts === 0
|
skill.importance === "incontournable" && coverage < coverageObjective
|
||||||
? "high"
|
? "high"
|
||||||
: skill.importance === "majeure" && experts === 0 && mentors === 0
|
: skill.importance === "majeure" &&
|
||||||
|
coverage < coverageObjective &&
|
||||||
|
experts === 0
|
||||||
? "high"
|
? "high"
|
||||||
: experts === 0 && mentors === 0
|
: coverage < coverageObjective
|
||||||
? "medium"
|
? "medium"
|
||||||
: "low";
|
: "low";
|
||||||
|
|
||||||
@@ -159,7 +177,7 @@ export class TeamReviewService {
|
|||||||
category: skill.category,
|
category: skill.category,
|
||||||
icon: skill.icon,
|
icon: skill.icon,
|
||||||
importance: skill.importance || "standard",
|
importance: skill.importance || "standard",
|
||||||
team_members: teamMembers,
|
teamMembers,
|
||||||
experts,
|
experts,
|
||||||
mentors,
|
mentors,
|
||||||
learners,
|
learners,
|
||||||
@@ -175,8 +193,8 @@ export class TeamReviewService {
|
|||||||
if (!categoriesMap.has(skill.category)) {
|
if (!categoriesMap.has(skill.category)) {
|
||||||
categoriesMap.set(skill.category, {
|
categoriesMap.set(skill.category, {
|
||||||
category: skill.category,
|
category: skill.category,
|
||||||
total_skills: 0,
|
totalSkills: 0,
|
||||||
covered_skills: 0,
|
coveredSkills: 0,
|
||||||
experts: 0,
|
experts: 0,
|
||||||
mentors: 0,
|
mentors: 0,
|
||||||
learners: 0,
|
learners: 0,
|
||||||
@@ -189,13 +207,13 @@ export class TeamReviewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryStats = categoriesMap.get(skill.category)!;
|
const categoryStats = categoriesMap.get(skill.category)!;
|
||||||
categoryStats.total_skills++;
|
categoryStats.totalSkills++;
|
||||||
|
|
||||||
const skillGap = skillGaps.find(
|
const skillGap = skillGaps.find(
|
||||||
(gap) => gap.skillId === skill.skill_id
|
(gap) => gap.skillId === skill.skill_id
|
||||||
);
|
);
|
||||||
if (skillGap) {
|
if (skillGap) {
|
||||||
if (skillGap.team_members > 0) categoryStats.covered_skills++;
|
if (skillGap.teamMembers > 0) categoryStats.coveredSkills++;
|
||||||
categoryStats.experts += skillGap.experts;
|
categoryStats.experts += skillGap.experts;
|
||||||
categoryStats.mentors += skillGap.mentors;
|
categoryStats.mentors += skillGap.mentors;
|
||||||
categoryStats.learners += skillGap.learners;
|
categoryStats.learners += skillGap.learners;
|
||||||
@@ -203,11 +221,14 @@ export class TeamReviewService {
|
|||||||
// Calculer la couverture des compétences critiques
|
// Calculer la couverture des compétences critiques
|
||||||
if (
|
if (
|
||||||
skillGap.importance === "incontournable" &&
|
skillGap.importance === "incontournable" &&
|
||||||
skillGap.coverage > 50
|
!isCoverageBelowObjective(skillGap.coverage, skillGap.importance)
|
||||||
) {
|
) {
|
||||||
categoryStats.criticalSkillsCoverage.incontournable++;
|
categoryStats.criticalSkillsCoverage.incontournable++;
|
||||||
}
|
}
|
||||||
if (skillGap.importance === "majeure" && skillGap.coverage > 50) {
|
if (
|
||||||
|
skillGap.importance === "majeure" &&
|
||||||
|
!isCoverageBelowObjective(skillGap.coverage, skillGap.importance)
|
||||||
|
) {
|
||||||
categoryStats.criticalSkillsCoverage.majeure++;
|
categoryStats.criticalSkillsCoverage.majeure++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,8 +240,8 @@ export class TeamReviewService {
|
|||||||
).map((category) => ({
|
).map((category) => ({
|
||||||
...category,
|
...category,
|
||||||
coverage:
|
coverage:
|
||||||
category.total_skills > 0
|
category.totalSkills > 0
|
||||||
? (category.covered_skills / category.total_skills) * 100
|
? (category.coveredSkills / category.totalSkills) * 100
|
||||||
: 0,
|
: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -236,7 +257,14 @@ export class TeamReviewService {
|
|||||||
incontournable:
|
incontournable:
|
||||||
(skillGaps
|
(skillGaps
|
||||||
.filter((gap) => gap.importance === "incontournable")
|
.filter((gap) => gap.importance === "incontournable")
|
||||||
.reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) /
|
.reduce(
|
||||||
|
(acc, gap) =>
|
||||||
|
acc +
|
||||||
|
(!isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
|
? 1
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
) /
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
skillGaps.filter((gap) => gap.importance === "incontournable")
|
skillGaps.filter((gap) => gap.importance === "incontournable")
|
||||||
@@ -246,7 +274,14 @@ export class TeamReviewService {
|
|||||||
majeure:
|
majeure:
|
||||||
(skillGaps
|
(skillGaps
|
||||||
.filter((gap) => gap.importance === "majeure")
|
.filter((gap) => gap.importance === "majeure")
|
||||||
.reduce((acc, gap) => acc + (gap.coverage > 50 ? 1 : 0), 0) /
|
.reduce(
|
||||||
|
(acc, gap) =>
|
||||||
|
acc +
|
||||||
|
(!isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
|
? 1
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
) /
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
skillGaps.filter((gap) => gap.importance === "majeure").length
|
skillGaps.filter((gap) => gap.importance === "majeure").length
|
||||||
@@ -298,7 +333,9 @@ export class TeamReviewService {
|
|||||||
|
|
||||||
// Analyser les gaps critiques par importance
|
// Analyser les gaps critiques par importance
|
||||||
const uncoveredIncontournables = skillGaps.filter(
|
const uncoveredIncontournables = skillGaps.filter(
|
||||||
(gap) => gap.importance === "incontournable" && gap.coverage < 50
|
(gap) =>
|
||||||
|
gap.importance === "incontournable" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
);
|
);
|
||||||
if (uncoveredIncontournables.length > 0) {
|
if (uncoveredIncontournables.length > 0) {
|
||||||
recommendations.push(
|
recommendations.push(
|
||||||
@@ -311,7 +348,9 @@ export class TeamReviewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uncoveredMajeures = skillGaps.filter(
|
const uncoveredMajeures = skillGaps.filter(
|
||||||
(gap) => gap.importance === "majeure" && gap.coverage < 30
|
(gap) =>
|
||||||
|
gap.importance === "majeure" &&
|
||||||
|
isCoverageBelowObjective(gap.coverage, gap.importance)
|
||||||
);
|
);
|
||||||
if (uncoveredMajeures.length > 0) {
|
if (uncoveredMajeures.length > 0) {
|
||||||
recommendations.push(
|
recommendations.push(
|
||||||
@@ -338,7 +377,7 @@ export class TeamReviewService {
|
|||||||
|
|
||||||
// Analyser la couverture des catégories
|
// Analyser la couverture des catégories
|
||||||
const lowCoverageCategories = categoryCoverage
|
const lowCoverageCategories = categoryCoverage
|
||||||
.filter((cat) => cat.coverage < 50)
|
.filter((cat) => cat.coverage < COVERAGE_OBJECTIVES.majeure)
|
||||||
.map((cat) => cat.category);
|
.map((cat) => cat.category);
|
||||||
if (lowCoverageCategories.length > 0) {
|
if (lowCoverageCategories.length > 0) {
|
||||||
recommendations.push(
|
recommendations.push(
|
||||||
|
|||||||
28
types/next-auth.d.ts
vendored
Normal file
28
types/next-auth.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { DefaultSession } from "next-auth";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
teamId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
teamId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "next-auth/jwt" {
|
||||||
|
interface JWT {
|
||||||
|
id: string;
|
||||||
|
teamId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user