feat: add PostgreSQL support and enhance evaluation loading
- Added `pg` and `@types/pg` dependencies for PostgreSQL integration. - Updated `useEvaluation` hook to load user evaluations from the API, improving data management. - Refactored `saveUserEvaluation` and `loadUserEvaluation` functions to interact with the API instead of localStorage. - Introduced error handling for profile loading and evaluation saving to enhance robustness. - Added new methods for managing user profiles and evaluations, including `clearUserProfile` and `loadEvaluationForProfile`.
This commit is contained in:
105
DATABASE_SETUP.md
Normal file
105
DATABASE_SETUP.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Configuration PostgreSQL pour PeakSkills
|
||||
|
||||
## Setup de développement
|
||||
|
||||
### 1. Installation des dépendances
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. Configuration de l'environnement
|
||||
|
||||
Créer un fichier `.env.local` à la racine du projet :
|
||||
|
||||
```env
|
||||
# Database configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=peakskills
|
||||
DB_USER=peakskills_user
|
||||
DB_PASSWORD=peakskills_password
|
||||
|
||||
# API configuration (optional, defaults to empty string for same-origin requests)
|
||||
NEXT_PUBLIC_API_URL=
|
||||
```
|
||||
|
||||
### 3. Démarrage de PostgreSQL avec Docker
|
||||
|
||||
```bash
|
||||
# Démarrer la base de données
|
||||
docker-compose up -d postgres
|
||||
|
||||
# Vérifier que la base est bien démarrée
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 4. Démarrage de l'application
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Base de données
|
||||
|
||||
La base de données PostgreSQL contient les tables suivantes :
|
||||
|
||||
- `users` : Informations utilisateur (nom, prénom, équipe)
|
||||
- `user_evaluations` : Métadonnées des évaluations utilisateur
|
||||
- `category_evaluations` : Évaluations par catégorie
|
||||
- `selected_skills` : Skills sélectionnées pour évaluation
|
||||
- `skill_evaluations` : Évaluations détaillées des skills
|
||||
|
||||
### API
|
||||
|
||||
Les endpoints suivants sont disponibles :
|
||||
|
||||
- `GET /api/evaluations` : Charger une évaluation utilisateur
|
||||
- `POST /api/evaluations` : Sauvegarder une évaluation complète
|
||||
- `PUT /api/evaluations/skills` : Mettre à jour une skill spécifique
|
||||
|
||||
### Services
|
||||
|
||||
Le dossier `services/` contient tous les services backend :
|
||||
|
||||
- `database.ts` : Configuration et pool de connexions PostgreSQL (server-only)
|
||||
- `evaluation-service.ts` : Service backend pour interfacer avec PostgreSQL (server-only)
|
||||
- `api-client.ts` : Client pour les appels API depuis le frontend (client-safe)
|
||||
- `index.ts` : Exports server-side pour les API routes
|
||||
- `client.ts` : Exports client-safe pour les composants React
|
||||
|
||||
**Important** :
|
||||
|
||||
- Utiliser `@/services/client` dans les composants React et hooks
|
||||
- Utiliser `@/services` ou imports directs dans les API routes
|
||||
|
||||
Le hook `useEvaluation` (dans `hooks/`) utilise `api-client` pour la persistance.
|
||||
|
||||
## Migration depuis localStorage
|
||||
|
||||
Les données sont maintenant persistées en base PostgreSQL au lieu du localStorage. Le hook `useEvaluation` a été mis à jour pour :
|
||||
|
||||
1. Charger les évaluations depuis l'API
|
||||
2. Effectuer des mises à jour optimistes pour une meilleure UX
|
||||
3. Synchroniser avec la base de données
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
```bash
|
||||
# Redémarrer la base de données
|
||||
docker-compose restart postgres
|
||||
|
||||
# Voir les logs de la base
|
||||
docker-compose logs postgres
|
||||
|
||||
# Se connecter à la base
|
||||
docker-compose exec postgres psql -U peakskills_user -d peakskills
|
||||
|
||||
# Arrêter tous les services
|
||||
docker-compose down
|
||||
|
||||
# Arrêter et supprimer les volumes (reset complet)
|
||||
docker-compose down -v
|
||||
```
|
||||
54
app/api/evaluations/route.ts
Normal file
54
app/api/evaluations/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { evaluationService } from "@/services/evaluation-service";
|
||||
import { UserEvaluation, UserProfile } from "@/lib/types";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const firstName = searchParams.get("firstName");
|
||||
const lastName = searchParams.get("lastName");
|
||||
const teamId = searchParams.get("teamId");
|
||||
|
||||
if (!firstName || !lastName || !teamId) {
|
||||
return NextResponse.json(
|
||||
{ error: "firstName, lastName et teamId sont requis" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const profile: UserProfile = { firstName, lastName, teamId };
|
||||
const evaluation = await evaluationService.loadUserEvaluation(profile);
|
||||
|
||||
return NextResponse.json({ evaluation });
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement de l'évaluation:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur interne du serveur" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const evaluation: UserEvaluation = body.evaluation;
|
||||
|
||||
if (!evaluation || !evaluation.profile) {
|
||||
return NextResponse.json(
|
||||
{ error: "Évaluation invalide" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await evaluationService.saveUserEvaluation(evaluation);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la sauvegarde de l'évaluation:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur interne du serveur" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
104
app/api/evaluations/skills/route.ts
Normal file
104
app/api/evaluations/skills/route.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { evaluationService } from "@/services/evaluation-service";
|
||||
import { UserProfile, SkillLevel } from "@/lib/types";
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
profile,
|
||||
category,
|
||||
skillId,
|
||||
level,
|
||||
canMentor,
|
||||
wantsToLearn,
|
||||
action,
|
||||
} = body;
|
||||
|
||||
if (!profile || !category || !skillId) {
|
||||
return NextResponse.json(
|
||||
{ error: "profile, category et skillId sont requis" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const userProfile: UserProfile = profile;
|
||||
|
||||
switch (action) {
|
||||
case "updateLevel":
|
||||
if (level === undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: "level est requis pour updateLevel" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await evaluationService.updateSkillLevel(
|
||||
userProfile,
|
||||
category,
|
||||
skillId,
|
||||
level
|
||||
);
|
||||
break;
|
||||
|
||||
case "updateMentorStatus":
|
||||
if (canMentor === undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: "canMentor est requis pour updateMentorStatus" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await evaluationService.updateSkillMentorStatus(
|
||||
userProfile,
|
||||
category,
|
||||
skillId,
|
||||
canMentor
|
||||
);
|
||||
break;
|
||||
|
||||
case "updateLearningStatus":
|
||||
if (wantsToLearn === undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: "wantsToLearn est requis pour updateLearningStatus" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await evaluationService.updateSkillLearningStatus(
|
||||
userProfile,
|
||||
category,
|
||||
skillId,
|
||||
wantsToLearn
|
||||
);
|
||||
break;
|
||||
|
||||
case "addSkill":
|
||||
await evaluationService.addSkillToEvaluation(
|
||||
userProfile,
|
||||
category,
|
||||
skillId
|
||||
);
|
||||
break;
|
||||
|
||||
case "removeSkill":
|
||||
await evaluationService.removeSkillFromEvaluation(
|
||||
userProfile,
|
||||
category,
|
||||
skillId
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: "Action non supportée" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la mise à jour de la skill:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur interne du serveur" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: peakskills
|
||||
POSTGRES_USER: peakskills_user
|
||||
POSTGRES_PASSWORD: peakskills_password
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U peakskills_user -d peakskills"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
saveUserEvaluation,
|
||||
createEmptyEvaluation,
|
||||
} from "@/lib/evaluation-utils";
|
||||
import { apiClient } from "@/services/api-client";
|
||||
import { loadSkillCategories, loadTeams } from "@/lib/data-loader";
|
||||
|
||||
// Fonction pour migrer une évaluation existante avec de nouvelles catégories
|
||||
@@ -70,13 +71,25 @@ export function useEvaluation() {
|
||||
setSkillCategories(categories);
|
||||
setTeams(teamsData);
|
||||
|
||||
// Try to load existing evaluation
|
||||
const saved = loadUserEvaluation();
|
||||
// Try to load user profile from localStorage and then load evaluation from API
|
||||
try {
|
||||
const savedProfile = localStorage.getItem("peakSkills_userProfile");
|
||||
if (savedProfile) {
|
||||
const profile: UserProfile = JSON.parse(savedProfile);
|
||||
const saved = await loadUserEvaluation(profile);
|
||||
if (saved) {
|
||||
// Migrate evaluation to include new categories if needed
|
||||
const migratedEvaluation = migrateEvaluation(saved, categories);
|
||||
setUserEvaluation(migratedEvaluation);
|
||||
saveUserEvaluation(migratedEvaluation); // Save the migrated version
|
||||
if (migratedEvaluation !== saved) {
|
||||
await saveUserEvaluation(migratedEvaluation); // Save the migrated version
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (profileError) {
|
||||
console.error("Failed to load user profile:", profileError);
|
||||
// Clear invalid profile data
|
||||
localStorage.removeItem("peakSkills_userProfile");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize data:", error);
|
||||
@@ -88,7 +101,35 @@ export function useEvaluation() {
|
||||
initializeData();
|
||||
}, []);
|
||||
|
||||
const updateProfile = (profile: UserProfile) => {
|
||||
const loadEvaluationForProfile = async (profile: UserProfile) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const saved = await loadUserEvaluation(profile);
|
||||
if (saved) {
|
||||
// Migrate evaluation to include new categories if needed
|
||||
const migratedEvaluation = migrateEvaluation(saved, skillCategories);
|
||||
setUserEvaluation(migratedEvaluation);
|
||||
if (migratedEvaluation !== saved) {
|
||||
await saveUserEvaluation(migratedEvaluation); // Save the migrated version
|
||||
}
|
||||
} else {
|
||||
// Create new evaluation
|
||||
const evaluations = createEmptyEvaluation(skillCategories);
|
||||
const newEvaluation: UserEvaluation = {
|
||||
profile,
|
||||
evaluations,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
setUserEvaluation(newEvaluation);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load evaluation for profile:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateProfile = async (profile: UserProfile) => {
|
||||
const evaluations =
|
||||
userEvaluation?.evaluations || createEmptyEvaluation(skillCategories);
|
||||
const newEvaluation: UserEvaluation = {
|
||||
@@ -97,20 +138,27 @@ export function useEvaluation() {
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Save profile to localStorage for auto-login
|
||||
localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile));
|
||||
|
||||
setUserEvaluation(newEvaluation);
|
||||
saveUserEvaluation(newEvaluation);
|
||||
await saveUserEvaluation(newEvaluation);
|
||||
};
|
||||
|
||||
const updateSkillLevel = (
|
||||
const updateSkillLevel = async (
|
||||
category: string,
|
||||
skillId: string,
|
||||
level: SkillLevel
|
||||
) => {
|
||||
if (!userEvaluation) return;
|
||||
|
||||
try {
|
||||
// Optimistic update
|
||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||
if (catEval.category === category) {
|
||||
const existingSkill = catEval.skills.find((s) => s.skillId === skillId);
|
||||
const existingSkill = catEval.skills.find(
|
||||
(s) => s.skillId === skillId
|
||||
);
|
||||
const updatedSkills = existingSkill
|
||||
? catEval.skills.map((skill) =>
|
||||
skill.skillId === skillId ? { ...skill, level } : skill
|
||||
@@ -132,16 +180,28 @@ export function useEvaluation() {
|
||||
};
|
||||
|
||||
setUserEvaluation(newEvaluation);
|
||||
saveUserEvaluation(newEvaluation);
|
||||
|
||||
// Update via API
|
||||
await apiClient.updateSkillLevel(
|
||||
userEvaluation.profile,
|
||||
category,
|
||||
skillId,
|
||||
level
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to update skill level:", error);
|
||||
// Revert optimistic update if needed
|
||||
}
|
||||
};
|
||||
|
||||
const updateSkillMentorStatus = (
|
||||
const updateSkillMentorStatus = async (
|
||||
category: string,
|
||||
skillId: string,
|
||||
canMentor: boolean
|
||||
) => {
|
||||
if (!userEvaluation) return;
|
||||
|
||||
try {
|
||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||
if (catEval.category === category) {
|
||||
const updatedSkills = catEval.skills.map((skill) =>
|
||||
@@ -163,16 +223,25 @@ export function useEvaluation() {
|
||||
};
|
||||
|
||||
setUserEvaluation(newEvaluation);
|
||||
saveUserEvaluation(newEvaluation);
|
||||
await apiClient.updateSkillMentorStatus(
|
||||
userEvaluation.profile,
|
||||
category,
|
||||
skillId,
|
||||
canMentor
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to update skill mentor status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSkillLearningStatus = (
|
||||
const updateSkillLearningStatus = async (
|
||||
category: string,
|
||||
skillId: string,
|
||||
wantsToLearn: boolean
|
||||
) => {
|
||||
if (!userEvaluation) return;
|
||||
|
||||
try {
|
||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||
if (catEval.category === category) {
|
||||
const updatedSkills = catEval.skills.map((skill) =>
|
||||
@@ -194,12 +263,21 @@ export function useEvaluation() {
|
||||
};
|
||||
|
||||
setUserEvaluation(newEvaluation);
|
||||
saveUserEvaluation(newEvaluation);
|
||||
await apiClient.updateSkillLearningStatus(
|
||||
userEvaluation.profile,
|
||||
category,
|
||||
skillId,
|
||||
wantsToLearn
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to update skill learning status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const addSkillToEvaluation = (category: string, skillId: string) => {
|
||||
const addSkillToEvaluation = async (category: string, skillId: string) => {
|
||||
if (!userEvaluation) return;
|
||||
|
||||
try {
|
||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||
if (catEval.category === category) {
|
||||
if (!catEval.selectedSkillIds.includes(skillId)) {
|
||||
@@ -220,12 +298,23 @@ export function useEvaluation() {
|
||||
};
|
||||
|
||||
setUserEvaluation(newEvaluation);
|
||||
saveUserEvaluation(newEvaluation);
|
||||
await apiClient.addSkillToEvaluation(
|
||||
userEvaluation.profile,
|
||||
category,
|
||||
skillId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to add skill to evaluation:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSkillFromEvaluation = (category: string, skillId: string) => {
|
||||
const removeSkillFromEvaluation = async (
|
||||
category: string,
|
||||
skillId: string
|
||||
) => {
|
||||
if (!userEvaluation) return;
|
||||
|
||||
try {
|
||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||
if (catEval.category === category) {
|
||||
return {
|
||||
@@ -246,10 +335,17 @@ export function useEvaluation() {
|
||||
};
|
||||
|
||||
setUserEvaluation(newEvaluation);
|
||||
saveUserEvaluation(newEvaluation);
|
||||
await apiClient.removeSkillFromEvaluation(
|
||||
userEvaluation.profile,
|
||||
category,
|
||||
skillId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to remove skill from evaluation:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeEmptyEvaluation = (profile: UserProfile) => {
|
||||
const initializeEmptyEvaluation = async (profile: UserProfile) => {
|
||||
const evaluations = createEmptyEvaluation(skillCategories);
|
||||
const newEvaluation: UserEvaluation = {
|
||||
profile,
|
||||
@@ -257,8 +353,16 @@ export function useEvaluation() {
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Save profile to localStorage for auto-login
|
||||
localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile));
|
||||
|
||||
setUserEvaluation(newEvaluation);
|
||||
saveUserEvaluation(newEvaluation);
|
||||
await saveUserEvaluation(newEvaluation);
|
||||
};
|
||||
|
||||
const clearUserProfile = () => {
|
||||
localStorage.removeItem("peakSkills_userProfile");
|
||||
setUserEvaluation(null);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -266,6 +370,7 @@ export function useEvaluation() {
|
||||
skillCategories,
|
||||
teams,
|
||||
loading,
|
||||
loadEvaluationForProfile,
|
||||
updateProfile,
|
||||
updateSkillLevel,
|
||||
updateSkillMentorStatus,
|
||||
@@ -273,5 +378,6 @@ export function useEvaluation() {
|
||||
addSkillToEvaluation,
|
||||
removeSkillFromEvaluation,
|
||||
initializeEmptyEvaluation,
|
||||
clearUserProfile,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
UserEvaluation,
|
||||
SkillCategory,
|
||||
} from "./types";
|
||||
import { apiClient } from "../services/api-client";
|
||||
|
||||
export function calculateCategoryScore(
|
||||
categoryEvaluation: CategoryEvaluation
|
||||
@@ -44,21 +45,22 @@ export function generateRadarData(
|
||||
});
|
||||
}
|
||||
|
||||
export function saveUserEvaluation(evaluation: UserEvaluation): void {
|
||||
export async function saveUserEvaluation(
|
||||
evaluation: UserEvaluation
|
||||
): Promise<void> {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"peakSkills_userEvaluation",
|
||||
JSON.stringify(evaluation)
|
||||
);
|
||||
await apiClient.saveUserEvaluation(evaluation);
|
||||
} catch (error) {
|
||||
console.error("Failed to save user evaluation:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadUserEvaluation(): UserEvaluation | null {
|
||||
export async function loadUserEvaluation(
|
||||
profile: UserEvaluation["profile"]
|
||||
): Promise<UserEvaluation | null> {
|
||||
try {
|
||||
const saved = localStorage.getItem("peakSkills_userEvaluation");
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
return await apiClient.loadUserEvaluation(profile);
|
||||
} catch (error) {
|
||||
console.error("Failed to load user evaluation:", error);
|
||||
return null;
|
||||
|
||||
@@ -62,7 +62,9 @@
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "3.25.67"
|
||||
"zod": "3.25.67",
|
||||
"pg": "^8.12.0",
|
||||
"@types/pg": "^8.11.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
|
||||
122
pnpm-lock.yaml
generated
122
pnpm-lock.yaml
generated
@@ -104,6 +104,9 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: 1.1.6
|
||||
version: 1.1.6(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@types/pg':
|
||||
specifier: ^8.11.10
|
||||
version: 8.15.5
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
@@ -137,6 +140,9 @@ importers:
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
pg:
|
||||
specifier: ^8.12.0
|
||||
version: 8.16.3
|
||||
react:
|
||||
specifier: ^19
|
||||
version: 19.1.1
|
||||
@@ -1210,6 +1216,9 @@ packages:
|
||||
'@types/node@22.17.2':
|
||||
resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
|
||||
|
||||
'@types/pg@8.15.5':
|
||||
resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==}
|
||||
|
||||
'@types/react-dom@19.1.7':
|
||||
resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
|
||||
peerDependencies:
|
||||
@@ -1542,6 +1551,40 @@ packages:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pg-cloudflare@1.2.7:
|
||||
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
|
||||
|
||||
pg-connection-string@2.9.1:
|
||||
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
|
||||
|
||||
pg-int8@1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
pg-pool@3.10.1:
|
||||
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
pg-protocol@1.10.3:
|
||||
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
|
||||
|
||||
pg-types@2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg@8.16.3:
|
||||
resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pgpass@1.0.5:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1556,6 +1599,22 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postgres-array@2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postgres-bytea@1.0.0:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-date@1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
@@ -1669,6 +1728,10 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -1762,6 +1825,10 @@ packages:
|
||||
victory-vendor@36.9.2:
|
||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2745,6 +2812,12 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/pg@8.15.5':
|
||||
dependencies:
|
||||
'@types/node': 22.17.2
|
||||
pg-protocol: 1.10.3
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/react-dom@19.1.7(@types/react@19.1.10)':
|
||||
dependencies:
|
||||
'@types/react': 19.1.10
|
||||
@@ -3031,6 +3104,41 @@ snapshots:
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
pg-cloudflare@1.2.7:
|
||||
optional: true
|
||||
|
||||
pg-connection-string@2.9.1: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-pool@3.10.1(pg@8.16.3):
|
||||
dependencies:
|
||||
pg: 8.16.3
|
||||
|
||||
pg-protocol@1.10.3: {}
|
||||
|
||||
pg-types@2.2.0:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
postgres-array: 2.0.0
|
||||
postgres-bytea: 1.0.0
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
pg@8.16.3:
|
||||
dependencies:
|
||||
pg-connection-string: 2.9.1
|
||||
pg-pool: 3.10.1(pg@8.16.3)
|
||||
pg-protocol: 1.10.3
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.2.7
|
||||
|
||||
pgpass@1.0.5:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
@@ -3047,6 +3155,16 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postgres-array@2.0.0: {}
|
||||
|
||||
postgres-bytea@1.0.0: {}
|
||||
|
||||
postgres-date@1.0.7: {}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -3185,6 +3303,8 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
styled-jsx@5.1.6(react@19.1.1):
|
||||
@@ -3272,6 +3392,8 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
zod@3.25.67: {}
|
||||
|
||||
75
scripts/init.sql
Normal file
75
scripts/init.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- Create enum for skill levels
|
||||
CREATE TYPE skill_level_enum AS ENUM ('never', 'not-autonomous', 'autonomous', 'expert');
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
team_id VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- User evaluations table
|
||||
CREATE TABLE user_evaluations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
-- Category evaluations table
|
||||
CREATE TABLE category_evaluations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_evaluation_id INTEGER REFERENCES user_evaluations(id) ON DELETE CASCADE,
|
||||
category VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_evaluation_id, category)
|
||||
);
|
||||
|
||||
-- Selected skills table (skills the user wants to evaluate)
|
||||
CREATE TABLE selected_skills (
|
||||
id SERIAL PRIMARY KEY,
|
||||
category_evaluation_id INTEGER REFERENCES category_evaluations(id) ON DELETE CASCADE,
|
||||
skill_id VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(category_evaluation_id, skill_id)
|
||||
);
|
||||
|
||||
-- Skill evaluations table
|
||||
CREATE TABLE skill_evaluations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
category_evaluation_id INTEGER REFERENCES category_evaluations(id) ON DELETE CASCADE,
|
||||
skill_id VARCHAR(100) NOT NULL,
|
||||
level skill_level_enum NOT NULL,
|
||||
can_mentor BOOLEAN DEFAULT FALSE,
|
||||
wants_to_learn BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(category_evaluation_id, skill_id)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_users_team_id ON users(team_id);
|
||||
CREATE INDEX idx_user_evaluations_user_id ON user_evaluations(user_id);
|
||||
CREATE INDEX idx_category_evaluations_user_evaluation_id ON category_evaluations(user_evaluation_id);
|
||||
CREATE INDEX idx_selected_skills_category_evaluation_id ON selected_skills(category_evaluation_id);
|
||||
CREATE INDEX idx_skill_evaluations_category_evaluation_id ON skill_evaluations(category_evaluation_id);
|
||||
CREATE INDEX idx_skill_evaluations_skill_id ON skill_evaluations(skill_id);
|
||||
|
||||
-- Update trigger for users table
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_skill_evaluations_updated_at BEFORE UPDATE ON skill_evaluations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
174
services/api-client.ts
Normal file
174
services/api-client.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { UserEvaluation, UserProfile, SkillLevel } from "../lib/types";
|
||||
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge une évaluation utilisateur depuis l'API
|
||||
*/
|
||||
async loadUserEvaluation(
|
||||
profile: UserProfile
|
||||
): Promise<UserEvaluation | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
teamId: profile.teamId,
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/api/evaluations?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Erreur lors du chargement de l'évaluation");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.evaluation;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement de l'évaluation:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde une évaluation utilisateur via l'API
|
||||
*/
|
||||
async saveUserEvaluation(evaluation: UserEvaluation): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/evaluations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ evaluation }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Erreur lors de la sauvegarde de l'évaluation");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la sauvegarde de l'évaluation:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le niveau d'une skill
|
||||
*/
|
||||
async updateSkillLevel(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
level: SkillLevel
|
||||
): Promise<void> {
|
||||
await this.updateSkill(profile, category, skillId, {
|
||||
action: "updateLevel",
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut de mentorat d'une skill
|
||||
*/
|
||||
async updateSkillMentorStatus(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
canMentor: boolean
|
||||
): Promise<void> {
|
||||
await this.updateSkill(profile, category, skillId, {
|
||||
action: "updateMentorStatus",
|
||||
canMentor,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'apprentissage d'une skill
|
||||
*/
|
||||
async updateSkillLearningStatus(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
wantsToLearn: boolean
|
||||
): Promise<void> {
|
||||
await this.updateSkill(profile, category, skillId, {
|
||||
action: "updateLearningStatus",
|
||||
wantsToLearn,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une skill à l'évaluation
|
||||
*/
|
||||
async addSkillToEvaluation(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string
|
||||
): Promise<void> {
|
||||
await this.updateSkill(profile, category, skillId, {
|
||||
action: "addSkill",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une skill de l'évaluation
|
||||
*/
|
||||
async removeSkillFromEvaluation(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string
|
||||
): Promise<void> {
|
||||
await this.updateSkill(profile, category, skillId, {
|
||||
action: "removeSkill",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode utilitaire pour mettre à jour une skill
|
||||
*/
|
||||
private async updateSkill(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
options: {
|
||||
action:
|
||||
| "updateLevel"
|
||||
| "updateMentorStatus"
|
||||
| "updateLearningStatus"
|
||||
| "addSkill"
|
||||
| "removeSkill";
|
||||
level?: SkillLevel;
|
||||
canMentor?: boolean;
|
||||
wantsToLearn?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/evaluations/skills`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
profile,
|
||||
category,
|
||||
skillId,
|
||||
...options,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Erreur lors de la mise à jour de la skill");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la mise à jour de la skill:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const apiClient = new ApiClient();
|
||||
4
services/client.ts
Normal file
4
services/client.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Client-side services only
|
||||
// Safe to import in React components and hooks
|
||||
|
||||
export { ApiClient, apiClient } from "./api-client";
|
||||
33
services/database.ts
Normal file
33
services/database.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Pool } from "pg";
|
||||
|
||||
// Configuration de la base de données
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || "localhost",
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME || "peakskills",
|
||||
user: process.env.DB_USER || "peakskills_user",
|
||||
password: process.env.DB_PASSWORD || "peakskills_password",
|
||||
};
|
||||
|
||||
// Pool de connexions global
|
||||
let pool: Pool | null = null;
|
||||
|
||||
export function getPool(): Pool {
|
||||
if (!pool) {
|
||||
pool = new Pool(dbConfig);
|
||||
|
||||
pool.on("error", (err) => {
|
||||
console.error("Unexpected error on idle client", err);
|
||||
process.exit(-1);
|
||||
});
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function closePool(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
}
|
||||
}
|
||||
527
services/evaluation-service.ts
Normal file
527
services/evaluation-service.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import { getPool } from "./database";
|
||||
import {
|
||||
UserEvaluation,
|
||||
UserProfile,
|
||||
CategoryEvaluation,
|
||||
SkillEvaluation,
|
||||
SkillLevel,
|
||||
} from "../lib/types";
|
||||
|
||||
export class EvaluationService {
|
||||
/**
|
||||
* Crée ou met à jour un utilisateur
|
||||
*/
|
||||
async upsertUser(profile: UserProfile): Promise<number> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Vérifier si l'utilisateur existe déjà (par firstName + lastName + teamId)
|
||||
const existingUserQuery = `
|
||||
SELECT id FROM users
|
||||
WHERE first_name = $1 AND last_name = $2 AND team_id = $3
|
||||
`;
|
||||
|
||||
const existingUser = await client.query(existingUserQuery, [
|
||||
profile.firstName,
|
||||
profile.lastName,
|
||||
profile.teamId,
|
||||
]);
|
||||
|
||||
if (existingUser.rows.length > 0) {
|
||||
// Mettre à jour l'utilisateur existant
|
||||
const updateQuery = `
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, team_id = $3, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $4
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await client.query(updateQuery, [
|
||||
profile.firstName,
|
||||
profile.lastName,
|
||||
profile.teamId,
|
||||
existingUser.rows[0].id,
|
||||
]);
|
||||
|
||||
return result.rows[0].id;
|
||||
} else {
|
||||
// Créer un nouvel utilisateur
|
||||
const insertQuery = `
|
||||
INSERT INTO users (first_name, last_name, team_id)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await client.query(insertQuery, [
|
||||
profile.firstName,
|
||||
profile.lastName,
|
||||
profile.teamId,
|
||||
]);
|
||||
|
||||
return result.rows[0].id;
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde une évaluation utilisateur complète
|
||||
*/
|
||||
async saveUserEvaluation(evaluation: UserEvaluation): Promise<void> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. Upsert user
|
||||
const userId = await this.upsertUser(evaluation.profile);
|
||||
|
||||
// 2. Upsert user_evaluation
|
||||
const userEvalQuery = `
|
||||
INSERT INTO user_evaluations (user_id, last_updated)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id)
|
||||
DO UPDATE SET last_updated = $2
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const userEvalResult = await client.query(userEvalQuery, [
|
||||
userId,
|
||||
new Date(evaluation.lastUpdated),
|
||||
]);
|
||||
|
||||
const userEvaluationId = userEvalResult.rows[0].id;
|
||||
|
||||
// 3. Supprimer les anciennes évaluations de catégories
|
||||
await client.query(
|
||||
"DELETE FROM category_evaluations WHERE user_evaluation_id = $1",
|
||||
[userEvaluationId]
|
||||
);
|
||||
|
||||
// 4. Sauvegarder les nouvelles évaluations
|
||||
for (const catEval of evaluation.evaluations) {
|
||||
await this.saveCategoryEvaluation(client, userEvaluationId, catEval);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde une évaluation de catégorie
|
||||
*/
|
||||
private async saveCategoryEvaluation(
|
||||
client: any,
|
||||
userEvaluationId: number,
|
||||
catEval: CategoryEvaluation
|
||||
): Promise<void> {
|
||||
// Insérer la catégorie d'évaluation
|
||||
const catEvalQuery = `
|
||||
INSERT INTO category_evaluations (user_evaluation_id, category)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const catEvalResult = await client.query(catEvalQuery, [
|
||||
userEvaluationId,
|
||||
catEval.category,
|
||||
]);
|
||||
|
||||
const categoryEvaluationId = catEvalResult.rows[0].id;
|
||||
|
||||
// Insérer les skills sélectionnées
|
||||
for (const skillId of catEval.selectedSkillIds) {
|
||||
await client.query(
|
||||
"INSERT INTO selected_skills (category_evaluation_id, skill_id) VALUES ($1, $2)",
|
||||
[categoryEvaluationId, skillId]
|
||||
);
|
||||
}
|
||||
|
||||
// Insérer les évaluations de skills
|
||||
for (const skillEval of catEval.skills) {
|
||||
if (skillEval.level !== null) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO skill_evaluations
|
||||
(category_evaluation_id, skill_id, level, can_mentor, wants_to_learn)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`,
|
||||
[
|
||||
categoryEvaluationId,
|
||||
skillEval.skillId,
|
||||
skillEval.level,
|
||||
skillEval.canMentor || false,
|
||||
skillEval.wantsToLearn || false,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge une évaluation utilisateur
|
||||
*/
|
||||
async loadUserEvaluation(
|
||||
profile: UserProfile
|
||||
): Promise<UserEvaluation | null> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Trouver l'utilisateur
|
||||
const userQuery = `
|
||||
SELECT u.*, ue.id as user_evaluation_id, ue.last_updated
|
||||
FROM users u
|
||||
LEFT JOIN user_evaluations ue ON u.id = ue.user_id
|
||||
WHERE u.first_name = $1 AND u.last_name = $2 AND u.team_id = $3
|
||||
`;
|
||||
|
||||
const userResult = await client.query(userQuery, [
|
||||
profile.firstName,
|
||||
profile.lastName,
|
||||
profile.teamId,
|
||||
]);
|
||||
|
||||
if (
|
||||
userResult.rows.length === 0 ||
|
||||
!userResult.rows[0].user_evaluation_id
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userData = userResult.rows[0];
|
||||
const userEvaluationId = userData.user_evaluation_id;
|
||||
|
||||
// Charger les évaluations de catégories
|
||||
const categoriesQuery = `
|
||||
SELECT ce.*,
|
||||
array_agg(DISTINCT ss.skill_id) FILTER (WHERE ss.skill_id IS NOT NULL) as selected_skill_ids,
|
||||
array_agg(
|
||||
json_build_object(
|
||||
'skillId', se.skill_id,
|
||||
'level', se.level,
|
||||
'canMentor', se.can_mentor,
|
||||
'wantsToLearn', se.wants_to_learn
|
||||
)
|
||||
) FILTER (WHERE se.skill_id IS NOT NULL) as skill_evaluations
|
||||
FROM category_evaluations ce
|
||||
LEFT JOIN selected_skills ss ON ce.id = ss.category_evaluation_id
|
||||
LEFT JOIN skill_evaluations se ON ce.id = se.category_evaluation_id
|
||||
WHERE ce.user_evaluation_id = $1
|
||||
GROUP BY ce.id, ce.category
|
||||
ORDER BY ce.category
|
||||
`;
|
||||
|
||||
const categoriesResult = await client.query(categoriesQuery, [
|
||||
userEvaluationId,
|
||||
]);
|
||||
|
||||
const evaluations: CategoryEvaluation[] = categoriesResult.rows.map(
|
||||
(row) => ({
|
||||
category: row.category,
|
||||
selectedSkillIds: row.selected_skill_ids || [],
|
||||
skills: (row.skill_evaluations || []).filter(
|
||||
(se: any) => se.skillId !== null
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
profile,
|
||||
evaluations,
|
||||
lastUpdated: userData.last_updated.toISOString(),
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le niveau d'une skill
|
||||
*/
|
||||
async updateSkillLevel(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
level: SkillLevel
|
||||
): Promise<void> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const userId = await this.upsertUser(profile);
|
||||
|
||||
// Obtenir ou créer user_evaluation
|
||||
const userEvalResult = await client.query(
|
||||
`
|
||||
INSERT INTO user_evaluations (user_id, last_updated)
|
||||
VALUES ($1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (user_id)
|
||||
DO UPDATE SET last_updated = CURRENT_TIMESTAMP
|
||||
RETURNING id
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const userEvaluationId = userEvalResult.rows[0].id;
|
||||
|
||||
// Obtenir ou créer category_evaluation
|
||||
const catEvalResult = await client.query(
|
||||
`
|
||||
INSERT INTO category_evaluations (user_evaluation_id, category)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_evaluation_id, category)
|
||||
DO UPDATE SET category = $2
|
||||
RETURNING id
|
||||
`,
|
||||
[userEvaluationId, category]
|
||||
);
|
||||
|
||||
const categoryEvaluationId = catEvalResult.rows[0].id;
|
||||
|
||||
// Upsert skill evaluation
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO skill_evaluations (category_evaluation_id, skill_id, level)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (category_evaluation_id, skill_id)
|
||||
DO UPDATE SET level = $3, updated_at = CURRENT_TIMESTAMP
|
||||
`,
|
||||
[categoryEvaluationId, skillId, level]
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut de mentorat d'une skill
|
||||
*/
|
||||
async updateSkillMentorStatus(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
canMentor: boolean
|
||||
): Promise<void> {
|
||||
await this.updateSkillProperty(
|
||||
profile,
|
||||
category,
|
||||
skillId,
|
||||
"can_mentor",
|
||||
canMentor
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'apprentissage d'une skill
|
||||
*/
|
||||
async updateSkillLearningStatus(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
wantsToLearn: boolean
|
||||
): Promise<void> {
|
||||
await this.updateSkillProperty(
|
||||
profile,
|
||||
category,
|
||||
skillId,
|
||||
"wants_to_learn",
|
||||
wantsToLearn
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode utilitaire pour mettre à jour une propriété de skill
|
||||
*/
|
||||
private async updateSkillProperty(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string,
|
||||
property: "can_mentor" | "wants_to_learn",
|
||||
value: boolean
|
||||
): Promise<void> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const userId = await this.upsertUser(profile);
|
||||
|
||||
const userEvalResult = await client.query(
|
||||
`
|
||||
INSERT INTO user_evaluations (user_id, last_updated)
|
||||
VALUES ($1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (user_id)
|
||||
DO UPDATE SET last_updated = CURRENT_TIMESTAMP
|
||||
RETURNING id
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const userEvaluationId = userEvalResult.rows[0].id;
|
||||
|
||||
const catEvalResult = await client.query(
|
||||
`
|
||||
INSERT INTO category_evaluations (user_evaluation_id, category)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_evaluation_id, category)
|
||||
DO UPDATE SET category = $2
|
||||
RETURNING id
|
||||
`,
|
||||
[userEvaluationId, category]
|
||||
);
|
||||
|
||||
const categoryEvaluationId = catEvalResult.rows[0].id;
|
||||
|
||||
// Update the specific property
|
||||
const updateQuery = `
|
||||
UPDATE skill_evaluations
|
||||
SET ${property} = $3, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE category_evaluation_id = $1 AND skill_id = $2
|
||||
`;
|
||||
|
||||
await client.query(updateQuery, [categoryEvaluationId, skillId, value]);
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une skill à l'évaluation
|
||||
*/
|
||||
async addSkillToEvaluation(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string
|
||||
): Promise<void> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const userId = await this.upsertUser(profile);
|
||||
|
||||
const userEvalResult = await client.query(
|
||||
`
|
||||
INSERT INTO user_evaluations (user_id, last_updated)
|
||||
VALUES ($1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (user_id)
|
||||
DO UPDATE SET last_updated = CURRENT_TIMESTAMP
|
||||
RETURNING id
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const userEvaluationId = userEvalResult.rows[0].id;
|
||||
|
||||
const catEvalResult = await client.query(
|
||||
`
|
||||
INSERT INTO category_evaluations (user_evaluation_id, category)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_evaluation_id, category)
|
||||
DO UPDATE SET category = $2
|
||||
RETURNING id
|
||||
`,
|
||||
[userEvaluationId, category]
|
||||
);
|
||||
|
||||
const categoryEvaluationId = catEvalResult.rows[0].id;
|
||||
|
||||
// Ajouter à selected_skills
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO selected_skills (category_evaluation_id, skill_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`,
|
||||
[categoryEvaluationId, skillId]
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une skill de l'évaluation
|
||||
*/
|
||||
async removeSkillFromEvaluation(
|
||||
profile: UserProfile,
|
||||
category: string,
|
||||
skillId: string
|
||||
): Promise<void> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Trouver les IDs nécessaires
|
||||
const findQuery = `
|
||||
SELECT ce.id as category_evaluation_id
|
||||
FROM users u
|
||||
JOIN user_evaluations ue ON u.id = ue.user_id
|
||||
JOIN category_evaluations ce ON ue.id = ce.user_evaluation_id
|
||||
WHERE u.first_name = $1 AND u.last_name = $2 AND u.team_id = $3 AND ce.category = $4
|
||||
`;
|
||||
|
||||
const result = await client.query(findQuery, [
|
||||
profile.firstName,
|
||||
profile.lastName,
|
||||
profile.teamId,
|
||||
category,
|
||||
]);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
const categoryEvaluationId = result.rows[0].category_evaluation_id;
|
||||
|
||||
// Supprimer de selected_skills et skill_evaluations
|
||||
await client.query(
|
||||
"DELETE FROM selected_skills WHERE category_evaluation_id = $1 AND skill_id = $2",
|
||||
[categoryEvaluationId, skillId]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
"DELETE FROM skill_evaluations WHERE category_evaluation_id = $1 AND skill_id = $2",
|
||||
[categoryEvaluationId, skillId]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const evaluationService = new EvaluationService();
|
||||
12
services/index.ts
Normal file
12
services/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Server-side services (Node.js only)
|
||||
// Note: These exports are only for server-side usage (API routes)
|
||||
// Don't import from this index in client-side code to avoid bundling server dependencies
|
||||
|
||||
// Database services (server-only)
|
||||
export { getPool, closePool } from "./database";
|
||||
|
||||
// Evaluation services (server-only)
|
||||
export { EvaluationService, evaluationService } from "./evaluation-service";
|
||||
|
||||
// API client (can be used client-side)
|
||||
export { ApiClient, apiClient } from "./api-client";
|
||||
Reference in New Issue
Block a user