Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5a857ae6b |
@@ -5,6 +5,7 @@ import {
|
|||||||
import { Users } from './users.entity';
|
import { Users } from './users.entity';
|
||||||
import { ParentsChildren } from './parents_children.entity';
|
import { ParentsChildren } from './parents_children.entity';
|
||||||
import { Dossier } from './dossiers.entity';
|
import { Dossier } from './dossiers.entity';
|
||||||
|
import { AmChildren } from './am_children.entity';
|
||||||
|
|
||||||
@Entity('parents', { schema: 'public' })
|
@Entity('parents', { schema: 'public' })
|
||||||
export class Parents {
|
export class Parents {
|
||||||
@@ -25,6 +26,17 @@ export class Parents {
|
|||||||
@Column({ name: 'numero_dossier', length: 20, nullable: true })
|
@Column({ name: 'numero_dossier', length: 20, nullable: true })
|
||||||
numero_dossier?: string;
|
numero_dossier?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placement AM↔enfant sélectionné sur le TdB parent (couple actif) — ticket #168.
|
||||||
|
* Null = le front prend le premier couple actif retourné par l’API.
|
||||||
|
*/
|
||||||
|
@Column({ name: 'id_placement_garde_courant', type: 'uuid', nullable: true })
|
||||||
|
id_placement_garde_courant?: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => AmChildren, { nullable: true, onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'id_placement_garde_courant', referencedColumnName: 'id' })
|
||||||
|
placement_garde_courant?: AmChildren | null;
|
||||||
|
|
||||||
// Lien vers enfants via la table enfants_parents
|
// Lien vers enfants via la table enfants_parents
|
||||||
@OneToMany(() => ParentsChildren, pc => pc.parent)
|
@OneToMany(() => ParentsChildren, pc => pc.parent)
|
||||||
parentChildren: ParentsChildren[];
|
parentChildren: ParentsChildren[];
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/** Identité minimale enfant pour le bandeau couple — ticket #168 */
|
||||||
|
export class CoupleGardeEnfantDto {
|
||||||
|
@ApiProperty({ format: 'uuid' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
prenom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
nom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
photo_url?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Identité minimale AM pour le bandeau couple — ticket #168 */
|
||||||
|
export class CoupleGardeAmDto {
|
||||||
|
@ApiProperty({ format: 'uuid', description: 'UUID utilisateur de l’AM' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
prenom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
nom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
photo_url?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Un couple de garde = placement actif enfant ↔ AM */
|
||||||
|
export class CoupleGardeDto {
|
||||||
|
@ApiProperty({
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Id du placement (enfants_assistantes_maternelles.id)',
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: CoupleGardeEnfantDto })
|
||||||
|
enfant: CoupleGardeEnfantDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: CoupleGardeAmDto })
|
||||||
|
am: CoupleGardeAmDto;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'True si c’est le couple actuellement sélectionné' })
|
||||||
|
courant: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CouplesGardeResponseDto {
|
||||||
|
@ApiProperty({ type: [CoupleGardeDto] })
|
||||||
|
couples: CoupleGardeDto[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
format: 'uuid',
|
||||||
|
nullable: true,
|
||||||
|
description: 'Id du couple courant (null si aucun / premier couple implicite côté client)',
|
||||||
|
})
|
||||||
|
couple_courant_id: string | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
/** Corps PUT couple de garde courant — ticket #168 */
|
||||||
|
export class DefinirCoupleGardeCourantDto {
|
||||||
|
@ApiProperty({
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Id du placement (enfants_assistantes_maternelles.id) à sélectionner',
|
||||||
|
})
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
couple_id: string;
|
||||||
|
}
|
||||||
@@ -13,7 +13,10 @@ describe('ParentsController', () => {
|
|||||||
createParentDossierStaff: jest.fn(),
|
createParentDossierStaff: jest.fn(),
|
||||||
addCoParentStaff: jest.fn(),
|
addCoParentStaff: jest.fn(),
|
||||||
};
|
};
|
||||||
const parentsServiceMock = {};
|
const parentsServiceMock = {
|
||||||
|
listerCouplesGarde: jest.fn(),
|
||||||
|
definirCoupleGardeCourant: jest.fn(),
|
||||||
|
};
|
||||||
const userServiceMock = {};
|
const userServiceMock = {};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -39,6 +42,31 @@ describe('ParentsController', () => {
|
|||||||
expect(controller).toBeDefined();
|
expect(controller).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('listerCouplesGarde délègue au service (#168)', async () => {
|
||||||
|
parentsServiceMock.listerCouplesGarde.mockResolvedValue({
|
||||||
|
couples: [],
|
||||||
|
couple_courant_id: null,
|
||||||
|
});
|
||||||
|
const res = await controller.listerCouplesGarde('parent-uuid');
|
||||||
|
expect(parentsServiceMock.listerCouplesGarde).toHaveBeenCalledWith('parent-uuid');
|
||||||
|
expect(res.couples).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('definirCoupleGardeCourant délègue au service (#168)', async () => {
|
||||||
|
parentsServiceMock.definirCoupleGardeCourant.mockResolvedValue({
|
||||||
|
couples: [{ id: 'pl-1', courant: true }],
|
||||||
|
couple_courant_id: 'pl-1',
|
||||||
|
});
|
||||||
|
const res = await controller.definirCoupleGardeCourant('parent-uuid', {
|
||||||
|
couple_id: 'pl-1',
|
||||||
|
});
|
||||||
|
expect(parentsServiceMock.definirCoupleGardeCourant).toHaveBeenCalledWith(
|
||||||
|
'parent-uuid',
|
||||||
|
'pl-1',
|
||||||
|
);
|
||||||
|
expect(res.couple_courant_id).toBe('pl-1');
|
||||||
|
});
|
||||||
|
|
||||||
it('createDossier delegates to authService.createParentDossierStaff with CGU accepted', async () => {
|
it('createDossier delegates to authService.createParentDossierStaff with CGU accepted', async () => {
|
||||||
authServiceMock.createParentDossierStaff.mockResolvedValue({
|
authServiceMock.createParentDossierStaff.mockResolvedValue({
|
||||||
message: 'Dossier famille créé et validé. Un e-mail de création de mot de passe a été envoyé.',
|
message: 'Dossier famille créé et validé. Un e-mail de création de mot de passe a été envoyé.',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
|
Put,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ParentsService } from './parents.service';
|
import { ParentsService } from './parents.service';
|
||||||
@@ -39,6 +40,8 @@ import { User } from 'src/common/decorators/user.decorator';
|
|||||||
import { PendingFamilyDto } from './dto/pending-family.dto';
|
import { PendingFamilyDto } from './dto/pending-family.dto';
|
||||||
import { DossierFamilleCompletDto } from './dto/dossier-famille-complet.dto';
|
import { DossierFamilleCompletDto } from './dto/dossier-famille-complet.dto';
|
||||||
import { mapParentForApi, mapParentsForApi } from './parents.mapper';
|
import { mapParentForApi, mapParentsForApi } from './parents.mapper';
|
||||||
|
import { CouplesGardeResponseDto } from './dto/couples-garde.dto';
|
||||||
|
import { DefinirCoupleGardeCourantDto } from './dto/definir-couple-garde-courant.dto';
|
||||||
|
|
||||||
@ApiTags('Parents')
|
@ApiTags('Parents')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
@@ -51,6 +54,39 @@ export class ParentsController {
|
|||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Get('me/couples-garde')
|
||||||
|
@Roles(RoleType.PARENT)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Lister les couples de garde du parent connecté — ticket #168',
|
||||||
|
description:
|
||||||
|
'Retourne les placements actifs enfant↔AM rattachés au parent, ' +
|
||||||
|
'avec indication du couple courant (bandeau TdB quotidien).',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, type: CouplesGardeResponseDto })
|
||||||
|
@ApiResponse({ status: 403, description: 'Réservé au rôle parent' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Parent introuvable' })
|
||||||
|
listerCouplesGarde(@User('id') userId: string): Promise<CouplesGardeResponseDto> {
|
||||||
|
return this.parentsService.listerCouplesGarde(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('me/couples-garde/courant')
|
||||||
|
@Roles(RoleType.PARENT)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Définir le couple de garde courant — ticket #168',
|
||||||
|
description:
|
||||||
|
'Persiste la préférence de couple actif (enfant|nounou) pour contextualiser le TdB.',
|
||||||
|
})
|
||||||
|
@ApiBody({ type: DefinirCoupleGardeCourantDto })
|
||||||
|
@ApiResponse({ status: 200, type: CouplesGardeResponseDto })
|
||||||
|
@ApiResponse({ status: 400, description: 'Couple hors périmètre du parent' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Parent ou couple introuvable' })
|
||||||
|
definirCoupleGardeCourant(
|
||||||
|
@User('id') userId: string,
|
||||||
|
@Body() dto: DefinirCoupleGardeCourantDto,
|
||||||
|
): Promise<CouplesGardeResponseDto> {
|
||||||
|
return this.parentsService.definirCoupleGardeCourant(userId, dto.couple_id);
|
||||||
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
|
||||||
@Post('dossier')
|
@Post('dossier')
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
import { DossierFamille, DossierFamilleEnfant } from 'src/entities/dossier_famille.entity';
|
import { DossierFamille, DossierFamilleEnfant } from 'src/entities/dossier_famille.entity';
|
||||||
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
|
import { AmChildren } from 'src/entities/am_children.entity';
|
||||||
import { ParentsController } from './parents.controller';
|
import { ParentsController } from './parents.controller';
|
||||||
import { ParentsService } from './parents.service';
|
import { ParentsService } from './parents.service';
|
||||||
import { Users } from 'src/entities/users.entity';
|
import { Users } from 'src/entities/users.entity';
|
||||||
@@ -13,7 +14,14 @@ import { AuthModule } from '../auth/auth.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Parents, Users, DossierFamille, DossierFamilleEnfant, ParentsChildren]),
|
TypeOrmModule.forFeature([
|
||||||
|
Parents,
|
||||||
|
Users,
|
||||||
|
DossierFamille,
|
||||||
|
DossierFamilleEnfant,
|
||||||
|
ParentsChildren,
|
||||||
|
AmChildren,
|
||||||
|
]),
|
||||||
forwardRef(() => UserModule),
|
forwardRef(() => UserModule),
|
||||||
forwardRef(() => AuthModule),
|
forwardRef(() => AuthModule),
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
|
|||||||
@@ -1,12 +1,43 @@
|
|||||||
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { ParentsService } from './parents.service';
|
import { ParentsService } from './parents.service';
|
||||||
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { Users } from 'src/entities/users.entity';
|
||||||
|
import { DossierFamille } from 'src/entities/dossier_famille.entity';
|
||||||
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
|
import { AmChildren } from 'src/entities/am_children.entity';
|
||||||
|
|
||||||
describe('ParentsService', () => {
|
describe('ParentsService — couples de garde (#168)', () => {
|
||||||
let service: ParentsService;
|
let service: ParentsService;
|
||||||
|
|
||||||
|
const parentsRepository = {
|
||||||
|
findOne: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
};
|
||||||
|
const parentsChildrenRepository = {
|
||||||
|
find: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
};
|
||||||
|
const amChildrenRepository = {
|
||||||
|
find: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [ParentsService],
|
providers: [
|
||||||
|
ParentsService,
|
||||||
|
{ provide: getRepositoryToken(Parents), useValue: parentsRepository },
|
||||||
|
{ provide: getRepositoryToken(Users), useValue: {} },
|
||||||
|
{ provide: getRepositoryToken(DossierFamille), useValue: {} },
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(ParentsChildren),
|
||||||
|
useValue: parentsChildrenRepository,
|
||||||
|
},
|
||||||
|
{ provide: getRepositoryToken(AmChildren), useValue: amChildrenRepository },
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<ParentsService>(ParentsService);
|
service = module.get<ParentsService>(ParentsService);
|
||||||
@@ -15,4 +46,107 @@ describe('ParentsService', () => {
|
|||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('listerCouplesGarde', () => {
|
||||||
|
it('retourne une liste vide si le parent n’a pas d’enfant', async () => {
|
||||||
|
parentsRepository.findOne.mockResolvedValue({
|
||||||
|
user_id: 'p1',
|
||||||
|
id_placement_garde_courant: null,
|
||||||
|
});
|
||||||
|
parentsChildrenRepository.find.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const res = await service.listerCouplesGarde('p1');
|
||||||
|
expect(res).toEqual({ couples: [], couple_courant_id: null });
|
||||||
|
expect(amChildrenRepository.find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mappe les placements actifs en couples et marque le courant', async () => {
|
||||||
|
parentsRepository.findOne.mockResolvedValue({
|
||||||
|
user_id: 'p1',
|
||||||
|
id_placement_garde_courant: 'pl-2',
|
||||||
|
});
|
||||||
|
parentsChildrenRepository.find.mockResolvedValue([
|
||||||
|
{ enfantId: 'e1' },
|
||||||
|
{ enfantId: 'e2' },
|
||||||
|
]);
|
||||||
|
amChildrenRepository.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'pl-1',
|
||||||
|
amId: 'am-1',
|
||||||
|
child: { id: 'e1', first_name: 'Léo', last_name: 'M', photo_url: null },
|
||||||
|
am: { user: { prenom: 'Marie', nom: 'N', photo_url: '/a.jpg' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pl-2',
|
||||||
|
amId: 'am-2',
|
||||||
|
child: { id: 'e2', first_name: 'Léa', last_name: 'M', photo_url: null },
|
||||||
|
am: { user: { prenom: 'Sophie', nom: 'P', photo_url: null } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await service.listerCouplesGarde('p1');
|
||||||
|
expect(res.couples).toHaveLength(2);
|
||||||
|
expect(res.couple_courant_id).toBe('pl-2');
|
||||||
|
expect(res.couples[1].courant).toBe(true);
|
||||||
|
expect(res.couples[0].am.prenom).toBe('Marie');
|
||||||
|
expect(res.couples[0].enfant.prenom).toBe('Léo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('404 si parent inconnu', async () => {
|
||||||
|
parentsRepository.findOne.mockResolvedValue(null);
|
||||||
|
await expect(service.listerCouplesGarde('x')).rejects.toBeInstanceOf(
|
||||||
|
NotFoundException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('definirCoupleGardeCourant', () => {
|
||||||
|
it('persiste le couple si l’enfant est rattaché au parent', async () => {
|
||||||
|
parentsRepository.findOne.mockResolvedValue({
|
||||||
|
user_id: 'p1',
|
||||||
|
id_placement_garde_courant: null,
|
||||||
|
});
|
||||||
|
amChildrenRepository.findOne.mockResolvedValue({
|
||||||
|
id: 'pl-1',
|
||||||
|
enfantId: 'e1',
|
||||||
|
date_fin: null,
|
||||||
|
});
|
||||||
|
parentsChildrenRepository.findOne.mockResolvedValue({
|
||||||
|
parentId: 'p1',
|
||||||
|
enfantId: 'e1',
|
||||||
|
});
|
||||||
|
parentsRepository.update.mockResolvedValue({ affected: 1 });
|
||||||
|
// second call via listerCouplesGarde
|
||||||
|
parentsChildrenRepository.find.mockResolvedValue([{ enfantId: 'e1' }]);
|
||||||
|
amChildrenRepository.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'pl-1',
|
||||||
|
amId: 'am-1',
|
||||||
|
child: { id: 'e1', first_name: 'Léo', last_name: null, photo_url: null },
|
||||||
|
am: { user: { prenom: 'Marie', nom: null, photo_url: null } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await service.definirCoupleGardeCourant('p1', 'pl-1');
|
||||||
|
expect(parentsRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ user_id: 'p1' },
|
||||||
|
{ id_placement_garde_courant: 'pl-1' },
|
||||||
|
);
|
||||||
|
expect(res.couple_courant_id).toBe('pl-1');
|
||||||
|
expect(res.couples[0].courant).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('400 si le couple n’appartient pas au parent', async () => {
|
||||||
|
parentsRepository.findOne.mockResolvedValue({ user_id: 'p1' });
|
||||||
|
amChildrenRepository.findOne.mockResolvedValue({
|
||||||
|
id: 'pl-1',
|
||||||
|
enfantId: 'e99',
|
||||||
|
});
|
||||||
|
parentsChildrenRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.definirCoupleGardeCourant('p1', 'pl-1'),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { In, Repository } from 'typeorm';
|
import { In, IsNull, Repository } from 'typeorm';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
import { DossierFamille } from 'src/entities/dossier_famille.entity';
|
import { DossierFamille } from 'src/entities/dossier_famille.entity';
|
||||||
import { RoleType, Users } from 'src/entities/users.entity';
|
import { RoleType, Users } from 'src/entities/users.entity';
|
||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
import { Children } from 'src/entities/children.entity';
|
import { Children } from 'src/entities/children.entity';
|
||||||
import { UpdateParentFicheAdminDto } from './dto/update-parent-fiche-admin.dto';
|
import { UpdateParentFicheAdminDto } from './dto/update-parent-fiche-admin.dto';
|
||||||
|
import { AmChildren } from 'src/entities/am_children.entity';
|
||||||
|
import { CouplesGardeResponseDto } from './dto/couples-garde.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ParentsService {
|
export class ParentsService {
|
||||||
@@ -32,6 +34,8 @@ export class ParentsService {
|
|||||||
private readonly dossierFamilleRepository: Repository<DossierFamille>,
|
private readonly dossierFamilleRepository: Repository<DossierFamille>,
|
||||||
@InjectRepository(ParentsChildren)
|
@InjectRepository(ParentsChildren)
|
||||||
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
||||||
|
@InjectRepository(AmChildren)
|
||||||
|
private readonly amChildrenRepository: Repository<AmChildren>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Création d’un parent
|
// Création d’un parent
|
||||||
@@ -505,4 +509,108 @@ export class ParentsService {
|
|||||||
}
|
}
|
||||||
return raw.map((r: { id: string }) => r.id);
|
return raw.map((r: { id: string }) => r.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste les couples de garde (enfant ↔ AM) du parent connecté — ticket #168.
|
||||||
|
* Un couple = un placement actif dans enfants_assistantes_maternelles pour un enfant du parent.
|
||||||
|
*/
|
||||||
|
async listerCouplesGarde(parentUserId: string): Promise<CouplesGardeResponseDto> {
|
||||||
|
const parent = await this.parentsRepository.findOne({
|
||||||
|
where: { user_id: parentUserId },
|
||||||
|
});
|
||||||
|
if (!parent) {
|
||||||
|
throw new NotFoundException('Parent introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const liensEnfants = await this.parentsChildrenRepository.find({
|
||||||
|
where: { parentId: parentUserId },
|
||||||
|
select: ['enfantId'],
|
||||||
|
});
|
||||||
|
const enfantIds = liensEnfants.map((l) => l.enfantId);
|
||||||
|
if (enfantIds.length === 0) {
|
||||||
|
return { couples: [], couple_courant_id: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const placements = await this.amChildrenRepository.find({
|
||||||
|
where: { enfantId: In(enfantIds), date_fin: IsNull() },
|
||||||
|
relations: ['child', 'am', 'am.user'],
|
||||||
|
order: { date_debut: 'ASC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let idCourant = parent.id_placement_garde_courant ?? null;
|
||||||
|
const idsValides = new Set(placements.map((p) => p.id));
|
||||||
|
if (idCourant && !idsValides.has(idCourant)) {
|
||||||
|
idCourant = null;
|
||||||
|
await this.parentsRepository.update(
|
||||||
|
{ user_id: parentUserId },
|
||||||
|
{ id_placement_garde_courant: null },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!idCourant && placements.length === 1) {
|
||||||
|
idCourant = placements[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const couples = placements.map((p) => {
|
||||||
|
const amUser = p.am?.user;
|
||||||
|
return {
|
||||||
|
id: p.id,
|
||||||
|
enfant: {
|
||||||
|
id: p.child.id,
|
||||||
|
prenom: p.child.first_name ?? null,
|
||||||
|
nom: p.child.last_name ?? null,
|
||||||
|
photo_url: p.child.photo_url ?? null,
|
||||||
|
},
|
||||||
|
am: {
|
||||||
|
id: p.amId,
|
||||||
|
prenom: amUser?.prenom ?? null,
|
||||||
|
nom: amUser?.nom ?? null,
|
||||||
|
photo_url: amUser?.photo_url ?? null,
|
||||||
|
},
|
||||||
|
courant: idCourant != null && p.id === idCourant,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
couples,
|
||||||
|
couple_courant_id: idCourant,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persiste le couple de garde actif pour le parent — ticket #168.
|
||||||
|
*/
|
||||||
|
async definirCoupleGardeCourant(
|
||||||
|
parentUserId: string,
|
||||||
|
coupleId: string,
|
||||||
|
): Promise<CouplesGardeResponseDto> {
|
||||||
|
const parent = await this.parentsRepository.findOne({
|
||||||
|
where: { user_id: parentUserId },
|
||||||
|
});
|
||||||
|
if (!parent) {
|
||||||
|
throw new NotFoundException('Parent introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const placement = await this.amChildrenRepository.findOne({
|
||||||
|
where: { id: coupleId, date_fin: IsNull() },
|
||||||
|
});
|
||||||
|
if (!placement) {
|
||||||
|
throw new NotFoundException('Couple de garde introuvable ou inactif');
|
||||||
|
}
|
||||||
|
|
||||||
|
const lien = await this.parentsChildrenRepository.findOne({
|
||||||
|
where: { parentId: parentUserId, enfantId: placement.enfantId },
|
||||||
|
});
|
||||||
|
if (!lien) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Ce couple ne concerne pas un enfant rattaché à ce parent',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.parentsRepository.update(
|
||||||
|
{ user_id: parentUserId },
|
||||||
|
{ id_placement_garde_courant: coupleId },
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.listerCouplesGarde(parentUserId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -154,7 +154,10 @@ CREATE INDEX idx_assistantes_maternelles_numero_dossier
|
|||||||
CREATE TABLE parents (
|
CREATE TABLE parents (
|
||||||
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
||||||
id_co_parent UUID REFERENCES utilisateurs(id),
|
id_co_parent UUID REFERENCES utilisateurs(id),
|
||||||
numero_dossier VARCHAR(20)
|
numero_dossier VARCHAR(20),
|
||||||
|
-- Préférence couple de garde actif (TdB quotidien) — ticket #168
|
||||||
|
-- FK ajoutée après création de enfants_assistantes_maternelles (voir ALTER plus bas)
|
||||||
|
id_placement_garde_courant UUID
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_parents_numero_dossier
|
CREATE INDEX idx_parents_numero_dossier
|
||||||
@@ -206,6 +209,16 @@ CREATE UNIQUE INDEX uq_enfant_garde_active
|
|||||||
ON enfants_assistantes_maternelles (id_enfant)
|
ON enfants_assistantes_maternelles (id_enfant)
|
||||||
WHERE date_fin IS NULL;
|
WHERE date_fin IS NULL;
|
||||||
|
|
||||||
|
-- FK couple courant parent → placement (#168) — après table enfants_assistantes_maternelles
|
||||||
|
ALTER TABLE parents
|
||||||
|
ADD CONSTRAINT fk_parents_placement_garde_courant
|
||||||
|
FOREIGN KEY (id_placement_garde_courant)
|
||||||
|
REFERENCES enfants_assistantes_maternelles(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_parents_placement_garde_courant
|
||||||
|
ON parents(id_placement_garde_courant)
|
||||||
|
WHERE id_placement_garde_courant IS NOT NULL;
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : dossier_famille (inscription parent — ticket #119)
|
-- Table : dossier_famille (inscription parent — ticket #119)
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Ticket #168 — Couple de garde courant (préférence parent)
|
||||||
|
-- Idempotent : safe à rejouer.
|
||||||
|
|
||||||
|
ALTER TABLE parents
|
||||||
|
ADD COLUMN IF NOT EXISTS id_placement_garde_courant UUID
|
||||||
|
REFERENCES enfants_assistantes_maternelles(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_parents_placement_garde_courant
|
||||||
|
ON parents(id_placement_garde_courant)
|
||||||
|
WHERE id_placement_garde_courant IS NOT NULL;
|
||||||
Reference in New Issue
Block a user