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:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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.';
|
||||||
Reference in New Issue
Block a user