feat(#131): back placement AM↔enfant + BDD garde/sans_garde
- API PATCH …/fiche, POST/DELETE …/enfants/:enfantId, GET avec amChildren - Table enfants_assistantes_maternelles (option D, 1 garde active/enfant) - Statuts enfant: garde/sans_garde remplacent actif (inscription → sans_garde) - BDD.sql canonique réécrit; migration pour BDD existantes - Seed test: hash bcrypt corrigé pour mot de passe « password » Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4985726bc6
commit
52e40d0001
47
backend/src/entities/am_children.entity.ts
Normal file
47
backend/src/entities/am_children.entity.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { AssistanteMaternelle } from './assistantes_maternelles.entity';
|
||||||
|
import { Children } from './children.entity';
|
||||||
|
import { Users } from './users.entity';
|
||||||
|
|
||||||
|
@Entity('enfants_assistantes_maternelles', { schema: 'public' })
|
||||||
|
export class AmChildren {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'id_am', type: 'uuid' })
|
||||||
|
amId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'id_enfant', type: 'uuid' })
|
||||||
|
enfantId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'date_debut', type: 'date' })
|
||||||
|
date_debut: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'date_fin', type: 'date', nullable: true })
|
||||||
|
date_fin?: Date;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
|
||||||
|
cree_le: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'cree_par', type: 'uuid', nullable: true })
|
||||||
|
cree_par?: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => AssistanteMaternelle, (am) => am.amChildren, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'id_am', referencedColumnName: 'user_id' })
|
||||||
|
am: AssistanteMaternelle;
|
||||||
|
|
||||||
|
@ManyToOne(() => Children, (c) => c.amLinks, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
|
||||||
|
child: Children;
|
||||||
|
|
||||||
|
@ManyToOne(() => Users, { nullable: true, onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'cree_par', referencedColumnName: 'id' })
|
||||||
|
createdBy?: Users;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm';
|
import { Entity, PrimaryColumn, Column, OneToOne, OneToMany, JoinColumn } from 'typeorm';
|
||||||
import { Users } from './users.entity';
|
import { Users } from './users.entity';
|
||||||
|
import { AmChildren } from './am_children.entity';
|
||||||
|
|
||||||
@Entity('assistantes_maternelles')
|
@Entity('assistantes_maternelles')
|
||||||
export class AssistanteMaternelle {
|
export class AssistanteMaternelle {
|
||||||
@ -51,4 +52,7 @@ export class AssistanteMaternelle {
|
|||||||
/** Numéro de dossier (format AAAA-NNNNNN), même valeur que sur utilisateurs (ticket #103) */
|
/** Numéro de dossier (format AAAA-NNNNNN), même valeur que sur utilisateurs (ticket #103) */
|
||||||
@Column({ name: 'numero_dossier', length: 20, nullable: true })
|
@Column({ name: 'numero_dossier', length: 20, nullable: true })
|
||||||
numero_dossier?: string;
|
numero_dossier?: string;
|
||||||
|
|
||||||
|
@OneToMany(() => AmChildren, (ac) => ac.am)
|
||||||
|
amChildren: AmChildren[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Parents } from './parents.entity';
|
import { Parents } from './parents.entity';
|
||||||
import { ParentsChildren } from './parents_children.entity';
|
import { ParentsChildren } from './parents_children.entity';
|
||||||
|
import { AmChildren } from './am_children.entity';
|
||||||
import { Dossier } from './dossiers.entity';
|
import { Dossier } from './dossiers.entity';
|
||||||
|
|
||||||
export enum StatutEnfantType {
|
export enum StatutEnfantType {
|
||||||
A_NAITRE = 'a_naitre',
|
A_NAITRE = 'a_naitre',
|
||||||
ACTIF = 'actif',
|
|
||||||
SCOLARISE = 'scolarise',
|
SCOLARISE = 'scolarise',
|
||||||
|
GARDE = 'garde',
|
||||||
|
SANS_GARDE = 'sans_garde',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GenreType {
|
export enum GenreType {
|
||||||
@ -68,6 +70,9 @@ export class Children {
|
|||||||
@OneToMany(() => ParentsChildren, pc => pc.child)
|
@OneToMany(() => ParentsChildren, pc => pc.child)
|
||||||
parentLinks: ParentsChildren[];
|
parentLinks: ParentsChildren[];
|
||||||
|
|
||||||
|
@OneToMany(() => AmChildren, (ac) => ac.child)
|
||||||
|
amLinks: AmChildren[];
|
||||||
|
|
||||||
// Relation avec Dossier
|
// Relation avec Dossier
|
||||||
@OneToMany(() => Dossier, d => d.child)
|
@OneToMany(() => Dossier, d => d.child)
|
||||||
dossiers: Dossier[];
|
dossiers: Dossier[];
|
||||||
|
|||||||
@ -12,11 +12,14 @@ import { AssistantesMaternellesService } from './assistantes_maternelles.service
|
|||||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
import { Roles } from 'src/common/decorators/roles.decorator';
|
import { Roles } from 'src/common/decorators/roles.decorator';
|
||||||
import { RoleType } from 'src/entities/users.entity';
|
import { RoleType, Users } from 'src/entities/users.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 { 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 { mapAmForApi, mapAmsForApi } from './assistantes_maternelles.mapper';
|
||||||
|
|
||||||
@ApiTags("Assistantes Maternelles")
|
@ApiTags("Assistantes Maternelles")
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
@ -31,28 +34,74 @@ export class AssistantesMaternellesController {
|
|||||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
||||||
@ApiBody({ type: CreateAssistanteDto })
|
@ApiBody({ type: CreateAssistanteDto })
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
async create(@Body() dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||||
return this.assistantesMaternellesService.create(dto);
|
const am = await this.assistantesMaternellesService.create(dto);
|
||||||
|
return mapAmForApi(am);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
|
@ApiOperation({ summary: 'Récupérer la liste des nounous (inclut amChildren actifs) — ticket #131' })
|
||||||
@ApiResponse({ status: 200, description: 'Liste des nounous' })
|
@ApiResponse({ status: 200, description: 'Liste des nounous' })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
||||||
getAll(): Promise<AssistanteMaternelle[]> {
|
async getAll(): Promise<AssistanteMaternelle[]> {
|
||||||
return this.assistantesMaternellesService.findAll();
|
const ams = await this.assistantesMaternellesService.findAll();
|
||||||
|
return mapAmsForApi(ams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@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" })
|
||||||
@ApiOperation({ summary: 'Récupérer une nounou par id' })
|
@ApiOperation({ summary: 'Récupérer une nounou par id (inclut amChildren) — ticket #131' })
|
||||||
@ApiResponse({ status: 200, description: 'Détails de la nounou' })
|
@ApiResponse({ status: 200, description: 'Détails de la nounou' })
|
||||||
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
|
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||||
getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> {
|
async getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> {
|
||||||
return this.assistantesMaternellesService.findOne(user_id);
|
const am = await this.assistantesMaternellesService.findOne(user_id);
|
||||||
|
return mapAmForApi(am);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
|
@Patch(':id/fiche')
|
||||||
|
@ApiBody({ type: UpdateAmFicheAdminDto })
|
||||||
|
@ApiOperation({ summary: 'Mettre à jour la fiche AM (identité + pro) — ticket #131' })
|
||||||
|
@ApiParam({ name: 'id', description: "UUID utilisateur de l'AM" })
|
||||||
|
@ApiResponse({ status: 200, description: 'Fiche AM mise à jour' })
|
||||||
|
async updateFicheAdmin(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateAmFicheAdminDto,
|
||||||
|
): Promise<AssistanteMaternelle> {
|
||||||
|
const am = await this.assistantesMaternellesService.updateFicheAdmin(id, dto);
|
||||||
|
return mapAmForApi(am);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
|
@Post(':id/enfants/:enfantId')
|
||||||
|
@ApiOperation({ summary: 'Rattacher un enfant à une AM (statut enfant → garde) — ticket #131' })
|
||||||
|
@ApiParam({ name: 'id', description: "UUID utilisateur de l'AM" })
|
||||||
|
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||||
|
@ApiResponse({ status: 200, description: 'AM avec enfants mis à jour' })
|
||||||
|
async attachEnfant(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('enfantId') enfantId: string,
|
||||||
|
@User() currentUser: Users,
|
||||||
|
): Promise<AssistanteMaternelle> {
|
||||||
|
const am = await this.assistantesMaternellesService.attachEnfant(id, enfantId, currentUser);
|
||||||
|
return mapAmForApi(am);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
|
@Delete(':id/enfants/:enfantId')
|
||||||
|
@ApiOperation({ summary: "Clôturer le placement d'un enfant chez une AM — ticket #131" })
|
||||||
|
@ApiParam({ name: 'id', description: "UUID utilisateur de l'AM" })
|
||||||
|
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||||
|
@ApiResponse({ status: 200, description: 'AM avec enfants mis à jour' })
|
||||||
|
async detachEnfant(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('enfantId') enfantId: string,
|
||||||
|
): Promise<AssistanteMaternelle> {
|
||||||
|
const am = await this.assistantesMaternellesService.detachEnfant(id, enfantId);
|
||||||
|
return mapAmForApi(am);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||||
@ -63,14 +112,15 @@ export class AssistantesMaternellesController {
|
|||||||
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
||||||
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
async update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||||
return this.assistantesMaternellesService.update(id, dto);
|
const am = await this.assistantesMaternellesService.update(id, dto);
|
||||||
|
return mapAmForApi(am);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Supprimer une nounou' })
|
@ApiOperation({ summary: 'Supprimer une nounou' })
|
||||||
@ApiResponse({ status: 200, description: 'Nounou supprimée avec succès' })
|
@ApiResponse({ status: 200, description: 'Nounou supprimée avec succès' })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins, gestionnaires et administrateurs' })
|
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||||
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
|
||||||
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
@ApiParam({ name: 'id', description: "UUID de la nounou" })
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
|
import { AmChildren } from 'src/entities/am_children.entity';
|
||||||
|
import { sanitizeUserForApi } from '../../common/utils/sanitize-user-for-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sérialisation API fiche AM — ticket #131.
|
||||||
|
* Expose `amChildren` actifs (date_fin null) avec enfant imbriqué, sans secrets user.
|
||||||
|
*/
|
||||||
|
export function mapAmForApi(am: AssistanteMaternelle): AssistanteMaternelle {
|
||||||
|
const activeChildren = (am.amChildren ?? []).filter((link) => !link.date_fin);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...am,
|
||||||
|
user: sanitizeUserForApi(am.user)!,
|
||||||
|
amChildren: activeChildren.map((link) => ({
|
||||||
|
...link,
|
||||||
|
child: link.child,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapAmsForApi(ams: AssistanteMaternelle[]): AssistanteMaternelle[] {
|
||||||
|
return ams.map(mapAmForApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterActiveAmChildren(links: AmChildren[] | undefined): AmChildren[] {
|
||||||
|
return (links ?? []).filter((link) => !link.date_fin);
|
||||||
|
}
|
||||||
@ -2,12 +2,14 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
|
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
|
||||||
import { AssistantesMaternellesController } from './assistantes_maternelles.controller';
|
import { AssistantesMaternellesController } from './assistantes_maternelles.controller';
|
||||||
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 { Children } from 'src/entities/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, Users]),
|
imports: [TypeOrmModule.forFeature([AssistanteMaternelle, AmChildren, Children, Users]),
|
||||||
AuthModule
|
AuthModule
|
||||||
],
|
],
|
||||||
controllers: [AssistantesMaternellesController],
|
controllers: [AssistantesMaternellesController],
|
||||||
|
|||||||
@ -5,11 +5,16 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { 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 { Children, StatutEnfantType } from 'src/entities/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';
|
||||||
|
|
||||||
|
const AM_CHILDREN_RELATIONS = ['user', 'amChildren', 'amChildren.child'] as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssistantesMaternellesService {
|
export class AssistantesMaternellesService {
|
||||||
@ -17,10 +22,13 @@ export class AssistantesMaternellesService {
|
|||||||
@InjectRepository(AssistanteMaternelle)
|
@InjectRepository(AssistanteMaternelle)
|
||||||
private readonly assistantesMaternelleRepository: Repository<AssistanteMaternelle>,
|
private readonly assistantesMaternelleRepository: Repository<AssistanteMaternelle>,
|
||||||
@InjectRepository(Users)
|
@InjectRepository(Users)
|
||||||
private readonly usersRepository: Repository<Users>
|
private readonly usersRepository: Repository<Users>,
|
||||||
|
@InjectRepository(AmChildren)
|
||||||
|
private readonly amChildrenRepository: Repository<AmChildren>,
|
||||||
|
@InjectRepository(Children)
|
||||||
|
private readonly childrenRepository: Repository<Children>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Création d’une assistante maternelle
|
|
||||||
async create(dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
async create(dto: CreateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||||
const user = await this.usersRepository.findOneBy({ id: dto.user_id });
|
const user = await this.usersRepository.findOneBy({ id: dto.user_id });
|
||||||
if (!user) throw new NotFoundException('Utilisateur introuvable');
|
if (!user) throw new NotFoundException('Utilisateur introuvable');
|
||||||
@ -49,30 +57,167 @@ export class AssistantesMaternellesService {
|
|||||||
return this.assistantesMaternelleRepository.save(entity);
|
return this.assistantesMaternelleRepository.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Liste des assistantes maternelles
|
|
||||||
async findAll(): Promise<AssistanteMaternelle[]> {
|
async findAll(): Promise<AssistanteMaternelle[]> {
|
||||||
return this.assistantesMaternelleRepository.find({
|
return this.assistantesMaternelleRepository.find({
|
||||||
relations: ['user'],
|
relations: [...AM_CHILDREN_RELATIONS],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer une assistante maternelle par user_id
|
|
||||||
async findOne(user_id: string): Promise<AssistanteMaternelle> {
|
async findOne(user_id: string): Promise<AssistanteMaternelle> {
|
||||||
const assistante = await this.assistantesMaternelleRepository.findOne({
|
const assistante = await this.assistantesMaternelleRepository.findOne({
|
||||||
where: { user_id },
|
where: { user_id },
|
||||||
relations: ['user'],
|
relations: [...AM_CHILDREN_RELATIONS],
|
||||||
});
|
});
|
||||||
if (!assistante) throw new NotFoundException('Assistante maternelle introuvable');
|
if (!assistante) throw new NotFoundException('Assistante maternelle introuvable');
|
||||||
return assistante;
|
return assistante;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mise à jour
|
|
||||||
async update(id: string, dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
async update(id: string, dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
|
||||||
await this.assistantesMaternelleRepository.update(id, dto);
|
await this.assistantesMaternelleRepository.update(id, dto);
|
||||||
return this.findOne(id);
|
return this.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppression d’une assistante maternelle
|
/**
|
||||||
|
* Mise à jour fiche AM (identité + champs pro) par admin/gestionnaire. Ticket #131.
|
||||||
|
*/
|
||||||
|
async updateFicheAdmin(amUserId: string, dto: UpdateAmFicheAdminDto): Promise<AssistanteMaternelle> {
|
||||||
|
const am = await this.findOne(amUserId);
|
||||||
|
const user = am.user;
|
||||||
|
|
||||||
|
if (dto.email && dto.email !== user.email) {
|
||||||
|
const existing = await this.usersRepository.findOne({ where: { email: dto.email } });
|
||||||
|
if (existing && existing.id !== user.id) {
|
||||||
|
throw new ConflictException('Cet email est déjà utilisé');
|
||||||
|
}
|
||||||
|
user.email = dto.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.nom !== undefined) user.nom = dto.nom;
|
||||||
|
if (dto.prenom !== undefined) user.prenom = dto.prenom;
|
||||||
|
if (dto.telephone !== undefined) user.telephone = dto.telephone;
|
||||||
|
if (dto.adresse !== undefined) user.adresse = dto.adresse;
|
||||||
|
if (dto.ville !== undefined) user.ville = dto.ville;
|
||||||
|
if (dto.code_postal !== undefined) user.code_postal = dto.code_postal;
|
||||||
|
if (dto.statut !== undefined) user.statut = dto.statut;
|
||||||
|
|
||||||
|
await this.usersRepository.save(user);
|
||||||
|
|
||||||
|
const amPatch: Partial<AssistanteMaternelle> = {};
|
||||||
|
if (dto.approval_number !== undefined) amPatch.approval_number = dto.approval_number;
|
||||||
|
if (dto.residence_city !== undefined) amPatch.residence_city = dto.residence_city;
|
||||||
|
if (dto.max_children !== undefined) amPatch.max_children = dto.max_children;
|
||||||
|
if (dto.places_available !== undefined) amPatch.places_available = dto.places_available;
|
||||||
|
if (dto.biography !== undefined) amPatch.biography = dto.biography;
|
||||||
|
if (dto.available !== undefined) amPatch.available = dto.available;
|
||||||
|
|
||||||
|
if (Object.keys(amPatch).length > 0) {
|
||||||
|
await this.assistantesMaternelleRepository.update(amUserId, amPatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findOne(amUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rattacher un enfant à une AM (placement actif). Ticket #131.
|
||||||
|
* Passe le statut enfant à `garde` (sauf a_naitre / scolarise).
|
||||||
|
*/
|
||||||
|
async attachEnfant(amUserId: string, enfantId: string, createdBy?: Users): Promise<AssistanteMaternelle> {
|
||||||
|
const am = await this.findOne(amUserId);
|
||||||
|
|
||||||
|
const existingForAm = await this.amChildrenRepository.findOne({
|
||||||
|
where: { amId: amUserId, enfantId, date_fin: IsNull() },
|
||||||
|
});
|
||||||
|
if (existingForAm) {
|
||||||
|
throw new ConflictException('Cet enfant est déjà rattaché à cette assistante maternelle');
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = await this.childrenRepository.findOne({ where: { id: enfantId } });
|
||||||
|
if (!child) {
|
||||||
|
throw new NotFoundException('Enfant introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeForChild = await this.amChildrenRepository.findOne({
|
||||||
|
where: { enfantId, date_fin: IsNull() },
|
||||||
|
});
|
||||||
|
if (activeForChild && activeForChild.amId !== amUserId) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Cet enfant est déjà en garde chez une autre assistante maternelle',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeCount = await this.amChildrenRepository.count({
|
||||||
|
where: { amId: amUserId, date_fin: IsNull() },
|
||||||
|
});
|
||||||
|
if (am.max_children != null && activeCount >= am.max_children) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Capacité maximale atteinte (${am.max_children} enfant(s))`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.amChildrenRepository.save(
|
||||||
|
this.amChildrenRepository.create({
|
||||||
|
amId: amUserId,
|
||||||
|
enfantId,
|
||||||
|
date_debut: new Date(),
|
||||||
|
cree_par: createdBy?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.applyGardeStatusOnAttach(child);
|
||||||
|
|
||||||
|
return this.findOne(amUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clôturer le placement AM ↔ enfant. Ticket #131.
|
||||||
|
* Repasse l'enfant en `sans_garde` s'il n'a plus de placement actif.
|
||||||
|
*/
|
||||||
|
async detachEnfant(amUserId: string, enfantId: string): Promise<AssistanteMaternelle> {
|
||||||
|
await this.findOne(amUserId);
|
||||||
|
|
||||||
|
const link = await this.amChildrenRepository.findOne({
|
||||||
|
where: { amId: amUserId, enfantId, date_fin: IsNull() },
|
||||||
|
relations: ['child'],
|
||||||
|
});
|
||||||
|
if (!link) {
|
||||||
|
throw new NotFoundException('Lien assistante maternelle-enfant introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
link.date_fin = new Date();
|
||||||
|
await this.amChildrenRepository.save(link);
|
||||||
|
|
||||||
|
const remaining = await this.amChildrenRepository.count({
|
||||||
|
where: { enfantId, date_fin: IsNull() },
|
||||||
|
});
|
||||||
|
if (remaining === 0 && link.child) {
|
||||||
|
await this.applySansGardeStatusOnDetach(link.child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findOne(amUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyGardeStatusOnAttach(child: Children): Promise<void> {
|
||||||
|
if (
|
||||||
|
child.status === StatutEnfantType.A_NAITRE ||
|
||||||
|
child.status === StatutEnfantType.SCOLARISE
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
child.status = StatutEnfantType.GARDE;
|
||||||
|
await this.childrenRepository.save(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applySansGardeStatusOnDetach(child: Children): Promise<void> {
|
||||||
|
if (
|
||||||
|
child.status === StatutEnfantType.A_NAITRE ||
|
||||||
|
child.status === StatutEnfantType.SCOLARISE
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
child.status = StatutEnfantType.SANS_GARDE;
|
||||||
|
await this.childrenRepository.save(child);
|
||||||
|
}
|
||||||
|
|
||||||
async remove(id: string): Promise<{ message: string }> {
|
async remove(id: string): Promise<{ message: string }> {
|
||||||
await this.assistantesMaternelleRepository.delete(id);
|
await this.assistantesMaternelleRepository.delete(id);
|
||||||
return { message: 'Assistante maternelle supprimée' };
|
return { message: 'Assistante maternelle supprimée' };
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsEmail,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Max,
|
||||||
|
MaxLength,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { StatutUtilisateurType } from 'src/entities/users.entity';
|
||||||
|
|
||||||
|
/** Mise à jour fiche AM par admin/gestionnaire (doc 28 §6.1, ticket #131). */
|
||||||
|
export class UpdateAmFicheAdminDto {
|
||||||
|
@ApiPropertyOptional({ example: 'MARTIN' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
nom?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'Claire' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
prenom?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'claire@example.com' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '0612345678' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20)
|
||||||
|
telephone?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '5 place Bellecour' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
adresse?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'Lyon' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(150)
|
||||||
|
ville?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '69002' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10)
|
||||||
|
code_postal?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: StatutUtilisateurType })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(StatutUtilisateurType)
|
||||||
|
statut?: StatutUtilisateurType;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'AGR-2024-12345' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(50)
|
||||||
|
approval_number?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'Lyon' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
residence_city?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 4 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(10)
|
||||||
|
max_children?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 2 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(10)
|
||||||
|
places_available?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
biography?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
available?: boolean;
|
||||||
|
}
|
||||||
@ -176,7 +176,7 @@ describe('AuthService (#118 create-password API)', () => {
|
|||||||
parentsServiceMock.getDossierFamilleByNumero.mockResolvedValue({
|
parentsServiceMock.getDossierFamilleByNumero.mockResolvedValue({
|
||||||
numero_dossier: '2026-000021',
|
numero_dossier: '2026-000021',
|
||||||
parents: [{ user_id: 'p1', email: 'claire@test.fr', statut: StatutUtilisateurType.REFUSE }],
|
parents: [{ user_id: 'p1', email: 'claire@test.fr', statut: StatutUtilisateurType.REFUSE }],
|
||||||
enfants: [{ id: 'e1', first_name: 'Emma', status: 'actif' }],
|
enfants: [{ id: 'e1', first_name: 'Emma', status: 'sans_garde' }],
|
||||||
texte_motivation: 'Motivation test',
|
texte_motivation: 'Motivation test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -551,7 +551,7 @@ export class AuthService {
|
|||||||
? new Date(enfantDto.date_previsionnelle_naissance)
|
? new Date(enfantDto.date_previsionnelle_naissance)
|
||||||
: undefined;
|
: undefined;
|
||||||
enfant.photo_url = urlPhoto || undefined;
|
enfant.photo_url = urlPhoto || undefined;
|
||||||
enfant.status = enfantDto.date_naissance ? StatutEnfantType.ACTIF : StatutEnfantType.A_NAITRE;
|
enfant.status = enfantDto.date_naissance ? StatutEnfantType.SANS_GARDE : StatutEnfantType.A_NAITRE;
|
||||||
enfant.consent_photo = false;
|
enfant.consent_photo = false;
|
||||||
enfant.is_multiple = enfantDto.grossesse_multiple || false;
|
enfant.is_multiple = enfantDto.grossesse_multiple || false;
|
||||||
|
|
||||||
@ -1063,7 +1063,7 @@ export class AuthService {
|
|||||||
if (enfantDto.genre !== undefined) enfant.gender = enfantDto.genre;
|
if (enfantDto.genre !== undefined) enfant.gender = enfantDto.genre;
|
||||||
if (enfantDto.date_naissance !== undefined) {
|
if (enfantDto.date_naissance !== undefined) {
|
||||||
enfant.birth_date = new Date(enfantDto.date_naissance);
|
enfant.birth_date = new Date(enfantDto.date_naissance);
|
||||||
enfant.status = StatutEnfantType.ACTIF;
|
enfant.status = StatutEnfantType.SANS_GARDE;
|
||||||
}
|
}
|
||||||
if (enfantDto.date_previsionnelle_naissance !== undefined) {
|
if (enfantDto.date_previsionnelle_naissance !== undefined) {
|
||||||
enfant.due_date = new Date(enfantDto.date_previsionnelle_naissance);
|
enfant.due_date = new Date(enfantDto.date_previsionnelle_naissance);
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
import { GenreType, StatutEnfantType } from 'src/entities/children.entity';
|
import { GenreType, StatutEnfantType } from 'src/entities/children.entity';
|
||||||
|
|
||||||
export class CreateEnfantsDto {
|
export class CreateEnfantsDto {
|
||||||
@ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.ACTIF })
|
@ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.SANS_GARDE })
|
||||||
@IsEnum(StatutEnfantType)
|
@IsEnum(StatutEnfantType)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
status: StatutEnfantType;
|
status: StatutEnfantType;
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export class EnfantsService {
|
|||||||
|
|
||||||
// Vérif métier simple
|
// Vérif métier simple
|
||||||
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) {
|
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) {
|
||||||
throw new BadRequestException('Un enfant actif doit avoir une date de naissance');
|
throw new BadRequestException('Un enfant né doit avoir une date de naissance');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérif doublon éventuel (ex: même prénom + date de naissance pour ce parent)
|
// Vérif doublon éventuel (ex: même prénom + date de naissance pour ce parent)
|
||||||
|
|||||||
228
database/BDD.sql
228
database/BDD.sql
@ -1,3 +1,9 @@
|
|||||||
|
-- ==========================================================
|
||||||
|
-- P'titsPas — Schéma PostgreSQL (création from scratch)
|
||||||
|
-- Fichier canonique : toute nouvelle BDD doit partir d'ici.
|
||||||
|
-- Migrations dans database/migrations/ : BDD existantes uniquement.
|
||||||
|
-- ==========================================================
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
@ -5,40 +11,63 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN
|
||||||
CREATE TYPE role_type AS ENUM ('parent', 'gestionnaire', 'super_admin', 'administrateur', 'assistante_maternelle');
|
CREATE TYPE role_type AS ENUM (
|
||||||
|
'parent', 'gestionnaire', 'super_admin', 'administrateur', 'assistante_maternelle'
|
||||||
|
);
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN
|
||||||
CREATE TYPE genre_type AS ENUM ('H', 'F', 'Autre');
|
CREATE TYPE genre_type AS ENUM ('H', 'F', 'Autre');
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN
|
||||||
CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu','refuse');
|
CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente', 'actif', 'suspendu', 'refuse');
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN
|
||||||
CREATE TYPE statut_enfant_type AS ENUM ('a_naitre','actif','scolarise');
|
CREATE TYPE statut_enfant_type AS ENUM ('a_naitre', 'garde', 'sans_garde', 'scolarise');
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN
|
||||||
CREATE TYPE statut_dossier_type AS ENUM ('envoye','accepte','refuse');
|
CREATE TYPE statut_dossier_type AS ENUM ('envoye', 'accepte', 'refuse');
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN
|
||||||
CREATE TYPE statut_contrat_type AS ENUM ('brouillon','en_attente_signature','valide','resilie');
|
CREATE TYPE statut_contrat_type AS ENUM (
|
||||||
|
'brouillon', 'en_attente_signature', 'valide', 'resilie'
|
||||||
|
);
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN
|
||||||
CREATE TYPE statut_avenant_type AS ENUM ('propose','accepte','refuse');
|
CREATE TYPE statut_avenant_type AS ENUM ('propose', 'accepte', 'refuse');
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN
|
||||||
CREATE TYPE type_evenement_type AS ENUM ('absence_enfant','conge_am','conge_parent','arret_maladie_am','evenement_rpe');
|
CREATE TYPE type_evenement_type AS ENUM (
|
||||||
|
'absence_enfant', 'conge_am', 'conge_parent', 'arret_maladie_am', 'evenement_rpe'
|
||||||
|
);
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN
|
||||||
CREATE TYPE statut_evenement_type AS ENUM ('propose','valide','refuse');
|
CREATE TYPE statut_evenement_type AS ENUM ('propose', 'valide', 'refuse');
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN
|
||||||
CREATE TYPE statut_validation_type AS ENUM ('en_attente','valide','refuse');
|
CREATE TYPE statut_validation_type AS ENUM ('en_attente', 'valide', 'refuse');
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'situation_familiale_type') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'situation_familiale_type') THEN
|
||||||
CREATE TYPE situation_familiale_type AS ENUM ('celibataire','marie','concubinage','pacse','separe','divorce','veuf','parent_isole');
|
CREATE TYPE situation_familiale_type AS ENUM (
|
||||||
|
'celibataire', 'marie', 'concubinage', 'pacse', 'separe', 'divorce', 'veuf', 'parent_isole'
|
||||||
|
);
|
||||||
END IF;
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Table : relais (avant utilisateurs — FK relais_id)
|
||||||
|
-- ==========================================================
|
||||||
|
CREATE TABLE relais (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
nom VARCHAR(255) NOT NULL,
|
||||||
|
adresse TEXT NOT NULL,
|
||||||
|
horaires_ouverture JSONB,
|
||||||
|
ligne_fixe VARCHAR(20),
|
||||||
|
actif BOOLEAN DEFAULT true,
|
||||||
|
notes TEXT,
|
||||||
|
cree_le TIMESTAMPTZ DEFAULT now(),
|
||||||
|
modifie_le TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : utilisateurs
|
-- Table : utilisateurs
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
@ -46,26 +75,33 @@ CREATE TABLE utilisateurs (
|
|||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
||||||
password TEXT, -- NULL avant création via token
|
password TEXT,
|
||||||
prenom VARCHAR(100),
|
prenom VARCHAR(100),
|
||||||
nom VARCHAR(100),
|
nom VARCHAR(100),
|
||||||
genre genre_type,
|
genre genre_type,
|
||||||
role role_type NOT NULL,
|
role role_type NOT NULL,
|
||||||
statut statut_utilisateur_type DEFAULT 'en_attente',
|
statut statut_utilisateur_type DEFAULT 'en_attente',
|
||||||
telephone VARCHAR(20), -- Unifié (mobile privilégié)
|
telephone VARCHAR(20),
|
||||||
adresse TEXT,
|
adresse TEXT,
|
||||||
date_naissance DATE,
|
date_naissance DATE,
|
||||||
lieu_naissance_ville VARCHAR(100),
|
lieu_naissance_ville VARCHAR(100),
|
||||||
lieu_naissance_pays VARCHAR(100),
|
lieu_naissance_pays VARCHAR(100),
|
||||||
photo_url TEXT, -- Obligatoire pour AM, non utilisé pour parents
|
photo_url TEXT,
|
||||||
consentement_photo BOOLEAN DEFAULT false,
|
consentement_photo BOOLEAN DEFAULT false,
|
||||||
date_consentement_photo TIMESTAMPTZ,
|
date_consentement_photo TIMESTAMPTZ,
|
||||||
token_creation_mdp VARCHAR(255), -- Token pour créer MDP après validation
|
token_creation_mdp VARCHAR(255),
|
||||||
token_creation_mdp_expire_le TIMESTAMPTZ, -- Expiration 7 jours
|
token_creation_mdp_expire_le TIMESTAMPTZ,
|
||||||
-- Ticket #127 : réinitialisation mot de passe oublié (distinct de token_creation_mdp)
|
password_reset_token VARCHAR(255),
|
||||||
password_reset_token VARCHAR(255) NULL,
|
password_reset_expires TIMESTAMPTZ,
|
||||||
password_reset_expires TIMESTAMPTZ NULL,
|
token_reprise VARCHAR(255),
|
||||||
|
token_reprise_expire_le TIMESTAMPTZ,
|
||||||
changement_mdp_obligatoire BOOLEAN DEFAULT false,
|
changement_mdp_obligatoire BOOLEAN DEFAULT false,
|
||||||
|
numero_dossier VARCHAR(20),
|
||||||
|
cgu_version_acceptee INTEGER,
|
||||||
|
cgu_acceptee_le TIMESTAMPTZ,
|
||||||
|
privacy_version_acceptee INTEGER,
|
||||||
|
privacy_acceptee_le TIMESTAMPTZ,
|
||||||
|
relais_id UUID REFERENCES relais(id) ON DELETE SET NULL,
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
cree_le TIMESTAMPTZ DEFAULT now(),
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now(),
|
modifie_le TIMESTAMPTZ DEFAULT now(),
|
||||||
ville VARCHAR(150),
|
ville VARCHAR(150),
|
||||||
@ -74,15 +110,22 @@ CREATE TABLE utilisateurs (
|
|||||||
situation_familiale situation_familiale_type
|
situation_familiale situation_familiale_type
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Index pour recherche par token
|
CREATE INDEX idx_utilisateurs_token_creation_mdp
|
||||||
CREATE INDEX idx_utilisateurs_token_creation_mdp
|
ON utilisateurs(token_creation_mdp)
|
||||||
ON utilisateurs(token_creation_mdp)
|
|
||||||
WHERE token_creation_mdp IS NOT NULL;
|
WHERE token_creation_mdp IS NOT NULL;
|
||||||
|
|
||||||
CREATE INDEX idx_utilisateurs_password_reset_token
|
CREATE INDEX idx_utilisateurs_password_reset_token
|
||||||
ON utilisateurs(password_reset_token)
|
ON utilisateurs(password_reset_token)
|
||||||
WHERE password_reset_token IS NOT NULL;
|
WHERE password_reset_token IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_utilisateurs_token_reprise
|
||||||
|
ON utilisateurs(token_reprise)
|
||||||
|
WHERE token_reprise IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_utilisateurs_numero_dossier
|
||||||
|
ON utilisateurs(numero_dossier)
|
||||||
|
WHERE numero_dossier IS NOT NULL;
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : assistantes_maternelles
|
-- Table : assistantes_maternelles
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
@ -97,17 +140,27 @@ CREATE TABLE assistantes_maternelles (
|
|||||||
date_agrement DATE,
|
date_agrement DATE,
|
||||||
annee_experience SMALLINT,
|
annee_experience SMALLINT,
|
||||||
specialite VARCHAR(100),
|
specialite VARCHAR(100),
|
||||||
place_disponible INT
|
place_disponible INT,
|
||||||
|
numero_dossier VARCHAR(20)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_assistantes_maternelles_numero_dossier
|
||||||
|
ON assistantes_maternelles(numero_dossier)
|
||||||
|
WHERE numero_dossier IS NOT NULL;
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : parents
|
-- Table : parents
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_parents_numero_dossier
|
||||||
|
ON parents(numero_dossier)
|
||||||
|
WHERE numero_dossier IS NOT NULL;
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : enfants
|
-- Table : enfants
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
@ -116,7 +169,7 @@ CREATE TABLE enfants (
|
|||||||
statut statut_enfant_type,
|
statut statut_enfant_type,
|
||||||
prenom VARCHAR(100),
|
prenom VARCHAR(100),
|
||||||
nom VARCHAR(100),
|
nom VARCHAR(100),
|
||||||
genre genre_type NOT NULL, -- Obligatoire selon CDC
|
genre genre_type NOT NULL,
|
||||||
date_naissance DATE,
|
date_naissance DATE,
|
||||||
date_prevue_naissance DATE,
|
date_prevue_naissance DATE,
|
||||||
photo_url TEXT,
|
photo_url TEXT,
|
||||||
@ -135,7 +188,27 @@ CREATE TABLE enfants_parents (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : dossier_famille (inscription parent, schéma simplifié — ticket #119)
|
-- Table : enfants_assistantes_maternelles (placement AM ↔ enfant — #131)
|
||||||
|
-- ==========================================================
|
||||||
|
CREATE TABLE enfants_assistantes_maternelles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
id_am UUID NOT NULL REFERENCES assistantes_maternelles(id_utilisateur) ON DELETE CASCADE,
|
||||||
|
id_enfant UUID NOT NULL REFERENCES enfants(id) ON DELETE CASCADE,
|
||||||
|
date_debut DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||||
|
date_fin DATE NULL,
|
||||||
|
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
cree_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_eam_am ON enfants_assistantes_maternelles(id_am);
|
||||||
|
CREATE INDEX idx_eam_enfant ON enfants_assistantes_maternelles(id_enfant);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX uq_enfant_garde_active
|
||||||
|
ON enfants_assistantes_maternelles (id_enfant)
|
||||||
|
WHERE date_fin IS NULL;
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Table : dossier_famille (inscription parent — ticket #119)
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
CREATE TABLE dossier_famille (
|
CREATE TABLE dossier_famille (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
@ -276,10 +349,6 @@ CREATE TABLE notifications (
|
|||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : validations
|
-- Table : validations
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Historique des décisions (validation / refus / suspension de compte).
|
|
||||||
-- Colonnes commentaire + valide_par : requises par l’API Nest (TypeORM).
|
|
||||||
-- FK en ON DELETE SET NULL : conserver la ligne si l’utilisateur référencé
|
|
||||||
-- est supprimé (voir database/docs/FK_POLICIES.md).
|
|
||||||
CREATE TABLE validations (
|
CREATE TABLE validations (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
|
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
|
||||||
@ -305,13 +374,10 @@ CREATE TABLE configuration (
|
|||||||
modifie_par UUID REFERENCES utilisateurs(id)
|
modifie_par UUID REFERENCES utilisateurs(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Index pour performance
|
|
||||||
CREATE INDEX idx_configuration_cle ON configuration(cle);
|
CREATE INDEX idx_configuration_cle ON configuration(cle);
|
||||||
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
|
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
|
||||||
|
|
||||||
-- Seed initial de configuration
|
|
||||||
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
|
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
|
||||||
-- === Configuration Email (SMTP) ===
|
|
||||||
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
|
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
|
||||||
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
|
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
|
||||||
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
|
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
|
||||||
@ -320,14 +386,10 @@ INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
|
|||||||
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
|
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
|
||||||
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
|
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
|
||||||
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
|
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
|
||||||
|
|
||||||
-- === Configuration Application ===
|
|
||||||
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
|
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
|
||||||
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
|
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
|
||||||
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
|
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
|
||||||
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
|
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
|
||||||
|
|
||||||
-- === Configuration Sécurité ===
|
|
||||||
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
|
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
|
||||||
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
|
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
|
||||||
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
|
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
|
||||||
@ -338,19 +400,18 @@ INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
|
|||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
CREATE TABLE documents_legaux (
|
CREATE TABLE documents_legaux (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
type VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
|
type VARCHAR(50) NOT NULL,
|
||||||
version INTEGER NOT NULL, -- Numéro de version (auto-incrémenté)
|
version INTEGER NOT NULL,
|
||||||
fichier_nom VARCHAR(255) NOT NULL, -- Nom original du fichier
|
fichier_nom VARCHAR(255) NOT NULL,
|
||||||
fichier_path VARCHAR(500) NOT NULL, -- Chemin de stockage
|
fichier_path VARCHAR(500) NOT NULL,
|
||||||
fichier_hash VARCHAR(64) NOT NULL, -- Hash SHA-256 pour intégrité
|
fichier_hash VARCHAR(64) NOT NULL,
|
||||||
actif BOOLEAN DEFAULT false, -- Version actuellement active
|
actif BOOLEAN DEFAULT false,
|
||||||
televerse_par UUID REFERENCES utilisateurs(id), -- Qui a uploadé
|
televerse_par UUID REFERENCES utilisateurs(id),
|
||||||
televerse_le TIMESTAMPTZ DEFAULT now(), -- Date d'upload
|
televerse_le TIMESTAMPTZ DEFAULT now(),
|
||||||
active_le TIMESTAMPTZ, -- Date d'activation
|
active_le TIMESTAMPTZ,
|
||||||
UNIQUE(type, version) -- Pas de doublon version
|
UNIQUE(type, version)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Index pour performance
|
|
||||||
CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif);
|
CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif);
|
||||||
CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC);
|
CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC);
|
||||||
|
|
||||||
@ -361,73 +422,23 @@ CREATE TABLE acceptations_documents (
|
|||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
||||||
id_document UUID REFERENCES documents_legaux(id),
|
id_document UUID REFERENCES documents_legaux(id),
|
||||||
type_document VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
|
type_document VARCHAR(50) NOT NULL,
|
||||||
version_document INTEGER NOT NULL, -- Version acceptée
|
version_document INTEGER NOT NULL,
|
||||||
accepte_le TIMESTAMPTZ DEFAULT now(), -- Date d'acceptation
|
accepte_le TIMESTAMPTZ DEFAULT now(),
|
||||||
ip_address INET, -- IP de l'utilisateur (RGPD)
|
ip_address INET,
|
||||||
user_agent TEXT -- Navigateur (preuve)
|
user_agent TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Index pour performance
|
|
||||||
CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur);
|
CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur);
|
||||||
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
|
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : relais
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE relais (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
nom VARCHAR(255) NOT NULL,
|
|
||||||
adresse TEXT NOT NULL,
|
|
||||||
horaires_ouverture JSONB,
|
|
||||||
ligne_fixe VARCHAR(20),
|
|
||||||
actif BOOLEAN DEFAULT true,
|
|
||||||
notes TEXT,
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Modification Table : utilisateurs (ajout colonnes documents et relais)
|
|
||||||
-- ==========================================================
|
|
||||||
ALTER TABLE utilisateurs
|
|
||||||
ADD COLUMN IF NOT EXISTS cgu_version_acceptee INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS cgu_acceptee_le TIMESTAMPTZ,
|
|
||||||
ADD COLUMN IF NOT EXISTS privacy_version_acceptee INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ,
|
|
||||||
ADD COLUMN IF NOT EXISTS relais_id UUID REFERENCES relais(id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Ticket #103 : Numéro de dossier (AAAA-NNNNNN, séquence par année)
|
-- Ticket #103 : Numéro de dossier (AAAA-NNNNNN, séquence par année)
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
CREATE TABLE IF NOT EXISTS numero_dossier_sequence (
|
CREATE TABLE numero_dossier_sequence (
|
||||||
annee INT PRIMARY KEY,
|
annee INT PRIMARY KEY,
|
||||||
prochain INT NOT NULL DEFAULT 1
|
prochain INT NOT NULL DEFAULT 1
|
||||||
);
|
);
|
||||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL;
|
|
||||||
ALTER TABLE assistantes_maternelles ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL;
|
|
||||||
ALTER TABLE parents ADD COLUMN IF NOT EXISTS numero_dossier VARCHAR(20) NULL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_utilisateurs_numero_dossier ON utilisateurs(numero_dossier) WHERE numero_dossier IS NOT NULL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_assistantes_maternelles_numero_dossier ON assistantes_maternelles(numero_dossier) WHERE numero_dossier IS NOT NULL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_parents_numero_dossier ON parents(numero_dossier) WHERE numero_dossier IS NOT NULL;
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Ticket #110 : Token reprise après refus (lien email)
|
|
||||||
-- ==========================================================
|
|
||||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise VARCHAR(255) NULL;
|
|
||||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise_expire_le TIMESTAMPTZ NULL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_utilisateurs_token_reprise ON utilisateurs(token_reprise) WHERE token_reprise IS NOT NULL;
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Ticket #127 : Mot de passe oublié (token dédié, ≠ inscription)
|
|
||||||
-- ==========================================================
|
|
||||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS password_reset_token VARCHAR(255) NULL;
|
|
||||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS password_reset_expires TIMESTAMPTZ NULL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_utilisateurs_password_reset_token ON utilisateurs(password_reset_token) WHERE password_reset_token IS NOT NULL;
|
|
||||||
|
|
||||||
-- Lieu de naissance (aligné CREATE TABLE utilisateurs — idempotent si colonnes déjà présentes)
|
|
||||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS lieu_naissance_ville VARCHAR(100) NULL;
|
|
||||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS lieu_naissance_pays VARCHAR(100) NULL;
|
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Seed : Documents légaux génériques v1
|
-- Seed : Documents légaux génériques v1
|
||||||
@ -438,10 +449,7 @@ INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_
|
|||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Seed : Super Administrateur par défaut
|
-- Seed : Super Administrateur par défaut
|
||||||
-- ==========================================================
|
-- Email: admin@ptits-pas.fr | Mot de passe: 4dm1n1strateur
|
||||||
-- Email: admin@ptits-pas.fr
|
|
||||||
-- Mot de passe: 4dm1n1strateur (hashé bcrypt)
|
|
||||||
-- IMPORTANT: Changer ce mot de passe en production !
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
INSERT INTO utilisateurs (
|
INSERT INTO utilisateurs (
|
||||||
email,
|
email,
|
||||||
|
|||||||
@ -15,12 +15,13 @@ Ce projet contient la **base de données** pour l'application PtitsPas, avec scr
|
|||||||
|
|
||||||
## Structure du projet
|
## Structure du projet
|
||||||
|
|
||||||
- `migrations/` : scripts SQL pour la création et l'import de la base
|
- **`BDD.sql`** : schéma canonique (création from scratch — utilisé par `docker-compose.yml` racine)
|
||||||
- `bdd/data_test/` : fichiers CSV pour l'import de données de test
|
- `migrations/` : scripts SQL pour **mettre à jour** une BDD déjà en service
|
||||||
- `docs/` : documentation métier et technique
|
- `bdd/data_test/` : fichiers CSV (import legacy)
|
||||||
- `seed/` : scripts de seed
|
- `docs/` : documentation métier et technique (`ENUMS.md`, `FK_POLICIES.md`)
|
||||||
|
- `seed/` : scripts de seed (`03_seed_test_data.sql` — jeu dashboard admin)
|
||||||
- `tests/` : tests SQL
|
- `tests/` : tests SQL
|
||||||
- `docker-compose.dev.yml` : configuration Docker pour le développement
|
- `docker-compose.dev.yml` : Postgres standalone (BDD.sql + seed auto)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
"id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo","est_multiple"
|
"id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo","est_multiple"
|
||||||
"5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","actif","Emma","Dupont","F","2020-06-01",,,False,,False
|
"5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","sans_garde","Emma","Dupont","F","2020-06-01",,,False,,False
|
||||||
"a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","actif",,,,"2020-01-01","2025-01-01",,False,,False
|
"a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","sans_garde",,,,"2020-01-01","2025-01-01",,False,,False
|
||||||
"e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","actif","Emma","Martin",,"2023-02-15",,,False,,False
|
"e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","sans_garde","Emma","Martin",,"2023-02-15",,,False,,False
|
||||||
"e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d","actif","Noah","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","actif","Léa","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","actif","Chloé","Rousseau",,"2022-04-20",,,False,,False
|
"e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f","sans_garde","Chloé","Rousseau",,"2022-04-20",,,False,,False
|
||||||
"e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","actif","Hugo","Rousseau",,"2024-03-10",,,False,,False
|
"e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","sans_garde","Hugo","Rousseau",,"2024-03-10",,,False,,False
|
||||||
"e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b","actif","Maxime","Lecomte",,"2023-04-15",,,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
|
"edd19cd1-bb67-4f14-8a37-c66b75c94537","scolarise","Lucas","Durand","H","2018-09-15",,,False,,False
|
||||||
|
|||||||
|
@ -14,9 +14,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql
|
- ./BDD.sql:/docker-entrypoint-initdb.d/01_init.sql
|
||||||
- ./migrations/07_import.sql:/docker-entrypoint-initdb.d/07_import.sql
|
- ./seed/03_seed_test_data.sql:/docker-entrypoint-initdb.d/02_seed_test_data.sql
|
||||||
- ./bdd/data_test:/bdd/data_test
|
|
||||||
- postgres_standalone_data:/var/lib/postgresql/data
|
- postgres_standalone_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- ptitspas_dev
|
- ptitspas_dev
|
||||||
|
|||||||
@ -51,12 +51,17 @@ Ce document recense **toutes les valeurs énumérées** utilisées dans la base
|
|||||||
| Valeur | Description |
|
| Valeur | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `a_naitre` | Enfant à naître (date prévue renseignée) |
|
| `a_naitre` | Enfant à naître (date prévue renseignée) |
|
||||||
| `actif` | Enfant pris en charge / en cours de garde |
|
| `sans_garde` | Enfant né, pas encore placé chez une AM (défaut à l'inscription) |
|
||||||
|
| `garde` | Enfant placé chez une AM (`enfants_assistantes_maternelles` actif) |
|
||||||
| `scolarise` | Enfant scolarisé, garde potentiellement périscolaire |
|
| `scolarise` | Enfant scolarisé, garde potentiellement périscolaire |
|
||||||
|
|
||||||
**Contraintes associées** :
|
**Contraintes associées** :
|
||||||
- `a_naitre` → **`date_prevue_naissance` obligatoire**
|
- `a_naitre` → **`date_prevue_naissance` obligatoire**
|
||||||
- `actif`/`scolarise` → **`date_naissance` obligatoire**
|
- `sans_garde` / `garde` / `scolarise` → **`date_naissance` obligatoire**
|
||||||
|
|
||||||
|
**Transitions** (API admin #131) :
|
||||||
|
- Rattachement AM ↔ enfant → `garde` (sauf `a_naitre` / `scolarise`)
|
||||||
|
- Détachement sans autre placement actif → `sans_garde`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,9 @@ Documenter, de façon unique et partagée, les règles de suppression/mise à jo
|
|||||||
| **parents(id_co_parent)** → `utilisateurs(id)` | **SET NULL** | Conserver le parent principal si co-parent disparaît |
|
| **parents(id_co_parent)** → `utilisateurs(id)` | **SET NULL** | Conserver le parent principal si co-parent disparaît |
|
||||||
| **enfants_parents(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Nettoyage liaisons N:N |
|
| **enfants_parents(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Nettoyage liaisons N:N |
|
||||||
| **enfants_parents(id_enfant)** → `enfants(id)` | **CASCADE** | Idem |
|
| **enfants_parents(id_enfant)** → `enfants(id)` | **CASCADE** | Idem |
|
||||||
|
| **enfants_assistantes_maternelles(id_am)** → `assistantes_maternelles(id_utilisateur)` | **CASCADE** | Placement supprimé avec l’AM |
|
||||||
|
| **enfants_assistantes_maternelles(id_enfant)** → `enfants(id)` | **CASCADE** | Idem |
|
||||||
|
| **enfants_assistantes_maternelles(cree_par)** → `utilisateurs(id)` | **SET NULL** | Historique placement conservé |
|
||||||
| **dossiers(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Dossier n’a pas de sens sans parent |
|
| **dossiers(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Dossier n’a pas de sens sans parent |
|
||||||
| **dossiers(id_enfant)** → `enfants(id)` | **CASCADE** | Dossier n’a pas de sens sans enfant |
|
| **dossiers(id_enfant)** → `enfants(id)` | **CASCADE** | Dossier n’a pas de sens sans enfant |
|
||||||
| **messages(id_dossier)** → `dossiers(id)` | **CASCADE** | Messages détruits avec le dossier |
|
| **messages(id_dossier)** → `dossiers(id)` | **CASCADE** | Messages détruits avec le dossier |
|
||||||
|
|||||||
36
database/migrations/2026_enfants_assistantes_maternelles.sql
Normal file
36
database/migrations/2026_enfants_assistantes_maternelles.sql
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
-- Ticket #131 / #141 — Placement AM ↔ enfant + statuts garde/sans_garde
|
||||||
|
-- ⚠️ BDD EXISTANTES UNIQUEMENT — ne pas exécuter sur une base créée via BDD.sql à jour.
|
||||||
|
-- Idempotent : safe à rejouer sur recette / prod.
|
||||||
|
|
||||||
|
-- 1) Nouveaux statuts enfant
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TYPE statut_enfant_type ADD VALUE IF NOT EXISTS 'garde';
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TYPE statut_enfant_type ADD VALUE IF NOT EXISTS 'sans_garde';
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- actif → sans_garde (enfants sans placement AM connu au déploiement)
|
||||||
|
UPDATE enfants SET statut = 'sans_garde' WHERE statut = 'actif';
|
||||||
|
|
||||||
|
-- 2) Table de placement AM ↔ enfant
|
||||||
|
CREATE TABLE IF NOT EXISTS enfants_assistantes_maternelles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
id_am UUID NOT NULL REFERENCES assistantes_maternelles(id_utilisateur) ON DELETE CASCADE,
|
||||||
|
id_enfant UUID NOT NULL REFERENCES enfants(id) ON DELETE CASCADE,
|
||||||
|
date_debut DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||||
|
date_fin DATE NULL,
|
||||||
|
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
cree_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_eam_am ON enfants_assistantes_maternelles(id_am);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_eam_enfant ON enfants_assistantes_maternelles(id_enfant);
|
||||||
|
|
||||||
|
-- Au plus une garde active par enfant
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_enfant_garde_active
|
||||||
|
ON enfants_assistantes_maternelles (id_enfant)
|
||||||
|
WHERE date_fin IS NULL;
|
||||||
@ -65,12 +65,12 @@ ON CONFLICT (id_utilisateur) DO NOTHING;
|
|||||||
|
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
-- Enfants
|
-- Enfants
|
||||||
-- - child A : déjà né (statut = 'actif' et date_naissance requise)
|
-- - child A : déjà né (statut = 'sans_garde' et date_naissance requise)
|
||||||
-- - child B : à naître (statut = 'a_naitre' et date_prevue_naissance requise)
|
-- - child B : à naître (statut = 'a_naitre' et date_prevue_naissance requise)
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
INSERT INTO enfants (id, prenom, nom, statut, date_naissance, jumeau_multiple)
|
INSERT INTO enfants (id, prenom, nom, statut, date_naissance, jumeau_multiple)
|
||||||
VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'actif', '2022-04-12', false)
|
VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'sans_garde', '2022-04-12', false)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance, jumeau_multiple)
|
INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance, jumeau_multiple)
|
||||||
|
|||||||
@ -18,15 +18,15 @@ BEGIN;
|
|||||||
|
|
||||||
INSERT INTO utilisateurs (id, email, password, prenom, nom, role, statut, telephone, adresse, ville, code_postal, profession, situation_familiale, date_naissance, consentement_photo)
|
INSERT INTO utilisateurs (id, email, password, prenom, nom, role, statut, telephone, adresse, ville, code_postal, profession, situation_familiale, date_naissance, consentement_photo)
|
||||||
VALUES
|
VALUES
|
||||||
('a0000001-0001-0001-0001-000000000001', 'sophie.bernard@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Sophie', 'BERNARD', 'administrateur', 'actif', '0678123456', '12 Avenue Gabriel Péri', 'Bezons', '95870', 'Responsable administrative', 'marie', '1978-03-15', false),
|
('a0000001-0001-0001-0001-000000000001', 'sophie.bernard@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Sophie', 'BERNARD', 'administrateur', 'actif', '0678123456', '12 Avenue Gabriel Péri', 'Bezons', '95870', 'Responsable administrative', 'marie', '1978-03-15', false),
|
||||||
('a0000002-0002-0002-0002-000000000002', 'lucas.moreau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Lucas', 'MOREAU', 'gestionnaire', 'actif', '0687234567', '8 Rue Jean Jaurès', 'Bezons', '95870', 'Gestionnaire des placements', 'celibataire', '1985-09-22', false),
|
('a0000002-0002-0002-0002-000000000002', 'lucas.moreau@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Lucas', 'MOREAU', 'gestionnaire', 'actif', '0687234567', '8 Rue Jean Jaurès', 'Bezons', '95870', 'Gestionnaire des placements', 'celibataire', '1985-09-22', false),
|
||||||
('a0000003-0003-0003-0003-000000000003', 'marie.dubois@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Marie', 'DUBOIS', 'assistante_maternelle', 'actif', '0696345678', '25 Rue de la République', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1980-06-08', true),
|
('a0000003-0003-0003-0003-000000000003', 'marie.dubois@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Marie', 'DUBOIS', 'assistante_maternelle', 'actif', '0696345678', '25 Rue de la République', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1980-06-08', true),
|
||||||
('a0000004-0004-0004-0004-000000000004', 'fatima.elmansouri@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Fatima', 'EL MANSOURI', 'assistante_maternelle', 'actif', '0675456789', '17 Boulevard Aristide Briand', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1975-11-12', true),
|
('a0000004-0004-0004-0004-000000000004', 'fatima.elmansouri@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Fatima', 'EL MANSOURI', 'assistante_maternelle', 'actif', '0675456789', '17 Boulevard Aristide Briand', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1975-11-12', true),
|
||||||
('a0000005-0005-0005-0005-000000000005', 'claire.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Claire', 'MARTIN', 'parent', 'actif', '0689567890', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Infirmière', 'marie', '1990-04-03', false),
|
('a0000005-0005-0005-0005-000000000005', 'claire.martin@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Claire', 'MARTIN', 'parent', 'actif', '0689567890', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Infirmière', 'marie', '1990-04-03', false),
|
||||||
('a0000006-0006-0006-0006-000000000006', 'thomas.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Thomas', 'MARTIN', 'parent', 'actif', '0678456789', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Ingénieur', 'marie', '1988-07-18', false),
|
('a0000006-0006-0006-0006-000000000006', 'thomas.martin@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Thomas', 'MARTIN', 'parent', 'actif', '0678456789', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Ingénieur', 'marie', '1988-07-18', false),
|
||||||
('a0000007-0007-0007-0007-000000000007', 'amelie.durand@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Amélie', 'DURAND', 'parent', 'actif', '0667788990', '23 Rue Victor Hugo', 'Bezons', '95870', 'Comptable', 'divorce', '1987-12-14', false),
|
('a0000007-0007-0007-0007-000000000007', 'amelie.durand@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Amélie', 'DURAND', 'parent', 'actif', '0667788990', '23 Rue Victor Hugo', 'Bezons', '95870', 'Comptable', 'divorce', '1987-12-14', false),
|
||||||
('a0000008-0008-0008-0008-000000000008', 'julien.rousseau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Julien', 'ROUSSEAU', 'parent', 'actif', '0656677889', '14 Rue Pasteur', 'Bezons', '95870', 'Commercial', 'divorce', '1985-08-29', false),
|
('a0000008-0008-0008-0008-000000000008', 'julien.rousseau@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'Julien', 'ROUSSEAU', 'parent', 'actif', '0656677889', '14 Rue Pasteur', 'Bezons', '95870', 'Commercial', 'divorce', '1985-08-29', false),
|
||||||
('a0000009-0009-0009-0009-000000000009', 'david.lecomte@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'David', 'LECOMTE', 'parent', 'actif', '0645566778', '31 Rue Émile Zola', 'Bezons', '95870', 'Développeur web', 'parent_isole', '1992-10-07', false)
|
('a0000009-0009-0009-0009-000000000009', 'david.lecomte@ptits-pas.fr', '$2b$10$vhzSJ6qGzul3jhtmXUJoV.3sGzPghdB0dDBx3Di1CHKrMZOwP7RGS', 'David', 'LECOMTE', 'parent', 'actif', '0645566778', '31 Rue Émile Zola', 'Bezons', '95870', 'Développeur web', 'parent_isole', '1992-10-07', false)
|
||||||
ON CONFLICT (email) DO NOTHING;
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
-- ========== PARENTS (avec co-parent pour le couple Martin) ==========
|
-- ========== PARENTS (avec co-parent pour le couple Martin) ==========
|
||||||
@ -51,12 +51,12 @@ ON CONFLICT (id_utilisateur) DO NOTHING;
|
|||||||
-- ========== ENFANTS ==========
|
-- ========== ENFANTS ==========
|
||||||
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple)
|
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple)
|
||||||
VALUES
|
VALUES
|
||||||
('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'actif', true),
|
('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', 'actif', 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', 'actif', 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', 'actif', false),
|
('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', 'actif', 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', 'actif', false)
|
('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'sans_garde', false)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- ========== ENFANTS_PARENTS (liaison N:N) ==========
|
-- ========== ENFANTS_PARENTS (liaison N:N) ==========
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user