Aller au contenu principal

Formulaire multi-étapes / workflow

Le problème

Un document passe par une séquence d'états — Brouillon → Soumis → Approuvé / Rejeté → Clôturé. Chaque transition est un bouton visible uniquement par le bon rôle. Certains champs n'apparaissent qu'après la soumission ; d'autres se figent après l'approbation. Les documents bloqués doivent relancer quelqu'un après N jours.

Le modèle

Trois fonctionnalités du framework se composent pour modéliser le workflow :

FonctionnalitéCe qu'elle fait
Colonne de statut avec règle ENUMAffiche l'état courant comme une puce colorée.
Actions personnalisées sur le dialogueUn bouton par transition (Soumettre, Approuver, Rejeter). L'expression Visible quand de chaque action masque le bouton en dehors de son état valide.
Conditions de formulaire (Visible quand, Obligatoire quand, Désactivé quand)Afficher / masquer / figer des champs selon le statut courant.
Job NomaflowUne relance nocturne qui pousse les documents bloqués.

La recette

1. Schéma

CREATE TABLE expense_claims (
id SERIAL PRIMARY KEY,
employee VARCHAR(64) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
description TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
submitted_at TIMESTAMP,
approved_by VARCHAR(64),
approved_at TIMESTAMP,
rejected_by VARCHAR(64),
rejected_at TIMESTAMP,
reject_reason TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);

2. Dictionnaire — l'enum de statut

ValeurLibelléCouleur
draftBrouillon#94a3b8
submittedSoumis#60a5fa
approvedApprouvé#4ade80
rejectedRejeté#f87171
closedClôturé#475569

3. Requêtes d'écriture par statut (une par transition)

-- submit
UPDATE expense_claims
SET status = 'submitted', submitted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = :id AND status = 'draft';

-- approve
UPDATE expense_claims
SET status = 'approved', approved_by = :session_user, approved_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = :id AND status = 'submitted';

-- reject
UPDATE expense_claims
SET status = 'rejected', rejected_by = :session_user, rejected_at = CURRENT_TIMESTAMP,
reject_reason = :reject_reason, updated_at = CURRENT_TIMESTAMP
WHERE id = :id AND status = 'submitted';

Le garde-fou AND status = '<attendu>' au niveau SQL empêche les doubles transitions accidentelles (un déposant ne peut pas re-soumettre ; un manager ne peut pas approuver une demande rejetée).

4. Écran — actions + conditions

Paramètres → Écrans → expense-claims.

Actions

ActionConnecteur / RequêteVisible quandRôles
Soumettreexpense-claims / submitstatus == 'draft' && employee == session.userTous (filtré par le filtre niveau ligne)
Approuverexpense-claims / approvestatus == 'submitted' && 'manager' in session.rolesManager
Rejeterexpense-claims / rejectstatus == 'submitted' && 'manager' in session.rolesManager. Le dialogue de confirmation demande reject_reason.

Conditions de champs

ChampVisible quandObligatoire quandDésactivé quand
Descriptiontoujoursstatus == 'draft'status != 'draft'
Amounttoujoursstatus == 'draft'status != 'draft'
Submitted atstatus != 'draft'toujours (lecture seule)
Approved by / atstatus == 'approved'toujours
Reject reasonstatus == 'rejected'toujours

Le dialogue est la même ligne — il masque / fige simplement différents champs selon le statut.

5. Codes de permission

Les transitions génèrent automatiquement trois codes d'écriture :

  • sql:expense-claims:submit:write
  • sql:expense-claims:approve:write
  • sql:expense-claims:reject:write

Rôles :

RôleCodes accordés
employeesql:expense-claims:submit:write (ne peut soumettre que les siennes — le garde-fou SQL gère le reste)
managersql:expense-claims:approve:write + sql:expense-claims:reject:write

6. Le job de relance

Relance des demandes bloquées — même modèle que le job CRM nocturne deals bloqués :

-- nag-stale-submissions
SELECT id, employee, amount, submitted_at
FROM expense_claims
WHERE status = 'submitted'
AND submitted_at < CURRENT_TIMESTAMP - INTERVAL '5 days';

Job Nomaflow, quotidien à 09:00, poste sur le canal Slack #expenses-managers.

Variantes

Vous voulez…Faites ceci
Une chaîne d'approbateurs libre (liste variable d'approbateurs)Ajoutez une table enfant approval_steps ; le statut devient awaiting:<step_n>. Les requêtes d'approbation incrémentent le compteur d'étape. Plus complexe mais avec les mêmes primitives.
Un journal d'historique de chaque transitionAjoutez une table expense_audit ; chaque requête de transition insère une ligne depuis une étape Python.
Réouvrir les rejetésAjoutez une action reopen visible quand status == 'rejected', qui rebascule en draft.
Notifier le demandeur à chaque transitionBloc de notifications sur chaque requête d'écriture — voir Jobs → Notifications.

Pour aller plus loin