mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r1.1): socle backend du référentiel — modèle, contrat, API, seed, tests
- migration r1_referentiel : Category (EQUIPMENT/COMPONENT_TYPE), Location (site → zone, lat/lng + colonne PostGIS générée geography(Point,4326) + index GIST), Asset (statut d'équipement), AssetComponent (organe sans emplacement PAR CONSTRUCTION), Team, invitation sur User ; migration autosuffisante (CREATE EXTENSION IF NOT EXISTS postgis) - contrat : 21 nouvelles opérations (26 total), générateur OpenAPI étendu aux paramètres de chemin ; spec + client web régénérés dans ce commit - API : modules categories/locations/assets/teams + gestion des personnes (liste, rôles, invitation lien 7 j à usage unique, activation publique qui connecte directement, mise à jour rôle/équipes) — tout sous @RequirePermission ; invariants en service (profondeur 2, kinds, catégorie jamais supprimée) - seed : parc de la maquette validée (5 sites + 8 zones, 8 appareils, organes A1/B2, 9 catégories, 2 équipes) — idempotent - 36 tests verts (couverture 96 % stmts / 85 % branches) : recette site→zone→appareil→organes, matrice vivante, invitation→activation ; smoke test sur build de prod - CI : postgres → postgis/postgis:18-3.6 (la migration R1 l'exige) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
92
apps/api/src/teams/teams.service.ts
Normal file
92
apps/api/src/teams/teams.service.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { Team, TeamCreate, TeamsResponse, TeamUpdate } from '@siop/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
const teamInclude = {
|
||||
members: { include: { role: true }, orderBy: { displayName: 'asc' } },
|
||||
} satisfies Prisma.TeamInclude;
|
||||
|
||||
type TeamRow = Prisma.TeamGetPayload<{ include: typeof teamInclude }>;
|
||||
|
||||
@Injectable()
|
||||
export class TeamsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(): Promise<TeamsResponse> {
|
||||
const rows = await this.prisma.team.findMany({
|
||||
include: teamInclude,
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
return { teams: rows.map((t) => this.toDto(t)) };
|
||||
}
|
||||
|
||||
async create(dto: TeamCreate): Promise<Team> {
|
||||
try {
|
||||
const created = await this.prisma.team.create({
|
||||
data: {
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
members: dto.memberIds?.length
|
||||
? { connect: dto.memberIds.map((id) => ({ id })) }
|
||||
: undefined,
|
||||
},
|
||||
include: teamInclude,
|
||||
});
|
||||
return this.toDto(created);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new ConflictException('Ce nom d’équipe existe déjà');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, dto: TeamUpdate): Promise<Team> {
|
||||
try {
|
||||
const updated = await this.prisma.team.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
members: dto.memberIds
|
||||
? { set: dto.memberIds.map((memberId) => ({ id: memberId })) }
|
||||
: undefined,
|
||||
},
|
||||
include: teamInclude,
|
||||
});
|
||||
return this.toDto(updated);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') {
|
||||
throw new NotFoundException('Équipe inconnue');
|
||||
}
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new ConflictException('Ce nom d’équipe existe déjà');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private toDto(row: TeamRow): Team {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
members: row.members.map((m) => ({
|
||||
id: m.id,
|
||||
displayName: m.displayName,
|
||||
roleName: m.role.name,
|
||||
initials: m.displayName
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0]!.toUpperCase())
|
||||
.join(''),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user