diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 4fb9b45..64f08a3 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -16,7 +16,7 @@ import { AllExceptionsFilter } from './common/filters/all_exceptions.filters'; import { EnfantsModule } from './routes/enfants/enfants.module'; import { AppConfigModule } from './modules/config/config.module'; import { DocumentsLegauxModule } from './modules/documents-legaux'; -import { AbsencesGardeModule } from './modules/absences-garde'; +import { EvenementsAgendaModule } from './modules/evenements-agenda'; import { CardsModule } from './modules/cards'; import { RelaisModule } from './routes/relais/relais.module'; import { DossiersModule } from './routes/dossiers/dossiers.module'; @@ -58,7 +58,7 @@ import { SuppressionsModule } from './routes/suppressions/suppressions.module'; AuthModule, AppConfigModule, DocumentsLegauxModule, - AbsencesGardeModule, + EvenementsAgendaModule, CardsModule, RelaisModule, DossiersModule, diff --git a/backend/src/entities/card_instances.entity.ts b/backend/src/entities/card_instances.entity.ts index 3fa92da..b02906a 100644 --- a/backend/src/entities/card_instances.entity.ts +++ b/backend/src/entities/card_instances.entity.ts @@ -9,7 +9,7 @@ import { UpdateDateColumn, } from 'typeorm'; import { AmChildren } from './am_children.entity'; -import { AbsencesGarde } from './absences_garde.entity'; +import { EvenementsAgenda } from './evenements_agenda.entity'; import { Users } from './users.entity'; import { CardType } from './card_types.entity'; import { CardAudienceMember } from './card_audience_members.entity'; @@ -45,12 +45,12 @@ export class CardInstance { @JoinColumn({ name: 'id_placement', referencedColumnName: 'id' }) placement: AmChildren; - @Column({ name: 'id_absence', type: 'uuid', nullable: true }) - id_absence?: string; + @Column({ name: 'id_evenement', type: 'uuid', nullable: true }) + id_evenement?: string; - @ManyToOne(() => AbsencesGarde, { nullable: true, onDelete: 'SET NULL' }) - @JoinColumn({ name: 'id_absence', referencedColumnName: 'id' }) - absence?: AbsencesGarde; + @ManyToOne(() => EvenementsAgenda, { nullable: true, onDelete: 'SET NULL' }) + @JoinColumn({ name: 'id_evenement', referencedColumnName: 'id' }) + evenement?: EvenementsAgenda; @Column({ name: 'cree_par', type: 'uuid', nullable: true }) cree_par?: string; diff --git a/backend/src/entities/absences_garde.entity.ts b/backend/src/entities/evenements_agenda.entity.ts similarity index 72% rename from backend/src/entities/absences_garde.entity.ts rename to backend/src/entities/evenements_agenda.entity.ts index 4799a17..33b693b 100644 --- a/backend/src/entities/absences_garde.entity.ts +++ b/backend/src/entities/evenements_agenda.entity.ts @@ -10,25 +10,25 @@ import { import { AmChildren } from './am_children.entity'; import { Users } from './users.entity'; -export enum TypeAbsenceGardeType { +export enum TypeEvenementAgendaType { ABSENCE_ENFANT = 'absence_enfant', CONGE_AM = 'conge_am', ARRET_MALADIE_AM = 'arret_maladie_am', } -export enum StatutAbsenceGardeType { +export enum StatutEvenementAgendaType { EN_ATTENTE = 'en_attente', ACCEPTE = 'accepte', REFUSE = 'refuse', } /** - * Vérité métier des absences / congés / arrêts par couple AM↔enfant. - * Les cartes (module ultérieur) collectent ; cette table stocke. - * Ticket #193. + * Vérité métier agenda par couple AM↔enfant (placement). + * Types V1 : absences / congés / arrêts ; extensible (soins, école, mairie…). + * Les cartes collectent ; cette table historise. Ticket #205. */ -@Entity('absences_garde', { schema: 'public' }) -export class AbsencesGarde { +@Entity('evenements_agenda', { schema: 'public' }) +export class EvenementsAgenda { @PrimaryGeneratedColumn('uuid') id: string; @@ -41,11 +41,11 @@ export class AbsencesGarde { @Column({ type: 'enum', - enum: TypeAbsenceGardeType, - enumName: 'type_absence_garde_type', + enum: TypeEvenementAgendaType, + enumName: 'type_evenement_agenda_type', name: 'type', }) - type: TypeAbsenceGardeType; + type: TypeEvenementAgendaType; @Column({ name: 'date_debut', type: 'date' }) date_debut: string; @@ -55,12 +55,12 @@ export class AbsencesGarde { @Column({ type: 'enum', - enum: StatutAbsenceGardeType, - enumName: 'statut_absence_garde_type', + enum: StatutEvenementAgendaType, + enumName: 'statut_evenement_agenda_type', name: 'statut', - default: StatutAbsenceGardeType.EN_ATTENTE, + default: StatutEvenementAgendaType.EN_ATTENTE, }) - statut: StatutAbsenceGardeType; + statut: StatutEvenementAgendaType; /** Purge auto pour en_attente/refuse ; accepte → infinity. */ @Column({ name: 'expire_at', type: 'timestamptz' }) diff --git a/backend/src/modules/absences-garde/index.ts b/backend/src/modules/absences-garde/index.ts deleted file mode 100644 index 11a9a04..0000000 --- a/backend/src/modules/absences-garde/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AbsencesGardeModule } from './absences-garde.module'; -export { AbsencesGardeService } from './absences-garde.service'; diff --git a/backend/src/modules/cards/cards.controller.ts b/backend/src/modules/cards/cards.controller.ts index 1b8ce80..ce1e599 100644 --- a/backend/src/modules/cards/cards.controller.ts +++ b/backend/src/modules/cards/cards.controller.ts @@ -65,7 +65,7 @@ export class CardsController { @Roles(RoleType.PARENT, RoleType.ASSISTANTE_MATERNELLE) @HttpCode(HttpStatus.CREATED) @ApiOperation({ - summary: 'Créer une bulle (+ hook absences_garde) — #194', + summary: 'Créer une bulle (+ hook evenements_agenda) — #194', }) @ApiBody({ type: CreerCarteDto }) @ApiResponse({ status: 201, type: CarteDto }) diff --git a/backend/src/modules/cards/cards.module.ts b/backend/src/modules/cards/cards.module.ts index fd418ab..e7295d1 100644 --- a/backend/src/modules/cards/cards.module.ts +++ b/backend/src/modules/cards/cards.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AbsencesGardeModule } from '../absences-garde'; +import { EvenementsAgendaModule } from '../evenements-agenda'; import { CardType } from 'src/entities/card_types.entity'; import { CardInstance } from 'src/entities/card_instances.entity'; import { CardAudienceMember } from 'src/entities/card_audience_members.entity'; @@ -16,7 +16,7 @@ import { CardsRealtimeService } from './cards-realtime.service'; @Module({ imports: [ - AbsencesGardeModule, + EvenementsAgendaModule, TypeOrmModule.forFeature([ CardType, CardInstance, diff --git a/backend/src/modules/cards/cards.service.spec.ts b/backend/src/modules/cards/cards.service.spec.ts index ff621f5..31260a2 100644 --- a/backend/src/modules/cards/cards.service.spec.ts +++ b/backend/src/modules/cards/cards.service.spec.ts @@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { ForbiddenException } from '@nestjs/common'; import { CardsService } from './cards.service'; import { CardsRealtimeService } from './cards-realtime.service'; -import { AbsencesGardeService } from '../absences-garde/absences-garde.service'; +import { EvenementsAgendaService } from '../evenements-agenda/evenements-agenda.service'; import { CardType, CardResponseModeType } from 'src/entities/card_types.entity'; import { CardInstance, @@ -15,7 +15,7 @@ import { CardResponse } from 'src/entities/card_responses.entity'; import { AmChildren } from 'src/entities/am_children.entity'; import { ParentsChildren } from 'src/entities/parents_children.entity'; import { RoleType } from 'src/entities/users.entity'; -import { StatutAbsenceGardeType, TypeAbsenceGardeType } from 'src/entities/absences_garde.entity'; +import { StatutEvenementAgendaType, TypeEvenementAgendaType } from 'src/entities/evenements_agenda.entity'; describe('CardsService (#194)', () => { let service: CardsService; @@ -41,7 +41,7 @@ describe('CardsService (#194)', () => { }; const amChildrenRepo = { findOne: jest.fn() }; const parentsChildrenRepo = { find: jest.fn(), findOne: jest.fn() }; - const absencesService = { + const evenementsService = { creer: jest.fn(), maj: jest.fn(), supprimer: jest.fn(), @@ -58,7 +58,7 @@ describe('CardsService (#194)', () => { { provide: getRepositoryToken(CardResponse), useValue: responsesRepo }, { provide: getRepositoryToken(AmChildren), useValue: amChildrenRepo }, { provide: getRepositoryToken(ParentsChildren), useValue: parentsChildrenRepo }, - { provide: AbsencesGardeService, useValue: absencesService }, + { provide: EvenementsAgendaService, useValue: evenementsService }, { provide: CardsRealtimeService, useValue: realtime }, ], }).compile(); @@ -88,10 +88,10 @@ describe('CardsService (#194)', () => { enfantId: 'e-1', date_fin: null, }); - absencesService.creer.mockResolvedValue({ + evenementsService.creer.mockResolvedValue({ id: 'abs-1', - type: TypeAbsenceGardeType.CONGE_AM, - statut: StatutAbsenceGardeType.EN_ATTENTE, + type: TypeEvenementAgendaType.CONGE_AM, + statut: StatutEvenementAgendaType.EN_ATTENTE, }); parentsChildrenRepo.find.mockResolvedValue([ { parentId: 'p-1', enfantId: 'e-1' }, @@ -101,7 +101,7 @@ describe('CardsService (#194)', () => { id: 'card-1', type_code: 'conge_am', id_placement: 'pl-1', - id_absence: 'abs-1', + id_evenement: 'abs-1', cree_par: 'am-1', operation: CardOperationType.CREATE, statut: CardInstanceStatutType.OUVERTE, @@ -130,7 +130,7 @@ describe('CardsService (#194)', () => { date_fin: '2026-11-07', }); - expect(absencesService.creer).toHaveBeenCalled(); + expect(evenementsService.creer).toHaveBeenCalled(); expect(audienceRepo.save).toHaveBeenCalled(); expect(realtime.emitToUsers).toHaveBeenCalledWith( ['am-1', 'p-1', 'p-2'], diff --git a/backend/src/modules/cards/cards.service.ts b/backend/src/modules/cards/cards.service.ts index 6f4c734..7630e74 100644 --- a/backend/src/modules/cards/cards.service.ts +++ b/backend/src/modules/cards/cards.service.ts @@ -6,11 +6,11 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Repository } from 'typeorm'; -import { AbsencesGardeService } from '../absences-garde/absences-garde.service'; +import { EvenementsAgendaService } from '../evenements-agenda/evenements-agenda.service'; import { - StatutAbsenceGardeType, - TypeAbsenceGardeType, -} from 'src/entities/absences_garde.entity'; + StatutEvenementAgendaType, + TypeEvenementAgendaType, +} from 'src/entities/evenements_agenda.entity'; import { AmChildren } from 'src/entities/am_children.entity'; import { ParentsChildren } from 'src/entities/parents_children.entity'; import { RoleType } from 'src/entities/users.entity'; @@ -49,7 +49,7 @@ export class CardsService { private readonly amChildrenRepo: Repository, @InjectRepository(ParentsChildren) private readonly parentsChildrenRepo: Repository, - private readonly absencesService: AbsencesGardeService, + private readonly evenementsService: EvenementsAgendaService, private readonly realtime: CardsRealtimeService, ) {} @@ -110,11 +110,11 @@ export class CardsService { ); const operation = dto.operation ?? CardOperationType.CREATE; - let absenceId = dto.id_absence; + let absenceId = dto.id_evenement; const absenceType = this.mapAbsenceType(dto.type_code); if (operation === CardOperationType.CREATE) { - const absence = await this.absencesService.creer(userId, role, { + const absence = await this.evenementsService.creer(userId, role, { id_placement: dto.id_placement, type: absenceType, date_debut: dto.date_debut, @@ -124,16 +124,16 @@ export class CardsService { absenceId = absence.id; } else { if (!absenceId) { - throw new BadRequestException('id_absence requis pour operation=update'); + throw new BadRequestException('id_evenement requis pour operation=update'); } - await this.absencesService.maj(userId, role, absenceId, { + await this.evenementsService.maj(userId, role, absenceId, { date_debut: dto.date_debut, date_fin: dto.date_fin, motif: dto.motif, // Congé accepté modifié → repasse en attente via cartes respond flow; // pour absence enfant update immédiat déjà fait. ...(dto.type_code === 'conge_am' - ? { statut: StatutAbsenceGardeType.EN_ATTENTE } + ? { statut: StatutEvenementAgendaType.EN_ATTENTE } : {}), }); } @@ -146,7 +146,7 @@ export class CardsService { const card = this.cardsRepo.create({ type_code: type.code, id_placement: dto.id_placement, - id_absence: absenceId, + id_evenement: absenceId, cree_par: userId, operation, statut: statutInitial, @@ -211,15 +211,15 @@ export class CardsService { comment: dto.comment?.trim() || undefined, }); - if (card.id_absence) { + if (card.id_evenement) { if (dto.action === CardResponseActionType.ACCEPT || dto.action === CardResponseActionType.ACK) { - await this.absencesService.maj(userId, role, card.id_absence, { - statut: StatutAbsenceGardeType.ACCEPTE, + await this.evenementsService.maj(userId, role, card.id_evenement, { + statut: StatutEvenementAgendaType.ACCEPTE, }); card.statut = CardInstanceStatutType.TRAITEE; } else if (dto.action === CardResponseActionType.REFUSE) { - await this.absencesService.maj(userId, role, card.id_absence, { - statut: StatutAbsenceGardeType.REFUSE, + await this.evenementsService.maj(userId, role, card.id_evenement, { + statut: StatutEvenementAgendaType.REFUSE, motif: dto.comment!.trim(), }); card.statut = CardInstanceStatutType.REFUSEE; @@ -264,12 +264,12 @@ export class CardsService { throw new BadRequestException('Dates invalides'); } - if (card.id_absence) { - await this.absencesService.maj(userId, role, card.id_absence, { + if (card.id_evenement) { + await this.evenementsService.maj(userId, role, card.id_evenement, { date_debut: debut, date_fin: fin, motif: dto.motif, - statut: StatutAbsenceGardeType.EN_ATTENTE, + statut: StatutEvenementAgendaType.EN_ATTENTE, }); } @@ -298,17 +298,17 @@ export class CardsService { throw new ForbiddenException('Seul le créateur peut supprimer cette carte'); } const audienceIds = await this.audienceUserIds(card); - if (card.id_absence) { + if (card.id_evenement) { const absStatut = card.statut === CardInstanceStatutType.TRAITEE ? null - : card.id_absence; + : card.id_evenement; // Supprime l’absence liée si pas encore acceptée définitivement if ( card.statut === CardInstanceStatutType.OUVERTE || card.statut === CardInstanceStatutType.REFUSEE ) { - await this.absencesService.supprimer(userId, role, card.id_absence); + await this.evenementsService.supprimer(userId, role, card.id_evenement); } void absStatut; } @@ -336,13 +336,13 @@ export class CardsService { return rows.map((r) => r.id_utilisateur); } - private mapAbsenceType(typeCode: string): TypeAbsenceGardeType { + private mapAbsenceType(typeCode: string): TypeEvenementAgendaType { if (typeCode === 'absence_enfant' || typeCode === 'absence_enfant_modif') { - return TypeAbsenceGardeType.ABSENCE_ENFANT; + return TypeEvenementAgendaType.ABSENCE_ENFANT; } - if (typeCode === 'conge_am') return TypeAbsenceGardeType.CONGE_AM; + if (typeCode === 'conge_am') return TypeEvenementAgendaType.CONGE_AM; if (typeCode === 'arret_maladie_am') { - return TypeAbsenceGardeType.ARRET_MALADIE_AM; + return TypeEvenementAgendaType.ARRET_MALADIE_AM; } throw new BadRequestException(`Type non mappé à une absence: ${typeCode}`); } @@ -350,7 +350,7 @@ export class CardsService { private async linkAbsenceCard(absenceId: string, cardId: string): Promise { // Update direct pour éviter droits maj vides await this.cardsRepo.manager.query( - `UPDATE absences_garde SET id_card_instance = $1, modifie_le = now() WHERE id = $2`, + `UPDATE evenements_agenda SET id_card_instance = $1, modifie_le = now() WHERE id = $2`, [cardId, absenceId], ); } @@ -471,7 +471,7 @@ export class CardsService { titre: card.type?.titre ?? card.type_code, couleur: card.type?.couleur ?? null, id_placement: card.id_placement, - id_absence: card.id_absence ?? null, + id_evenement: card.id_evenement ?? null, operation: card.operation, statut: card.statut, payload: card.payload ?? {}, diff --git a/backend/src/modules/cards/dto/cards.dto.ts b/backend/src/modules/cards/dto/cards.dto.ts index d2fc18b..01474e0 100644 --- a/backend/src/modules/cards/dto/cards.dto.ts +++ b/backend/src/modules/cards/dto/cards.dto.ts @@ -38,11 +38,11 @@ export class CreerCarteDto { motif?: string; @ApiPropertyOptional({ - description: 'Requis pour operation=update (même id absences_garde)', + description: 'Requis pour operation=update (même id evenements_agenda)', }) @IsOptional() @IsUUID() - id_absence?: string; + id_evenement?: string; @ApiPropertyOptional({ enum: CardOperationType, default: CardOperationType.CREATE }) @IsOptional() @@ -98,7 +98,7 @@ export class CarteDto { id_placement: string; @ApiPropertyOptional() - id_absence?: string | null; + id_evenement?: string | null; @ApiProperty({ enum: CardOperationType }) operation: CardOperationType; diff --git a/backend/src/modules/absences-garde/dto/absences-garde.dto.ts b/backend/src/modules/evenements-agenda/dto/evenements-agenda.dto.ts similarity index 68% rename from backend/src/modules/absences-garde/dto/absences-garde.dto.ts rename to backend/src/modules/evenements-agenda/dto/evenements-agenda.dto.ts index 326be46..ac93684 100644 --- a/backend/src/modules/absences-garde/dto/absences-garde.dto.ts +++ b/backend/src/modules/evenements-agenda/dto/evenements-agenda.dto.ts @@ -9,18 +9,18 @@ import { MinLength, } from 'class-validator'; import { - StatutAbsenceGardeType, - TypeAbsenceGardeType, -} from 'src/entities/absences_garde.entity'; + StatutEvenementAgendaType, + TypeEvenementAgendaType, +} from 'src/entities/evenements_agenda.entity'; -export class CreerAbsenceGardeDto { +export class CreerEvenementAgendaDto { @ApiProperty({ description: 'Placement AM↔enfant (couple)' }) @IsUUID() id_placement: string; - @ApiProperty({ enum: TypeAbsenceGardeType }) - @IsEnum(TypeAbsenceGardeType) - type: TypeAbsenceGardeType; + @ApiProperty({ enum: TypeEvenementAgendaType }) + @IsEnum(TypeEvenementAgendaType) + type: TypeEvenementAgendaType; @ApiProperty({ example: '2026-10-01' }) @IsDateString() @@ -37,7 +37,7 @@ export class CreerAbsenceGardeDto { motif?: string; } -export class MajAbsenceGardeDto { +export class MajEvenementAgendaDto { @ApiPropertyOptional({ example: '2026-10-02' }) @IsOptional() @IsDateString() @@ -48,10 +48,10 @@ export class MajAbsenceGardeDto { @IsDateString() date_fin?: string; - @ApiPropertyOptional({ enum: StatutAbsenceGardeType }) + @ApiPropertyOptional({ enum: StatutEvenementAgendaType }) @IsOptional() - @IsEnum(StatutAbsenceGardeType) - statut?: StatutAbsenceGardeType; + @IsEnum(StatutEvenementAgendaType) + statut?: StatutEvenementAgendaType; @ApiPropertyOptional({ description: 'Motivation de refus (parent) ou motif créateur', @@ -63,15 +63,15 @@ export class MajAbsenceGardeDto { motif?: string; } -export class AbsenceGardeDto { +export class EvenementAgendaDto { @ApiProperty() id: string; @ApiProperty() id_placement: string; - @ApiProperty({ enum: TypeAbsenceGardeType }) - type: TypeAbsenceGardeType; + @ApiProperty({ enum: TypeEvenementAgendaType }) + type: TypeEvenementAgendaType; @ApiProperty() date_debut: string; @@ -79,8 +79,8 @@ export class AbsenceGardeDto { @ApiProperty() date_fin: string; - @ApiProperty({ enum: StatutAbsenceGardeType }) - statut: StatutAbsenceGardeType; + @ApiProperty({ enum: StatutEvenementAgendaType }) + statut: StatutEvenementAgendaType; @ApiProperty() expire_at: string; @@ -113,7 +113,7 @@ export class AbsenceGardeDto { modifie_le: string; } -export class ListeAbsencesGardeDto { - @ApiProperty({ type: [AbsenceGardeDto] }) - items: AbsenceGardeDto[]; +export class ListeEvenementsAgendaDto { + @ApiProperty({ type: [EvenementAgendaDto] }) + items: EvenementAgendaDto[]; } diff --git a/backend/src/modules/absences-garde/absences-garde.controller.spec.ts b/backend/src/modules/evenements-agenda/evenements-agenda.controller.spec.ts similarity index 74% rename from backend/src/modules/absences-garde/absences-garde.controller.spec.ts rename to backend/src/modules/evenements-agenda/evenements-agenda.controller.spec.ts index 84844d7..7769f5a 100644 --- a/backend/src/modules/absences-garde/absences-garde.controller.spec.ts +++ b/backend/src/modules/evenements-agenda/evenements-agenda.controller.spec.ts @@ -1,13 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AbsencesGardeController } from './absences-garde.controller'; -import { AbsencesGardeService } from './absences-garde.service'; +import { EvenementsAgendaController } from './evenements-agenda.controller'; +import { EvenementsAgendaService } from './evenements-agenda.service'; import { AuthGuard } from 'src/common/guards/auth.guard'; import { RolesGuard } from 'src/common/guards/roles.guard'; import { RoleType } from 'src/entities/users.entity'; -import { TypeAbsenceGardeType } from 'src/entities/absences_garde.entity'; +import { TypeEvenementAgendaType } from 'src/entities/evenements_agenda.entity'; -describe('AbsencesGardeController (#172)', () => { - let controller: AbsencesGardeController; +describe('EvenementsAgendaController (#172)', () => { + let controller: EvenementsAgendaController; const serviceMock = { lister: jest.fn(), creer: jest.fn(), @@ -17,8 +17,8 @@ describe('AbsencesGardeController (#172)', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [AbsencesGardeController], - providers: [{ provide: AbsencesGardeService, useValue: serviceMock }], + controllers: [EvenementsAgendaController], + providers: [{ provide: EvenementsAgendaService, useValue: serviceMock }], }) .overrideGuard(AuthGuard) .useValue({ canActivate: () => true }) @@ -26,7 +26,7 @@ describe('AbsencesGardeController (#172)', () => { .useValue({ canActivate: () => true }) .compile(); - controller = module.get(AbsencesGardeController); + controller = module.get(EvenementsAgendaController); jest.clearAllMocks(); }); @@ -59,7 +59,7 @@ describe('AbsencesGardeController (#172)', () => { serviceMock.creer.mockResolvedValue({ id: 'a1' }); const dto = { id_placement: 'pl-1', - type: TypeAbsenceGardeType.ABSENCE_ENFANT, + type: TypeEvenementAgendaType.ABSENCE_ENFANT, date_debut: '2026-10-01', date_fin: '2026-10-02', }; diff --git a/backend/src/modules/absences-garde/absences-garde.controller.ts b/backend/src/modules/evenements-agenda/evenements-agenda.controller.ts similarity index 62% rename from backend/src/modules/absences-garde/absences-garde.controller.ts rename to backend/src/modules/evenements-agenda/evenements-agenda.controller.ts index 02163ca..46a037a 100644 --- a/backend/src/modules/absences-garde/absences-garde.controller.ts +++ b/backend/src/modules/evenements-agenda/evenements-agenda.controller.ts @@ -25,49 +25,49 @@ import { RolesGuard } from 'src/common/guards/roles.guard'; import { Roles } from 'src/common/decorators/roles.decorator'; import { User } from 'src/common/decorators/user.decorator'; import { - StatutAbsenceGardeType, - TypeAbsenceGardeType, -} from 'src/entities/absences_garde.entity'; + StatutEvenementAgendaType, + TypeEvenementAgendaType, +} from 'src/entities/evenements_agenda.entity'; import { RoleType } from 'src/entities/users.entity'; -import { AbsencesGardeService } from './absences-garde.service'; +import { EvenementsAgendaService } from './evenements-agenda.service'; import { - AbsenceGardeDto, - CreerAbsenceGardeDto, - ListeAbsencesGardeDto, - MajAbsenceGardeDto, -} from './dto/absences-garde.dto'; + EvenementAgendaDto, + CreerEvenementAgendaDto, + ListeEvenementsAgendaDto, + MajEvenementAgendaDto, +} from './dto/evenements-agenda.dto'; -@ApiTags('Absences garde') +@ApiTags('Événements agenda') @ApiBearerAuth('access-token') -@Controller('absences-garde') +@Controller('evenements-agenda') @UseGuards(AuthGuard, RolesGuard) -export class AbsencesGardeController { - constructor(private readonly absencesGardeService: AbsencesGardeService) {} +export class EvenementsAgendaController { + constructor(private readonly evenementsAgendaService: EvenementsAgendaService) {} @Get() @Roles(RoleType.PARENT, RoleType.ASSISTANTE_MATERNELLE) @ApiOperation({ - summary: 'Lister les absences / congés / arrêts — ticket #172', + summary: 'Lister les absences / congés / arrêts — ticket #205 / ex-#172', description: 'Sans placementId : tous les placements du user (parent = famille multi-AM ; AM = tous accueils). ' + 'Avec placementId : un couple enfant–AM.', }) @ApiQuery({ name: 'placementId', required: false }) - @ApiQuery({ name: 'type', required: false, enum: TypeAbsenceGardeType }) - @ApiQuery({ name: 'statut', required: false, enum: StatutAbsenceGardeType }) + @ApiQuery({ name: 'type', required: false, enum: TypeEvenementAgendaType }) + @ApiQuery({ name: 'statut', required: false, enum: StatutEvenementAgendaType }) @ApiQuery({ name: 'from', required: false, description: 'YYYY-MM-DD' }) @ApiQuery({ name: 'to', required: false, description: 'YYYY-MM-DD' }) - @ApiResponse({ status: 200, type: ListeAbsencesGardeDto }) + @ApiResponse({ status: 200, type: ListeEvenementsAgendaDto }) lister( @User('id') userId: string, @User('role') role: RoleType, @Query('placementId') placementId?: string, - @Query('type') type?: TypeAbsenceGardeType, - @Query('statut') statut?: StatutAbsenceGardeType, + @Query('type') type?: TypeEvenementAgendaType, + @Query('statut') statut?: StatutEvenementAgendaType, @Query('from') from?: string, @Query('to') to?: string, - ): Promise { - return this.absencesGardeService.lister(userId, role, { + ): Promise { + return this.evenementsAgendaService.lister(userId, role, { placementId, type, statut, @@ -79,15 +79,15 @@ export class AbsencesGardeController { @Post() @Roles(RoleType.PARENT, RoleType.ASSISTANTE_MATERNELLE) @ApiOperation({ summary: 'Créer une période d’absence / congé / arrêt — #172' }) - @ApiBody({ type: CreerAbsenceGardeDto }) - @ApiResponse({ status: 201, type: AbsenceGardeDto }) + @ApiBody({ type: CreerEvenementAgendaDto }) + @ApiResponse({ status: 201, type: EvenementAgendaDto }) @HttpCode(HttpStatus.CREATED) creer( @User('id') userId: string, @User('role') role: RoleType, - @Body() dto: CreerAbsenceGardeDto, - ): Promise { - return this.absencesGardeService.creer(userId, role, dto); + @Body() dto: CreerEvenementAgendaDto, + ): Promise { + return this.evenementsAgendaService.creer(userId, role, dto); } @Patch(':id') @@ -98,15 +98,15 @@ export class AbsencesGardeController { 'Parent : accept/refuse congé (motivation si refus), ack arrêt. ' + 'AM : modifier dates (en attente / refuse / accepté), republication après refus.', }) - @ApiBody({ type: MajAbsenceGardeDto }) - @ApiResponse({ status: 200, type: AbsenceGardeDto }) + @ApiBody({ type: MajEvenementAgendaDto }) + @ApiResponse({ status: 200, type: EvenementAgendaDto }) maj( @User('id') userId: string, @User('role') role: RoleType, @Param('id', ParseUUIDPipe) id: string, - @Body() dto: MajAbsenceGardeDto, - ): Promise { - return this.absencesGardeService.maj(userId, role, id, dto); + @Body() dto: MajEvenementAgendaDto, + ): Promise { + return this.evenementsAgendaService.maj(userId, role, id, dto); } @Delete(':id') @@ -119,6 +119,6 @@ export class AbsencesGardeController { @User('role') role: RoleType, @Param('id', ParseUUIDPipe) id: string, ): Promise { - await this.absencesGardeService.supprimer(userId, role, id); + await this.evenementsAgendaService.supprimer(userId, role, id); } } diff --git a/backend/src/modules/absences-garde/absences-garde.module.ts b/backend/src/modules/evenements-agenda/evenements-agenda.module.ts similarity index 57% rename from backend/src/modules/absences-garde/absences-garde.module.ts rename to backend/src/modules/evenements-agenda/evenements-agenda.module.ts index 26480e0..50d8de6 100644 --- a/backend/src/modules/absences-garde/absences-garde.module.ts +++ b/backend/src/modules/evenements-agenda/evenements-agenda.module.ts @@ -2,15 +2,15 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AbsencesGarde } from 'src/entities/absences_garde.entity'; +import { EvenementsAgenda } from 'src/entities/evenements_agenda.entity'; import { AmChildren } from 'src/entities/am_children.entity'; import { ParentsChildren } from 'src/entities/parents_children.entity'; -import { AbsencesGardeController } from './absences-garde.controller'; -import { AbsencesGardeService } from './absences-garde.service'; +import { EvenementsAgendaController } from './evenements-agenda.controller'; +import { EvenementsAgendaService } from './evenements-agenda.service'; @Module({ imports: [ - TypeOrmModule.forFeature([AbsencesGarde, AmChildren, ParentsChildren]), + TypeOrmModule.forFeature([EvenementsAgenda, AmChildren, ParentsChildren]), JwtModule.registerAsync({ imports: [ConfigModule], useFactory: (config: ConfigService) => ({ @@ -20,8 +20,8 @@ import { AbsencesGardeService } from './absences-garde.service'; inject: [ConfigService], }), ], - controllers: [AbsencesGardeController], - providers: [AbsencesGardeService], - exports: [AbsencesGardeService], + controllers: [EvenementsAgendaController], + providers: [EvenementsAgendaService], + exports: [EvenementsAgendaService], }) -export class AbsencesGardeModule {} +export class EvenementsAgendaModule {} diff --git a/backend/src/modules/absences-garde/absences-garde.service.spec.ts b/backend/src/modules/evenements-agenda/evenements-agenda.service.spec.ts similarity index 80% rename from backend/src/modules/absences-garde/absences-garde.service.spec.ts rename to backend/src/modules/evenements-agenda/evenements-agenda.service.spec.ts index 1c40cd7..e14e59b 100644 --- a/backend/src/modules/absences-garde/absences-garde.service.spec.ts +++ b/backend/src/modules/evenements-agenda/evenements-agenda.service.spec.ts @@ -1,18 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { ForbiddenException, BadRequestException } from '@nestjs/common'; -import { AbsencesGardeService } from './absences-garde.service'; +import { EvenementsAgendaService } from './evenements-agenda.service'; import { - AbsencesGarde, - StatutAbsenceGardeType, - TypeAbsenceGardeType, -} from 'src/entities/absences_garde.entity'; + EvenementsAgenda, + StatutEvenementAgendaType, + TypeEvenementAgendaType, +} from 'src/entities/evenements_agenda.entity'; import { AmChildren } from 'src/entities/am_children.entity'; import { ParentsChildren } from 'src/entities/parents_children.entity'; import { RoleType } from 'src/entities/users.entity'; -describe('AbsencesGardeService (#172)', () => { - let service: AbsencesGardeService; +describe('EvenementsAgendaService (#172)', () => { + let service: EvenementsAgendaService; const absencesRepo = { create: jest.fn((x) => x), save: jest.fn(async (x) => ({ @@ -37,8 +37,8 @@ describe('AbsencesGardeService (#172)', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - AbsencesGardeService, - { provide: getRepositoryToken(AbsencesGarde), useValue: absencesRepo }, + EvenementsAgendaService, + { provide: getRepositoryToken(EvenementsAgenda), useValue: absencesRepo }, { provide: getRepositoryToken(AmChildren), useValue: amChildrenRepo }, { provide: getRepositoryToken(ParentsChildren), @@ -46,7 +46,7 @@ describe('AbsencesGardeService (#172)', () => { }, ], }).compile(); - service = module.get(AbsencesGardeService); + service = module.get(EvenementsAgendaService); jest.clearAllMocks(); }); @@ -68,10 +68,10 @@ describe('AbsencesGardeService (#172)', () => { absencesRepo.findOne.mockResolvedValue({ id: 'abs-1', id_placement: 'pl-1', - type: TypeAbsenceGardeType.ABSENCE_ENFANT, + type: TypeEvenementAgendaType.ABSENCE_ENFANT, date_debut: '2026-10-01', date_fin: '2026-10-02', - statut: StatutAbsenceGardeType.ACCEPTE, + statut: StatutEvenementAgendaType.ACCEPTE, expire_at: new Date('9999-12-31'), cree_le: new Date(), modifie_le: new Date(), @@ -85,15 +85,15 @@ describe('AbsencesGardeService (#172)', () => { const res = await service.creer('p-1', RoleType.PARENT, { id_placement: 'pl-1', - type: TypeAbsenceGardeType.ABSENCE_ENFANT, + type: TypeEvenementAgendaType.ABSENCE_ENFANT, date_debut: '2026-10-01', date_fin: '2026-10-02', }); expect(absencesRepo.save).toHaveBeenCalled(); const savedArg = absencesRepo.save.mock.calls[0][0]; - expect(savedArg.statut).toBe(StatutAbsenceGardeType.ACCEPTE); - expect(res.type).toBe(TypeAbsenceGardeType.ABSENCE_ENFANT); + expect(savedArg.statut).toBe(StatutEvenementAgendaType.ACCEPTE); + expect(res.type).toBe(TypeEvenementAgendaType.ABSENCE_ENFANT); }); it('parent ne peut pas créer un congé AM', async () => { @@ -110,7 +110,7 @@ describe('AbsencesGardeService (#172)', () => { await expect( service.creer('p-1', RoleType.PARENT, { id_placement: 'pl-1', - type: TypeAbsenceGardeType.CONGE_AM, + type: TypeEvenementAgendaType.CONGE_AM, date_debut: '2026-10-01', date_fin: '2026-10-05', }), @@ -121,10 +121,10 @@ describe('AbsencesGardeService (#172)', () => { absencesRepo.findOne.mockResolvedValue({ id: 'abs-1', id_placement: 'pl-1', - type: TypeAbsenceGardeType.CONGE_AM, + type: TypeEvenementAgendaType.CONGE_AM, date_debut: '2026-10-01', date_fin: '2026-10-05', - statut: StatutAbsenceGardeType.EN_ATTENTE, + statut: StatutEvenementAgendaType.EN_ATTENTE, expire_at: new Date(), }); amChildrenRepo.findOne.mockResolvedValue({ @@ -140,7 +140,7 @@ describe('AbsencesGardeService (#172)', () => { await expect( service.maj('p-1', RoleType.PARENT, 'abs-1', { - statut: StatutAbsenceGardeType.REFUSE, + statut: StatutEvenementAgendaType.REFUSE, }), ).rejects.toBeInstanceOf(BadRequestException); }); @@ -159,7 +159,7 @@ describe('AbsencesGardeService (#172)', () => { await expect( service.creer('p-1', RoleType.PARENT, { id_placement: 'pl-1', - type: TypeAbsenceGardeType.ABSENCE_ENFANT, + type: TypeEvenementAgendaType.ABSENCE_ENFANT, date_debut: '2026-10-10', date_fin: '2026-10-01', }), diff --git a/backend/src/modules/absences-garde/absences-garde.service.ts b/backend/src/modules/evenements-agenda/evenements-agenda.service.ts similarity index 79% rename from backend/src/modules/absences-garde/absences-garde.service.ts rename to backend/src/modules/evenements-agenda/evenements-agenda.service.ts index dd251ae..1c88723 100644 --- a/backend/src/modules/absences-garde/absences-garde.service.ts +++ b/backend/src/modules/evenements-agenda/evenements-agenda.service.ts @@ -7,19 +7,19 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { In, IsNull, Repository } from 'typeorm'; import { - AbsencesGarde, - StatutAbsenceGardeType, - TypeAbsenceGardeType, -} from 'src/entities/absences_garde.entity'; + EvenementsAgenda, + StatutEvenementAgendaType, + TypeEvenementAgendaType, +} from 'src/entities/evenements_agenda.entity'; import { AmChildren } from 'src/entities/am_children.entity'; import { ParentsChildren } from 'src/entities/parents_children.entity'; import { RoleType } from 'src/entities/users.entity'; import { - AbsenceGardeDto, - CreerAbsenceGardeDto, - ListeAbsencesGardeDto, - MajAbsenceGardeDto, -} from './dto/absences-garde.dto'; + EvenementAgendaDto, + CreerEvenementAgendaDto, + ListeEvenementsAgendaDto, + MajEvenementAgendaDto, +} from './dto/evenements-agenda.dto'; const TTL_EN_ATTENTE_MS = 15 * 24 * 60 * 60 * 1000; const TTL_REFUSE_MS = 7 * 24 * 60 * 60 * 1000; @@ -27,10 +27,10 @@ const TTL_REFUSE_MS = 7 * 24 * 60 * 60 * 1000; const EXPIRE_ACCEPTE = new Date('9999-12-31T23:59:59.999Z'); @Injectable() -export class AbsencesGardeService { +export class EvenementsAgendaService { constructor( - @InjectRepository(AbsencesGarde) - private readonly absencesRepo: Repository, + @InjectRepository(EvenementsAgenda) + private readonly absencesRepo: Repository, @InjectRepository(AmChildren) private readonly amChildrenRepo: Repository, @InjectRepository(ParentsChildren) @@ -42,12 +42,12 @@ export class AbsencesGardeService { role: RoleType, opts: { placementId?: string; - type?: TypeAbsenceGardeType; - statut?: StatutAbsenceGardeType; + type?: TypeEvenementAgendaType; + statut?: StatutEvenementAgendaType; from?: string; to?: string; }, - ): Promise { + ): Promise { const placementIds = await this.resolvePlacementIds( userId, role, @@ -86,8 +86,8 @@ export class AbsencesGardeService { async creer( userId: string, role: RoleType, - dto: CreerAbsenceGardeDto, - ): Promise { + dto: CreerEvenementAgendaDto, + ): Promise { this.assertDates(dto.date_debut, dto.date_fin); await this.assertCanAccessPlacement(userId, role, dto.id_placement); this.assertCanCreateType(role, dto.type); @@ -111,8 +111,8 @@ export class AbsencesGardeService { userId: string, role: RoleType, id: string, - dto: MajAbsenceGardeDto, - ): Promise { + dto: MajEvenementAgendaDto, + ): Promise { const row = await this.absencesRepo.findOne({ where: { id } }); if (!row) { throw new NotFoundException('Absence introuvable'); @@ -128,15 +128,15 @@ export class AbsencesGardeService { this.assertCanEditDates(role, row); row.date_debut = debut; row.date_fin = fin; - if (row.statut === StatutAbsenceGardeType.EN_ATTENTE) { - row.expire_at = this.expireAtFor(StatutAbsenceGardeType.EN_ATTENTE); + if (row.statut === StatutEvenementAgendaType.EN_ATTENTE) { + row.expire_at = this.expireAtFor(StatutEvenementAgendaType.EN_ATTENTE); } } if (dto.statut !== undefined && dto.statut !== row.statut) { this.assertCanChangeStatut(role, row, dto.statut, dto.motif); if ( - dto.statut === StatutAbsenceGardeType.REFUSE && + dto.statut === StatutEvenementAgendaType.REFUSE && (!dto.motif || !dto.motif.trim()) ) { throw new BadRequestException( @@ -169,7 +169,7 @@ export class AbsencesGardeService { if ( role === RoleType.PARENT && - row.type !== TypeAbsenceGardeType.ABSENCE_ENFANT + row.type !== TypeEvenementAgendaType.ABSENCE_ENFANT ) { throw new ForbiddenException( 'Un parent ne peut supprimer que les absences enfant', @@ -183,7 +183,7 @@ export class AbsencesGardeService { id: string, userId: string, role: RoleType, - ): Promise { + ): Promise { const row = await this.absencesRepo.findOne({ where: { id }, relations: ['placement', 'placement.child', 'placement.am', 'placement.am.user'], @@ -195,19 +195,19 @@ export class AbsencesGardeService { return this.toDto(row); } - private statutInitial(type: TypeAbsenceGardeType): StatutAbsenceGardeType { - if (type === TypeAbsenceGardeType.ABSENCE_ENFANT) { - return StatutAbsenceGardeType.ACCEPTE; + private statutInitial(type: TypeEvenementAgendaType): StatutEvenementAgendaType { + if (type === TypeEvenementAgendaType.ABSENCE_ENFANT) { + return StatutEvenementAgendaType.ACCEPTE; } - return StatutAbsenceGardeType.EN_ATTENTE; + return StatutEvenementAgendaType.EN_ATTENTE; } - private expireAtFor(statut: StatutAbsenceGardeType): Date { + private expireAtFor(statut: StatutEvenementAgendaType): Date { const now = Date.now(); - if (statut === StatutAbsenceGardeType.ACCEPTE) { + if (statut === StatutEvenementAgendaType.ACCEPTE) { return EXPIRE_ACCEPTE; } - if (statut === StatutAbsenceGardeType.REFUSE) { + if (statut === StatutEvenementAgendaType.REFUSE) { return new Date(now + TTL_REFUSE_MS); } return new Date(now + TTL_EN_ATTENTE_MS); @@ -221,9 +221,9 @@ export class AbsencesGardeService { } } - private assertCanCreateType(role: RoleType, type: TypeAbsenceGardeType): void { + private assertCanCreateType(role: RoleType, type: TypeEvenementAgendaType): void { if (role === RoleType.PARENT) { - if (type !== TypeAbsenceGardeType.ABSENCE_ENFANT) { + if (type !== TypeEvenementAgendaType.ABSENCE_ENFANT) { throw new ForbiddenException( 'Un parent ne peut créer que des absences enfant', ); @@ -232,8 +232,8 @@ export class AbsencesGardeService { } if (role === RoleType.ASSISTANTE_MATERNELLE) { if ( - type !== TypeAbsenceGardeType.CONGE_AM && - type !== TypeAbsenceGardeType.ARRET_MALADIE_AM + type !== TypeEvenementAgendaType.CONGE_AM && + type !== TypeEvenementAgendaType.ARRET_MALADIE_AM ) { throw new ForbiddenException( 'Une AM ne peut créer que congé ou arrêt maladie', @@ -246,10 +246,10 @@ export class AbsencesGardeService { private assertCanEditDates( role: RoleType, - row: AbsencesGarde, + row: EvenementsAgenda, ): void { if (role === RoleType.PARENT) { - if (row.type !== TypeAbsenceGardeType.ABSENCE_ENFANT) { + if (row.type !== TypeEvenementAgendaType.ABSENCE_ENFANT) { throw new ForbiddenException( 'Un parent ne peut modifier que les absences enfant', ); @@ -258,13 +258,13 @@ export class AbsencesGardeService { } if (role === RoleType.ASSISTANTE_MATERNELLE) { if ( - row.type === TypeAbsenceGardeType.CONGE_AM || - row.type === TypeAbsenceGardeType.ARRET_MALADIE_AM + row.type === TypeEvenementAgendaType.CONGE_AM || + row.type === TypeEvenementAgendaType.ARRET_MALADIE_AM ) { if ( - row.statut !== StatutAbsenceGardeType.EN_ATTENTE && - row.statut !== StatutAbsenceGardeType.REFUSE && - row.statut !== StatutAbsenceGardeType.ACCEPTE + row.statut !== StatutEvenementAgendaType.EN_ATTENTE && + row.statut !== StatutEvenementAgendaType.REFUSE && + row.statut !== StatutEvenementAgendaType.ACCEPTE ) { throw new ForbiddenException('Statut incompatible avec une modification'); } @@ -278,31 +278,31 @@ export class AbsencesGardeService { private assertCanChangeStatut( role: RoleType, - row: AbsencesGarde, - next: StatutAbsenceGardeType, + row: EvenementsAgenda, + next: StatutEvenementAgendaType, _motif?: string, ): void { if (role === RoleType.PARENT) { // Accept / refuse congé ; ack arrêt (accepte) if ( - row.type === TypeAbsenceGardeType.CONGE_AM || - row.type === TypeAbsenceGardeType.ARRET_MALADIE_AM + row.type === TypeEvenementAgendaType.CONGE_AM || + row.type === TypeEvenementAgendaType.ARRET_MALADIE_AM ) { if ( - next !== StatutAbsenceGardeType.ACCEPTE && - next !== StatutAbsenceGardeType.REFUSE + next !== StatutEvenementAgendaType.ACCEPTE && + next !== StatutEvenementAgendaType.REFUSE ) { throw new BadRequestException('Transition de statut invalide'); } if ( - row.type === TypeAbsenceGardeType.ARRET_MALADIE_AM && - next === StatutAbsenceGardeType.REFUSE + row.type === TypeEvenementAgendaType.ARRET_MALADIE_AM && + next === StatutEvenementAgendaType.REFUSE ) { throw new BadRequestException( 'Un arrêt maladie ne se refuse pas (accusé seulement)', ); } - if (row.statut !== StatutAbsenceGardeType.EN_ATTENTE) { + if (row.statut !== StatutEvenementAgendaType.EN_ATTENTE) { throw new BadRequestException('Cette demande n’est plus en attente'); } return; @@ -314,9 +314,9 @@ export class AbsencesGardeService { if (role === RoleType.ASSISTANTE_MATERNELLE) { // Remise en attente après refus OU modification d’un congé déjà accepté (S2b) if ( - next === StatutAbsenceGardeType.EN_ATTENTE && - (row.statut === StatutAbsenceGardeType.REFUSE || - row.statut === StatutAbsenceGardeType.ACCEPTE) + next === StatutEvenementAgendaType.EN_ATTENTE && + (row.statut === StatutEvenementAgendaType.REFUSE || + row.statut === StatutEvenementAgendaType.ACCEPTE) ) { return; } @@ -396,7 +396,7 @@ export class AbsencesGardeService { throw new ForbiddenException('Rôle non autorisé'); } - private toDto(row: AbsencesGarde): AbsenceGardeDto { + private toDto(row: EvenementsAgenda): EvenementAgendaDto { const child = row.placement?.child; const amUser = row.placement?.am?.user; return { diff --git a/backend/src/modules/evenements-agenda/index.ts b/backend/src/modules/evenements-agenda/index.ts new file mode 100644 index 0000000..566e0f2 --- /dev/null +++ b/backend/src/modules/evenements-agenda/index.ts @@ -0,0 +1,2 @@ +export { EvenementsAgendaModule } from './evenements-agenda.module'; +export { EvenementsAgendaService } from './evenements-agenda.service'; diff --git a/database/BDD.sql b/database/BDD.sql index b868b7e..cb5cea0 100644 --- a/database/BDD.sql +++ b/database/BDD.sql @@ -35,13 +35,13 @@ DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN CREATE TYPE statut_avenant_type AS ENUM ('propose', 'accepte', 'refuse'); END IF; - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_absence_garde_type') THEN - CREATE TYPE type_absence_garde_type AS ENUM ( + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_agenda_type') THEN + CREATE TYPE type_evenement_agenda_type AS ENUM ( 'absence_enfant', 'conge_am', 'arret_maladie_am' ); END IF; - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_absence_garde_type') THEN - CREATE TYPE statut_absence_garde_type AS ENUM ( + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_agenda_type') THEN + CREATE TYPE statut_evenement_agenda_type AS ENUM ( 'en_attente', 'accepte', 'refuse' ); END IF; @@ -322,32 +322,32 @@ CREATE TABLE avenants_contrats ( ); -- ========================================================== --- Table : absences_garde (vérité métier absences/congés/arrêt) +-- Table : evenements_agenda (vérité métier agenda (absences/congés/arrêt + futurs types)) -- 1 ligne = 1 période ; rattachement = placement AM↔enfant (#193) -- ========================================================== -CREATE TABLE absences_garde ( +CREATE TABLE evenements_agenda ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id_placement UUID NOT NULL REFERENCES enfants_assistantes_maternelles(id) ON DELETE CASCADE, - type type_absence_garde_type NOT NULL, + type type_evenement_agenda_type NOT NULL, date_debut DATE NOT NULL, date_fin DATE NOT NULL, - statut statut_absence_garde_type NOT NULL DEFAULT 'en_attente', + statut statut_evenement_agenda_type NOT NULL DEFAULT 'en_attente', expire_at TIMESTAMPTZ NOT NULL, cree_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL, id_card_instance UUID, motif TEXT, cree_le TIMESTAMPTZ NOT NULL DEFAULT now(), modifie_le TIMESTAMPTZ NOT NULL DEFAULT now(), - CONSTRAINT chk_absences_garde_dates CHECK (date_fin >= date_debut) + CONSTRAINT chk_evenements_agenda_dates CHECK (date_fin >= date_debut) ); -CREATE INDEX idx_absences_garde_placement_dates - ON absences_garde (id_placement, date_debut, date_fin); -CREATE INDEX idx_absences_garde_placement_type_statut - ON absences_garde (id_placement, type, statut); -CREATE INDEX idx_absences_garde_expire_at - ON absences_garde (expire_at) +CREATE INDEX idx_evenements_agenda_placement_dates + ON evenements_agenda (id_placement, date_debut, date_fin); +CREATE INDEX idx_evenements_agenda_placement_type_statut + ON evenements_agenda (id_placement, type, statut); +CREATE INDEX idx_evenements_agenda_expire_at + ON evenements_agenda (expire_at) WHERE statut IN ('en_attente', 'refuse'); -- ========================================================== diff --git a/database/docs/ENUMS.md b/database/docs/ENUMS.md index 71162cb..b93cdf1 100644 --- a/database/docs/ENUMS.md +++ b/database/docs/ENUMS.md @@ -106,7 +106,7 @@ Ce document recense **toutes les valeurs énumérées** utilisées dans la base ## 7) Type d’absence de garde — `type` -**Tables/colonnes** : `absences_garde.type` +**Tables/colonnes** : `evenements_agenda.type` **Valeurs autorisées** : | Valeur | Description | @@ -115,13 +115,13 @@ Ce document recense **toutes les valeurs énumérées** utilisées dans la base | `conge_am` | Congé de l’assistante maternelle | | `arret_maladie_am` | Arrêt maladie AM | -> Remplace l’ancien enum `type_evenement_type` / table `evenements` (supprimés ticket #193). +> Remplace l’ancien enum `type_evenement_type` / table `evenements` (supprimés ticket #205 / ex-#193). --- ## 8) Statut d’absence de garde — `statut` -**Tables/colonnes** : `absences_garde.statut` +**Tables/colonnes** : `evenements_agenda.statut` **Valeurs autorisées** : | Valeur | Description | diff --git a/database/docs/FK_POLICIES.md b/database/docs/FK_POLICIES.md index 9714b35..84e7f36 100644 --- a/database/docs/FK_POLICIES.md +++ b/database/docs/FK_POLICIES.md @@ -42,8 +42,8 @@ Documenter, de façon unique et partagée, les règles de suppression/mise à jo | **contrats(id_dossier)** → `dossiers(id)` | **CASCADE** | 1:1, contrat détruit si dossier supprimé | | **avenants_contrats(id_contrat)** → `contrats(id)` | **CASCADE** | Avenants détruits avec le contrat | | **avenants_contrats(initie_par)** → `utilisateurs(id)` | **SET NULL** | Historiser l’avenant sans bloquer | -| **absences_garde(id_placement)** → `enfants_assistantes_maternelles(id)` | **CASCADE** | Plus de couple = plus d’absences | -| **absences_garde(cree_par)** → `utilisateurs(id)` | **SET NULL** | Conserver la période si user supprimé | +| **evenements_agenda(id_placement)** → `enfants_assistantes_maternelles(id)` | **CASCADE** | Plus de couple = plus d’absences | +| **evenements_agenda(cree_par)** → `utilisateurs(id)` | **SET NULL** | Conserver la période si user supprimé | | **signalements_bugs(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Conserver le ticket même si compte supprimé | | **uploads(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Fichier reste référencé sans l’auteur | | **notifications(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Notifications propres à l’utilisateur | @@ -77,8 +77,8 @@ Documenter, de façon unique et partagée, les règles de suppression/mise à jo - `avenants_contrats.initie_par` → **SET NULL** ### Événements -- `absences_garde.id_placement` → **CASCADE** -- `absences_garde.cree_par` → **SET NULL** +- `evenements_agenda.id_placement` → **CASCADE** +- `evenements_agenda.cree_par` → **SET NULL** ### Divers - `signalements_bugs.id_utilisateur` → **SET NULL** @@ -105,7 +105,7 @@ Documenter, de façon unique et partagée, les règles de suppression/mise à jo 4. **Suppression d’un enfant** - Supprimer `enfants(id=childA)` - - Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `absences_garde` des placements du childA (CASCADE via EAM). + - Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `evenements_agenda` des placements du childA (CASCADE via EAM). 5. **Suppression d’un utilisateur AM** - Supprimer `utilisateurs(id=amB)` diff --git a/database/migrations/2026_evenements_agenda_rename.sql b/database/migrations/2026_evenements_agenda_rename.sql new file mode 100644 index 0000000..4554fbe --- /dev/null +++ b/database/migrations/2026_evenements_agenda_rename.sql @@ -0,0 +1,106 @@ +-- Ticket #205 — Renommer absences_garde → evenements_agenda +-- Migration base live. Idempotent autant que possible. + +-- --------------------------------------------------------------------------- +-- 1. Table + enums +-- --------------------------------------------------------------------------- +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'absences_garde') + AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'evenements_agenda') THEN + ALTER TABLE absences_garde RENAME TO evenements_agenda; + END IF; +END $$; + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_absence_garde_type') + AND NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_agenda_type') THEN + ALTER TYPE type_absence_garde_type RENAME TO type_evenement_agenda_type; + END IF; + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_absence_garde_type') + AND NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_agenda_type') THEN + ALTER TYPE statut_absence_garde_type RENAME TO statut_evenement_agenda_type; + END IF; +END $$; + +-- Contraintes / index (noms) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_absences_garde_dates') THEN + ALTER TABLE evenements_agenda RENAME CONSTRAINT chk_absences_garde_dates TO chk_evenements_agenda_dates; + END IF; +END $$; + +ALTER INDEX IF EXISTS idx_absences_garde_placement_dates + RENAME TO idx_evenements_agenda_placement_dates; +ALTER INDEX IF EXISTS idx_absences_garde_placement_type_statut + RENAME TO idx_evenements_agenda_placement_type_statut; +ALTER INDEX IF EXISTS idx_absences_garde_expire_at + RENAME TO idx_evenements_agenda_expire_at; +ALTER INDEX IF EXISTS absences_garde_pkey + RENAME TO evenements_agenda_pkey; + +COMMENT ON TABLE evenements_agenda IS + 'Vérité métier agenda (absences, congés, arrêts, futurs soins/école/mairie/sorties…) par placement. Les cartes collectent ; cette table historise.'; +COMMENT ON COLUMN evenements_agenda.expire_at IS + 'Purge auto (job) pour en_attente/refuse. accepte → infinity.'; +COMMENT ON COLUMN evenements_agenda.id_card_instance IS + 'Réf. carte de collecte (module Cartes).'; + +-- --------------------------------------------------------------------------- +-- 2. card_instances.id_absence → id_evenement +-- --------------------------------------------------------------------------- +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'card_instances' AND column_name = 'id_absence' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'card_instances' AND column_name = 'id_evenement' + ) THEN + ALTER TABLE card_instances RENAME COLUMN id_absence TO id_evenement; + END IF; +END $$; + +-- Recréer index si ancien nom +ALTER INDEX IF EXISTS idx_card_instances_absence + RENAME TO idx_card_instances_evenement; + +-- FK : drop ancienne (nom variable) puis recreate +DO $$ +DECLARE + fk_name text; +BEGIN + SELECT conname INTO fk_name + FROM pg_constraint + WHERE conrelid = 'card_instances'::regclass + AND contype = 'f' + AND pg_get_constraintdef(oid) ILIKE '%id_evenement%' + LIMIT 1; + + IF fk_name IS NULL THEN + SELECT conname INTO fk_name + FROM pg_constraint + WHERE conrelid = 'card_instances'::regclass + AND contype = 'f' + AND pg_get_constraintdef(oid) ILIKE '%id_absence%' + LIMIT 1; + END IF; + + IF fk_name IS NOT NULL THEN + EXECUTE format('ALTER TABLE card_instances DROP CONSTRAINT %I', fk_name); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'card_instances_id_evenement_fkey' + ) THEN + ALTER TABLE card_instances + ADD CONSTRAINT card_instances_id_evenement_fkey + FOREIGN KEY (id_evenement) + REFERENCES evenements_agenda(id) + ON DELETE SET NULL; + END IF; +END $$; diff --git a/database/tests/sql/verify.sql b/database/tests/sql/verify.sql index 4104ab0..df39b34 100644 --- a/database/tests/sql/verify.sql +++ b/database/tests/sql/verify.sql @@ -81,7 +81,7 @@ ORDER BY c.cree_le DESC; \echo '=== 9) Absences_garde (30 derniers jours) =====================' SELECT ag.id, ag.type, ag.id_placement, ag.date_debut, ag.date_fin, ag.statut, ag.expire_at -FROM absences_garde ag +FROM evenements_agenda ag WHERE ag.date_debut >= (NOW()::date - INTERVAL '30 days') ORDER BY ag.date_debut DESC; @@ -136,7 +136,7 @@ WHERE c.id IS NULL; -- Absences sans placement SELECT ag.* -FROM absences_garde ag +FROM evenements_agenda ag LEFT JOIN enfants_assistantes_maternelles eam ON eam.id = ag.id_placement WHERE eam.id IS NULL; @@ -150,10 +150,10 @@ WHERE m.id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd' ORDER BY m.cree_le DESC LIMIT 20; --- Absences par placement et période (idx_absences_garde_placement_dates) +-- Absences par placement et période (idx_evenements_agenda_placement_dates) EXPLAIN ANALYZE SELECT ag.* -FROM absences_garde ag +FROM evenements_agenda ag WHERE ag.id_placement IS NOT NULL AND ag.date_debut >= '2025-01-01'; @@ -198,7 +198,7 @@ SELECT (SELECT COUNT(*) FROM messages) AS nb_messages, (SELECT COUNT(*) FROM contrats) AS nb_contrats, (SELECT COUNT(*) FROM avenants_contrats) AS nb_avenants, - (SELECT COUNT(*) FROM absences_garde) AS nb_absences_garde, + (SELECT COUNT(*) FROM evenements_agenda) AS nb_evenements_agenda, (SELECT COUNT(*) FROM uploads) AS nb_uploads, (SELECT COUNT(*) FROM notifications) AS nb_notifications, (SELECT COUNT(*) FROM validations) AS nb_validations; diff --git a/docs/24_DECISIONS-PROJET.md b/docs/24_DECISIONS-PROJET.md index 426643c..b1451d5 100644 --- a/docs/24_DECISIONS-PROJET.md +++ b/docs/24_DECISIONS-PROJET.md @@ -572,7 +572,7 @@ POST /repos/jmartin/petitspas/pulls/{index}/merge ### 32. Module Absences + Cartes (séparation back / collecte) -**Décision** : ✅ **Back `absences_garde` = vérité métier** ; **Cartes = file d’attention / collecte** (plugin isolé). Types SYSTEM (absence, congé, arrêt) indéboulonnables ; types OPTIONNELS (sondages…) plus tard. Annulation = DELETE. `expire_at` dès V1. +**Décision** : ✅ **Back `evenements_agenda` = vérité métier** ; **Cartes = file d’attention / collecte** (plugin isolé). Types SYSTEM (absence, congé, arrêt) indéboulonnables ; types OPTIONNELS (sondages…) plus tard. Annulation = DELETE. `expire_at` dès V1. **Justification** : - Premier vrai module d’interaction multi-acteurs @@ -615,7 +615,7 @@ POST /repos/jmartin/petitspas/pulls/{index}/merge | 25/11/2025 | 1.0 | Création du document - Toutes les décisions initiales | | 09/02/2026 | 1.1 | Configuration initiale : un seul panneau Paramètres (3 sections) dans le dashboard, plus de Setup Wizard dédié ; navigation bloquée jusqu'à sauvegarde | | 16/06/2026 | 1.2 | Décision 5bis — familles recomposées, contournement v1.0.0 ; lien doc [28](./28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md) | -| 24/09/2026 | 1.4 | Décision 32 — Absences (`absences_garde`) ≠ Cartes ; Epic C recentré ; drop `evenements` | +| 24/09/2026 | 1.4 | Décision 32 — Absences (`evenements_agenda`) ≠ Cartes ; Epic C recentré ; drop `evenements` | --- diff --git a/docs/30_DECOUPAGE-TICKETS-QUOTIDIEN.md b/docs/30_DECOUPAGE-TICKETS-QUOTIDIEN.md index ee69363..939bc47 100644 --- a/docs/30_DECOUPAGE-TICKETS-QUOTIDIEN.md +++ b/docs/30_DECOUPAGE-TICKETS-QUOTIDIEN.md @@ -98,13 +98,13 @@ Liste des enfants / foyers pour l’AM + contexte courant (symétrique A3). ## Epic C — Absences + Cartes (file d’attention) -**Séparation :** back **`absences_garde`** = vérité métier (périodes / placement) ; module **Cartes** = collecte / bulles / workflow. Annulation = DELETE. `expire_at` dès le modèle. +**Séparation :** back **`evenements_agenda`** = vérité métier (périodes / placement) ; module **Cartes** = collecte / bulles / workflow. Annulation = DELETE. `expire_at` dès le modèle. ### Back (ordre) | Étape | Contenu | |-------|---------| -| BDD | Table `absences_garde` + drop `evenements` | +| BDD | Table `evenements_agenda` + drop `evenements` | | API | CRUD + **GET liste** (`placementId` \| tous les placements du user) | | Cartes SYSTEM | Module `cards/` types S1–S3 (sans sondages V1) | | Realtime | WS/SSE bulles → **SSE** `GET /cards/stream` (#195) | diff --git a/docs/31_MINI-SPEC-QUOTIDIEN-PARENT-AM.md b/docs/31_MINI-SPEC-QUOTIDIEN-PARENT-AM.md index 05d0d8a..8f2e521 100644 --- a/docs/31_MINI-SPEC-QUOTIDIEN-PARENT-AM.md +++ b/docs/31_MINI-SPEC-QUOTIDIEN-PARENT-AM.md @@ -72,7 +72,7 @@ Bandeau : **TdB** · **Agenda** · **Contrat** · menu user (recherche AM, param | Maladie AM | AM | Parent **ack** (« bien reçu ») ; **aucun** doc médical | | Sortie / sondage | — | **Plus tard** (types optionnels) | -**Stockage :** table `absences_garde` (1 ligne = période, `id_placement`, `expire_at`). Les **cartes** collectent ; elles ne sont pas la source de vérité. Annulation = DELETE. +**Stockage :** table `evenements_agenda` (1 ligne = période, `id_placement`, `expire_at`). Les **cartes** collectent ; elles ne sont pas la source de vérité. Annulation = DELETE. **Péremption cartes :** mémoire courte (`retention_days` / `purge_at`) — pas un historique de vie (≠ messagerie). diff --git a/frontend/lib/services/api/absences_garde_service.dart b/frontend/lib/services/api/absences_garde_service.dart index 0b5181d..2bef801 100644 --- a/frontend/lib/services/api/absences_garde_service.dart +++ b/frontend/lib/services/api/absences_garde_service.dart @@ -10,7 +10,7 @@ class AbsencesGardeService { final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers; final uri = Uri.parse(ApiConfig.baseUrl + - '/absences-garde' + + '/evenements-agenda' + (placementId != null ? '?placementId=$placementId' : '')); final res = await http.get(uri, headers: headers); @@ -33,7 +33,7 @@ class AbsencesGardeService { final token = await TokenService.getToken(); final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers; - final uri = Uri.parse('${ApiConfig.baseUrl}/absences-garde'); + final uri = Uri.parse('${ApiConfig.baseUrl}/evenements-agenda'); final res = await http.post( uri, headers: headers, @@ -62,7 +62,7 @@ class AbsencesGardeService { final token = await TokenService.getToken(); final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers; - final uri = Uri.parse('${ApiConfig.baseUrl}/absences-garde/$id'); + final uri = Uri.parse('${ApiConfig.baseUrl}/evenements-agenda/$id'); final Map body = {}; if (dateDebut != null) body['date_debut'] = dateDebut; if (dateFin != null) body['date_fin'] = dateFin; @@ -85,7 +85,7 @@ class AbsencesGardeService { final token = await TokenService.getToken(); final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers; - final uri = Uri.parse('${ApiConfig.baseUrl}/absences-garde/$id'); + final uri = Uri.parse('${ApiConfig.baseUrl}/evenements-agenda/$id'); final res = await http.delete(uri, headers: headers); if (res.statusCode != 204) { throw Exception("Erreur de suppression d'absence : ${res.statusCode}");