diff --git a/app/api/teams/[teamId]/route.ts b/app/api/teams/[teamId]/route.ts new file mode 100644 index 0000000..becaea3 --- /dev/null +++ b/app/api/teams/[teamId]/route.ts @@ -0,0 +1,62 @@ +import { NextResponse } from "next/server"; +import { TeamsService } from "@/services"; +import { Team } from "@/lib/types"; + +interface RouteParams { + params: { + teamId: string; + }; +} + +export async function GET(request: Request, { params }: RouteParams) { + try { + const team = await TeamsService.getTeamById(params.teamId); + + if (!team) { + return NextResponse.json({ error: "Team not found" }, { status: 404 }); + } + + return NextResponse.json(team); + } catch (error) { + console.error("Error loading team:", error); + return NextResponse.json({ error: "Failed to load team" }, { status: 500 }); + } +} + +export async function PUT(request: Request, { params }: RouteParams) { + try { + const updates: Partial> = await request.json(); + + const team = await TeamsService.updateTeam(params.teamId, updates); + + if (!team) { + return NextResponse.json({ error: "Team not found" }, { status: 404 }); + } + + return NextResponse.json(team); + } catch (error) { + console.error("Error updating team:", error); + return NextResponse.json( + { error: "Failed to update team" }, + { status: 500 } + ); + } +} + +export async function DELETE(request: Request, { params }: RouteParams) { + try { + const deleted = await TeamsService.deleteTeam(params.teamId); + + if (!deleted) { + return NextResponse.json({ error: "Team not found" }, { status: 404 }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error deleting team:", error); + return NextResponse.json( + { error: "Failed to delete team" }, + { status: 500 } + ); + } +} diff --git a/app/api/teams/direction/[direction]/route.ts b/app/api/teams/direction/[direction]/route.ts new file mode 100644 index 0000000..05b5344 --- /dev/null +++ b/app/api/teams/direction/[direction]/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { TeamsService } from "@/services"; + +interface RouteParams { + params: { + direction: string; + }; +} + +export async function GET(request: Request, { params }: RouteParams) { + try { + const teams = await TeamsService.getTeamsByDirection(params.direction); + return NextResponse.json(teams); + } catch (error) { + console.error("Error loading teams by direction:", error); + return NextResponse.json( + { error: "Failed to load teams by direction" }, + { status: 500 } + ); + } +} diff --git a/app/api/teams/directions/route.ts b/app/api/teams/directions/route.ts new file mode 100644 index 0000000..e8bff71 --- /dev/null +++ b/app/api/teams/directions/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { TeamsService } from "@/services"; + +export async function GET() { + try { + const directions = await TeamsService.getDirections(); + return NextResponse.json(directions); + } catch (error) { + console.error("Error loading directions:", error); + return NextResponse.json( + { error: "Failed to load directions" }, + { status: 500 } + ); + } +} diff --git a/app/api/teams/route.ts b/app/api/teams/route.ts index f4a0730..4b95c69 100644 --- a/app/api/teams/route.ts +++ b/app/api/teams/route.ts @@ -1,20 +1,11 @@ import { NextResponse } from "next/server"; -import fs from "fs"; -import path from "path"; +import { TeamsService } from "@/services"; import { Team } from "@/lib/types"; export async function GET() { try { - const filePath = path.join(process.cwd(), "data", "teams.json"); - - if (!fs.existsSync(filePath)) { - return NextResponse.json({ teams: [] }); - } - - const fileContent = fs.readFileSync(filePath, "utf-8"); - const data = JSON.parse(fileContent); - - return NextResponse.json(data.teams as Team[]); + const teams = await TeamsService.getTeams(); + return NextResponse.json(teams); } catch (error) { console.error("Error loading teams:", error); return NextResponse.json( @@ -23,3 +14,27 @@ export async function GET() { ); } } + +export async function POST(request: Request) { + try { + const teamData: Omit = + await request.json(); + + // Validate required fields + if (!teamData.id || !teamData.name || !teamData.direction) { + return NextResponse.json( + { error: "Missing required fields: id, name, direction" }, + { status: 400 } + ); + } + + const team = await TeamsService.createTeam(teamData); + return NextResponse.json(team, { status: 201 }); + } catch (error) { + console.error("Error creating team:", error); + return NextResponse.json( + { error: "Failed to create team" }, + { status: 500 } + ); + } +} diff --git a/scripts/init.sql b/scripts/init.sql index 3ef00ac..eae442b 100644 --- a/scripts/init.sql +++ b/scripts/init.sql @@ -1,12 +1,21 @@ -- Create enum for skill levels CREATE TYPE skill_level_enum AS ENUM ('never', 'not-autonomous', 'autonomous', 'expert'); +-- Teams table +CREATE TABLE teams ( + id VARCHAR(50) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + direction VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + -- 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, + team_id VARCHAR(50) REFERENCES teams(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -51,7 +60,19 @@ CREATE TABLE skill_evaluations ( UNIQUE(category_evaluation_id, skill_id) ); +-- Insert initial teams data +INSERT INTO teams (id, name, direction) VALUES + ('frontend', 'Frontend', 'Engineering'), + ('backend', 'Backend', 'Engineering'), + ('devops', 'DevOps', 'Engineering'), + ('mobile', 'Mobile', 'Engineering'), + ('data', 'Data Science', 'Engineering'), + ('product', 'Product', 'Product'), + ('design', 'Design', 'Product'), + ('marketing', 'Marketing', 'Business'); + -- Indexes for performance +CREATE INDEX idx_teams_direction ON teams(direction); 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); @@ -68,6 +89,9 @@ BEGIN END; $$ language 'plpgsql'; +CREATE TRIGGER update_teams_updated_at BEFORE UPDATE ON teams + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/services/api-client.ts b/services/api-client.ts index e1ed1ed..bf88ac1 100644 --- a/services/api-client.ts +++ b/services/api-client.ts @@ -1,4 +1,4 @@ -import { UserEvaluation, UserProfile, SkillLevel } from "../lib/types"; +import { UserEvaluation, UserProfile, SkillLevel, Team } from "../lib/types"; export class ApiClient { private baseUrl: string; @@ -168,6 +168,159 @@ export class ApiClient { throw error; } } + + /** + * Charge toutes les équipes + */ + async loadTeams(): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/teams`); + + if (!response.ok) { + throw new Error("Erreur lors du chargement des équipes"); + } + + return await response.json(); + } catch (error) { + console.error("Erreur lors du chargement des équipes:", error); + return []; + } + } + + /** + * Charge une équipe par ID + */ + async loadTeamById(teamId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/teams/${teamId}`); + + if (!response.ok) { + if (response.status === 404) { + return null; + } + throw new Error("Erreur lors du chargement de l'équipe"); + } + + return await response.json(); + } catch (error) { + console.error("Erreur lors du chargement de l'équipe:", error); + return null; + } + } + + /** + * Charge les équipes par direction + */ + async loadTeamsByDirection(direction: string): Promise { + try { + const response = await fetch( + `${this.baseUrl}/api/teams/direction/${direction}` + ); + + if (!response.ok) { + throw new Error("Erreur lors du chargement des équipes par direction"); + } + + return await response.json(); + } catch (error) { + console.error( + "Erreur lors du chargement des équipes par direction:", + error + ); + return []; + } + } + + /** + * Charge toutes les directions + */ + async loadDirections(): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/teams/directions`); + + if (!response.ok) { + throw new Error("Erreur lors du chargement des directions"); + } + + return await response.json(); + } catch (error) { + console.error("Erreur lors du chargement des directions:", error); + return []; + } + } + + /** + * Crée une nouvelle équipe + */ + async createTeam( + team: Omit + ): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/teams`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(team), + }); + + if (!response.ok) { + throw new Error("Erreur lors de la création de l'équipe"); + } + + return await response.json(); + } catch (error) { + console.error("Erreur lors de la création de l'équipe:", error); + return null; + } + } + + /** + * Met à jour une équipe + */ + async updateTeam( + teamId: string, + updates: Partial> + ): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/teams/${teamId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updates), + }); + + if (!response.ok) { + throw new Error("Erreur lors de la mise à jour de l'équipe"); + } + + return await response.json(); + } catch (error) { + console.error("Erreur lors de la mise à jour de l'équipe:", error); + return null; + } + } + + /** + * Supprime une équipe + */ + async deleteTeam(teamId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/teams/${teamId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Erreur lors de la suppression de l'équipe"); + } + + return true; + } catch (error) { + console.error("Erreur lors de la suppression de l'équipe:", error); + return false; + } + } } // Instance singleton diff --git a/services/index.ts b/services/index.ts index 77d115d..cbdb761 100644 --- a/services/index.ts +++ b/services/index.ts @@ -8,5 +8,8 @@ export { getPool, closePool } from "./database"; // Evaluation services (server-only) export { EvaluationService, evaluationService } from "./evaluation-service"; +// Teams services (server-only) +export { TeamsService } from "./teams-service"; + // API client (can be used client-side) export { ApiClient, apiClient } from "./api-client"; diff --git a/services/teams-service.ts b/services/teams-service.ts new file mode 100644 index 0000000..5ca0740 --- /dev/null +++ b/services/teams-service.ts @@ -0,0 +1,171 @@ +import { getPool } from "./database"; +import { Team } from "@/lib/types"; + +export class TeamsService { + /** + * Get all teams + */ + static async getTeams(): Promise { + const pool = getPool(); + const query = ` + SELECT id, name, direction + FROM teams + ORDER BY direction, name + `; + + try { + const result = await pool.query(query); + return result.rows; + } catch (error) { + console.error("Error fetching teams:", error); + throw new Error("Failed to fetch teams"); + } + } + + /** + * Get teams by direction + */ + static async getTeamsByDirection(direction: string): Promise { + const pool = getPool(); + const query = ` + SELECT id, name, direction + FROM teams + WHERE direction = $1 + ORDER BY name + `; + + try { + const result = await pool.query(query, [direction]); + return result.rows; + } catch (error) { + console.error("Error fetching teams by direction:", error); + throw new Error("Failed to fetch teams by direction"); + } + } + + /** + * Get team by ID + */ + static async getTeamById(id: string): Promise { + const pool = getPool(); + const query = ` + SELECT id, name, direction + FROM teams + WHERE id = $1 + `; + + try { + const result = await pool.query(query, [id]); + return result.rows[0] || null; + } catch (error) { + console.error("Error fetching team by ID:", error); + throw new Error("Failed to fetch team"); + } + } + + /** + * Create a new team + */ + static async createTeam( + team: Omit + ): Promise { + const pool = getPool(); + const query = ` + INSERT INTO teams (id, name, direction) + VALUES ($1, $2, $3) + RETURNING id, name, direction + `; + + try { + const result = await pool.query(query, [ + team.id, + team.name, + team.direction, + ]); + return result.rows[0]; + } catch (error) { + console.error("Error creating team:", error); + throw new Error("Failed to create team"); + } + } + + /** + * Update a team + */ + static async updateTeam( + id: string, + updates: Partial> + ): Promise { + const pool = getPool(); + + const fields = []; + const values = []; + let paramIndex = 1; + + if (updates.name !== undefined) { + fields.push(`name = $${paramIndex++}`); + values.push(updates.name); + } + + if (updates.direction !== undefined) { + fields.push(`direction = $${paramIndex++}`); + values.push(updates.direction); + } + + if (fields.length === 0) { + return this.getTeamById(id); + } + + values.push(id); + const query = ` + UPDATE teams + SET ${fields.join(", ")} + WHERE id = $${paramIndex} + RETURNING id, name, direction + `; + + try { + const result = await pool.query(query, values); + return result.rows[0] || null; + } catch (error) { + console.error("Error updating team:", error); + throw new Error("Failed to update team"); + } + } + + /** + * Delete a team + */ + static async deleteTeam(id: string): Promise { + const pool = getPool(); + const query = `DELETE FROM teams WHERE id = $1`; + + try { + const result = await pool.query(query, [id]); + return (result.rowCount ?? 0) > 0; + } catch (error) { + console.error("Error deleting team:", error); + throw new Error("Failed to delete team"); + } + } + + /** + * Get all directions + */ + static async getDirections(): Promise { + const pool = getPool(); + const query = ` + SELECT DISTINCT direction + FROM teams + ORDER BY direction + `; + + try { + const result = await pool.query(query); + return result.rows.map((row) => row.direction); + } catch (error) { + console.error("Error fetching directions:", error); + throw new Error("Failed to fetch directions"); + } + } +}