---
phase: screens
kind: level
level: form
---

# Level — Form screens (SmartForm)

Use this level when the user needs to **create**, **edit** or **inspect** a
single entity instance via a structured input form.

## When to use

Trigger words: "form", "edit", "create", "new", "fill", "update", "details",
"fiche", "saisir", "modifier".

Mandatory when:
- The section code ends in `-detail`, `-edit`, `-create`, `-form`, `-new`.
- The bound use cases include verbs like "create", "register", "update",
  "modify", "edit", "fill in".

## What to produce

One SmartForm per detail section. Pick the layout based on entity complexity.

### Flat fields (small entities, < 8 fields)

```markdown
### SCR-HR-DEPARTMENTS-CREATE-001 — Nouveau département (SmartForm)
- **Entité** : Department (ENT-005)
- **Permission** : `hr.departments.create`
- **Cas d'usage liés** : UC-HR-DEPARTMENTS-CREATE-001
- **Mode** : create
- **Champs** : code (text, requis — TYPED referential code the user enters, NOT system-allocated : no `**Code pattern**` on the entity), name (text, requis), manager (lookup → Employee), description (textarea)
```

> Do NOT author per-field column spans (`col N`) — they are not consumed
> downstream: the layout is decided by `formLayout` + the generator's odd-run
> balancing (`/ui-design` directives).

### Sectioned layout (one page, titled groups)

```markdown
### SCR-HR-EMPLOYEES-CREATE-001 — Nouvel employé (SmartForm)
- **Entité** : Employee (ENT-003)
- **Permission** : `hr.employees.create`
- **Cas d'usage liés** : UC-HR-EMPLOYEES-CREATE-001
- **Mode** : create
- **Section « Identité »** : userId (lookup → User, requis), birthDate (date)
- **Section « Contrat »** : hireDate (date, requis), contractEndDate (date), workRate (number)
```

Same bullet grammar as the tabs (`- **Section « X »** : field, field, …`), but
everything stays on ONE page: each Section renders as a titled card, all
visible in one scroll. Section labels are shared by the form AND the sectioned
detail render (`form.section.<key>` — there is no `detail.section.*`).

### Tabbed layout (complex entities, multiple aspects)

```markdown
### SCR-HR-EMPLOYEES-DETAIL-001 — Fiche employé (SmartForm)
- **Entité** : Employee (ENT-003)
- **Permission** : `hr.employees.update`
- **Cas d'usage liés** : UC-HR-EMPLOYEES-DETAIL-001, UC-HR-EMPLOYEES-DETAIL-002
- **Mode** : edit
- **Onglet « Information »** : code (text, requis, readonly), firstName, lastName, email (requis), phone
- **Onglet « Emploi »** : department (lookup → Department, requis), manager (lookup → Employee), position (requis), hireDate (date, requis), status (enum: active/inactive/onLeave)
- **Onglet « Rémunération »** : salary (currency), bonusYTD (currency), totalCompensation (currency, readonly, calculé `salary + bonusYTD`)
```

The richer per-field config (`type`, `options`, `computed`) is in
`references/smartcomponents.md`.

### Choosing the layout — decision table

| Situation | Form |
|---|---|
| ≤ ~7 fields, one subject | Flat (no grouping bullet) — no grouping beats arbitrary grouping |
| 8–14 fields, ONE reading moment (the user scans everything) | **Sections** — 2 to 4, each ≥ 2 fields, all visible in one scroll |
| ≥ ~15 fields, OR aspects consulted independently | **Tabs** — 2 to 5 inner (the shared-bar budget applies) |
| A 360 fiche rich in related tabs | **Sections** — la barre est déjà occupée par le 360 : les champs propres ne s'y ajoutent pas, ils se lisent au-dessus (cartes de sections, fiche unifiée) |
| Never | Mixing `Section` and `Onglet` at the first level of one screen; Section-inside-Tab nesting is NOT v1 |

The criterion: a Section stays under the eyes (continuous reading — the edit
render is "read-first" by default, each section opens for editing on demand),
a Tab hides the others (disjoint aspects). On a 360 fiche the related tabs
ARE the disjoint aspects; the record's own fields are the continuous reading
— Sections, above the bar, never one more tab competing with the satellites.
Name the groups by BUSINESS MEANING
(Identity, Contract, Billing), never by technical type — same rule as the tabs.

## Field choice algorithm

For each entity attribute, decide whether to include it:

| Attribute kind            | `create` | `edit` | `detail` |
|---------------------------|:--------:|:------:|:--------:|
| Required scalar           | yes      | yes    | yes      |
| Optional scalar           | yes      | yes    | yes      |
| FK (lookup)               | yes      | yes    | yes      |
| Status enum               | usually no (or `readonly` — it is the lifecycle anchor) | yes  | yes      |
| **Later-phase field** (payment / exit / closure…) | **NEVER** — phase it (see « Cycle de vie ») | when the status allows (status-gated) | status-gated |
| Computed                  | **NEVER**| yes (readonly) | yes (readonly) |
| `createdAt`/`updatedAt`   | no       | no (or readonly) | yes (readonly) |
| `id` / pk                 | no (auto)| readonly | readonly |
| Sensitive (password hash) | special  | special| no       |

## Cycle de vie — phased fields

A form is SMART when it asks each question at the moment the business can
answer it. A field belongs to a **lifecycle phase** when its value only exists
once the record has advanced through its status workflow: an invoice's
`paymentDate` exists once PAYEE (captured by the « Marquer payée » action), an
employee's `departureDate`/`departureReason` once TERMINATED, a vehicle's exit
readings once SORTI. Asking them at creation is the anti-pattern this bullet
kills — the create form must never ask a payment date.

Declare the phases with ONE bullet on the form screen (same family as
`**Section « X »**`):

```markdown
- **Cycle de vie** : statut `status` ;
  phase « paiement » (statuts : PAYEE ; action : marquerPayee) : paymentDate (requis), paymentMethod ;
  phase « sortie » (statuts : TERMINATED) : departureDate, departureReason ;
  phase « soumission » (statuts : SOUMISE/ENVOYEE/PAYEE) : requis dès ce stade : dueDate
```

Grammar:
- `statut \`<champ>\`` names the entity's state-semantics enum ONCE (the same
  attribute the Flow business rules transition — see
  `create-business-rules` § Workflow rules). Statuses are the enum's values
  **verbatim** (`/`-separated for several).
- Each `phase « <clé> » (statuts : S1[/S2…][ ; action : <codeAction>])` lists the
  fields the phase OWNS: **hidden at create**, visible on edit only while the
  status is in the list. `action :` names the custom action that captures them
  (its `payloadParameters` become the input dialog — the sanctioned write path);
  without it the fields simply unlock on the edit fiche.
- `(requis)` on an owned field → mandatory once the phase is reached (the
  capturing dialog enforces it too).
- `requis dès ce stade : <champs>` lists fields that STAY on the create form as
  optional inputs but become mandatory once the phase's statuses are reached —
  the draft pattern: create incomplete, complete before submitting. These
  fields are also declared normally in **Champs**/**Section** bullets.

Rules:
- A phase needs a status anchor that EXISTS: enum value verbatim, and a Flow
  business rule reaching it (audits XD-007 / PRD-120 verify).
- A phased field is **never `requis`** in its own declaration — its column must
  stay nullable (the record exists before the phase); its obligation is
  phase-scoped, expressed as above.
- The statusField itself is never phased; `phase « creation »` is never written
  (un-phased fields ARE the creation phase).
- Sections stay the GROUPING lever, orthogonal: give the « Sortie » fields
  their `**Section « Sortie »**` card when they warrant one — a section whose
  every field is phased disappears entirely from the create form (no empty
  shell at check-in).
- No state-semantics enum on the entity → no Cycle de vie bullet. Don't invent
  a lifecycle for a referential.

The four canonical shapes:

| Case | Declaration |
|---|---|
| Invoice.paymentDate — captured by « Marquer payée » | `phase « paiement » (statuts : PAYEE ; action : marquerPayee) : paymentDate (requis)` |
| Employee sortie — unlocks on the edit fiche | `phase « sortie » (statuts : TERMINATED) : departureDate, departureReason` |
| Vehicle entrée/sortie — phased SECTION | `**Section « Sortie »** : exitDate, exitReason` + `phase « sortie » (statuts : SORTI ; action : sortirVehicule) : exitDate (requis), exitReason` |
| Draft invoice — create incomplete, complete before sending | `phase « soumission » (statuts : SOUMISE/ENVOYEE/PAYEE) : requis dès ce stade : dueDate` |

Downstream: `/ba-create-prd` mines the bullet 1:1 into the pagespec's
first-order `lifecycle` block (canonical schema `lib/page-spec-lifecycle.ts`);
the generators compile it — create form/DTO exclusion, status-gated edit,
status-guarded validation — end-to-end.

## Tabs guidelines

- 2 to 5 INNER tabs maximum — and the SHARED bar (inner + related tabs, band
  cartouches excluded) is budgeted deterministically: > 7 warn, > 9 err
  (RTV-009 at authoring, RTV-108 on the pagespecs — `lib/detail-tab-strip`
  thresholds). The remedy is never dropping a tab: `affichage summary`
  (band cartouche) for inert satellites, `Section` bullets for own fields.
- Each tab groups attributes by **business meaning** (Identity, Employment,
  Compensation, History) — not by technical type.
- Each tab needs ≥2 fields, otherwise inline them elsewhere.
- Computed-heavy read-only summary tabs come last.

## Mode rules

- `create` → fields for a new entity. Computed fields ABSENT. Related tabs
  FORBIDDEN (the entity does not exist yet).
- `edit` → fields for an existing entity. Computed fields PRESENT and `readonly`.
  Audit fields optional and `readonly` if shown. Related tabs allowed.
- `detail` → all fields read-only. Use for view-only pages (HR profile, audit
  views). Related tabs allowed and recommended when the entity has incoming
  1:N relationships.
- **Coded entity** (the entity's `entité.md` block declares a `**Code pattern**`)
  → the `code` field is NEVER an editable input: ABSENT in `create` mode (the
  engine allocates it at insert — the user has nothing to type) and `readonly`
  in `edit`/`detail` (display the allocated code). ONE sanctioned exception:
  when the `**Code pattern**` line declares `surchargeable à la création`
  (`supplied on create`), an OPTIONAL code field is allowed in `create` mode —
  the generated form mounts the SmartCodeField (suggestions + manual entry,
  validated by the socle's `ISuppliedCodeGuard`; imports/reprises arrive WITH
  their number). Never required, and NEVER in edit (codes are immutable after
  creation). Only entities WITHOUT a code pattern may list `code` as an
  editable Champ — and on a REFERENCE table (`lookup`) only when the user
  DECIDED that table carries a code (`- **Code décidé**` in `entité.md`, audit
  DM-022): by default a reference value has no code at all, its libellé names it.
  Audit SCR-015 rejects the violation.

## Related entity tabs — the 360 view

A real fiche is the **hub** for everything IN RELATION with it (a Client ⟷ its
Factures, Contacts, Litiges, Contrats — a relational web, not a parent/child
hierarchy). In `edit`/`detail` mode a SmartForm exposes those relations as
extra tabs **inside the same page**, each fetching the related entity's records
through the FK it carries toward this screen's entity, gated by the actor's
permission on the RELATED entity.

The content of each tab follows the NATURE of the relation via `affichage`
(default `table`): `table` for transactional volumes (factures, échanges),
`cards` for visual/low-volume records (adresses, contacts), `summary` for a
count cartouche (litiges ouverts, relations 1:1).

`screen.md` entry with related tabs:

```markdown
### SCR-CRM-CLIENTS-DETAIL-001 — Fiche client (SmartForm)
- **Entité** : Client (ENT-001)
- **Permission** : `crm.clients.read`
- **Cas d'usage liés** : UC-CRM-CLIENTS-DETAIL-001
- **Mode** : detail
- **Résumé** : name ; status ; siret, category
- **Onglet « Informations »** : code (text, readonly), name (text), siret (text), category (lookup → ClientCategory)
- **Onglet « Adresse de facturation »** : billingStreet, billingZip, billingCity
- **Onglet lié « Factures »** : entité Invoice, FK clientId, affichage table → SCR-CRM-INVOICES-LIST-001 (`crm.invoices.read`)
- **Onglet lié « Contacts »** : entité Contact, FK clientId, affichage cards → SCR-CRM-CONTACTS-LIST-001
- **Onglet lié « Litiges »** : entité Claim, FK clientId, affichage summary → SCR-CRM-CLAIMS-LIST-001
```

(The `- **Résumé** :` line is the detail summary band — § "Detail summary
band (Résumé)" at the end of this file. Author it whenever the shared bar
exceeds 3 tabs; the `affichage summary` tab above renders as a band
cartouche, not a strip trigger.)

A hub form that DELIBERATELY carries no related tabs (portal / read-only
module) must say so explicitly — the audit treats the absence as an error
otherwise (SCR-009):

```markdown
- **Sans onglets liés** : module portail lecture seule, la navigation liée est volontairement absente
```

#### Une cible dans une AUTRE application : le jeton `app`

Un projet client génère UNE application React qui héberge TOUTES les
applications métier, donc un onglet lié peut légitimement viser une entité
d'une autre application (fiche Client dans `CRM` → Factures dans
`FACTURATION`). La page générée construit alors ses imports depuis
`@/features/{app}/{module}/…` : sans l'application, elle prend celle de la
PAGE, un chemin qui n'existe pas, et la page ne compile pas.

```markdown
- **Onglet lié « Factures »** : entité Invoice, FK clientId, app FACTURATION, affichage table → SCR-FACTURATION-FACTURES-LIST-001 (`facturation.factures.read`)
```

Le jeton est **optionnel et rarement nécessaire** : `entité.md` écrit déjà
`scope cross-module (FACTURATION/FACTURES)` sur la relation, et
`derive-related-tabs` en déduit l'application. Écris-le quand la relation ne
tranche pas (entité homonyme dans deux applications, écran cible pas encore
rédigé). Ne l'écris **jamais** pour une cible de ta propre application : le
champ n'est émis dans le PRD que lorsqu'il dit quelque chose.

**Conséquence côté produit** : un onglet qui sort du module de sa page est
rendu par le générateur sous la garde du catalogue du tenant — si l'application
Facturation n'a pas été livrée à ce client, l'onglet n'apparaît pas. C'est
automatique ; rien à écrire pour l'obtenir. Pour un onglet qui doit rester
visible quoi qu'il arrive (cas rare : module toujours livré avec le sien), le
PRD porte `"availabilityCheck": false`.

### Rules for related tabs

1. Allowed **only** in `edit`/`detail` mode (forbidden in `create` — the record
   has no id yet to relate through). Enforced by the CLI (RTV-005).
2. The related entity must exist in the module's `entité.md` (RTV-001).
3. The relationship FK must exist as a Relations entry ON the related entity,
   pointing toward this screen's entity (Client ⟷ Invoices ⇒ `Invoice` carries
   `clientId`) (RTV-002).
4. The navigation target must be an existing `SmartListView` (or `SmartCard`)
   for that related entity — the generated tab embeds the records pre-filtered
   by this record's id and navigates to that screen's routes (RTV-003).
5. A per-tab permission is optional but recommended — it gates whether the tab
   appears (and fetches) for the current actor. Omitted, it defaults to
   `{relatedModule}.{relatedSection}.read` (RTV-004 checks it against rbac.md).

Do NOT hand-derive the candidates: run the colocated CLI first —

```bash
npx --prefer-offline tsx skills/business-analyse/create-screen/cli/derive-related-tabs/index.ts \
  --spec '{"baRoot":".smartstack/ba","app":"CRM","module":"CLIENTS","mode":"derive"}'
```

It scans the Relations graph (incoming 1:N, 1:1, N:M via jonction — never
`scope core`), resolves each candidate's screenTarget + permission, and
SUGGESTS a display mode with its reason. The conversation decides which
candidates become tabs, their labels and final modes; an unresolved
screenTarget means « create the list screen first, or drop the candidate » —
never invent a code.

### Common related-tab patterns

| Entity (fiche) | Typical related tabs                              |
|----------------|---------------------------------------------------|
| Client         | Factures, Contacts, Avoirs, Litiges, Contrats     |
| Project        | Tâches, Documents, Membres, Jalons, Tickets       |
| Order          | Lignes, Paiements, Livraisons, Avoirs             |
| Employee       | Contrats, Absences, Évaluations, Formations       |
| Account (CRM)  | Opportunités, Activités, Contacts, Documents      |

An entity with ≥1 incoming 1:N relation must have at least one related tab OR
the explicit `Sans onglets liés` justification. The audit blocks (SCR-009, err)
when both are missing.

**A « Documents » tab is a related tab on the module's file-METADATA entity**
(e.g. `Project *→N ProjectDocuments`) — that entity must exist in `entité.md`
(rule C-6 of `/ba-create-data-model`: `FileName`, `StoredFileName`,
`ContentType`, `FileSizeBytes` + parent FK; the platform's
`IFileStorageService` stores the bytes — never conclude "no file mechanism
exists", never `varbinary`). The upload/download go through DEDICATED
endpoints (attachments pattern,
`development/backend/data-layer/references/file-storage.md`) — never a custom
action with `payloadParameters[].type: 'file'` (not wired for multipart;
SCR-016/PRD-107 flag it). The screen only declares the tab.

## Common mistakes

- **Computed fields in create mode** → fails type-mapping rule.
- **`readonly` as a string** → it is a strict boolean.
- **Lookup field without an entity** → fails post-check rule 5.
- **More than 5 inner tabs, or a crowded shared bar** → inner and related tabs
  share the tab bar — count both (RTV-009/RTV-108 budget it: > 7 warn,
  > 9 err). Regroup own fields into `Section` bullets (they read above the
  bar on the unified fiche) and switch inert satellites to
  `affichage summary` (band cartouches).
- **Related tabs in create mode** → forbidden, no parent ID yet.
- **A navigation target pointing to a non-existent screen** → fails SCR-008.
- **Required field with no business reason** → ask the user first.

## Detail summary band (Résumé)

Optional, DETAIL screens only: `- **Résumé** : <champTitre> ; <champStatut> ;
<champ1>, <champ2>` — a header band above the tabs (big title value, status
badge, up to 4 label/value pairs). Author it on 360 fiches where the record's
identity + state must stay visible from every tab. Fields name entity
attributes; the PRD maps it to `summary` on the detail pagespec (PRD-119).
