feat: adding teams in PG

This commit is contained in:
Julien Froidefond
2025-08-21 09:44:29 +02:00
parent 4e82a6d860
commit 345ff5baa0
8 changed files with 478 additions and 14 deletions

View File

@@ -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<Omit<Team, "id">> = 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 }
);
}
}

View File

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

View File

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

View File

@@ -1,20 +1,11 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import fs from "fs"; import { TeamsService } from "@/services";
import path from "path";
import { Team } from "@/lib/types"; import { Team } from "@/lib/types";
export async function GET() { export async function GET() {
try { try {
const filePath = path.join(process.cwd(), "data", "teams.json"); const teams = await TeamsService.getTeams();
return NextResponse.json(teams);
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[]);
} catch (error) { } catch (error) {
console.error("Error loading teams:", error); console.error("Error loading teams:", error);
return NextResponse.json( return NextResponse.json(
@@ -23,3 +14,27 @@ export async function GET() {
); );
} }
} }
export async function POST(request: Request) {
try {
const teamData: Omit<Team, "created_at" | "updated_at"> =
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 }
);
}
}

View File

@@ -1,12 +1,21 @@
-- Create enum for skill levels -- Create enum for skill levels
CREATE TYPE skill_level_enum AS ENUM ('never', 'not-autonomous', 'autonomous', 'expert'); 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 -- Users table
CREATE TABLE users ( CREATE TABLE users (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
first_name VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL,
last_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, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_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) 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 -- Indexes for performance
CREATE INDEX idx_teams_direction ON teams(direction);
CREATE INDEX idx_users_team_id ON users(team_id); 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_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_category_evaluations_user_evaluation_id ON category_evaluations(user_evaluation_id);
@@ -68,6 +89,9 @@ BEGIN
END; END;
$$ language 'plpgsql'; $$ 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 CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@@ -1,4 +1,4 @@
import { UserEvaluation, UserProfile, SkillLevel } from "../lib/types"; import { UserEvaluation, UserProfile, SkillLevel, Team } from "../lib/types";
export class ApiClient { export class ApiClient {
private baseUrl: string; private baseUrl: string;
@@ -168,6 +168,159 @@ export class ApiClient {
throw error; throw error;
} }
} }
/**
* Charge toutes les équipes
*/
async loadTeams(): Promise<Team[]> {
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<Team | null> {
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<Team[]> {
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<string[]> {
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<Team, "created_at" | "updated_at">
): Promise<Team | null> {
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<Omit<Team, "id">>
): Promise<Team | null> {
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<boolean> {
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 // Instance singleton

View File

@@ -8,5 +8,8 @@ export { getPool, closePool } from "./database";
// Evaluation services (server-only) // Evaluation services (server-only)
export { EvaluationService, evaluationService } from "./evaluation-service"; export { EvaluationService, evaluationService } from "./evaluation-service";
// Teams services (server-only)
export { TeamsService } from "./teams-service";
// API client (can be used client-side) // API client (can be used client-side)
export { ApiClient, apiClient } from "./api-client"; export { ApiClient, apiClient } from "./api-client";

171
services/teams-service.ts Normal file
View File

@@ -0,0 +1,171 @@
import { getPool } from "./database";
import { Team } from "@/lib/types";
export class TeamsService {
/**
* Get all teams
*/
static async getTeams(): Promise<Team[]> {
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<Team[]> {
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<Team | null> {
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<Team, "created_at" | "updated_at">
): Promise<Team> {
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<Omit<Team, "id">>
): Promise<Team | null> {
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<boolean> {
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<string[]> {
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");
}
}
}