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,
|
saveUserEvaluation,
|
||||||
createEmptyEvaluation,
|
createEmptyEvaluation,
|
||||||
} from "@/lib/evaluation-utils";
|
} from "@/lib/evaluation-utils";
|
||||||
|
import { apiClient } from "@/services/api-client";
|
||||||
import { loadSkillCategories, loadTeams } from "@/lib/data-loader";
|
import { loadSkillCategories, loadTeams } from "@/lib/data-loader";
|
||||||
|
|
||||||
// Fonction pour migrer une évaluation existante avec de nouvelles catégories
|
// Fonction pour migrer une évaluation existante avec de nouvelles catégories
|
||||||
@@ -70,13 +71,25 @@ export function useEvaluation() {
|
|||||||
setSkillCategories(categories);
|
setSkillCategories(categories);
|
||||||
setTeams(teamsData);
|
setTeams(teamsData);
|
||||||
|
|
||||||
// Try to load existing evaluation
|
// Try to load user profile from localStorage and then load evaluation from API
|
||||||
const saved = loadUserEvaluation();
|
try {
|
||||||
if (saved) {
|
const savedProfile = localStorage.getItem("peakSkills_userProfile");
|
||||||
// Migrate evaluation to include new categories if needed
|
if (savedProfile) {
|
||||||
const migratedEvaluation = migrateEvaluation(saved, categories);
|
const profile: UserProfile = JSON.parse(savedProfile);
|
||||||
setUserEvaluation(migratedEvaluation);
|
const saved = await loadUserEvaluation(profile);
|
||||||
saveUserEvaluation(migratedEvaluation); // Save the migrated version
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize data:", error);
|
console.error("Failed to initialize data:", error);
|
||||||
@@ -88,7 +101,35 @@ export function useEvaluation() {
|
|||||||
initializeData();
|
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 =
|
const evaluations =
|
||||||
userEvaluation?.evaluations || createEmptyEvaluation(skillCategories);
|
userEvaluation?.evaluations || createEmptyEvaluation(skillCategories);
|
||||||
const newEvaluation: UserEvaluation = {
|
const newEvaluation: UserEvaluation = {
|
||||||
@@ -97,159 +138,214 @@ export function useEvaluation() {
|
|||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save profile to localStorage for auto-login
|
||||||
|
localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile));
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
saveUserEvaluation(newEvaluation);
|
await saveUserEvaluation(newEvaluation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSkillLevel = (
|
const updateSkillLevel = async (
|
||||||
category: string,
|
category: string,
|
||||||
skillId: string,
|
skillId: string,
|
||||||
level: SkillLevel
|
level: SkillLevel
|
||||||
) => {
|
) => {
|
||||||
if (!userEvaluation) return;
|
if (!userEvaluation) return;
|
||||||
|
|
||||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
try {
|
||||||
if (catEval.category === category) {
|
// Optimistic update
|
||||||
const existingSkill = catEval.skills.find((s) => s.skillId === skillId);
|
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||||
const updatedSkills = existingSkill
|
if (catEval.category === category) {
|
||||||
? catEval.skills.map((skill) =>
|
const existingSkill = catEval.skills.find(
|
||||||
skill.skillId === skillId ? { ...skill, level } : skill
|
(s) => s.skillId === skillId
|
||||||
)
|
);
|
||||||
: [...catEval.skills, { skillId, level }];
|
const updatedSkills = existingSkill
|
||||||
|
? catEval.skills.map((skill) =>
|
||||||
|
skill.skillId === skillId ? { ...skill, level } : skill
|
||||||
|
)
|
||||||
|
: [...catEval.skills, { skillId, level }];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...catEval,
|
...catEval,
|
||||||
skills: updatedSkills,
|
skills: updatedSkills,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return catEval;
|
return catEval;
|
||||||
});
|
});
|
||||||
|
|
||||||
const newEvaluation: UserEvaluation = {
|
const newEvaluation: UserEvaluation = {
|
||||||
...userEvaluation,
|
...userEvaluation,
|
||||||
evaluations: updatedEvaluations,
|
evaluations: updatedEvaluations,
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
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,
|
category: string,
|
||||||
skillId: string,
|
skillId: string,
|
||||||
canMentor: boolean
|
canMentor: boolean
|
||||||
) => {
|
) => {
|
||||||
if (!userEvaluation) return;
|
if (!userEvaluation) return;
|
||||||
|
|
||||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
try {
|
||||||
if (catEval.category === category) {
|
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||||
const updatedSkills = catEval.skills.map((skill) =>
|
if (catEval.category === category) {
|
||||||
skill.skillId === skillId ? { ...skill, canMentor } : skill
|
const updatedSkills = catEval.skills.map((skill) =>
|
||||||
);
|
skill.skillId === skillId ? { ...skill, canMentor } : skill
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...catEval,
|
...catEval,
|
||||||
skills: updatedSkills,
|
skills: updatedSkills,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return catEval;
|
return catEval;
|
||||||
});
|
});
|
||||||
|
|
||||||
const newEvaluation: UserEvaluation = {
|
const newEvaluation: UserEvaluation = {
|
||||||
...userEvaluation,
|
...userEvaluation,
|
||||||
evaluations: updatedEvaluations,
|
evaluations: updatedEvaluations,
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
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,
|
category: string,
|
||||||
skillId: string,
|
skillId: string,
|
||||||
wantsToLearn: boolean
|
wantsToLearn: boolean
|
||||||
) => {
|
) => {
|
||||||
if (!userEvaluation) return;
|
if (!userEvaluation) return;
|
||||||
|
|
||||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
try {
|
||||||
if (catEval.category === category) {
|
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||||
const updatedSkills = catEval.skills.map((skill) =>
|
if (catEval.category === category) {
|
||||||
skill.skillId === skillId ? { ...skill, wantsToLearn } : skill
|
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 {
|
return {
|
||||||
...catEval,
|
...catEval,
|
||||||
selectedSkillIds: [...catEval.selectedSkillIds, skillId],
|
skills: updatedSkills,
|
||||||
skills: [...catEval.skills, { skillId, level: null }],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
return catEval;
|
||||||
return catEval;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const newEvaluation: UserEvaluation = {
|
const newEvaluation: UserEvaluation = {
|
||||||
...userEvaluation,
|
...userEvaluation,
|
||||||
evaluations: updatedEvaluations,
|
evaluations: updatedEvaluations,
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
saveUserEvaluation(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;
|
if (!userEvaluation) return;
|
||||||
|
|
||||||
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
try {
|
||||||
if (catEval.category === category) {
|
const updatedEvaluations = userEvaluation.evaluations.map((catEval) => {
|
||||||
return {
|
if (catEval.category === category) {
|
||||||
...catEval,
|
if (!catEval.selectedSkillIds.includes(skillId)) {
|
||||||
selectedSkillIds: catEval.selectedSkillIds.filter(
|
return {
|
||||||
(id) => id !== skillId
|
...catEval,
|
||||||
),
|
selectedSkillIds: [...catEval.selectedSkillIds, skillId],
|
||||||
skills: catEval.skills.filter((skill) => skill.skillId !== skillId),
|
skills: [...catEval.skills, { skillId, level: null }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return catEval;
|
}
|
||||||
});
|
return catEval;
|
||||||
|
});
|
||||||
|
|
||||||
const newEvaluation: UserEvaluation = {
|
const newEvaluation: UserEvaluation = {
|
||||||
...userEvaluation,
|
...userEvaluation,
|
||||||
evaluations: updatedEvaluations,
|
evaluations: updatedEvaluations,
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
saveUserEvaluation(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 evaluations = createEmptyEvaluation(skillCategories);
|
||||||
const newEvaluation: UserEvaluation = {
|
const newEvaluation: UserEvaluation = {
|
||||||
profile,
|
profile,
|
||||||
@@ -257,8 +353,16 @@ export function useEvaluation() {
|
|||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save profile to localStorage for auto-login
|
||||||
|
localStorage.setItem("peakSkills_userProfile", JSON.stringify(profile));
|
||||||
|
|
||||||
setUserEvaluation(newEvaluation);
|
setUserEvaluation(newEvaluation);
|
||||||
saveUserEvaluation(newEvaluation);
|
await saveUserEvaluation(newEvaluation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearUserProfile = () => {
|
||||||
|
localStorage.removeItem("peakSkills_userProfile");
|
||||||
|
setUserEvaluation(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -266,6 +370,7 @@ export function useEvaluation() {
|
|||||||
skillCategories,
|
skillCategories,
|
||||||
teams,
|
teams,
|
||||||
loading,
|
loading,
|
||||||
|
loadEvaluationForProfile,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
updateSkillLevel,
|
updateSkillLevel,
|
||||||
updateSkillMentorStatus,
|
updateSkillMentorStatus,
|
||||||
@@ -273,5 +378,6 @@ export function useEvaluation() {
|
|||||||
addSkillToEvaluation,
|
addSkillToEvaluation,
|
||||||
removeSkillFromEvaluation,
|
removeSkillFromEvaluation,
|
||||||
initializeEmptyEvaluation,
|
initializeEmptyEvaluation,
|
||||||
|
clearUserProfile,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
UserEvaluation,
|
UserEvaluation,
|
||||||
SkillCategory,
|
SkillCategory,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { apiClient } from "../services/api-client";
|
||||||
|
|
||||||
export function calculateCategoryScore(
|
export function calculateCategoryScore(
|
||||||
categoryEvaluation: CategoryEvaluation
|
categoryEvaluation: CategoryEvaluation
|
||||||
@@ -44,21 +45,22 @@ export function generateRadarData(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveUserEvaluation(evaluation: UserEvaluation): void {
|
export async function saveUserEvaluation(
|
||||||
|
evaluation: UserEvaluation
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
await apiClient.saveUserEvaluation(evaluation);
|
||||||
"peakSkills_userEvaluation",
|
|
||||||
JSON.stringify(evaluation)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save user evaluation:", 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 {
|
try {
|
||||||
const saved = localStorage.getItem("peakSkills_userEvaluation");
|
return await apiClient.loadUserEvaluation(profile);
|
||||||
return saved ? JSON.parse(saved) : null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load user evaluation:", error);
|
console.error("Failed to load user evaluation:", error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -62,7 +62,9 @@
|
|||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.9",
|
"vaul": "^0.9.9",
|
||||||
"zod": "3.25.67"
|
"zod": "3.25.67",
|
||||||
|
"pg": "^8.12.0",
|
||||||
|
"@types/pg": "^8.11.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
|
|||||||
122
pnpm-lock.yaml
generated
122
pnpm-lock.yaml
generated
@@ -104,6 +104,9 @@ importers:
|
|||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: 1.1.6
|
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)
|
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:
|
autoprefixer:
|
||||||
specifier: ^10.4.20
|
specifier: ^10.4.20
|
||||||
version: 10.4.21(postcss@8.5.6)
|
version: 10.4.21(postcss@8.5.6)
|
||||||
@@ -137,6 +140,9 @@ importers:
|
|||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
pg:
|
||||||
|
specifier: ^8.12.0
|
||||||
|
version: 8.16.3
|
||||||
react:
|
react:
|
||||||
specifier: ^19
|
specifier: ^19
|
||||||
version: 19.1.1
|
version: 19.1.1
|
||||||
@@ -1210,6 +1216,9 @@ packages:
|
|||||||
'@types/node@22.17.2':
|
'@types/node@22.17.2':
|
||||||
resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
|
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':
|
'@types/react-dom@19.1.7':
|
||||||
resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
|
resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1542,6 +1551,40 @@ packages:
|
|||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -1556,6 +1599,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
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:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
@@ -1669,6 +1728,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
split2@4.2.0:
|
||||||
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
streamsearch@1.1.0:
|
streamsearch@1.1.0:
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -1762,6 +1825,10 @@ packages:
|
|||||||
victory-vendor@36.9.2:
|
victory-vendor@36.9.2:
|
||||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
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:
|
yallist@5.0.0:
|
||||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2745,6 +2812,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
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)':
|
'@types/react-dom@19.1.7(@types/react@19.1.10)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.1.10
|
'@types/react': 19.1.10
|
||||||
@@ -3031,6 +3104,41 @@ snapshots:
|
|||||||
|
|
||||||
object-assign@4.1.1: {}
|
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: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
postcss-value-parser@4.2.0: {}
|
postcss-value-parser@4.2.0: {}
|
||||||
@@ -3047,6 +3155,16 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.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:
|
prop-types@15.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@@ -3185,6 +3303,8 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
split2@4.2.0: {}
|
||||||
|
|
||||||
streamsearch@1.1.0: {}
|
streamsearch@1.1.0: {}
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.1.1):
|
styled-jsx@5.1.6(react@19.1.1):
|
||||||
@@ -3272,6 +3392,8 @@ snapshots:
|
|||||||
d3-time: 3.1.0
|
d3-time: 3.1.0
|
||||||
d3-timer: 3.0.1
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
zod@3.25.67: {}
|
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