SuppressionService + DELETE /dossiers/:numero, cascades DELETE /users et DELETE /enfants?deleteDossier, flag sans_enfant, specs front #160. Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
8.5 KiB
TypeScript
269 lines
8.5 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
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 { SuppressionService } from '../suppressions/suppression.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';
|
|
|
|
/**
|
|
* Dossiers unifiés — détail (#119) + liste (#153) + sans_enfant (#159).
|
|
*/
|
|
@Injectable()
|
|
export class DossiersService {
|
|
constructor(
|
|
@InjectRepository(Parents)
|
|
private readonly parentsRepository: Repository<Parents>,
|
|
@InjectRepository(AssistanteMaternelle)
|
|
private readonly amRepository: Repository<AssistanteMaternelle>,
|
|
private readonly parentsService: ParentsService,
|
|
private readonly suppressionService: SuppressionService,
|
|
) {}
|
|
|
|
/**
|
|
* 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');
|
|
});
|
|
|
|
for (const item of filtered) {
|
|
if (item.type === 'famille') {
|
|
const n = await this.suppressionService.countEnfantsForNumero(
|
|
item.numero_dossier,
|
|
);
|
|
item.sans_enfant = n === 0;
|
|
} else {
|
|
item.sans_enfant = false;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
throw new NotFoundException('Numéro de dossier requis.');
|
|
}
|
|
|
|
// 1) Famille : un parent a ce numéro ?
|
|
const parentWithNum = await this.parentsRepository.findOne({
|
|
where: { numero_dossier: num },
|
|
select: ['user_id'],
|
|
});
|
|
if (parentWithNum) {
|
|
const dossier = await this.parentsService.getDossierFamilleByNumero(num);
|
|
return { type: 'family', dossier };
|
|
}
|
|
|
|
// 2) AM : une assistante maternelle a ce numéro ?
|
|
const am = await this.amRepository.findOne({
|
|
where: { numero_dossier: num },
|
|
relations: ['user'],
|
|
});
|
|
if (am?.user) {
|
|
const dossier: DossierAmCompletDto = {
|
|
numero_dossier: num,
|
|
user: this.toDossierAmUserDto(am.user),
|
|
numero_agrement: am.approval_number,
|
|
nir: am.nir,
|
|
biographie: am.biography,
|
|
disponible: am.available,
|
|
ville_residence: am.residence_city,
|
|
date_agrement: am.agreement_date,
|
|
annees_experience: am.years_experience,
|
|
specialite: am.specialty,
|
|
nb_max_enfants: am.max_children,
|
|
place_disponible: am.places_available,
|
|
};
|
|
return { type: 'am', dossier };
|
|
}
|
|
|
|
throw new NotFoundException('Aucun dossier trouvé pour ce numéro.');
|
|
}
|
|
|
|
private toDossierAmUserDto(user: {
|
|
id: string;
|
|
email: string;
|
|
prenom?: string;
|
|
nom?: string;
|
|
telephone?: string;
|
|
adresse?: string;
|
|
ville?: string;
|
|
code_postal?: string;
|
|
profession?: string;
|
|
date_naissance?: Date;
|
|
lieu_naissance_ville?: string;
|
|
lieu_naissance_pays?: string;
|
|
photo_url?: string;
|
|
consentement_photo?: boolean;
|
|
statut: any;
|
|
}): DossierAmUserDto {
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
prenom: user.prenom,
|
|
nom: user.nom,
|
|
telephone: user.telephone,
|
|
adresse: user.adresse,
|
|
ville: user.ville,
|
|
code_postal: user.code_postal,
|
|
profession: user.profession,
|
|
date_naissance: user.date_naissance,
|
|
lieu_naissance_ville: user.lieu_naissance_ville,
|
|
lieu_naissance_pays: user.lieu_naissance_pays,
|
|
photo_url: user.photo_url,
|
|
consentement_photo: user.consentement_photo,
|
|
statut: user.statut,
|
|
};
|
|
}
|
|
}
|