Aller au contenu

Detail pages

Status: Implemented Last reviewed: 2026-04-18 Sources of truth: templates/skeletons/skeleton_detail.html. Reference implementations: apps/practices/templates/practices/practice_detail.html, apps/entities/templates/entities/entity_detail.html, apps/dentists/templates/dentists/dentist_detail.html.

Scope

Conventions for object-detail pages: heading + status + actions, two-column card grid for content, card header style, key/value field rendering, audit footer (created / modified), responsive behaviour.

Out of scope (covered elsewhere): - Status badges (which class for which state) → guidelines/ui/badges.md. - Embedded mini-lists inside a detail card → guidelines/ui/lists.md "When to break the rules" section. - Editing the displayed object → guidelines/ui/forms.md.

Hard rules

  1. Heading area: title + status badges + action buttons in a single d-flex row. Wrap title and badges on the left, action group on the right. Badges sit under the title in a <div class="mt-2">, never inline with the <h2>.
  2. Cards use card border-0 shadow-sm h-100 inside a col-md-6 mb-4 grid cell. border-0 is intentional — base.css puts a border on every card globally (don't fight it).
  3. Card headers use card-header card-header-neutral with <i class="bi-..."></i> {% trans "Title" %} only — never <h5> or <h6> inside. Card headers are visual chrome, not semantic headings; promoting them to <h5>/<h6> breaks document outline and the card-header-neutral styling.
  4. Key/value display uses <p class="mb-2"><strong>{% trans "Label" %} :</strong> {{ value|default:"-" }}</p> — never a <table> for two-column layout. Tables are for data (multiple rows of comparable records); they are not a layout primitive.
  5. - (or ) for empty optional fields, never blank. Use |default:"-" consistently. Blank cells confuse "field is empty" with "field doesn't apply."
  6. All labels translatable. {% trans "Nom" %} not Nom. Same for badge text and button labels.

Required structure

Copy from templates/skeletons/skeleton_detail.html. The shape:

{% extends 'base.html' %}
{% load i18n %}

{% block title %}{{ entity.code }} — {{ entity.name }} - Aletheia{% endblock %}

{% block breadcrumbs %}{% include 'components/breadcrumbs.html' %}{% endblock %}

{% block content %}
<div class="row">
    <div class="col-12">
        {# --- Heading --- #}
        <div class="d-flex justify-content-between align-items-center mb-4">
            <div>
                <h2 class="mb-1">
                    <i class="bi bi-buildings"></i> {{ entity.code }} — {{ entity.name }}
                </h2>
                <div class="mt-2 d-flex gap-2 flex-wrap">
                    <span class="badge badge-type {{ entity.entity_type_badge_class }}">
                        {{ entity.get_entity_type_display }}
                    </span>
                    {% if entity.lifecycle_status == 'active' %}
                    <span class="badge badge-status-active">{{ entity.get_lifecycle_status_display }}</span>
                    {% else %}
                    <span class="badge badge-status-inactive">{{ entity.get_lifecycle_status_display }}</span>
                    {% endif %}
                </div>
            </div>
            <div class="d-flex gap-2">
                <a href="{% url '...' entity.code %}" class="btn btn-action-primary">
                    <i class="bi bi-pencil"></i> {% trans "Modifier" %}
                </a>
                <a href="{% url '...' %}" class="btn btn-action-secondary">
                    <i class="bi bi-arrow-left"></i> {% trans "Retour" %}
                </a>
            </div>
        </div>

        {# --- Cards grid --- #}
        <div class="row">
            <div class="col-md-6 mb-4">
                <div class="card border-0 shadow-sm h-100">
                    <div class="card-header card-header-neutral">
                        <i class="bi bi-info-circle"></i> {% trans "Identité" %}
                    </div>
                    <div class="card-body">
                        <p class="mb-2"><strong>{% trans "Nom" %} :</strong> {{ obj.name|default:"-" }}</p>
                        <p class="mb-2"><strong>{% trans "Email" %} :</strong> {{ obj.email|default:"-" }}</p>
                        <p class="mb-0"><strong>{% trans "Statut" %} :</strong>
                            {% if obj.active %}
                                <span class="badge badge-status-active">{% trans "Oui" %}</span>
                            {% else %}
                                <span class="badge badge-status-inactive">{% trans "Non" %}</span>
                            {% endif %}
                        </p>
                    </div>
                </div>
            </div>

            <div class="col-md-6 mb-4">
                <div class="card border-0 shadow-sm h-100">
                    {# … second card … #}
                </div>
            </div>

            {# --- Audit footer (always full-width, always last) --- #}
            <div class="col-12">
                <div class="card border-0 shadow-sm">
                    <div class="card-header card-header-neutral">
                        <i class="bi bi-clock-history"></i> {% trans "Audit" %}
                    </div>
                    <div class="card-body">
                        <div class="row">
                            <div class="col-md-6">
                                <small class="text-muted">
                                    {% trans "Créé le" %} {{ obj.created_at|date:"d/m/Y H:i" }}
                                    {% if obj.created_by %}{% trans "par" %} {{ obj.created_by.get_full_name }}{% endif %}
                                </small>
                            </div>
                            <div class="col-md-6 text-md-end">
                                <small class="text-muted">
                                    {% trans "Modifié le" %} {{ obj.updated_at|date:"d/m/Y H:i" }}
                                    {% if obj.updated_by %}{% trans "par" %} {{ obj.updated_by.get_full_name }}{% endif %}
                                </small>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

Heading

  • Container: d-flex justify-content-between align-items-center mb-4 (note mb-4, not mb-3 — detail headings get more breathing room than list headings).
  • Title: <h2 class="mb-1"> with a Bootstrap Icon. The title string can include code + name (e.g. "VSM-001 — Cabinet Saint-Marc") — the icon comes first, then the text.
  • Status row under the title: <div class="mt-2 d-flex gap-2 flex-wrap"> with one or more <span class="badge ..."> per guidelines/ui/badges.md. Multiple badges (type + lifecycle + country) are fine; wrap on small screens.
  • Action group: <div class="d-flex gap-2"> containing buttons.

Action buttons

Button Class Icon Use for
Modifier btn-action-primary bi-pencil Edit (link to form)
Supprimer btn-action-destructive bi-trash Delete (usually opens confirm modal)
Retour btn-action-secondary bi-arrow-left Back to list / parent

Order: secondary navigation actions on the left, then primary edit, then destructive at the right edge if present. The skeleton shows Modifier + Retour; real pages often add navigation links (e.g. "Voir le registre", "Fiche détenteur") before Modifier — that's fine, slot them between Retour and Modifier as btn-action-secondary.

Conditional actions (visible only for users with a feature flag or specific role) wrap each button in {% if 'feature' in user_features %}…{% endif %} — not the entire action group.

Cards grid

The two-column grid is the default. Use <div class="col-md-6 mb-4"> for half-width cards and <div class="col-12 mb-4"> for full-width cards. h-100 on the inner card balances the two halves of a row visually.

When to use full width

  • The audit footer (always full-width, always last).
  • A card that contains a table or chart that would be cramped at half-width.
  • A card with > 8 fields — the half-width column gets visually heavy.

When the two-column grid breaks down

  • Very narrow viewports (< md): cards stack naturally — the col-md-6 becomes col-12.
  • More than ~6 cards: consider tabs or a sticky in-page nav. Stack the categories instead of pouring 12 cards into one scrolling page.
  • Heterogeneous content (one card has 3 fields, the next has 20): use mb-4 per card and accept staggered heights instead of forcing h-100 parity. h-100 is for visual symmetry within a row, not a hard rule.

Card headers

  • Class: card-header card-header-neutral.
  • Content: <i class="bi bi-..."></i> {% trans "Title" %} — icon first, single space, translatable title. Optionally append a count badge: <span class="badge badge-status-neutral ms-2">{{ count }}</span>.
  • Hard rule restated: never put <h5>, <h6>, or any heading element inside card-header-neutral. Headers are visual chrome; promoting them to headings breaks the page's document outline and the styling.

Key/value display

The standard:

<p class="mb-2"><strong>{% trans "Label" %} :</strong> {{ value|default:"-" }}</p>

Conventions:

  • Last field in a card uses mb-0 to avoid trailing whitespace inside the card-body. Everything before uses mb-2.
  • {% trans "Label" %} : — note the space before :. French typography. Always wrap the label in {% trans %} even though it looks "obvious" — see LANGUAGE_CODE='nl-be' rationale in CLAUDE.md.
  • {{ value|default:"-" }} for optional fields. Use (em dash) if you prefer; pick one and stick to it within an app.
  • Inline badges and codes are allowed: <p class="mb-2"><strong>Code :</strong> <code>{{ obj.code }}</code></p> or with a status badge. Keep the <strong>Label :</strong> prefix consistent.
  • Never wrap a key/value pair in a <div> with custom classes — the <p> is the layout primitive.

Layout tables vs. data tables

The "no <table> for key/value" rule is precise: it bans layout tables (two-column <tr><th>label</th><td>value</td></tr> rows). It does NOT ban data tables — multi-row, multi-column tables of comparable records (e.g. "Tarifs par acte" with code + amount + bounds). Data tables inside a detail-page card are explicitly fine; use the same table table-hover table-sm styling as list pages (see guidelines/ui/lists.md "When to break the rules" → "Embedded mini-lists").

How to tell which you have: - Layout table = always 2 columns, label cell is text-only, one row per field. → Convert to <p>. - Data table = N columns, headers describe a record's fields, body has N rows where N varies. → Keep as <table>.

The standard "created / modified" footer renders inside a full-width card at the bottom. The skeleton template has the exact markup. Conventions:

  • Always the last card in the layout.
  • text-md-end on the right column so "Modifié le ..." aligns to the right edge on md+. Stacks left-aligned on mobile.
  • Date format: |date:"d/m/Y H:i" (French short).
  • {% if obj.updated_by %}{% trans "par" %} {{ obj.updated_by.get_full_name }}{% endif %} — author is conditional; use get_full_name not username.

If your model uses TimeStampedModel + AuditModel from apps/core/, the created_at / updated_at / created_by / updated_by fields are guaranteed.

Anti-patterns

  • <h5> or <h6> inside card-header card-header-neutral. Use the icon + text pattern. (15 audit hits in 2026-04 — see substitution cheatsheet below.)
  • ❌ Two-column <table> for key/value display. Use <p> paragraphs.
  • ❌ Inline status badges next to the <h2> title — they go under it in a <div class="mt-2">.
  • ❌ Action buttons styled with raw Bootstrap classes (btn-primary, btn-danger). Use btn-action-primary/secondary/destructive.
  • ❌ Cards without h-100 in a half-width grid — creates jagged row heights when adjacent cards differ in content length.
  • ❌ Audit footer placed mid-page or split into two cards. Always full-width at the bottom.
  • ❌ Hardcoded date format ({{ obj.created_at }}) — use |date:"d/m/Y H:i" for consistency.
  • <p><b>... instead of <p><strong>... — semantic difference; <strong> is the standard.
  • ❌ Conditional rendering at the card level when only a field is conditional. Hide the field, not the whole card.

Substitution cheatsheet (for the 2026-04 audit)

The audit (roadmap/backlog/ui-design-system-mechanical-fixes-2026-04.md) found 15 violations of the <h5>/<h6>-in-card-header rule. Each is a substitution against the canonical pattern:

<!-- BEFORE -->
<div class="card-header card-header-neutral">
    <h5><i class="bi bi-..."></i> Titre</h5>
</div>

<!-- AFTER -->
<div class="card-header card-header-neutral">
    <i class="bi bi-..."></i> {% trans "Titre" %}
</div>

Files (per the audit): - apps/imports/templates/imports/{patient,dentist,appointment}_import.html line 14 - apps/dataquality/templates/dataquality/scan_form.html line 11, scan_detail.html line 189 - Plus ~7 more across imports/, dataquality/, sync/

For the layout-table audit findings: the audit reported ~10 violations, but spot-checks (e.g. apps/annuaire/templates/annuaire/practitioner_detail.html:80, apps/nomos_cnam/templates/nomos_cnam/praticien_detail.html:89) show those are legitimate data tables (roles list, tarifs list), not layout tables. The actual layout-table-violation count is likely lower than 10 — recount during the fix pass and update the audit backlog.

Known deviations

  • The 15 <h5>/<h6>-in-card-header violations listed in the substitution cheatsheet above. All have the same one-edit fix.
  • The layout-table count from the 2026-04 audit appears overstated (false positives on data tables). Verify per-file before fixing.
  • Some detail pages render an entity.country badge as badge-status-neutral (see entity_detail.html:29). Strictly speaking, country is a category not a status — should be badge-type. Listed here as a candidate for a future challenge pass against ui/badges.md, not a hard violation.

When to break the rules

  • Pages that are detail-like but mostly read-only summaries (e.g. annuaire practitioner_detail.html showing INPI/RPPS data) often have one large card with a data table inside, not the two-column key/value grid. The heading + audit footer conventions still apply.
  • Drilldown / report pages in apps/finance/ are detail-shaped (one entity, full-width view) but have charts and pivots, not key/value cards. Skip the cards grid; keep the heading + action button pattern.
  • Wizards and multi-step setup flows are not detail pages — they're forms with intermediate states. Use guidelines/ui/forms.md, not this file.