feat(#194): schéma cartes SYSTEM (types, instances, audience, responses).

Migration + seeds absence/congé/arrêt + entités TypeORM.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-24 11:27:42 +02:00
co-authored by Cursor
parent 147051821a
commit c784de8198
5 changed files with 359 additions and 0 deletions
@@ -0,0 +1,35 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { CardInstance } from './card_instances.entity';
import { Users } from './users.entity';
@Entity('card_audience_members', { schema: 'public' })
export class CardAudienceMember {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'id_card', type: 'uuid' })
id_card: string;
@ManyToOne(() => CardInstance, (c) => c.audience, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_card', referencedColumnName: 'id' })
card: CardInstance;
@Column({ name: 'id_utilisateur', type: 'uuid' })
id_utilisateur: string;
@ManyToOne(() => Users, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
@Column({ name: 'role_snapshot', type: 'varchar', length: 64 })
role_snapshot: string;
@Column({ name: 'is_creator', type: 'boolean', default: false })
is_creator: boolean;
}
@@ -0,0 +1,97 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { AmChildren } from './am_children.entity';
import { AbsencesGarde } from './absences_garde.entity';
import { Users } from './users.entity';
import { CardType } from './card_types.entity';
import { CardAudienceMember } from './card_audience_members.entity';
import { CardResponse } from './card_responses.entity';
export enum CardInstanceStatutType {
OUVERTE = 'ouverte',
REFUSEE = 'refusee',
TRAITEE = 'traitee',
}
export enum CardOperationType {
CREATE = 'create',
UPDATE = 'update',
}
@Entity('card_instances', { schema: 'public' })
export class CardInstance {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'type_code', type: 'varchar', length: 64 })
type_code: string;
@ManyToOne(() => CardType, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'type_code', referencedColumnName: 'code' })
type: CardType;
@Column({ name: 'id_placement', type: 'uuid' })
id_placement: string;
@ManyToOne(() => AmChildren, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_placement', referencedColumnName: 'id' })
placement: AmChildren;
@Column({ name: 'id_absence', type: 'uuid', nullable: true })
id_absence?: string;
@ManyToOne(() => AbsencesGarde, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'id_absence', referencedColumnName: 'id' })
absence?: AbsencesGarde;
@Column({ name: 'cree_par', type: 'uuid', nullable: true })
cree_par?: string;
@ManyToOne(() => Users, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'cree_par', referencedColumnName: 'id' })
createdBy?: Users;
@Column({
type: 'enum',
enum: CardOperationType,
enumName: 'card_operation_type',
name: 'operation',
default: CardOperationType.CREATE,
})
operation: CardOperationType;
@Column({
type: 'enum',
enum: CardInstanceStatutType,
enumName: 'card_instance_statut_type',
name: 'statut',
default: CardInstanceStatutType.OUVERTE,
})
statut: CardInstanceStatutType;
@Column({ name: 'payload', type: 'jsonb', default: {} })
payload: Record<string, unknown>;
@Column({ name: 'purge_at', type: 'timestamptz' })
purge_at: Date;
@OneToMany(() => CardAudienceMember, (m) => m.card)
audience: CardAudienceMember[];
@OneToMany(() => CardResponse, (r) => r.card)
responses: CardResponse[];
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
cree_le: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
modifie_le: Date;
}
@@ -0,0 +1,50 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { CardInstance } from './card_instances.entity';
import { Users } from './users.entity';
export enum CardResponseActionType {
ACCEPT = 'accept',
REFUSE = 'refuse',
ACK = 'ack',
}
@Entity('card_responses', { schema: 'public' })
export class CardResponse {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'id_card', type: 'uuid' })
id_card: string;
@ManyToOne(() => CardInstance, (c) => c.responses, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_card', referencedColumnName: 'id' })
card: CardInstance;
@Column({ name: 'id_utilisateur', type: 'uuid' })
id_utilisateur: string;
@ManyToOne(() => Users, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
@Column({
type: 'enum',
enum: CardResponseActionType,
enumName: 'card_response_action_type',
name: 'action',
})
action: CardResponseActionType;
@Column({ name: 'comment', type: 'text', nullable: true })
comment?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
cree_le: Date;
}
+54
View File
@@ -0,0 +1,54 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
export enum CardResponseModeType {
NONE = 'none',
ACK = 'ack',
ACCEPT_REFUSE = 'accept_refuse',
}
@Entity('card_types', { schema: 'public' })
export class CardType {
@PrimaryColumn({ name: 'code', type: 'varchar', length: 64 })
code: string;
@Column({ name: 'system', type: 'boolean', default: true })
system: boolean;
@Column({ name: 'titre', type: 'varchar', length: 120 })
titre: string;
@Column({ name: 'emitter_roles', type: 'text', array: true })
emitter_roles: string[];
@Column({ name: 'recipient_roles', type: 'text', array: true })
recipient_roles: string[];
@Column({ name: 'audience_resolver', type: 'varchar', length: 64 })
audience_resolver: string;
@Column({
type: 'enum',
enum: CardResponseModeType,
enumName: 'card_response_mode_type',
name: 'response_mode',
})
response_mode: CardResponseModeType;
@Column({ name: 'retention_days', type: 'int', default: 14 })
retention_days: number;
@Column({ name: 'couleur', type: 'varchar', length: 32, nullable: true })
couleur?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
cree_le: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
modifie_le: Date;
}
+123
View File
@@ -0,0 +1,123 @@
-- Ticket #194 — Module Cartes SYSTEM (collecte absences/congés/arrêt)
-- Idempotent.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'card_response_mode_type') THEN
CREATE TYPE card_response_mode_type AS ENUM (
'none', 'ack', 'accept_refuse'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'card_instance_statut_type') THEN
CREATE TYPE card_instance_statut_type AS ENUM (
'ouverte', 'refusee', 'traitee'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'card_operation_type') THEN
CREATE TYPE card_operation_type AS ENUM ('create', 'update');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'card_response_action_type') THEN
CREATE TYPE card_response_action_type AS ENUM (
'accept', 'refuse', 'ack'
);
END IF;
END $$;
CREATE TABLE IF NOT EXISTS card_types (
code VARCHAR(64) PRIMARY KEY,
system BOOLEAN NOT NULL DEFAULT true,
titre VARCHAR(120) NOT NULL,
emitter_roles TEXT[] NOT NULL,
recipient_roles TEXT[] NOT NULL,
audience_resolver VARCHAR(64) NOT NULL,
response_mode card_response_mode_type NOT NULL,
retention_days INT NOT NULL DEFAULT 14,
couleur VARCHAR(32),
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
modifie_le TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS card_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type_code VARCHAR(64) NOT NULL REFERENCES card_types(code),
id_placement UUID NOT NULL
REFERENCES enfants_assistantes_maternelles(id) ON DELETE CASCADE,
id_absence UUID REFERENCES absences_garde(id) ON DELETE SET NULL,
cree_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
operation card_operation_type NOT NULL DEFAULT 'create',
statut card_instance_statut_type NOT NULL DEFAULT 'ouverte',
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
purge_at TIMESTAMPTZ NOT NULL,
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
modifie_le TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_card_instances_placement
ON card_instances (id_placement, statut, cree_le DESC);
CREATE INDEX IF NOT EXISTS idx_card_instances_purge
ON card_instances (purge_at);
CREATE INDEX IF NOT EXISTS idx_card_instances_absence
ON card_instances (id_absence)
WHERE id_absence IS NOT NULL;
CREATE TABLE IF NOT EXISTS card_audience_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_card UUID NOT NULL REFERENCES card_instances(id) ON DELETE CASCADE,
id_utilisateur UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE,
role_snapshot VARCHAR(64) NOT NULL,
is_creator BOOLEAN NOT NULL DEFAULT false,
UNIQUE (id_card, id_utilisateur)
);
CREATE INDEX IF NOT EXISTS idx_card_audience_user
ON card_audience_members (id_utilisateur, id_card);
CREATE TABLE IF NOT EXISTS card_responses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_card UUID NOT NULL REFERENCES card_instances(id) ON DELETE CASCADE,
id_utilisateur UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE,
action card_response_action_type NOT NULL,
comment TEXT,
cree_le TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_card_responses_card
ON card_responses (id_card, cree_le DESC);
-- Seeds SYSTEM
INSERT INTO card_types (
code, system, titre, emitter_roles, recipient_roles,
audience_resolver, response_mode, retention_days, couleur
) VALUES
(
'absence_enfant', true, 'Absence enfant',
ARRAY['parent'], ARRAY['assistante_maternelle'],
'couple_am', 'none', 7, 'peach'
),
(
'absence_enfant_modif', true, 'Absence enfant modifiée',
ARRAY['parent'], ARRAY['assistante_maternelle'],
'couple_am', 'ack', 7, 'peach'
),
(
'conge_am', true, 'Congé AM',
ARRAY['assistante_maternelle'], ARRAY['parent'],
'couple_parents', 'accept_refuse', 14, 'lavender'
),
(
'arret_maladie_am', true, 'Arrêt maladie AM',
ARRAY['assistante_maternelle'], ARRAY['parent'],
'couple_parents', 'ack', 14, 'pink'
)
ON CONFLICT (code) DO UPDATE SET
titre = EXCLUDED.titre,
emitter_roles = EXCLUDED.emitter_roles,
recipient_roles = EXCLUDED.recipient_roles,
audience_resolver = EXCLUDED.audience_resolver,
response_mode = EXCLUDED.response_mode,
retention_days = EXCLUDED.retention_days,
couleur = EXCLUDED.couleur,
modifie_le = now();
COMMENT ON TABLE card_types IS 'Catalogue types de cartes (SYSTEM seeds #194 ; OPTIONNEL plus tard).';
COMMENT ON TABLE card_instances IS 'Bulles / file d’attention — collecte, pas vérité métier absences.';