Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
946d8edcd2 | ||
|
|
c8cb82dd24 | ||
|
|
9cd180bf6a |
@@ -33,7 +33,6 @@ model Child {
|
||||
dateOfBirth DateTime
|
||||
photoUrl String?
|
||||
photoConsent Boolean @default(false)
|
||||
isMultiple Boolean @default(false)
|
||||
isUnborn Boolean @default(false)
|
||||
parentId String
|
||||
parent Parent @relation(fields: [parentId], references: [id])
|
||||
|
||||
@@ -63,9 +63,6 @@ export class Children {
|
||||
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
|
||||
consent_photo_at?: Date;
|
||||
|
||||
@Column({ default: false, name: 'est_multiple', type: 'boolean' })
|
||||
is_multiple: boolean;
|
||||
|
||||
// Lien via table de jointure enfants_parents
|
||||
@OneToMany(() => ParentsChildren, pc => pc.child)
|
||||
parentLinks: ParentsChildren[];
|
||||
|
||||
@@ -564,7 +564,6 @@ export class AuthService {
|
||||
enfant.status = enfantDto.date_naissance ? StatutEnfantType.SANS_GARDE : StatutEnfantType.A_NAITRE;
|
||||
enfant.consent_photo = !!enfantDto.consent_photo;
|
||||
enfant.consent_photo_at = enfant.consent_photo ? new Date() : null!;
|
||||
enfant.is_multiple = enfantDto.grossesse_multiple || false;
|
||||
|
||||
const enfantEnregistre = await manager.save(Children, enfant);
|
||||
enfantsEnregistres.push(enfantEnregistre);
|
||||
@@ -1387,9 +1386,6 @@ export class AuthService {
|
||||
enfant.status = StatutEnfantType.A_NAITRE;
|
||||
}
|
||||
}
|
||||
if (enfantDto.grossesse_multiple !== undefined) {
|
||||
enfant.is_multiple = enfantDto.grossesse_multiple;
|
||||
}
|
||||
if (enfantDto.consent_photo !== undefined) {
|
||||
enfant.consent_photo = !!enfantDto.consent_photo;
|
||||
enfant.consent_photo_at = enfant.consent_photo
|
||||
|
||||
@@ -55,11 +55,6 @@ export class EnfantInscriptionDto {
|
||||
@IsString()
|
||||
photo_filename?: string;
|
||||
|
||||
@ApiProperty({ example: false, required: false, description: 'Grossesse multiple (jumeaux, triplés, etc.)' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
grossesse_multiple?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
required: false,
|
||||
|
||||
@@ -74,11 +74,6 @@ export class CreateEnfantsDto {
|
||||
@IsDateString()
|
||||
consent_photo_at?: string;
|
||||
|
||||
@ApiProperty({ default: false })
|
||||
@Transform(toBoolean)
|
||||
@IsBoolean()
|
||||
is_multiple: boolean;
|
||||
|
||||
/**
|
||||
* Parent pivot du foyer — obligatoire pour staff (gestionnaire/admin).
|
||||
* Ignoré / interdit en externe pour un PARENT (ticket #132).
|
||||
|
||||
@@ -29,9 +29,6 @@ export class EnfantResponseDto {
|
||||
@ApiProperty({ example: false })
|
||||
consent_photo: boolean;
|
||||
|
||||
@ApiProperty({ example: false })
|
||||
is_multiple: boolean;
|
||||
|
||||
@ApiProperty({ example: 'UUID-parent' })
|
||||
parent_id: string;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ export class EnfantsService {
|
||||
photo_url: photoUrl,
|
||||
consent_photo: !!dto.consent_photo,
|
||||
consent_photo_at: consentAt,
|
||||
is_multiple: !!dto.is_multiple,
|
||||
});
|
||||
await this.childrenRepository.save(child);
|
||||
|
||||
|
||||
@@ -54,9 +54,6 @@ export class DossierFamilleEnfantDto {
|
||||
description: 'Consentement affichage photo (colonne consentement_photo)',
|
||||
})
|
||||
consent_photo?: boolean;
|
||||
|
||||
@ApiProperty({ required: false, description: 'Grossesse multiple (est_multiple)' })
|
||||
est_multiple?: boolean;
|
||||
}
|
||||
|
||||
/** Réponse GET /parents/dossier-famille/:numeroDossier – dossier famille complet. Ticket #119 */
|
||||
|
||||
@@ -370,7 +370,6 @@ export class ParentsService {
|
||||
status: child.status,
|
||||
photo_url: child.photo_url ?? undefined,
|
||||
consent_photo: child.consent_photo,
|
||||
est_multiple: child.is_multiple,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import 'reflect-metadata';
|
||||
import { GestionnairesController } from './gestionnaires.controller';
|
||||
import { GestionnairesService } from './gestionnaires.service';
|
||||
import { RoleType } from 'src/entities/users.entity';
|
||||
|
||||
describe('GestionnairesController', () => {
|
||||
let controller: GestionnairesController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [GestionnairesController],
|
||||
providers: [GestionnairesService],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<GestionnairesController>(GestionnairesController);
|
||||
describe('GestionnairesController roles (#161)', () => {
|
||||
it('POST /gestionnaires autorise SUPER_ADMIN et ADMINISTRATEUR', () => {
|
||||
const roles = Reflect.getMetadata('roles', GestionnairesController.prototype.create);
|
||||
expect(roles).toEqual(
|
||||
expect.arrayContaining([RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR]),
|
||||
);
|
||||
expect(roles).not.toContain(RoleType.GESTIONNAIRE);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
it('PATCH /gestionnaires/:id autorise SUPER_ADMIN et ADMINISTRATEUR', () => {
|
||||
const roles = Reflect.getMetadata('roles', GestionnairesController.prototype.update);
|
||||
expect(roles).toEqual(
|
||||
expect.arrayContaining([RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR]),
|
||||
);
|
||||
expect(roles).not.toContain(RoleType.GESTIONNAIRE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,10 +25,10 @@ import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||
export class GestionnairesController {
|
||||
constructor(private readonly gestionnairesService: GestionnairesService) { }
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN)
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiResponse({ status: 201, description: 'Le gestionnaire a été créé avec succès.', type: Users })
|
||||
@ApiResponse({ status: 409, description: 'Conflit. L\'email est déjà utilisé.' })
|
||||
@ApiOperation({ summary: 'Création d\'un gestionnaire' })
|
||||
@ApiOperation({ summary: 'Création d\'un gestionnaire (admin / super admin)' })
|
||||
@ApiBody({ type: CreateGestionnaireDto })
|
||||
@Post()
|
||||
create(@Body() dto: CreateGestionnaireDto): Promise<Users> {
|
||||
@@ -43,7 +43,7 @@ export class GestionnairesController {
|
||||
return this.gestionnairesService.findAll();
|
||||
}
|
||||
|
||||
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
|
||||
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Récupérer un gestionnaire par ID' })
|
||||
@ApiResponse({ status: 400, description: 'ID invalide' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||
@@ -56,8 +56,8 @@ export class GestionnairesController {
|
||||
return this.gestionnairesService.findOne(id);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN)
|
||||
@ApiOperation({ summary: 'Mettre à jour un gestionnaire' })
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Mettre à jour un gestionnaire (admin / super admin)' })
|
||||
@ApiResponse({ status: 200, description: 'Le gestionnaire a été mis à jour avec succès.', type: Users })
|
||||
@ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import 'reflect-metadata';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
import { RoleType } from 'src/entities/users.entity';
|
||||
|
||||
describe('UserController', () => {
|
||||
let controller: UserController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<UserController>(UserController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
describe('UserController roles (#161)', () => {
|
||||
it('POST /users/admin autorise SUPER_ADMIN et ADMINISTRATEUR', () => {
|
||||
const roles = Reflect.getMetadata('roles', UserController.prototype.createAdmin);
|
||||
expect(roles).toEqual(
|
||||
expect.arrayContaining([RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR]),
|
||||
);
|
||||
expect(roles).not.toContain(RoleType.GESTIONNAIRE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,10 +22,10 @@ export class UserController {
|
||||
private readonly suppressionService: SuppressionService,
|
||||
) { }
|
||||
|
||||
// Création d'un administrateur (réservée aux super admins)
|
||||
// Création d'un administrateur (admin + super admin) — #161
|
||||
@Post('admin')
|
||||
@Roles(RoleType.SUPER_ADMIN)
|
||||
@ApiOperation({ summary: 'Créer un nouvel administrateur (super admin seulement)' })
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Créer un nouvel administrateur (admin / super admin)' })
|
||||
createAdmin(
|
||||
@Body() dto: CreateAdminDto,
|
||||
@User() currentUser: Users
|
||||
|
||||
@@ -1,18 +1,87 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
|
||||
|
||||
describe('UserService.createAdmin (#161)', () => {
|
||||
const usersRepository = {
|
||||
findOneBy: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserService],
|
||||
}).compile();
|
||||
const dto = {
|
||||
email: 'nouveau.admin@ptits-pas.fr',
|
||||
password: 'Password1!',
|
||||
prenom: 'Nina',
|
||||
nom: 'Admin',
|
||||
telephone: '0601020304',
|
||||
};
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new UserService(
|
||||
usersRepository as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
it('autorise un administrateur à créer un admin', async () => {
|
||||
usersRepository.findOneBy.mockResolvedValue(null);
|
||||
usersRepository.create.mockImplementation((data) => data);
|
||||
usersRepository.save.mockImplementation(async (entity) => ({
|
||||
id: 'new-admin',
|
||||
...entity,
|
||||
}));
|
||||
|
||||
const result = await service.createAdmin(dto as never, {
|
||||
id: 'admin-1',
|
||||
role: RoleType.ADMINISTRATEUR,
|
||||
} as never);
|
||||
|
||||
expect(result.role).toBe(RoleType.ADMINISTRATEUR);
|
||||
expect(result.statut).toBe(StatutUtilisateurType.ACTIF);
|
||||
expect(usersRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('autorise un super_admin à créer un admin', async () => {
|
||||
usersRepository.findOneBy.mockResolvedValue(null);
|
||||
usersRepository.create.mockImplementation((data) => data);
|
||||
usersRepository.save.mockImplementation(async (entity) => ({
|
||||
id: 'new-admin',
|
||||
...entity,
|
||||
}));
|
||||
|
||||
await expect(
|
||||
service.createAdmin(dto as never, {
|
||||
id: 'sa-1',
|
||||
role: RoleType.SUPER_ADMIN,
|
||||
} as never),
|
||||
).resolves.toMatchObject({ role: RoleType.ADMINISTRATEUR });
|
||||
});
|
||||
|
||||
it('refuse un gestionnaire (403 métier)', async () => {
|
||||
await expect(
|
||||
service.createAdmin(dto as never, {
|
||||
id: 'gest-1',
|
||||
role: RoleType.GESTIONNAIRE,
|
||||
} as never),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
expect(usersRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuse un email déjà utilisé', async () => {
|
||||
usersRepository.findOneBy.mockResolvedValue({ id: 'exists' });
|
||||
await expect(
|
||||
service.createAdmin(dto as never, {
|
||||
id: 'admin-1',
|
||||
role: RoleType.ADMINISTRATEUR,
|
||||
} as never),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,8 +117,14 @@ export class UserService {
|
||||
}
|
||||
|
||||
async createAdmin(dto: CreateAdminDto, currentUser: Users): Promise<Users> {
|
||||
if (currentUser.role !== RoleType.SUPER_ADMIN) {
|
||||
throw new ForbiddenException('Seuls les super administrateurs peuvent créer un administrateur');
|
||||
// #161 — admin et super_admin peuvent créer un administrateur
|
||||
if (
|
||||
currentUser.role !== RoleType.SUPER_ADMIN &&
|
||||
currentUser.role !== RoleType.ADMINISTRATEUR
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
'Seuls les administrateurs et super administrateurs peuvent créer un administrateur',
|
||||
);
|
||||
}
|
||||
|
||||
const exist = await this.usersRepository.findOneBy({ email: dto.email });
|
||||
|
||||
+1
-2
@@ -174,8 +174,7 @@ CREATE TABLE enfants (
|
||||
date_prevue_naissance DATE,
|
||||
photo_url TEXT,
|
||||
consentement_photo BOOLEAN DEFAULT false,
|
||||
date_consentement_photo TIMESTAMPTZ,
|
||||
est_multiple BOOLEAN DEFAULT false
|
||||
date_consentement_photo TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- ==========================================================
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo","est_multiple"
|
||||
"5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","sans_garde","Emma","Dupont","F","2020-06-01",,,False,,False
|
||||
"a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","sans_garde",,,,"2020-01-01","2025-01-01",,False,,False
|
||||
"e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","sans_garde","Emma","Martin",,"2023-02-15",,,False,,False
|
||||
"e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d","sans_garde","Noah","Martin",,"2023-02-15",,,False,,False
|
||||
"e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e","sans_garde","Léa","Martin",,"2023-02-15",,,False,,False
|
||||
"e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f","sans_garde","Chloé","Rousseau",,"2022-04-20",,,False,,False
|
||||
"e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","sans_garde","Hugo","Rousseau",,"2024-03-10",,,False,,False
|
||||
"e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b","sans_garde","Maxime","Lecomte",,"2023-04-15",,,False,,False
|
||||
"edd19cd1-bb67-4f14-8a37-c66b75c94537","scolarise","Lucas","Durand","H","2018-09-15",,,False,,False
|
||||
"id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo"
|
||||
"5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","sans_garde","Emma","Dupont","F","2020-06-01",,,False,
|
||||
"a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","sans_garde",,,,"2020-01-01","2025-01-01",,False,
|
||||
"e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","sans_garde","Emma","Martin",,"2023-02-15",,,False,
|
||||
"e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d","sans_garde","Noah","Martin",,"2023-02-15",,,False,
|
||||
"e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e","sans_garde","Léa","Martin",,"2023-02-15",,,False,
|
||||
"e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f","sans_garde","Chloé","Rousseau",,"2022-04-20",,,False,
|
||||
"e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","sans_garde","Hugo","Rousseau",,"2024-03-10",,,False,
|
||||
"e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b","sans_garde","Maxime","Lecomte",,"2023-04-15",,,False,
|
||||
"edd19cd1-bb67-4f14-8a37-c66b75c94537","scolarise","Lucas","Durand","H","2018-09-15",,,False,
|
||||
|
||||
|
@@ -0,0 +1,2 @@
|
||||
-- #152 — Suppression grossesse multiple / est_multiple
|
||||
ALTER TABLE enfants DROP COLUMN IF EXISTS est_multiple;
|
||||
@@ -69,12 +69,12 @@ ON CONFLICT (id_utilisateur) DO NOTHING;
|
||||
-- - child B : à naître (statut = 'a_naitre' et date_prevue_naissance requise)
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
INSERT INTO enfants (id, prenom, nom, statut, date_naissance, jumeau_multiple)
|
||||
VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'sans_garde', '2022-04-12', false)
|
||||
INSERT INTO enfants (id, prenom, nom, statut, date_naissance)
|
||||
VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'sans_garde', '2022-04-12')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance, jumeau_multiple)
|
||||
VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Mila', 'Parent', 'a_naitre', '2026-02-15', false)
|
||||
INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance)
|
||||
VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Mila', 'Parent', 'a_naitre', '2026-02-15')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
@@ -49,14 +49,14 @@ VALUES
|
||||
ON CONFLICT (id_utilisateur) DO NOTHING;
|
||||
|
||||
-- ========== ENFANTS ==========
|
||||
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple)
|
||||
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut)
|
||||
VALUES
|
||||
('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'sans_garde', true),
|
||||
('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'sans_garde', true),
|
||||
('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'sans_garde', true),
|
||||
('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'sans_garde', false),
|
||||
('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'sans_garde', false),
|
||||
('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'sans_garde', false)
|
||||
('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'sans_garde'),
|
||||
('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'sans_garde'),
|
||||
('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'sans_garde'),
|
||||
('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'sans_garde'),
|
||||
('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'sans_garde'),
|
||||
('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'sans_garde')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ========== ENFANTS_PARENTS (liaison N:N) ==========
|
||||
|
||||
@@ -117,7 +117,6 @@ Table des enfants pris en charge.
|
||||
| `photo_url` | TEXT | | URL de la photo |
|
||||
| `consentement_photo` | BOOLEAN | DEFAULT false | Consentement photo |
|
||||
| `date_consentement_photo` | TIMESTAMPTZ | | Date du consentement |
|
||||
| `est_multiple` | BOOLEAN | DEFAULT false | Indique si grossesse multiple |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -276,9 +276,6 @@ export class Enfants {
|
||||
@Column({ name: 'consentement_photo', type: 'boolean', default: false })
|
||||
consentementPhoto: boolean;
|
||||
|
||||
@Column({ name: 'est_multiple', type: 'boolean', default: false })
|
||||
estMultiple: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: StatutEnfantType,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Mini-spec — Suppression complète grossesse multiple / `est_multiple`
|
||||
|
||||
**Ticket** : **#152** — https://git.ptits-pas.fr/jmartin/petitspas/issues/152
|
||||
**Branche** : `feature/152-remove-est-multiple` (depuis `develop`)
|
||||
**Périmètre** : **full stack** — BDD + back + front + scripts + docs. **Aucun fantôme.**
|
||||
|
||||
---
|
||||
|
||||
## Décision
|
||||
|
||||
On **supprime tout**. Pas de DTO « ignorés », pas de compat payload.
|
||||
|
||||
`forbidNonWhitelisted: true` ⇒ back et front **partent ensemble** (même feature / même déploiement).
|
||||
|
||||
---
|
||||
|
||||
## Alias retirés
|
||||
|
||||
`est_multiple` · `is_multiple` · `grossesse_multiple` · `multipleBirth` · `estMultiple` · `isMultiple` · `jumeau_multiple`
|
||||
|
||||
---
|
||||
|
||||
## Back / BDD (fait sur la branche)
|
||||
|
||||
- [x] Migration `database/migrations/2026_drop_enfants_est_multiple.sql`
|
||||
- [x] `BDD.sql`, seeds, CSV test
|
||||
- [x] Entity `Children` sans colonne
|
||||
- [x] DTO create/inscription/réponse/dossier famille **sans** le champ
|
||||
- [x] Services auth / enfants / parents : plus de mapping
|
||||
- [x] Prisma legacy `isMultiple` retiré
|
||||
|
||||
## Front (fait sur la branche)
|
||||
|
||||
- [x] Modèles admin / dossier / inscription
|
||||
- [x] Payloads inscription + reprise
|
||||
- [x] Modale enfant + wizard dossier famille
|
||||
- [x] Step3 inscription parent
|
||||
|
||||
## Scripts / docs
|
||||
|
||||
- [x] `tests/scripts/register-parent-*.mjs`
|
||||
- [x] `docs/10_DATABASE.md`, `docs/99_REGLES-CODAGE.md`
|
||||
- Docs tmp/archive #112 : mentions historiques OK (archive)
|
||||
|
||||
---
|
||||
|
||||
## Déploiement
|
||||
|
||||
1. Appliquer la migration SQL sur la BDD vivante
|
||||
2. Deploy back **et** front de cette branche
|
||||
3. Smoke : création enfant staff, inscription parent, reprise, wizard famille
|
||||
|
||||
## Vérif
|
||||
|
||||
```bash
|
||||
rg -n 'est_multiple|is_multiple|grossesse_multiple|multipleBirth|estMultiple|isMultiple|jumeau_multiple' \
|
||||
backend/src frontend/lib database tests/scripts docs/10_DATABASE.md docs/99_REGLES-CODAGE.md
|
||||
```
|
||||
→ **0** hit (hors ce fichier mini-spec / archives).
|
||||
|
||||
## Hors scope
|
||||
|
||||
- Métier futur « fratrie / jumeaux » → nouveau ticket
|
||||
- #155 rename Admin*
|
||||
- Ticket modales staff
|
||||
@@ -185,7 +185,6 @@ class EnfantDossier {
|
||||
final String? dueDate;
|
||||
final String? photoUrl;
|
||||
final bool consentPhoto;
|
||||
final bool estMultiple;
|
||||
|
||||
EnfantDossier({
|
||||
required this.id,
|
||||
@@ -197,7 +196,6 @@ class EnfantDossier {
|
||||
this.dueDate,
|
||||
this.photoUrl,
|
||||
this.consentPhoto = false,
|
||||
this.estMultiple = false,
|
||||
});
|
||||
|
||||
String get fullName => '${firstName ?? ''} ${lastName ?? ''}'.trim();
|
||||
@@ -231,8 +229,6 @@ class EnfantDossier {
|
||||
photoUrl: resolvedPhoto,
|
||||
consentPhoto:
|
||||
json['consent_photo'] == true || json['consentPhoto'] == true,
|
||||
estMultiple:
|
||||
json['est_multiple'] == true || json['estMultiple'] == true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ class EnfantAdminModel {
|
||||
final String status;
|
||||
final String? photoUrl;
|
||||
final bool consentPhoto;
|
||||
final bool isMultiple;
|
||||
final List<EnfantParentLink> parentLinks;
|
||||
/// Flag API #157 (sinon déduit de [parentLinks]).
|
||||
final bool? sansResponsable;
|
||||
@@ -27,7 +26,6 @@ class EnfantAdminModel {
|
||||
required this.status,
|
||||
this.photoUrl,
|
||||
this.consentPhoto = false,
|
||||
this.isMultiple = false,
|
||||
this.parentLinks = const [],
|
||||
this.sansResponsable,
|
||||
});
|
||||
@@ -56,7 +54,6 @@ class EnfantAdminModel {
|
||||
String? status,
|
||||
String? photoUrl,
|
||||
bool? consentPhoto,
|
||||
bool? isMultiple,
|
||||
List<EnfantParentLink>? parentLinks,
|
||||
bool? sansResponsable,
|
||||
}) {
|
||||
@@ -70,7 +67,6 @@ class EnfantAdminModel {
|
||||
status: status ?? this.status,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
consentPhoto: consentPhoto ?? this.consentPhoto,
|
||||
isMultiple: isMultiple ?? this.isMultiple,
|
||||
parentLinks: parentLinks ?? this.parentLinks,
|
||||
sansResponsable: sansResponsable ?? this.sansResponsable,
|
||||
);
|
||||
@@ -111,8 +107,6 @@ class EnfantAdminModel {
|
||||
),
|
||||
photoUrl: photoUrl,
|
||||
consentPhoto: consentPhoto,
|
||||
isMultiple: _parseBool(json['is_multiple']) ||
|
||||
_parseBool(json['est_multiple']),
|
||||
parentLinks: links,
|
||||
sansResponsable: sansResponsable,
|
||||
);
|
||||
@@ -127,7 +121,6 @@ class EnfantAdminModel {
|
||||
if (birthDate != null && birthDate!.isNotEmpty) 'birth_date': birthDate,
|
||||
if (dueDate != null && dueDate!.isNotEmpty) 'due_date': dueDate,
|
||||
'consent_photo': consentPhoto,
|
||||
'is_multiple': isMultiple,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class ChildData {
|
||||
String lastName;
|
||||
String dob; // Date de naissance ou prévisionnelle
|
||||
bool photoConsent;
|
||||
bool multipleBirth;
|
||||
bool isUnbornChild;
|
||||
File? imageFile;
|
||||
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
|
||||
@@ -40,7 +39,6 @@ class ChildData {
|
||||
this.lastName = '',
|
||||
this.dob = '',
|
||||
this.photoConsent = false,
|
||||
this.multipleBirth = false,
|
||||
this.isUnbornChild = false,
|
||||
this.imageFile,
|
||||
required this.cardColor, // Rendre requis dans le constructeur
|
||||
|
||||
@@ -38,7 +38,6 @@ class ChildData {
|
||||
/// Valeurs API : `H`, `F`, `Autre` (GenreType backend). Vide tant que non choisi.
|
||||
String genre;
|
||||
bool photoConsent;
|
||||
bool multipleBirth;
|
||||
bool isUnbornChild;
|
||||
File? imageFile;
|
||||
/// Octets de la photo (fiable à l’envoi API ; [imageFile] peut être absent sur le web).
|
||||
@@ -55,7 +54,6 @@ class ChildData {
|
||||
this.dob = '',
|
||||
this.genre = '',
|
||||
this.photoConsent = false,
|
||||
this.multipleBirth = false,
|
||||
this.isUnbornChild = false,
|
||||
this.imageFile,
|
||||
this.imageBytes,
|
||||
@@ -70,7 +68,6 @@ class ChildData {
|
||||
String? dob,
|
||||
String? genre,
|
||||
bool? photoConsent,
|
||||
bool? multipleBirth,
|
||||
bool? isUnbornChild,
|
||||
Object? imageFile = _unsetImage,
|
||||
Object? imageBytes = _unsetImageBytes,
|
||||
@@ -84,7 +81,6 @@ class ChildData {
|
||||
dob: dob ?? this.dob,
|
||||
genre: genre ?? this.genre,
|
||||
photoConsent: photoConsent ?? this.photoConsent,
|
||||
multipleBirth: multipleBirth ?? this.multipleBirth,
|
||||
isUnbornChild: isUnbornChild ?? this.isUnbornChild,
|
||||
imageFile: identical(imageFile, _unsetImage) ? this.imageFile : imageFile as File?,
|
||||
imageBytes:
|
||||
|
||||
@@ -121,7 +121,6 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
dob: '',
|
||||
isUnbornChild: false,
|
||||
photoConsent: false,
|
||||
multipleBirth: false,
|
||||
cardColor: cardColor,
|
||||
);
|
||||
registrationData.addChild(newChild);
|
||||
|
||||
@@ -62,7 +62,10 @@ class _GestionnaireDashboardScreenState extends State<GestionnaireDashboardScree
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UserManagementPanel(showAdministrateursTab: false),
|
||||
child: UserManagementPanel(
|
||||
showAdministrateursTab: false,
|
||||
allowStaffAccountCreation: false,
|
||||
),
|
||||
),
|
||||
const AppFooter(),
|
||||
],
|
||||
|
||||
@@ -166,7 +166,6 @@ class ParentRegistrationPayload {
|
||||
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
|
||||
final map = <String, dynamic>{
|
||||
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
|
||||
'grossesse_multiple': c.multipleBirth,
|
||||
'consent_photo': c.photoConsent,
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ class RepriseMapper {
|
||||
dob: dob,
|
||||
genre: e.gender ?? '',
|
||||
photoConsent: e.consentPhoto,
|
||||
multipleBirth: e.estMultiple,
|
||||
isUnbornChild: isUnborn,
|
||||
cardColor: _childCardColors[index % _childCardColors.length],
|
||||
repriseChildId: e.id,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/// Création comptes staff (gestionnaire / admin) — ticket #161.
|
||||
bool canCreateStaffAccounts(String? role) {
|
||||
final r = (role ?? '').trim().toLowerCase();
|
||||
return r == 'administrateur' || r == 'super_admin';
|
||||
}
|
||||
|
||||
/// Droits d’affichage poubelle — tickets #154 / #160.
|
||||
bool canDeleteMetier(String? role) {
|
||||
final r = (role ?? '').trim().toLowerCase();
|
||||
|
||||
@@ -55,7 +55,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
late String _status;
|
||||
late String? _gender;
|
||||
late bool _consentPhoto;
|
||||
late bool _isMultiple;
|
||||
bool _dirty = false;
|
||||
bool _saving = false;
|
||||
bool _deleting = false;
|
||||
@@ -142,7 +141,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
_gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn);
|
||||
}
|
||||
_consentPhoto = e?.consentPhoto ?? false;
|
||||
_isMultiple = e?.isMultiple ?? false;
|
||||
for (final c in [_prenomCtrl, _nomCtrl]) {
|
||||
c.addListener(_onNameChanged);
|
||||
}
|
||||
@@ -383,7 +381,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
else if (_dateToIso(_birthCtrl.text) != null)
|
||||
'birth_date': _dateToIso(_birthCtrl.text),
|
||||
'consent_photo': _consentPhoto,
|
||||
'is_multiple': _isMultiple,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
@@ -460,7 +457,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
||||
'birth_date': _dateToIso(_birthCtrl.text),
|
||||
},
|
||||
'consent_photo': _consentPhoto,
|
||||
'is_multiple': _isMultiple,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -1466,7 +1466,6 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
final map = <String, dynamic>{
|
||||
'genre': c.genre,
|
||||
'consent_photo': true,
|
||||
'grossesse_multiple': false,
|
||||
};
|
||||
|
||||
if (prenom.length >= 2) {
|
||||
@@ -1723,7 +1722,6 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
||||
'status': status,
|
||||
'gender': gender,
|
||||
'consent_photo': true,
|
||||
'is_multiple': false,
|
||||
};
|
||||
if (prenom.length >= 2) map['first_name'] = prenom;
|
||||
if (nom.length >= 2) map['last_name'] = nom;
|
||||
|
||||
@@ -15,9 +15,13 @@ class UserManagementPanel extends StatefulWidget {
|
||||
/// Afficher l'onglet Administrateurs (sinon sans Administrateurs).
|
||||
final bool showAdministrateursTab;
|
||||
|
||||
/// Création gestionnaire / admin (#161). False pour le dashboard gestionnaire.
|
||||
final bool allowStaffAccountCreation;
|
||||
|
||||
const UserManagementPanel({
|
||||
super.key,
|
||||
this.showAdministrateursTab = true,
|
||||
this.allowStaffAccountCreation = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -86,6 +90,15 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
|
||||
bool get _isDossiersTab => _subIndex == 0;
|
||||
|
||||
bool get _isStaffAccountsTab =>
|
||||
_subIndex == 4 || (widget.showAdministrateursTab && _subIndex == 5);
|
||||
|
||||
bool get _canShowAddButton {
|
||||
if (_isDossiersTab) return false;
|
||||
if (_isStaffAccountsTab && !widget.allowStaffAccountCreation) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
String _searchHintForTab() {
|
||||
switch (_subIndex) {
|
||||
case 0:
|
||||
@@ -293,7 +306,8 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
searchTooltip: _searchTooltipForTab(),
|
||||
filterControl: _subBarFilterControl(),
|
||||
// Pas de « Créer » sur l’onglet Dossiers (#153).
|
||||
onAddPressed: _isDossiersTab ? null : _handleAddPressed,
|
||||
// Pas de création staff pour le dashboard gestionnaire (#161).
|
||||
onAddPressed: _canShowAddButton ? _handleAddPressed : null,
|
||||
addLabel: 'Ajouter',
|
||||
subTabCount: labels.length,
|
||||
tabLabels: labels,
|
||||
@@ -305,6 +319,9 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
|
||||
Future<void> _handleAddPressed() async {
|
||||
// 1 Parents, 2 Enfants, 3 AM, 4 Gestionnaires, 5 Admin
|
||||
if (_isStaffAccountsTab && !widget.allowStaffAccountCreation) {
|
||||
return;
|
||||
}
|
||||
if (_subIndex == 1) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
|
||||
@@ -74,8 +74,7 @@ try {
|
||||
date_naissance: '2022-04-20',
|
||||
genre: 'F',
|
||||
photo_base64: toDataUri(chloeJpg),
|
||||
photo_filename: 'chloe_rousseau.jpg',
|
||||
grossesse_multiple: false,
|
||||
photo_filename: 'chloe_rousseau.jpg'
|
||||
},
|
||||
{
|
||||
prenom: 'Hugo',
|
||||
@@ -83,8 +82,7 @@ try {
|
||||
date_naissance: '2024-03-10',
|
||||
genre: 'H',
|
||||
photo_base64: toDataUri(hugoJpg),
|
||||
photo_filename: 'hugo_rousseau.jpg',
|
||||
grossesse_multiple: false,
|
||||
photo_filename: 'hugo_rousseau.jpg'
|
||||
},
|
||||
],
|
||||
presentation_dossier: presentationDossier,
|
||||
|
||||
@@ -40,8 +40,7 @@ const body = {
|
||||
date_naissance: '2023-04-15',
|
||||
genre: 'H',
|
||||
photo_base64: toDataUri(path.join(photosDir, 'lecomte-maxime.png')),
|
||||
photo_filename: 'maxime_lecomte.png',
|
||||
grossesse_multiple: false,
|
||||
photo_filename: 'maxime_lecomte.png'
|
||||
},
|
||||
],
|
||||
presentation_dossier: presentationDossier,
|
||||
|
||||
@@ -46,8 +46,7 @@ const body = {
|
||||
date_naissance: '2023-02-15',
|
||||
genre: 'F',
|
||||
photo_base64: toDataUri(path.join(photosDir, 'martin-emma.png')),
|
||||
photo_filename: 'emma_martin.png',
|
||||
grossesse_multiple: true,
|
||||
photo_filename: 'emma_martin.png'
|
||||
},
|
||||
{
|
||||
prenom: 'Noah',
|
||||
@@ -55,8 +54,7 @@ const body = {
|
||||
date_naissance: '2023-02-15',
|
||||
genre: 'H',
|
||||
photo_base64: toDataUri(path.join(photosDir, 'martin-noah.png')),
|
||||
photo_filename: 'noah_martin.png',
|
||||
grossesse_multiple: true,
|
||||
photo_filename: 'noah_martin.png'
|
||||
},
|
||||
{
|
||||
prenom: 'Léa',
|
||||
@@ -64,8 +62,7 @@ const body = {
|
||||
date_naissance: '2023-02-15',
|
||||
genre: 'F',
|
||||
photo_base64: toDataUri(path.join(photosDir, 'martin-lea.png')),
|
||||
photo_filename: 'lea_martin.png',
|
||||
grossesse_multiple: true,
|
||||
photo_filename: 'lea_martin.png'
|
||||
},
|
||||
],
|
||||
presentation_dossier: presentationDossier,
|
||||
|
||||
Reference in New Issue
Block a user