Commit cadc79a3 authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Lot 3 : le dossier de préparation

Annuaire, étiquettes, destinataires, tiers activables, modèles de message,
fiches réflexes, kit documentaire, astreintes et absences, applications métier,
parties prenantes, cockpits, mobilisations prêtes, types de crise, import CSV,
actions de préparation, indicateur, dossier gelé.

370 chaînes enveloppées, 238 traductions neuves. La couverture passe de
trente-neuf à cinquante-six pour cent.

── Douze masquages de « t », et une leçon

Les fichiers du dossier utilisaient « t » pour tout : le type de crise, la
tuile, le tiers, le trou d'astreinte. J'ai d'abord renommé à l'aveugle sur des
plages de lignes, ce qui a débordé du corps de la fonction et cassé deux
fichiers — « tiers.refetch() » désignant soudain le paramètre au lieu de la
requête. Repris avec un renommage qui borne la portée à la fonction fléchée
elle-même, en équilibrant les parenthèses.

Deux paramètres masquaient jusqu'au tableau qu'ils parcouraient
(« tiers.map((tiers) => … ») : ils s'appellent « fiche ».

── Un défaut de l'outillage, corrigé

Une valeur d'attribut tenant sur deux lignes était coupée en plein saut de
ligne, ce qui produisait un littéral non terminé. L'extracteur normalise
désormais les blancs avant de découper.

── Les intitulés d'onglet

Ils vivaient dans un tableau de constantes, hors de tout nœud JSX : quinze
libellés que l'enveloppeur ne pouvait pas voir et qui seraient restés français
quelle que soit la langue. C'est le cas général de tout texte qui n'est pas
écrit directement dans le rendu — il faudra le chercher fichier par fichier.

── Ce que ce lot met au jour, et qui dépasse le lot

Le texte produit par le serveur n'est pas couvert, et ne peut pas l'être par
cette méthode. Environ 183 chaînes, 815 mots : les libellés et conseils de
l'indicateur de préparation, les messages d'erreur des routes, les résumés
d'événements du journal. Ils arrivent en français dans une interface réglée en
anglais, et le compteur ne les voit pas — il ne mesure que l'interface.

Trois voies, à trancher : traduire côté serveur en lisant la langue du compte ;
faire renvoyer des codes que le client rend en clair ; ou assumer que la prose
du serveur reste française et le dire. Aucune n'est neutre, la deuxième change
le contrat de l'API.

Co-Authored-By: Claude (RCA)
parent 55de6479
Loading
Loading
Loading
Loading
Loading
+1 −1
Changes for apps/api/src/verification/interface.ts: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -159,7 +159,7 @@ export async function verifierInterface(r: Rapport): Promise<void> {
  // laisser la couverture reculer. Ce plancher se relève au fil des lots — il
  // ne se baisse jamais. Tant qu'il n'atteint pas 100, EX-21 n'est pas tenue,
  // et le chiffre le dit à chaque exécution.
  const PLANCHER = 39;
  const PLANCHER = 56;
  r.verifie("EX-21",
    `${employees.size} chaînes visibles sur ${visibles} passent par la traduction`
    + ` (${couverture} %, plancher ${PLANCHER} %)`,
+84 −50
Changes for apps/web/src/ecrans/Preparation.tsx: 84 added lines, 50 removed lines.
Original line number Diff line number Diff line
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api, ErreurApi } from "../api.js";
import { useT } from "../langues/index.js";

interface Tiers {
  id: string; categorie: string; nom: string; reference_contrat: string | null;
@@ -23,6 +24,7 @@ interface Fiche {
export function Preparation({
  criseId, organisationId, version,
}: { criseId: string; organisationId: string; version: number | null }) {
  const t = useT();
  const tiers = useQuery({
    queryKey: ["tiers-crise", criseId],
    queryFn: () => api.lire<{ tiers: Tiers[]; preparationVersion: number | null }>(
@@ -41,20 +43,31 @@ export function Preparation({
  return (
    <div className="scene">
      <header>
        <div className="etiq">Dossier gelé</div>
        <div className="etiq">{t("Dossier gelé")}</div>
        <h1 style={{ fontSize: 20 }}>
          Préparation, version {version ?? tiers.data?.preparationVersion ?? "—"}
        </h1>
        <p className="discret" style={{ fontSize: 13, margin: "4px 0 0" }}>
          Tel qu’il était au déclenchement. Les mises à jour ultérieures n’apparaissent
          pas ici.
          {t("Tel "
                                                                                        + "qu’il "
                                                                                        + "était "
                                                                                        + "au "
                                                                                        + "déclenchement. "
                                                                                        + "Les "
                                                                                        + "mises "
                                                                                        + "à "
                                                                                        + "jour "
                                                                                        + "ultérieures "
                                                                                        + "n’apparaissent "
                                                                                        + "pas "
                                                                                        + "ici.")}
        </p>
      </header>

      <div className="contenu">
        {(fiches.data?.fiches.length ?? 0) > 0 && (
          <section style={{ marginBottom: 24 }}>
            <h2>Fiches réflexes</h2>
            <h2>{t("Fiches réflexes")}</h2>
            <div className="pile">
              {fiches.data!.fiches.map((f) => (
                <details key={f.id} className="carte">
@@ -72,73 +85,76 @@ export function Preparation({
        )}

        <section>
          <h2>Tiers activables</h2>
          <h2>{t("Tiers activables")}</h2>
          {(tiers.data?.tiers.length ?? 0) === 0 && (
            <div className="vide">
              <strong>Aucun tiers au registre.</strong>
              Sans CERT, sans assureur et sans conseil juridique, une cellule cyber est
              aveugle.
              <strong>{t("Aucun tiers au registre.")}</strong>
              {t("Sans CERT, sans "
                                                                        + "assureur et sans "
                                                                        + "conseil juridique, "
                                                                        + "une cellule cyber "
                                                                        + "est aveugle.")}
            </div>
          )}
          <div className="pile">
            {tiers.data?.tiers.map((t) => {
              const activable = Boolean(t.numero_urgence && t.qui_appelle
                && t.si_pas_de_reponse);
            {tiers.data?.tiers.map((fiche) => {
              const activable = Boolean(fiche.numero_urgence && fiche.qui_appelle
                && fiche.si_pas_de_reponse);
              return (
                <div key={t.id} className={`carte${activable ? "" : " alerte"}`}>
                <div key={fiche.id} className={`carte${activable ? "" : " alerte"}`}>
                  <div className="ligne">
                    <strong>{t.nom}</strong>
                    <span className="pastille neutre sans-point">{t.categorie}</span>
                    {t.reference_contrat && (
                    <strong>{fiche.nom}</strong>
                    <span className="pastille neutre sans-point">{fiche.categorie}</span>
                    {fiche.reference_contrat && (
                      <span className="discret" style={{ fontSize: 12.5 }}>
                        contrat {t.reference_contrat}
                        contrat {fiche.reference_contrat}
                      </span>
                    )}
                    <div className="ligne pousse" style={{ gap: 6 }}>
                      <button className="accent" onClick={() => void activer(t.id)}>
                        Activer
                      <button className="accent" onClick={() => void activer(fiche.id)}>
                        {t("Activer")}
                      </button>
                      {/* Activer consigne l'appel ; ce qui manquait, c'était la
                          suite — a-t-il décroché, oui ou non. Sans elle, le
                          suite — a-fiche-il décroché, oui ou non. Sans elle, le
                          cockpit compte indéfiniment un tiers « en attente »
                          que quelqu'un a pourtant eu au téléphone. */}
                      <Retour criseId={criseId} tiersId={t.id} nom={t.nom}
                      <Retour criseId={criseId} tiersId={fiche.id} nom={fiche.nom}
                        surRetour={() => void tiers.refetch()} />
                    </div>
                  </div>

                  <div className="grille deux" style={{ marginTop: 10, gap: 8 }}>
                    <div>
                      <span className="etiq">Numéro d’urgence</span>
                      <span className="etiq">{t("Numéro d’urgence")}</span>
                      <div style={{ fontSize: 17, fontWeight: 600,
                        fontFamily: '"JetBrains Mono", monospace',
                        color: t.numero_urgence ? undefined : "var(--crit)" }}>
                        {t.numero_urgence ?? "NON RENSEIGNÉ"}
                        color: fiche.numero_urgence ? undefined : "var(--crit)" }}>
                        {fiche.numero_urgence ?? "NON RENSEIGNÉ"}
                      </div>
                    </div>
                    <div>
                      <span className="etiq">Qui appelle</span>
                      <div style={{ fontSize: 13.5 }}>{t.qui_appelle ?? "—"}</div>
                      <span className="etiq">{t("Qui appelle")}</span>
                      <div style={{ fontSize: 13.5 }}>{fiche.qui_appelle ?? "—"}</div>
                    </div>
                    {t.informations_a_preparer && (
                    {fiche.informations_a_preparer && (
                      <div style={{ gridColumn: "1 / -1" }}>
                        <span className="etiq">À préparer avant d’appeler</span>
                        <div style={{ fontSize: 13.5 }}>{t.informations_a_preparer}</div>
                        <span className="etiq">{t("À préparer avant d’appeler")}</span>
                        <div style={{ fontSize: 13.5 }}>{fiche.informations_a_preparer}</div>
                      </div>
                    )}
                    <div>
                      <span className="etiq">Délai annoncé</span>
                      <span className="etiq">{t("Délai annoncé")}</span>
                      <div style={{ fontSize: 13.5 }}>
                        {t.delai_attendu
                          ?? (t.delai_intervention_heures
                            ? `${t.delai_intervention_heures} h` : "—")}
                        {fiche.delai_attendu
                          ?? (fiche.delai_intervention_heures
                            ? `${fiche.delai_intervention_heures} h` : "—")}
                      </div>
                    </div>
                    <div style={{ gridColumn: "1 / -1" }}>
                      <span className="etiq">Si personne ne répond</span>
                      <span className="etiq">{t("Si personne ne répond")}</span>
                      <div style={{ fontSize: 13.5,
                        color: t.si_pas_de_reponse ? undefined : "var(--crit)" }}>
                        {t.si_pas_de_reponse ?? "conduite à tenir non définie"}
                        color: fiche.si_pas_de_reponse ? undefined : "var(--crit)" }}>
                        {fiche.si_pas_de_reponse ?? "conduite à tenir non définie"}
                      </div>
                    </div>
                  </div>
@@ -147,8 +163,15 @@ export function Preparation({
            })}
          </div>
          <p className="aide" style={{ marginTop: 10 }}>
            Activer un tiers consigne l’appel et ouvre une action de suivi&nbsp;: un
            prestataire appelé qui ne rappelle pas est l’angle mort classique d’une cellule.
            {t("Activer un tiers "
                                                                       + "consigne l’appel et "
                                                                       + "ouvre une action de "
                                                                       + "suivi : un "
                                                                       + "prestataire appelé "
                                                                       + "qui ne rappelle pas "
                                                                       + "est l’angle mort "
                                                                       + "classique d’une "
                                                                       + "cellule.")}
          </p>
        </section>

@@ -186,6 +209,7 @@ const poidsKit = (o: string) => {
 * depuis.
 */
function Kit({ criseId, organisationId }: { criseId: string; organisationId: string }) {
  const t = useT();
  const { data, isLoading } = useQuery({
    queryKey: ["kit", criseId],
    queryFn: () => api.lire<{
@@ -202,15 +226,19 @@ function Kit({ criseId, organisationId }: { criseId: string; organisationId: str

  return (
    <section style={{ marginTop: 26 }}>
      <h2>Kit gelé</h2>
      <h2>{t("Kit gelé")}</h2>

      {isLoading && <p className="discret">Chargement…</p>}
      {isLoading && <p className="discret">{t("Chargement…")}</p>}

      {!isLoading && elements.length === 0 && (
        <div className="vide">
          <strong>Aucun élément dans le kit.</strong>
          Modèles de communiqué, plans de continuité, annexes réglementaires : ce qu’on
          n’a pas préparé avant, on ne l’écrira pas pendant.
          <strong>{t("Aucun élément dans le kit.")}</strong>
          {t("Modèles de communiqué, "
                                                                  + "plans de continuité, "
                                                                  + "annexes réglementaires : "
                                                                  + "ce qu’on n’a pas préparé "
                                                                  + "avant, on ne l’écrira "
                                                                  + "pas pendant.")}
        </div>
      )}

@@ -242,7 +270,7 @@ function Kit({ criseId, organisationId }: { criseId: string; organisationId: str
                  href={`/api/organisations/${organisationId}`
                    + `/preparation/kit/${e.id}/contenu`}
                  target="_blank" rel="noreferrer noopener">
                  Ouvrir
                  {t("Ouvrir")}
                </a>
              </div>
            ))}
@@ -265,6 +293,7 @@ function Kit({ criseId, organisationId }: { criseId: string; organisationId: str
function Retour({
  criseId, tiersId, nom, surRetour,
}: { criseId: string; tiersId: string; nom: string; surRetour: () => void }) {
  const t = useT();
  const [ouvert, definirOuvert] = useState(false);
  const [interlocuteur, definirInterlocuteur] = useState("");
  const [erreur, definirErreur] = useState<string | null>(null);
@@ -291,11 +320,11 @@ function Retour({
  }

  if (message) {
    return <span className="pastille ok sans-point" title={message}>consigné</span>;
    return <span className="pastille ok sans-point" title={message}>{t("consigné")}</span>;
  }

  if (!ouvert) {
    return <button className="petit" onClick={() => definirOuvert(true)}>Noter le retour</button>;
    return <button className="petit" onClick={() => definirOuvert(true)}>{t("Noter le retour")}</button>;
  }

  return (
@@ -306,16 +335,21 @@ function Retour({
        onChange={(e) => definirInterlocuteur(e.target.value)} />
      <div className="ligne" style={{ gap: 6, marginTop: 6, flexWrap: "wrap" }}>
        <button className="accent petit" onClick={() => void noter("reponse")}>
          Il a répondu
          {t("Il "
                                                                                            + "a "
                                                                                            + "répondu")}
        </button>
        <button className="danger petit" onClick={() => void noter("sans-reponse")}>
          Pas de réponse
          {t("Pas "
                                                                                                 + "de "
                                                                                                 + "réponse")}
        </button>
        <button className="discret petit" onClick={() => definirOuvert(false)}>Annuler</button>
        <button className="discret petit" onClick={() => definirOuvert(false)}>{t("Annuler")}</button>
      </div>
      <div className="aide">
        Le délai réel est calculé depuis l’activation : c’est lui qu’on comparera au délai
        contractuel au retour d’expérience.
        {t("Le délai réel est calculé depuis l’activation : "
                                       + "c’est lui qu’on comparera au délai contractuel au "
                                       + "retour d’expérience.")}
      </div>
    </div>
  );
+11 −6
Changes for apps/web/src/ecrans/preparation/ActionsPreparation.tsx: 11 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "../../api.js";
import { dateHeure } from "../../etat.js";
import { LIBELLES_STATUT, type StatutAction } from "@kastell/shared";
import { useT } from "../../langues/index.js";

interface ActionPreparation {
  id: string;
@@ -29,6 +30,7 @@ const STATUTS: StatutAction[] = ["a_faire", "en_cours", "faite", "abandonnee"];
 * d'exercice.
 */
export function ActionsPreparation({ organisationId }: { organisationId: string }) {
  const t = useT();
  const client = useQueryClient();

  const { data, isLoading } = useQuery({
@@ -42,18 +44,21 @@ export function ActionsPreparation({ organisationId }: { organisationId: string

  return (
    <section style={{ marginTop: 26 }}>
      <h2>Actions issues des retours d’expérience</h2>
      <h2>{t("Actions issues des retours d’expérience")}</h2>
      <p className="aide mesure" style={{ marginBottom: 12 }}>
        Une observation de RETEX qui ne devient pas une action est un regret. Celles-ci
        sont à tenir <strong>avant</strong> la prochaine crise, pas pendant.
        sont à tenir <strong>{t("avant")}</strong> {t("la prochaine crise, pas pendant.")}
      </p>

      {isLoading && <p className="discret">Chargement…</p>}
      {isLoading && <p className="discret">{t("Chargement…")}</p>}

      {!isLoading && actions.length === 0 && (
        <div className="vide">
          <strong>Aucune action de préparation.</strong>
          Elles naissent d’un retour d’expérience, en convertissant une observation.
          <strong>{t("Aucune action de préparation.")}</strong>
          {t("Elles naissent d’un "
                                                                     + "retour d’expérience, "
                                                                     + "en convertissant une "
                                                                     + "observation.")}
        </div>
      )}

@@ -64,7 +69,7 @@ export function ActionsPreparation({ organisationId }: { organisationId: string
          </p>
          <table>
            <thead>
              <tr><th>Action</th><th>Responsable</th><th>Échéance</th><th>Statut</th></tr>
              <tr><th>{t("Action")}</th><th>{t("Responsable")}</th><th>{t("Échéance")}</th><th>{t("Statut")}</th></tr>
            </thead>
            <tbody>
              {actions.map((a) => {
+56 −28

File changed.

Preview size limit exceeded, changes collapsed.

+52 −20
Changes for apps/web/src/ecrans/preparation/Applications.tsx: 52 added lines, 20 removed lines.
Original line number Diff line number Diff line
import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { api, ErreurApi } from "../../api.js";
import { useT } from "../../langues/index.js";

interface Application {
  id: string;
@@ -39,6 +40,7 @@ const PASTILLE: Record<string, string> = {
 * tombé sans dire quoi faire.
 */
export function Applications({ organisationId }: { organisationId: string }) {
  const t = useT();
  const client = useQueryClient();
  const [erreur, definirErreur] = useState<string | null>(null);
  const [edite, definirEdite] = useState<Partial<Application> | null>(null);
@@ -119,9 +121,19 @@ export function Applications({ organisationId }: { organisationId: string }) {
  return (
    <div>
      <p className="aide mesure" style={{ marginTop: 0 }}>
        Ce que l’entreprise fait tourner, et ce qui permet de continuer sans. Le jour J, on
        ne dresse pas l’inventaire de son système d’information&nbsp;: on le lit. L’écran de
        continuité, pendant la crise, ne fait que constater l’état de ces lignes.
        {t("Ce que l’entreprise "
                                                                     + "fait tourner, et ce "
                                                                     + "qui permet de "
                                                                     + "continuer sans. Le "
                                                                     + "jour J, on ne dresse "
                                                                     + "pas l’inventaire de "
                                                                     + "son système "
                                                                     + "d’information : on le "
                                                                     + "lit. L’écran de "
                                                                     + "continuité, pendant "
                                                                     + "la crise, ne fait que "
                                                                     + "constater l’état de "
                                                                     + "ces lignes.")}
      </p>

      {erreur && <div className="erreur">{erreur}</div>}
@@ -131,14 +143,14 @@ export function Applications({ organisationId }: { organisationId: string }) {
          <div className="ligne">
            <span className="etiq">{edite.id ? "Modifier l’application" : "Nouvelle application"}</span>
            <button className="discret petit pousse" onClick={() => definirEdite(null)}>
              Fermer
              {t("Fermer")}
            </button>
          </div>

          {champ("nom", "Nom de l’application")}

          <div className="champ">
            <label htmlFor="criticite">Criticité</label>
            <label htmlFor="criticite">{t("Criticité")}</label>
            <div className="ligne" style={{ gap: 6 }}>
              {CRITICITES.map(([cle, libelle]) => (
                <button key={cle}
@@ -149,8 +161,9 @@ export function Applications({ organisationId }: { organisationId: string }) {
              ))}
            </div>
            <div className="aide">
              Vitale&nbsp;: l’activité s’arrête sans elle. C’est ce qui trie l’écran de
              continuité pendant la crise.
              {t("Vitale : l’activité s’arrête sans elle. "
                                                   + "C’est ce qui trie l’écran de continuité "
                                                   + "pendant la crise.")}
            </div>
          </div>

@@ -175,10 +188,17 @@ export function Applications({ organisationId }: { organisationId: string }) {
            <input type="checkbox" checked={edite.donnees_sensibles ?? false}
              onChange={(e) => definirEdite((a) => ({
                ...a, donnees_sensibles: e.target.checked }))} />
            Elle traite des données personnelles
            {t("Elle "
                                                                                + "traite des "
                                                                                + "données "
                                                                                + "personnelles")}
          </label>
          <div className="aide" style={{ marginTop: -4 }}>
            Ce qui décide si une violation la concernant ouvre un délai de notification.
            {t("Ce qui décide si "
                                                                         + "une violation la "
                                                                         + "concernant ouvre "
                                                                         + "un délai de "
                                                                         + "notification.")}
          </div>

          {champ("notes", "Notes", { large: true })}
@@ -190,33 +210,44 @@ export function Applications({ organisationId }: { organisationId: string }) {
            </button>
            {edite.id && (
              <button className="danger" onClick={() => void retirer(edite.id!)}>
                Retirer du registre
                {t("Retirer "
                                                                                                    + "du "
                                                                                                    + "registre")}
              </button>
            )}
            <button className="discret" onClick={() => definirEdite(null)}>Annuler</button>
            <button className="discret" onClick={() => definirEdite(null)}>{t("Annuler")}</button>
          </div>
        </div>
      ) : (
        <button className="accent" style={{ marginBottom: 14 }}
          onClick={() => definirEdite({ criticite: "importante" })}>
          Ajouter une application
          {t("Ajouter "
                                                                                 + "une "
                                                                                 + "application")}
        </button>
      )}

      {isLoading && <p className="discret">Chargement…</p>}
      {isLoading && <p className="discret">{t("Chargement…")}</p>}

      {!isLoading && applications.length === 0 && (
        <div className="vide">
          <strong>Aucune application au registre.</strong>
          Commencez par les vitales — celles dont l’arrêt suffit à arrêter l’activité. Cinq
          lignes justes valent mieux qu’un inventaire complet jamais relu.
          <strong>{t("Aucune application au registre.")}</strong>
          {t("Commencez par les "
                                                                       + "vitales — celles "
                                                                       + "dont l’arrêt suffit "
                                                                       + "à arrêter "
                                                                       + "l’activité. Cinq "
                                                                       + "lignes justes "
                                                                       + "valent mieux qu’un "
                                                                       + "inventaire complet "
                                                                       + "jamais relu.")}
        </div>
      )}

      {applications.length > 0 && (
        <table>
          <thead>
            <tr><th>Application</th><th>Criticité</th><th>Secours</th><th>Reprise</th><th /></tr>
            <tr><th>{t("Application")}</th><th>{t("Criticité")}</th><th>{t("Secours")}</th><th>{t("Reprise")}</th><th /></tr>
          </thead>
          <tbody>
            {applications.map((a) => (
@@ -225,7 +256,8 @@ export function Applications({ organisationId }: { organisationId: string }) {
                  <strong>{a.nom}</strong>
                  {a.donnees_sensibles && (
                    <span className="pastille alerte sans-point" style={{ marginLeft: 6 }}>
                      données personnelles
                      {t("données "
                                                                                                                    + "personnelles")}
                    </span>
                  )}
                  <div className="discret" style={{ fontSize: 12 }}>
@@ -237,12 +269,12 @@ export function Applications({ organisationId }: { organisationId: string }) {
                </td>
                <td style={{ fontSize: 13 }}>
                  {a.secours ?? (
                    <span style={{ color: "var(--alerte)" }}>aucun secours décrit</span>
                    <span style={{ color: "var(--alerte)" }}>{t("aucun secours décrit")}</span>
                  )}
                </td>
                <td className="nombres">{a.rto_heures !== null ? `${a.rto_heures} h` : "—"}</td>
                <td>
                  <button className="petit" onClick={() => definirEdite(a)}>Modifier</button>
                  <button className="petit" onClick={() => definirEdite(a)}>{t("Modifier")}</button>
                </td>
              </tr>
            ))}
Loading