feat(#153): GET /dossiers — liste unifiée familles + AM.
Endpoint staff pour l’onglet Dossiers : type, n°, libellé, emails, statut, a_valider, filtre q. Complète GET /dossiers/:numero (#119). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,12 +3,14 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Parents } from 'src/entities/parents.entity';
|
||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||
import { StatutUtilisateurType, Users } from 'src/entities/users.entity';
|
||||
import { ParentsService } from '../parents/parents.service';
|
||||
import { DossierUnifieDto } from './dto/dossier-unifie.dto';
|
||||
import { DossierAmCompletDto, DossierAmUserDto } from './dto/dossier-am-complet.dto';
|
||||
import { DossierListItemDto } from './dto/dossier-list-item.dto';
|
||||
|
||||
/**
|
||||
* Endpoint unifié GET /dossiers/:numeroDossier – AM ou famille. Ticket #119.
|
||||
* Dossiers unifiés — détail (#119) + liste (#153).
|
||||
*/
|
||||
@Injectable()
|
||||
export class DossiersService {
|
||||
@@ -20,6 +22,159 @@ export class DossiersService {
|
||||
private readonly parentsService: ParentsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Liste unifiée tous dossiers (familles + AM) ayant un numero_dossier.
|
||||
* Ticket #153 — optionnel `q` filtre n° / nom / prénom / email (côté serveur).
|
||||
*/
|
||||
async listDossiers(q?: string): Promise<DossierListItemDto[]> {
|
||||
const items: DossierListItemDto[] = [
|
||||
...(await this.listFamilleItems()),
|
||||
...(await this.listAmItems()),
|
||||
];
|
||||
|
||||
const needle = (q ?? '').trim().toLowerCase();
|
||||
const filtered = needle
|
||||
? items.filter((item) => this.matchesQuery(item, needle))
|
||||
: items;
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
// À valider d'abord, puis n° dossier décroissant
|
||||
if (a.a_valider !== b.a_valider) return a.a_valider ? -1 : 1;
|
||||
return b.numero_dossier.localeCompare(a.numero_dossier, 'fr');
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private async listFamilleItems(): Promise<DossierListItemDto[]> {
|
||||
const parents = await this.parentsRepository
|
||||
.createQueryBuilder('p')
|
||||
.innerJoinAndSelect('p.user', 'u')
|
||||
.leftJoinAndSelect('p.co_parent', 'cp')
|
||||
.where('p.numero_dossier IS NOT NULL')
|
||||
.andWhere("TRIM(p.numero_dossier) <> ''")
|
||||
.getMany();
|
||||
|
||||
const byNum = new Map<string, Parents[]>();
|
||||
for (const p of parents) {
|
||||
const num = (p.numero_dossier ?? '').trim();
|
||||
if (!num) continue;
|
||||
const group = byNum.get(num) ?? [];
|
||||
group.push(p);
|
||||
byNum.set(num, group);
|
||||
}
|
||||
|
||||
const items: DossierListItemDto[] = [];
|
||||
for (const [numero_dossier, group] of byNum) {
|
||||
const usersMap = new Map<string, Users>();
|
||||
for (const p of group) {
|
||||
if (p.user) usersMap.set(p.user.id, p.user);
|
||||
if (p.co_parent) usersMap.set(p.co_parent.id, p.co_parent);
|
||||
}
|
||||
const users = [...usersMap.values()].sort((a, b) => {
|
||||
const an = `${a.nom ?? ''} ${a.prenom ?? ''}`.toLowerCase();
|
||||
const bn = `${b.nom ?? ''} ${b.prenom ?? ''}`.toLowerCase();
|
||||
return an.localeCompare(bn, 'fr') || a.id.localeCompare(b.id);
|
||||
});
|
||||
if (users.length === 0) continue;
|
||||
|
||||
const names = users.map((u) => this.formatPersonName(u)).filter(Boolean);
|
||||
const libelle =
|
||||
names.length === 0
|
||||
? `Dossier ${numero_dossier}`
|
||||
: names.length === 1
|
||||
? names[0]
|
||||
: names.join(' & ');
|
||||
|
||||
const emails = users.map((u) => u.email).filter(Boolean);
|
||||
const user_ids = users.map((u) => u.id);
|
||||
const a_valider = users.some((u) => u.statut === StatutUtilisateurType.EN_ATTENTE);
|
||||
const statut = a_valider
|
||||
? StatutUtilisateurType.EN_ATTENTE
|
||||
: (users[0].statut ?? StatutUtilisateurType.ACTIF);
|
||||
const date_reference = this.minCreeLeIso(users);
|
||||
|
||||
items.push({
|
||||
type: 'famille',
|
||||
numero_dossier,
|
||||
libelle,
|
||||
emails,
|
||||
user_ids,
|
||||
statut,
|
||||
a_valider,
|
||||
date_reference,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private async listAmItems(): Promise<DossierListItemDto[]> {
|
||||
const ams = await this.amRepository
|
||||
.createQueryBuilder('am')
|
||||
.innerJoinAndSelect('am.user', 'u')
|
||||
.where('am.numero_dossier IS NOT NULL')
|
||||
.andWhere("TRIM(am.numero_dossier) <> ''")
|
||||
.getMany();
|
||||
|
||||
const byNum = new Map<string, AssistanteMaternelle>();
|
||||
for (const am of ams) {
|
||||
const num = (am.numero_dossier ?? '').trim();
|
||||
if (!num || !am.user) continue;
|
||||
// Un n° = une AM ; garder le premier
|
||||
if (!byNum.has(num)) byNum.set(num, am);
|
||||
}
|
||||
|
||||
const items: DossierListItemDto[] = [];
|
||||
for (const [numero_dossier, am] of byNum) {
|
||||
const u = am.user!;
|
||||
const libelle = this.formatPersonName(u) || `AM ${numero_dossier}`;
|
||||
const a_valider = u.statut === StatutUtilisateurType.EN_ATTENTE;
|
||||
items.push({
|
||||
type: 'assistante_maternelle',
|
||||
numero_dossier,
|
||||
libelle,
|
||||
emails: u.email ? [u.email] : [],
|
||||
user_ids: [u.id],
|
||||
statut: u.statut ?? StatutUtilisateurType.ACTIF,
|
||||
a_valider,
|
||||
date_reference: this.minCreeLeIso([u]),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private matchesQuery(item: DossierListItemDto, needle: string): boolean {
|
||||
const hay = [
|
||||
item.numero_dossier,
|
||||
item.libelle,
|
||||
...item.emails,
|
||||
item.statut,
|
||||
item.type,
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
return hay.includes(needle);
|
||||
}
|
||||
|
||||
private formatPersonName(u: Users): string {
|
||||
const prenom = (u.prenom ?? '').trim();
|
||||
const nom = (u.nom ?? '').trim();
|
||||
const nomFmt = nom ? nom.toUpperCase() : '';
|
||||
return [prenom, nomFmt].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
private minCreeLeIso(users: Users[]): string | null {
|
||||
let min: Date | null = null;
|
||||
for (const u of users) {
|
||||
const d = u.cree_le;
|
||||
if (!d) continue;
|
||||
const date = d instanceof Date ? d : new Date(d);
|
||||
if (Number.isNaN(date.getTime())) continue;
|
||||
if (!min || date < min) min = date;
|
||||
}
|
||||
return min ? min.toISOString() : null;
|
||||
}
|
||||
|
||||
async getDossierByNumero(numeroDossier: string): Promise<DossierUnifieDto> {
|
||||
const num = numeroDossier?.trim();
|
||||
if (!num) {
|
||||
|
||||
Reference in New Issue
Block a user