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:
Julien Froidefond
2025-08-21 08:46:52 +02:00
parent 488684fd9b
commit 4e82a6d860
14 changed files with 1467 additions and 125 deletions

105
DATABASE_SETUP.md Normal file
View 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
```

View 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 }
);
}
}

View 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
View 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:

View File

@@ -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();
if (saved) {
// Migrate evaluation to include new categories if needed
const migratedEvaluation = migrateEvaluation(saved, categories);
setUserEvaluation(migratedEvaluation);
saveUserEvaluation(migratedEvaluation); // Save the migrated version
// 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);
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,159 +138,214 @@ 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;
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
const existingSkill = catEval.skills.find((s) => s.skillId === skillId);
const updatedSkills = existingSkill
? catEval.skills.map((skill) =>
skill.skillId === skillId ? { ...skill, level } : skill
)
: [...catEval.skills, { skillId, level }];
try {
// Optimistic update
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
const existingSkill = catEval.skills.find(
(s) => s.skillId === skillId
);
const updatedSkills = existingSkill
? catEval.skills.map((skill) =>
skill.skillId === skillId ? { ...skill, level } : skill
)
: [...catEval.skills, { skillId, level }];
return {
...catEval,
skills: updatedSkills,
};
}
return catEval;
});
return {
...catEval,
skills: updatedSkills,
};
}
return catEval;
});
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
setUserEvaluation(newEvaluation);
saveUserEvaluation(newEvaluation);
setUserEvaluation(newEvaluation);
// Update via API
await apiClient.updateSkillLevel(
userEvaluation.profile,
category,
skillId,
level
);
} catch (error) {
console.error("Failed to update skill level:", error);
// Revert optimistic update if needed
}
};
const updateSkillMentorStatus = (
const updateSkillMentorStatus = async (
category: string,
skillId: string,
canMentor: boolean
) => {
if (!userEvaluation) return;
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
const updatedSkills = catEval.skills.map((skill) =>
skill.skillId === skillId ? { ...skill, canMentor } : skill
);
try {
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
const updatedSkills = catEval.skills.map((skill) =>
skill.skillId === skillId ? { ...skill, canMentor } : skill
);
return {
...catEval,
skills: updatedSkills,
};
}
return catEval;
});
return {
...catEval,
skills: updatedSkills,
};
}
return catEval;
});
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
setUserEvaluation(newEvaluation);
saveUserEvaluation(newEvaluation);
setUserEvaluation(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;
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
const updatedSkills = catEval.skills.map((skill) =>
skill.skillId === skillId ? { ...skill, wantsToLearn } : skill
);
try {
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
const updatedSkills = catEval.skills.map((skill) =>
skill.skillId === skillId ? { ...skill, wantsToLearn } : skill
);
return {
...catEval,
skills: updatedSkills,
};
}
return catEval;
});
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
setUserEvaluation(newEvaluation);
saveUserEvaluation(newEvaluation);
};
const addSkillToEvaluation = (category: string, skillId: string) => {
if (!userEvaluation) return;
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
if (!catEval.selectedSkillIds.includes(skillId)) {
return {
...catEval,
selectedSkillIds: [...catEval.selectedSkillIds, skillId],
skills: [...catEval.skills, { skillId, level: null }],
skills: updatedSkills,
};
}
}
return catEval;
});
return catEval;
});
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
setUserEvaluation(newEvaluation);
saveUserEvaluation(newEvaluation);
setUserEvaluation(newEvaluation);
await apiClient.updateSkillLearningStatus(
userEvaluation.profile,
category,
skillId,
wantsToLearn
);
} catch (error) {
console.error("Failed to update skill learning status:", error);
}
};
const removeSkillFromEvaluation = (category: string, skillId: string) => {
const addSkillToEvaluation = async (category: string, skillId: string) => {
if (!userEvaluation) return;
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
return {
...catEval,
selectedSkillIds: catEval.selectedSkillIds.filter(
(id) => id !== skillId
),
skills: catEval.skills.filter((skill) => skill.skillId !== skillId),
};
}
return catEval;
});
try {
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
if (!catEval.selectedSkillIds.includes(skillId)) {
return {
...catEval,
selectedSkillIds: [...catEval.selectedSkillIds, skillId],
skills: [...catEval.skills, { skillId, level: null }],
};
}
}
return catEval;
});
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
setUserEvaluation(newEvaluation);
saveUserEvaluation(newEvaluation);
setUserEvaluation(newEvaluation);
await apiClient.addSkillToEvaluation(
userEvaluation.profile,
category,
skillId
);
} catch (error) {
console.error("Failed to add skill to evaluation:", error);
}
};
const initializeEmptyEvaluation = (profile: UserProfile) => {
const removeSkillFromEvaluation = async (
category: string,
skillId: string
) => {
if (!userEvaluation) return;
try {
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
if (catEval.category === category) {
return {
...catEval,
selectedSkillIds: catEval.selectedSkillIds.filter(
(id) => id !== skillId
),
skills: catEval.skills.filter((skill) => skill.skillId !== skillId),
};
}
return catEval;
});
const newEvaluation: UserEvaluation = {
...userEvaluation,
evaluations: updatedEvaluations,
lastUpdated: new Date().toISOString(),
};
setUserEvaluation(newEvaluation);
await apiClient.removeSkillFromEvaluation(
userEvaluation.profile,
category,
skillId
);
} catch (error) {
console.error("Failed to remove skill from evaluation:", error);
}
};
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,
};
}

View File

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

View File

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

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

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