Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f2c3e1ef | ||
|
|
4007d7010e | ||
|
|
4261667198 | ||
|
|
15e2d8d850 | ||
|
|
da71803852 |
@@ -1,4 +1,4 @@
|
|||||||
import { Entity, PrimaryColumn, Column, OneToOne, OneToMany, JoinColumn } from 'typeorm';
|
import { Entity, PrimaryColumn, Column, OneToOne, OneToMany, JoinColumn, ManyToOne } from 'typeorm';
|
||||||
import { Users } from './users.entity';
|
import { Users } from './users.entity';
|
||||||
import { AmChildren } from './am_children.entity';
|
import { AmChildren } from './am_children.entity';
|
||||||
|
|
||||||
@@ -53,6 +53,16 @@ export class AssistanteMaternelle {
|
|||||||
@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 AM (couple actif) — ticket #171.
|
||||||
|
*/
|
||||||
|
@Column({ name: 'id_placement_garde_courant', type: 'uuid', nullable: true })
|
||||||
|
id_placement_garde_courant?: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => AmChildren, { nullable: true, onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'id_placement_garde_courant', referencedColumnName: 'id' })
|
||||||
|
placement_garde_courant?: AmChildren;
|
||||||
|
|
||||||
@OneToMany(() => AmChildren, (ac) => ac.am)
|
@OneToMany(() => AmChildren, (ac) => ac.am)
|
||||||
amChildren: AmChildren[];
|
amChildren: AmChildren[];
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-1
@@ -10,7 +10,10 @@ describe('AssistantesMaternellesController', () => {
|
|||||||
const authServiceMock = {
|
const authServiceMock = {
|
||||||
createAmDossierStaff: jest.fn(),
|
createAmDossierStaff: jest.fn(),
|
||||||
};
|
};
|
||||||
const amServiceMock = {};
|
const amServiceMock = {
|
||||||
|
listerCouplesGarde: jest.fn(),
|
||||||
|
definirCoupleGardeCourant: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@@ -66,4 +69,29 @@ describe('AssistantesMaternellesController', () => {
|
|||||||
);
|
);
|
||||||
expect(res.numero_dossier).toBe('2026-000001');
|
expect(res.numero_dossier).toBe('2026-000001');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('listerCouplesGarde délègue au service (#171)', async () => {
|
||||||
|
amServiceMock.listerCouplesGarde.mockResolvedValue({
|
||||||
|
couples: [],
|
||||||
|
couple_courant_id: null,
|
||||||
|
});
|
||||||
|
const res = await controller.listerCouplesGarde('am-uuid');
|
||||||
|
expect(amServiceMock.listerCouplesGarde).toHaveBeenCalledWith('am-uuid');
|
||||||
|
expect(res.couple_courant_id).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('definirCoupleGardeCourant délègue au service (#171)', async () => {
|
||||||
|
amServiceMock.definirCoupleGardeCourant.mockResolvedValue({
|
||||||
|
couples: [{ id: 'pl-1', courant: true }],
|
||||||
|
couple_courant_id: 'pl-1',
|
||||||
|
});
|
||||||
|
const res = await controller.definirCoupleGardeCourant('am-uuid', {
|
||||||
|
couple_id: 'pl-1',
|
||||||
|
});
|
||||||
|
expect(amServiceMock.definirCoupleGardeCourant).toHaveBeenCalledWith(
|
||||||
|
'am-uuid',
|
||||||
|
'pl-1',
|
||||||
|
);
|
||||||
|
expect(res.couple_courant_id).toBe('pl-1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
|
Put,
|
||||||
Body,
|
Body,
|
||||||
Patch,
|
Patch,
|
||||||
Param,
|
Param,
|
||||||
@@ -20,6 +21,8 @@ import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
|
|||||||
import { UpdateAmFicheAdminDto } from './dto/update-am-fiche-admin.dto';
|
import { UpdateAmFicheAdminDto } from './dto/update-am-fiche-admin.dto';
|
||||||
import { StaffCreateAmDossierDto } from './dto/staff-create-am-dossier.dto';
|
import { StaffCreateAmDossierDto } from './dto/staff-create-am-dossier.dto';
|
||||||
import { StaffCreateAmDossierResponseDto } from './dto/staff-create-am-dossier-response.dto';
|
import { StaffCreateAmDossierResponseDto } from './dto/staff-create-am-dossier-response.dto';
|
||||||
|
import { CouplesGardeAmResponseDto } from './dto/couples-garde-am.dto';
|
||||||
|
import { DefinirCoupleGardeCourantAmDto } from './dto/definir-couple-garde-courant-am.dto';
|
||||||
import { RolesGuard } from 'src/common/guards/roles.guard';
|
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||||
import { User } from 'src/common/decorators/user.decorator';
|
import { User } from 'src/common/decorators/user.decorator';
|
||||||
@@ -83,6 +86,36 @@ export class AssistantesMaternellesController {
|
|||||||
return mapAmsForApi(ams);
|
return mapAmsForApi(ams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('me/couples-garde')
|
||||||
|
@Roles(RoleType.ASSISTANTE_MATERNELLE)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Couples de garde de l’AM connectée — ticket #171',
|
||||||
|
description:
|
||||||
|
'Placements actifs (enfant + parents) pour peupler le bandeau couple TdB AM, ' +
|
||||||
|
'avec indication du couple courant.',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, type: CouplesGardeAmResponseDto })
|
||||||
|
listerCouplesGarde(
|
||||||
|
@User('id') userId: string,
|
||||||
|
): Promise<CouplesGardeAmResponseDto> {
|
||||||
|
return this.assistantesMaternellesService.listerCouplesGarde(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('me/couples-garde/courant')
|
||||||
|
@Roles(RoleType.ASSISTANTE_MATERNELLE)
|
||||||
|
@ApiOperation({ summary: 'Définir le couple de garde courant (AM) — ticket #171' })
|
||||||
|
@ApiBody({ type: DefinirCoupleGardeCourantAmDto })
|
||||||
|
@ApiResponse({ status: 200, type: CouplesGardeAmResponseDto })
|
||||||
|
definirCoupleGardeCourant(
|
||||||
|
@User('id') userId: string,
|
||||||
|
@Body() dto: DefinirCoupleGardeCourantAmDto,
|
||||||
|
): Promise<CouplesGardeAmResponseDto> {
|
||||||
|
return this.assistantesMaternellesService.definirCoupleGardeCourant(
|
||||||
|
userId,
|
||||||
|
dto.couple_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
||||||
|
|||||||
@@ -4,13 +4,21 @@ import { AssistantesMaternellesController } from './assistantes_maternelles.cont
|
|||||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
import { AmChildren } from 'src/entities/am_children.entity';
|
import { AmChildren } from 'src/entities/am_children.entity';
|
||||||
import { Children } from 'src/entities/children.entity';
|
import { Children } from 'src/entities/children.entity';
|
||||||
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Users } from 'src/entities/users.entity';
|
import { Users } from 'src/entities/users.entity';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([AssistanteMaternelle, AmChildren, Children, Users]),
|
imports: [
|
||||||
AuthModule
|
TypeOrmModule.forFeature([
|
||||||
|
AssistanteMaternelle,
|
||||||
|
AmChildren,
|
||||||
|
Children,
|
||||||
|
Users,
|
||||||
|
ParentsChildren,
|
||||||
|
]),
|
||||||
|
AuthModule,
|
||||||
],
|
],
|
||||||
controllers: [AssistantesMaternellesController],
|
controllers: [AssistantesMaternellesController],
|
||||||
providers: [AssistantesMaternellesService],
|
providers: [AssistantesMaternellesService],
|
||||||
|
|||||||
+138
-6
@@ -1,18 +1,150 @@
|
|||||||
|
import { NotFoundException } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
|
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
|
||||||
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
|
import { Users } from 'src/entities/users.entity';
|
||||||
|
import { AmChildren } from 'src/entities/am_children.entity';
|
||||||
|
import { Children } from 'src/entities/children.entity';
|
||||||
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
|
|
||||||
describe('AssistantesMaternellesService', () => {
|
describe('AssistantesMaternellesService — couples de garde (#171)', () => {
|
||||||
let service: AssistantesMaternellesService;
|
let service: AssistantesMaternellesService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
const amRepo = { findOne: jest.fn(), update: jest.fn() };
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const usersRepo = {};
|
||||||
providers: [AssistantesMaternellesService],
|
const amChildrenRepo = { find: jest.fn(), findOne: jest.fn() };
|
||||||
}).compile();
|
const childrenRepo = {};
|
||||||
|
const parentsChildrenRepo = { find: jest.fn() };
|
||||||
|
|
||||||
service = module.get<AssistantesMaternellesService>(AssistantesMaternellesService);
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AssistantesMaternellesService,
|
||||||
|
{ provide: getRepositoryToken(AssistanteMaternelle), useValue: amRepo },
|
||||||
|
{ provide: getRepositoryToken(Users), useValue: usersRepo },
|
||||||
|
{ provide: getRepositoryToken(AmChildren), useValue: amChildrenRepo },
|
||||||
|
{ provide: getRepositoryToken(Children), useValue: childrenRepo },
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(ParentsChildren),
|
||||||
|
useValue: parentsChildrenRepo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
service = module.get(AssistantesMaternellesService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('listerCouplesGarde', () => {
|
||||||
|
it('retourne vide si aucun placement', async () => {
|
||||||
|
amRepo.findOne.mockResolvedValue({
|
||||||
|
user_id: 'am-1',
|
||||||
|
id_placement_garde_courant: null,
|
||||||
|
});
|
||||||
|
amChildrenRepo.find.mockResolvedValue([]);
|
||||||
|
const res = await service.listerCouplesGarde('am-1');
|
||||||
|
expect(res).toEqual({ couples: [], couple_courant_id: null });
|
||||||
|
expect(parentsChildrenRepo.find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mappe placements + parents et marque le courant', async () => {
|
||||||
|
amRepo.findOne.mockResolvedValue({
|
||||||
|
user_id: 'am-1',
|
||||||
|
id_placement_garde_courant: 'pl-2',
|
||||||
|
});
|
||||||
|
amChildrenRepo.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'pl-1',
|
||||||
|
enfantId: 'e1',
|
||||||
|
child: { id: 'e1', first_name: 'Léa', last_name: 'M', photo_url: null },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pl-2',
|
||||||
|
enfantId: 'e2',
|
||||||
|
child: { id: 'e2', first_name: 'Emma', last_name: 'M', photo_url: null },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
parentsChildrenRepo.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
parentId: 'p1',
|
||||||
|
enfantId: 'e1',
|
||||||
|
parent: { user: { prenom: 'Claire', nom: 'Martin', photo_url: null } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentId: 'p2',
|
||||||
|
enfantId: 'e1',
|
||||||
|
parent: { user: { prenom: 'Thomas', nom: 'Martin', photo_url: null } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentId: 'p1',
|
||||||
|
enfantId: 'e2',
|
||||||
|
parent: { user: { prenom: 'Claire', nom: 'Martin', photo_url: null } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await service.listerCouplesGarde('am-1');
|
||||||
|
expect(res.couples).toHaveLength(2);
|
||||||
|
expect(res.couple_courant_id).toBe('pl-2');
|
||||||
|
expect(res.couples[1].courant).toBe(true);
|
||||||
|
expect(res.couples[0].parents).toHaveLength(2);
|
||||||
|
expect(res.couples[0].parents[0].prenom).toBe('Claire');
|
||||||
|
expect(res.couples[0].enfant.prenom).toBe('Léa');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('404 si AM inconnue', async () => {
|
||||||
|
amRepo.findOne.mockResolvedValue(null);
|
||||||
|
await expect(service.listerCouplesGarde('x')).rejects.toBeInstanceOf(
|
||||||
|
NotFoundException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('definirCoupleGardeCourant', () => {
|
||||||
|
it('persiste si le placement appartient à l’AM', async () => {
|
||||||
|
amRepo.findOne.mockResolvedValue({
|
||||||
|
user_id: 'am-1',
|
||||||
|
id_placement_garde_courant: null,
|
||||||
|
});
|
||||||
|
amChildrenRepo.findOne.mockResolvedValue({
|
||||||
|
id: 'pl-1',
|
||||||
|
amId: 'am-1',
|
||||||
|
enfantId: 'e1',
|
||||||
|
});
|
||||||
|
amRepo.update.mockResolvedValue({ affected: 1 });
|
||||||
|
amChildrenRepo.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'pl-1',
|
||||||
|
enfantId: 'e1',
|
||||||
|
child: { id: 'e1', first_name: 'Léa', last_name: null, photo_url: null },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
parentsChildrenRepo.find.mockResolvedValue([
|
||||||
|
{
|
||||||
|
parentId: 'p1',
|
||||||
|
enfantId: 'e1',
|
||||||
|
parent: { user: { prenom: 'Claire', nom: 'M', photo_url: null } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await service.definirCoupleGardeCourant('am-1', 'pl-1');
|
||||||
|
expect(amRepo.update).toHaveBeenCalledWith(
|
||||||
|
{ user_id: 'am-1' },
|
||||||
|
{ id_placement_garde_courant: 'pl-1' },
|
||||||
|
);
|
||||||
|
expect(res.couple_courant_id).toBe('pl-1');
|
||||||
|
expect(res.couples[0].courant).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('404 si placement d’une autre AM', async () => {
|
||||||
|
amRepo.findOne.mockResolvedValue({ user_id: 'am-1' });
|
||||||
|
amChildrenRepo.findOne.mockResolvedValue(null);
|
||||||
|
await expect(
|
||||||
|
service.definirCoupleGardeCourant('am-1', 'pl-x'),
|
||||||
|
).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { IsNull, Repository } from 'typeorm';
|
import { In, IsNull, Repository } from 'typeorm';
|
||||||
import { RoleType, Users } from 'src/entities/users.entity';
|
import { RoleType, Users } from 'src/entities/users.entity';
|
||||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
import { AmChildren } from 'src/entities/am_children.entity';
|
import { AmChildren } from 'src/entities/am_children.entity';
|
||||||
import { Children, StatutEnfantType } from 'src/entities/children.entity';
|
import { Children, StatutEnfantType } from 'src/entities/children.entity';
|
||||||
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
|
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
|
||||||
import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
|
import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
|
||||||
import { UpdateAmFicheAdminDto } from './dto/update-am-fiche-admin.dto';
|
import { UpdateAmFicheAdminDto } from './dto/update-am-fiche-admin.dto';
|
||||||
|
import { CouplesGardeAmResponseDto } from './dto/couples-garde-am.dto';
|
||||||
import { validateNir } from 'src/common/utils/nir.util';
|
import { validateNir } from 'src/common/utils/nir.util';
|
||||||
|
|
||||||
const AM_CHILDREN_RELATIONS = ['user', 'amChildren', 'amChildren.child'] as const;
|
const AM_CHILDREN_RELATIONS = ['user', 'amChildren', 'amChildren.child'] as const;
|
||||||
@@ -28,6 +30,8 @@ export class AssistantesMaternellesService {
|
|||||||
private readonly amChildrenRepository: Repository<AmChildren>,
|
private readonly amChildrenRepository: Repository<AmChildren>,
|
||||||
@InjectRepository(Children)
|
@InjectRepository(Children)
|
||||||
private readonly childrenRepository: Repository<Children>,
|
private readonly childrenRepository: Repository<Children>,
|
||||||
|
@InjectRepository(ParentsChildren)
|
||||||
|
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
async create(dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||||
@@ -264,4 +268,107 @@ export class AssistantesMaternellesService {
|
|||||||
await this.assistantesMaternelleRepository.delete(id);
|
await this.assistantesMaternelleRepository.delete(id);
|
||||||
return { message: 'Assistante maternelle supprimée' };
|
return { message: 'Assistante maternelle supprimée' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste les couples de garde (enfant ↔ parents) de l’AM connectée — ticket #171.
|
||||||
|
* Un couple = un placement actif dans enfants_assistantes_maternelles pour cette AM.
|
||||||
|
*/
|
||||||
|
async listerCouplesGarde(amUserId: string): Promise<CouplesGardeAmResponseDto> {
|
||||||
|
const am = await this.assistantesMaternelleRepository.findOne({
|
||||||
|
where: { user_id: amUserId },
|
||||||
|
});
|
||||||
|
if (!am) {
|
||||||
|
throw new NotFoundException('Assistante maternelle introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const placements = await this.amChildrenRepository.find({
|
||||||
|
where: { amId: amUserId, date_fin: IsNull() },
|
||||||
|
relations: ['child'],
|
||||||
|
order: { date_debut: 'ASC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (placements.length === 0) {
|
||||||
|
return { couples: [], couple_courant_id: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const enfantIds = placements.map((p) => p.enfantId);
|
||||||
|
const liens = await this.parentsChildrenRepository.find({
|
||||||
|
where: { enfantId: In(enfantIds) },
|
||||||
|
relations: ['parent', 'parent.user'],
|
||||||
|
});
|
||||||
|
const parentsByEnfant = new Map<
|
||||||
|
string,
|
||||||
|
{ id: string; prenom: string | null; nom: string | null; photo_url: string | null }[]
|
||||||
|
>();
|
||||||
|
for (const l of liens) {
|
||||||
|
const list = parentsByEnfant.get(l.enfantId) ?? [];
|
||||||
|
const u = l.parent?.user;
|
||||||
|
list.push({
|
||||||
|
id: l.parentId,
|
||||||
|
prenom: u?.prenom ?? null,
|
||||||
|
nom: u?.nom ?? null,
|
||||||
|
photo_url: u?.photo_url ?? null,
|
||||||
|
});
|
||||||
|
parentsByEnfant.set(l.enfantId, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
let idCourant = am.id_placement_garde_courant ?? null;
|
||||||
|
const idsValides = new Set(placements.map((p) => p.id));
|
||||||
|
if (idCourant && !idsValides.has(idCourant)) {
|
||||||
|
idCourant = null;
|
||||||
|
await this.assistantesMaternelleRepository.update(
|
||||||
|
{ user_id: amUserId },
|
||||||
|
{ id_placement_garde_courant: () => 'NULL' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!idCourant && placements.length === 1) {
|
||||||
|
idCourant = placements[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const couples = placements.map((p) => ({
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
parents: parentsByEnfant.get(p.enfantId) ?? [],
|
||||||
|
courant: idCourant != null && p.id === idCourant,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
couples,
|
||||||
|
couple_courant_id: idCourant,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persiste le couple de garde actif pour l’AM — ticket #171.
|
||||||
|
*/
|
||||||
|
async definirCoupleGardeCourant(
|
||||||
|
amUserId: string,
|
||||||
|
coupleId: string,
|
||||||
|
): Promise<CouplesGardeAmResponseDto> {
|
||||||
|
const am = await this.assistantesMaternelleRepository.findOne({
|
||||||
|
where: { user_id: amUserId },
|
||||||
|
});
|
||||||
|
if (!am) {
|
||||||
|
throw new NotFoundException('Assistante maternelle introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const placement = await this.amChildrenRepository.findOne({
|
||||||
|
where: { id: coupleId, amId: amUserId, date_fin: IsNull() },
|
||||||
|
});
|
||||||
|
if (!placement) {
|
||||||
|
throw new NotFoundException('Couple de garde introuvable ou inactif pour cette AM');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.assistantesMaternelleRepository.update(
|
||||||
|
{ user_id: amUserId },
|
||||||
|
{ id_placement_garde_courant: coupleId },
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.listerCouplesGarde(amUserId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/** Identité minimale enfant — ticket #171 (miroir #168) */
|
||||||
|
export class CoupleGardeAmEnfantDto {
|
||||||
|
@ApiProperty({ format: 'uuid' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
prenom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
nom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
photo_url?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Identité minimale parent pour bandeau AM — ticket #171 */
|
||||||
|
export class CoupleGardeAmParentDto {
|
||||||
|
@ApiProperty({ format: 'uuid', description: 'UUID utilisateur du parent' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
prenom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
nom?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
photo_url?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un couple côté AM = placement actif enfant ↔ foyer parents.
|
||||||
|
* `id` = enfants_assistantes_maternelles.id (même clé que #168).
|
||||||
|
*/
|
||||||
|
export class CoupleGardeAmDto {
|
||||||
|
@ApiProperty({
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Id du placement (enfants_assistantes_maternelles.id)',
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: CoupleGardeAmEnfantDto })
|
||||||
|
enfant: CoupleGardeAmEnfantDto;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: [CoupleGardeAmParentDto],
|
||||||
|
description: 'Parents rattachés à l’enfant (0–2 typiquement)',
|
||||||
|
})
|
||||||
|
parents: CoupleGardeAmParentDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'True si c’est le couple actuellement sélectionné' })
|
||||||
|
courant: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CouplesGardeAmResponseDto {
|
||||||
|
@ApiProperty({ type: [CoupleGardeAmDto] })
|
||||||
|
couples: CoupleGardeAmDto[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
format: 'uuid',
|
||||||
|
nullable: true,
|
||||||
|
description: 'Id du couple courant (null si aucun)',
|
||||||
|
})
|
||||||
|
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 AM — ticket #171 */
|
||||||
|
export class DefinirCoupleGardeCourantAmDto {
|
||||||
|
@ApiProperty({
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'Id du placement (enfants_assistantes_maternelles.id) à sélectionner',
|
||||||
|
})
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
couple_id: string;
|
||||||
|
}
|
||||||
+14
-1
@@ -143,7 +143,10 @@ CREATE TABLE assistantes_maternelles (
|
|||||||
annee_experience SMALLINT,
|
annee_experience SMALLINT,
|
||||||
specialite VARCHAR(100),
|
specialite VARCHAR(100),
|
||||||
place_disponible INT,
|
place_disponible INT,
|
||||||
numero_dossier VARCHAR(20)
|
numero_dossier VARCHAR(20),
|
||||||
|
-- Préférence couple de garde actif (TdB quotidien AM) — ticket #171
|
||||||
|
-- FK ajoutée après création de enfants_assistantes_maternelles (voir ALTER plus bas)
|
||||||
|
id_placement_garde_courant UUID
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_assistantes_maternelles_numero_dossier
|
CREATE INDEX idx_assistantes_maternelles_numero_dossier
|
||||||
@@ -221,6 +224,16 @@ CREATE INDEX idx_parents_placement_garde_courant
|
|||||||
ON parents(id_placement_garde_courant)
|
ON parents(id_placement_garde_courant)
|
||||||
WHERE id_placement_garde_courant IS NOT NULL;
|
WHERE id_placement_garde_courant IS NOT NULL;
|
||||||
|
|
||||||
|
-- FK couple courant AM → placement (#171)
|
||||||
|
ALTER TABLE assistantes_maternelles
|
||||||
|
ADD CONSTRAINT fk_am_placement_garde_courant
|
||||||
|
FOREIGN KEY (id_placement_garde_courant)
|
||||||
|
REFERENCES enfants_assistantes_maternelles(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_am_placement_garde_courant
|
||||||
|
ON assistantes_maternelles(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 #171 — Couple de garde courant (préférence AM)
|
||||||
|
-- Idempotent : safe à rejouer.
|
||||||
|
|
||||||
|
ALTER TABLE assistantes_maternelles
|
||||||
|
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_am_placement_garde_courant
|
||||||
|
ON assistantes_maternelles(id_placement_garde_courant)
|
||||||
|
WHERE id_placement_garde_courant IS NOT NULL;
|
||||||
@@ -39,13 +39,15 @@ class CoupleMembre {
|
|||||||
class CoupleGarde {
|
class CoupleGarde {
|
||||||
final String id;
|
final String id;
|
||||||
final CoupleMembre enfant;
|
final CoupleMembre enfant;
|
||||||
final CoupleMembre am;
|
final CoupleMembre? am;
|
||||||
|
final List<CoupleMembre> parents;
|
||||||
final bool courant;
|
final bool courant;
|
||||||
|
|
||||||
const CoupleGarde({
|
const CoupleGarde({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.enfant,
|
required this.enfant,
|
||||||
required this.am,
|
this.am,
|
||||||
|
this.parents = const [],
|
||||||
this.courant = false,
|
this.courant = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,9 +57,15 @@ class CoupleGarde {
|
|||||||
enfant: CoupleMembre.fromJson(
|
enfant: CoupleMembre.fromJson(
|
||||||
Map<String, dynamic>.from(json['enfant'] ?? const {}),
|
Map<String, dynamic>.from(json['enfant'] ?? const {}),
|
||||||
),
|
),
|
||||||
am: CoupleMembre.fromJson(
|
am: json['am'] != null
|
||||||
Map<String, dynamic>.from(json['am'] ?? const {}),
|
? CoupleMembre.fromJson(Map<String, dynamic>.from(json['am']))
|
||||||
),
|
: null,
|
||||||
|
parents: json['parents'] != null
|
||||||
|
? (json['parents'] as List)
|
||||||
|
.whereType<Map>()
|
||||||
|
.map((e) => CoupleMembre.fromJson(Map<String, dynamic>.from(e)))
|
||||||
|
.toList()
|
||||||
|
: const [],
|
||||||
courant: json['courant'] == true,
|
courant: json['courant'] == true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -67,6 +75,7 @@ class CoupleGarde {
|
|||||||
id: id,
|
id: id,
|
||||||
enfant: enfant,
|
enfant: enfant,
|
||||||
am: am,
|
am: am,
|
||||||
|
parents: parents,
|
||||||
courant: courant ?? this.courant,
|
courant: courant ?? this.courant,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
|
|||||||
_couplesError = null;
|
_couplesError = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
// TODO: Pour l'instant on utilise le service générique de test (CoupleGardeService.getCouplesGarde).
|
final res = await CoupleGardeService.getAmCouplesGarde();
|
||||||
// Dans le futur, l'API pour l'AM (enfants_accueillis) fournira la vraie liste.
|
|
||||||
final res = await CoupleGardeService.getCouplesGarde();
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_couples = res.couples;
|
_couples = res.couples;
|
||||||
@@ -67,7 +65,7 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
|
|||||||
if (couple.id == _selectedCoupleId) return;
|
if (couple.id == _selectedCoupleId) return;
|
||||||
setState(() => _selectedCoupleId = couple.id);
|
setState(() => _selectedCoupleId = couple.id);
|
||||||
try {
|
try {
|
||||||
final res = await CoupleGardeService.definirCoupleCourant(couple.id);
|
final res = await CoupleGardeService.definirAmCoupleCourant(couple.id);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_couples = res.couples;
|
_couples = res.couples;
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ class ApiConfig {
|
|||||||
static const String parentsCoupleGardeCourant =
|
static const String parentsCoupleGardeCourant =
|
||||||
'/parents/me/couples-garde/courant';
|
'/parents/me/couples-garde/courant';
|
||||||
static const String assistantesMaternelles = '/assistantes-maternelles';
|
static const String assistantesMaternelles = '/assistantes-maternelles';
|
||||||
/// Création dossier AM actif par le staff (#156) — body type register AM.
|
/// Couples de garde de l'AM connectée (#170 / #171).
|
||||||
|
static const String assistantesMaternellesCouplesGarde =
|
||||||
|
'/assistantes-maternelles/me/couples-garde';
|
||||||
|
static const String assistantesMaternellesCoupleGardeCourant =
|
||||||
|
'/assistantes-maternelles/me/couples-garde/courant';
|
||||||
static const String assistantesMaternellesDossier =
|
static const String assistantesMaternellesDossier =
|
||||||
'/assistantes-maternelles/dossier';
|
'/assistantes-maternelles/dossier';
|
||||||
static const String enfants = '/enfants';
|
static const String enfants = '/enfants';
|
||||||
|
|||||||
@@ -73,4 +73,45 @@ class CoupleGardeService {
|
|||||||
Map<String, dynamic>.from(decoded as Map),
|
Map<String, dynamic>.from(decoded as Map),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Liste les couples de garde et le couple courant de l'AM connectée.
|
||||||
|
static Future<CouplesGardeResponse> getAmCouplesGarde() async {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternellesCouplesGarde}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(
|
||||||
|
_extractError(response.body, 'Erreur chargement des couples de garde (AM)'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
return CouplesGardeResponse.fromJson(
|
||||||
|
Map<String, dynamic>.from(decoded as Map),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persiste le couple courant (AM) et renvoie la liste à jour.
|
||||||
|
static Future<CouplesGardeResponse> definirAmCoupleCourant(
|
||||||
|
String coupleId,
|
||||||
|
) async {
|
||||||
|
final response = await http.put(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternellesCoupleGardeCourant}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
body: jsonEncode({'couple_id': coupleId}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(
|
||||||
|
_extractError(response.body, 'Erreur sélection du couple de garde (AM)'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
return CouplesGardeResponse.fromJson(
|
||||||
|
Map<String, dynamic>.from(decoded as Map),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,17 +133,46 @@ class _CoupleCard extends StatelessWidget {
|
|||||||
required this.colorIndex,
|
required this.colorIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Libellé parents côté AM : « Sophie » ou « Thomas et Claire ».
|
||||||
|
static String _parentsLabel(List<CoupleMembre> parents) {
|
||||||
|
if (parents.isEmpty) return 'Parents';
|
||||||
|
String prenomOf(CoupleMembre p) {
|
||||||
|
final prenom = (p.prenom ?? '').trim();
|
||||||
|
if (prenom.isNotEmpty) return prenom;
|
||||||
|
return p.displayName(fallback: 'Parent');
|
||||||
|
}
|
||||||
|
if (parents.length == 1) return prenomOf(parents.first);
|
||||||
|
return '${prenomOf(parents[0])} et ${prenomOf(parents[1])}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Parent mode: enfant | AM
|
// Parent mode: enfant | AM (photo + prénom)
|
||||||
// AM mode: enfant | parent(s)
|
// AM mode: enfant | parent(s) — texte seul, pas de vignette parent
|
||||||
final isParentMode = mode == CoupleBandeauMode.parent;
|
final isParentMode = mode == CoupleBandeauMode.parent;
|
||||||
final fallbackRoleIcon = isParentMode ? Icons.volunteer_activism : Icons.person_outline;
|
|
||||||
final fallbackRoleName = isParentMode ? 'Nounou' : 'Parent';
|
|
||||||
|
|
||||||
// TODO: In AM mode, we should ideally display "parent1+parent2 empilés" or centered.
|
Widget amOrParentsWidget;
|
||||||
// For now, CoupleGarde only gives us `am` (which in AM mode might just represent one parent, or the system needs to feed both parents here).
|
|
||||||
// Assuming backend will populate `am` field with the parent data when called by AM.
|
if (isParentMode) {
|
||||||
|
amOrParentsWidget = _MembreTile(
|
||||||
|
photoUrl: couple.am?.photoUrl,
|
||||||
|
name: (couple.am?.prenom != null && couple.am!.prenom!.isNotEmpty)
|
||||||
|
? couple.am!.prenom!
|
||||||
|
: (couple.am?.displayName(fallback: 'Nounou') ?? 'Nounou'),
|
||||||
|
fallbackIcon: Icons.volunteer_activism,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
amOrParentsWidget = Text(
|
||||||
|
_parentsLabel(couple.parents),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: QuotidienTheme.ink,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 90,
|
height: 90,
|
||||||
@@ -181,13 +210,7 @@ class _CoupleCard extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 12),
|
padding: const EdgeInsets.only(left: 12),
|
||||||
child: _MembreTile(
|
child: amOrParentsWidget,
|
||||||
photoUrl: couple.am.photoUrl,
|
|
||||||
name: (couple.am.prenom != null && couple.am.prenom!.isNotEmpty)
|
|
||||||
? couple.am.prenom!
|
|
||||||
: couple.am.displayName(fallback: fallbackRoleName),
|
|
||||||
fallbackIcon: fallbackRoleIcon,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|||||||
Reference in New Issue
Block a user