Simpl-Resultat/docs/qa-onboarding.html
le king fu d11337ccf6
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
docs(onboarding): changelog, architecture, user guide and interactive QA checklist (#357)
Closes phase 1 of the onboarding wave (#351-#356) on the documentation side.

- CHANGELOG.md / CHANGELOG.fr.md: Added (guided empty states, getting-started
  checklist, sample folder, bank CSV export guide) and Changed (re-show button
  in Settings > Users, Import "?" popover linking to the guide) under
  [Unreleased] / [Non publié], same entries in both languages.
- docs/architecture.md: create_sample_import_folders (fs_commands 6 -> 7,
  total 36 -> 37) with its contract, onboardingService, useOnboardingChecklist,
  the onboarding_checklist_hidden preference, the samples/ corpus next to csv/,
  and an "Onboarding" section (four empty states and their signal, source of
  truth of the six checklist rows, sample-folder flow). Tree counts measured
  for the directories the wave touched.
- docs/guide-utilisateur.md chapters 3 and 11 + docs.dashboard.* and
  docs.settings.* keys (FR/EN). Hand-off from #355: docs.gettingStarted.steps
  aligned with the six-step quick start of the markdown, docs.import.steps
  mentions the sample folder, so the three guide copies agree.
- docs/qa-onboarding.html: interactive manual-test checklist, French only,
  static, no network dependency, state in localStorage, plain-text report to
  the clipboard with a visible fallback, reset button.

Resolves #357

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:25:55 -04:00

368 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QA — Parcours d'accueil</title>
<!--
Checklist de parcours pour la phase 1 de l'accueil (issues #351 à #357).
Page statique, français seulement, aucune dépendance réseau : tout est inline.
L'état (cases + commentaires) vit dans le localStorage du navigateur qui
ouvre le fichier ; « Copier le rapport » produit le texte brut à coller dans
Paramètres → Systèmes → Rétroaction.
-->
<style>
:root {
--bg: #faf9f7;
--card: #ffffff;
--fg: #1f1d1a;
--muted: #6b665f;
--border: #e3dfd9;
--accent: #b5542e;
--accent-fg: #ffffff;
--done-bg: #f0f5ec;
--focus: #2f6fb3;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1c1a18;
--card: #262320;
--fg: #ece8e2;
--muted: #a39d94;
--border: #3a3631;
--accent: #d9774c;
--accent-fg: #1c1a18;
--done-bg: #262d22;
--focus: #7fb0e8;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--fg);
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
padding: 24px 16px 64px;
}
main { max-width: 860px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin: 0 0 4px; }
h2 { font-size: 1.05rem; margin: 28px 0 8px; }
p { margin: 0 0 10px; }
.lead { color: var(--muted); }
.meta {
display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center;
margin: 16px 0 8px; color: var(--muted); font-size: 0.9rem;
}
.meta label { display: inline-flex; gap: 6px; align-items: center; }
.meta input[type="text"] {
width: 11rem;
}
.progress {
font-variant-numeric: tabular-nums; font-weight: 600; color: var(--fg);
}
.prereq {
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
padding: 12px 16px; margin: 12px 0 20px;
}
.prereq ul { margin: 6px 0 0; padding-left: 20px; }
ol.steps { list-style: none; margin: 0; padding: 0; counter-reset: step; }
ol.steps li {
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
padding: 12px 14px; margin-bottom: 10px;
display: grid; grid-template-columns: auto 1fr; gap: 6px 12px;
}
ol.steps li.done { background: var(--done-bg); }
ol.steps li .check { grid-row: 1 / span 3; padding-top: 2px; }
ol.steps li input[type="checkbox"] { width: 20px; height: 20px; margin: 0; cursor: pointer; }
ol.steps li .title { font-weight: 600; cursor: pointer; }
ol.steps li .title::before {
counter-increment: step; content: counter(step) ". ";
color: var(--muted); font-weight: 500;
}
ol.steps li .expect { color: var(--muted); font-size: 0.92rem; }
ol.steps li .expect code {
font: 0.88em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px;
}
ol.steps li .where { display: grid; gap: 4px; }
ol.steps li .where label { font-size: 0.85rem; color: var(--muted); }
input[type="text"], textarea {
width: 100%; font: inherit; color: var(--fg); background: var(--bg);
border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px;
}
input:focus-visible, textarea:focus-visible, button:focus-visible {
outline: 2px solid var(--focus); outline-offset: 1px;
}
.actions {
display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
margin-top: 20px; position: sticky; bottom: 0; padding: 12px 0;
background: var(--bg);
}
button {
font: inherit; font-weight: 600; cursor: pointer; border-radius: 8px;
padding: 8px 14px; border: 1px solid var(--border);
background: var(--card); color: var(--fg);
}
button.primary { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
button:hover { filter: brightness(0.97); }
.status { color: var(--muted); font-size: 0.9rem; min-height: 1.4em; }
.fallback { margin-top: 12px; }
.fallback[hidden] { display: none; }
.fallback textarea { min-height: 14rem; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.85rem; }
.note { font-size: 0.85rem; color: var(--muted); margin-top: 16px; }
@media (max-width: 520px) {
body { padding: 16px 16px 72px; }
.meta input[type="text"] { width: 100%; }
}
</style>
</head>
<body>
<main>
<h1>QA — Parcours d'accueil</h1>
<p class="lead">Première ouverture de Simpl'Résultat jusqu'à la liste de démarrage complète. Coche chaque étape quand elle se passe comme décrit, et note où tu as hésité, même une seconde : c'est l'hésitation qu'on cherche, pas l'échec.</p>
<div class="meta">
<label>Testeur <input type="text" id="tester" placeholder="prénom" autocomplete="off"></label>
<label>Version <input type="text" id="version" placeholder="ex. 0.17.0" autocomplete="off"></label>
<label>Système <input type="text" id="os" placeholder="Windows 11, Ubuntu 24.04…" autocomplete="off"></label>
<span class="progress" id="progress"></span>
</div>
<section class="prereq">
<strong>Avant de commencer</strong>
<ul>
<li>Une installation neuve, ou un profil neuf : la liste de démarrage n'apparaît que dans un profil sans transaction.</li>
<li>Pour l'étape Budget, une clé Base ou Premium ; sans clé, l'étape se présente verrouillée « Base et plus » et le total passe à 5 — note-le dans le champ, c'est attendu.</li>
<li>La page garde tes réponses dans ce navigateur. Rien n'est envoyé : c'est toi qui colles le rapport dans l'application.</li>
</ul>
</section>
<h2>Étapes</h2>
<ol class="steps" id="steps"></ol>
<div class="actions">
<button type="button" class="primary" id="copy">Copier le rapport</button>
<button type="button" id="reset">Réinitialiser</button>
<span class="status" id="status" role="status" aria-live="polite"></span>
</div>
<div class="fallback" id="fallback" hidden>
<p>Le presse-papiers n'est pas accessible depuis ce fichier : sélectionne le texte ci-dessous (il est déjà sélectionné) et copie-le avec Ctrl+C, puis colle-le dans Paramètres → Systèmes → Rétroaction.</p>
<textarea id="report" readonly aria-label="Rapport à copier"></textarea>
</div>
<p class="note">Les noms entre guillemets sont ceux des boutons tels qu'ils apparaissent en français. « Exemple Desjardins » et « Exemple Tangerine » sont des relevés inventés : aucune donnée réelle.</p>
</main>
<script>
(function () {
"use strict";
var STORAGE_KEY = "simpl-resultat.qa-onboarding.v1";
// One row per step of the issue. `expect` is what the tester should see.
var STEPS = [
{ id: "install", title: "Installer et lancer l'application",
expect: "L'installateur s'exécute sans avertissement bloquant et l'application s'ouvre sur la sélection de profil." },
{ id: "profile", title: "Créer un profil",
expect: "Le profil s'ouvre directement après sa création (NIP facultatif)." },
{ id: "dashboard-empty", title: "Tableau de bord vide avec la liste de démarrage à 1 / 6",
expect: "Une carte d'accueil remplace les graphiques, la liste montre six lignes, seule « Créer un profil » est cochée, le compteur indique 1 / 6 (ou 1 / 5 sans clé)." },
{ id: "cta-folder", title: "Cliquer « Choisir un dossier d'import »",
expect: "Le bouton de la carte d'accueil mène à la page Import." },
{ id: "import-no-folder", title: "Page Import sans dossier",
expect: "Un état vide « Aucun dossier d'import » avec le bouton « Choisir un dossier » et le lien « Comment exporter un CSV depuis ma banque ? »." },
{ id: "pick-folder", title: "Cliquer « Choisir un dossier » et prendre un dossier vide",
expect: "La fenêtre du système s'ouvre ; une fois le dossier choisi, l'état vide devient « Aucune source trouvée » et nomme le dossier." },
{ id: "create-sample", title: "Cliquer « Créer un dossier d'exemple »",
expect: "Un message annonce les deux dossiers créés et invite à cliquer sur l'un d'eux. Dans l'explorateur de fichiers, les deux sous-dossiers contiennent un fichier releve-AAAA-MM.csv." },
{ id: "two-sources", title: "Deux sources apparaissent",
expect: "« Exemple Desjardins » et « Exemple Tangerine » sont listées comme sources, sans actualiser à la main." },
{ id: "import-desjardins", title: "Importer « Exemple Desjardins » : aperçu, signes, doublons, rapport",
expect: "Le format est détecté (bandeau Desjardins), l'aperçu montre des sorties et des entrées dans le bon sens (pas besoin d'« Inverser les signes »), aucun doublon, puis le rapport d'import indique le nombre de transactions importées." },
{ id: "dashboard-compact", title: "Retour au tableau de bord : liste compacte à 4 / 6",
expect: "Les graphiques sont là, la liste est devenue une barre compacte au-dessus des filtres et affiche 4 / 6 (ou 4 / 5 sans clé) : dossier, source et premier import sont cochés." },
{ id: "manual-category", title: "Transactions : classer « GARDERIE LES PETITS PAS » à la main",
expect: "La transaction est sans catégorie après l'import ; lui en donner une à la main la garde classée." },
{ id: "five-of-six", title: "Liste à 5 / 6",
expect: "De retour sur le tableau de bord, « Classer une transaction à la main » est cochée." },
{ id: "budget", title: "Budget (clé Base ou Premium) : saisir un montant",
expect: "La page Budget montre la grille (pas l'état vide, puisqu'il y a des transactions) ; un montant saisi dans une cellule est conservé." },
{ id: "six-of-six", title: "Liste à 6 / 6, puis disparue",
expect: "Le tableau de bord affiche brièvement 6 / 6 ou plus aucune liste : une liste complète ne s'affiche plus." },
{ id: "hide-reshow", title: "« Masquer » sur un second profil, puis réaffichage depuis Paramètres",
expect: "Créer un second profil (clé Base ou plus) : sa liste est à 1 / 6 ; « Masquer » la retire de ce profil seulement, le premier profil n'est pas touché ; Paramètres → Utilisateurs montre « Afficher la liste de démarrage », qui la ramène." },
{ id: "guide-link", title: "Ouvrir le guide « Exporter un CSV depuis votre banque » depuis l'état vide d'Import",
expect: "Sur un profil sans dossier ou sans source, le lien de l'état vide ouvre le guide directement sur la sous-section, avec les procédures par banque." }
];
var state = load();
function load() {
var fresh = { meta: { tester: "", version: "", os: "" }, steps: {} };
try {
var raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return fresh;
var parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return fresh;
return {
meta: Object.assign(fresh.meta, parsed.meta || {}),
steps: parsed.steps && typeof parsed.steps === "object" ? parsed.steps : {}
};
} catch (e) {
return fresh;
}
}
function save() {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (e) {
setStatus("Impossible d'enregistrer dans ce navigateur : le rapport reste copiable, mais l'état sera perdu au rechargement.");
}
}
function stepState(id) {
if (!state.steps[id]) state.steps[id] = { done: false, note: "" };
return state.steps[id];
}
function setStatus(text) {
document.getElementById("status").textContent = text;
}
function updateProgress() {
var done = 0;
STEPS.forEach(function (s) { if (stepState(s.id).done) done += 1; });
document.getElementById("progress").textContent = done + " / " + STEPS.length + " étapes cochées";
}
function render() {
var list = document.getElementById("steps");
list.textContent = "";
STEPS.forEach(function (step) {
var st = stepState(step.id);
var li = document.createElement("li");
li.className = st.done ? "done" : "";
var check = document.createElement("div");
check.className = "check";
var box = document.createElement("input");
box.type = "checkbox";
box.id = "done-" + step.id;
box.checked = !!st.done;
box.addEventListener("change", function () {
st.done = box.checked;
li.className = st.done ? "done" : "";
save();
updateProgress();
});
check.appendChild(box);
var title = document.createElement("label");
title.className = "title";
title.htmlFor = box.id;
title.textContent = step.title;
var expect = document.createElement("div");
expect.className = "expect";
expect.textContent = step.expect;
var where = document.createElement("div");
where.className = "where";
var noteLabel = document.createElement("label");
noteLabel.htmlFor = "note-" + step.id;
noteLabel.textContent = "Où as-tu hésité ?";
var note = document.createElement("input");
note.type = "text";
note.id = noteLabel.htmlFor;
note.value = st.note || "";
note.autocomplete = "off";
note.addEventListener("input", function () {
st.note = note.value;
save();
});
where.appendChild(noteLabel);
where.appendChild(note);
li.appendChild(check);
li.appendChild(title);
li.appendChild(expect);
li.appendChild(where);
list.appendChild(li);
});
["tester", "version", "os"].forEach(function (key) {
var input = document.getElementById(key);
input.value = state.meta[key] || "";
input.oninput = function () {
state.meta[key] = input.value;
save();
};
});
updateProgress();
}
function pad(n) { return n < 10 ? "0" + n : String(n); }
function buildReport() {
var now = new Date();
var date = now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()) +
" " + pad(now.getHours()) + ":" + pad(now.getMinutes());
var done = 0;
var lines = [];
lines.push("QA parcours d'accueil — " + date);
if (state.meta.tester) lines.push("Testeur : " + state.meta.tester);
if (state.meta.version) lines.push("Version : " + state.meta.version);
if (state.meta.os) lines.push("Système : " + state.meta.os);
lines.push("");
STEPS.forEach(function (step, i) {
var st = stepState(step.id);
if (st.done) done += 1;
lines.push((st.done ? "[x] " : "[ ] ") + (i + 1) + ". " + step.title);
if (st.note && st.note.trim()) lines.push(" Hésitation : " + st.note.trim());
});
lines.splice(lines.indexOf("") , 0, "Étapes cochées : " + done + " / " + STEPS.length);
return lines.join("\n");
}
function showFallback(text) {
var box = document.getElementById("fallback");
var area = document.getElementById("report");
area.value = text;
box.hidden = false;
area.focus();
area.select();
}
document.getElementById("copy").addEventListener("click", function () {
var text = buildReport();
document.getElementById("fallback").hidden = true;
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
navigator.clipboard.writeText(text).then(function () {
setStatus("Rapport copié. Colle-le dans Paramètres → Systèmes → Rétroaction.");
}, function () {
setStatus("Copie automatique refusée : utilise la zone ci-dessous.");
showFallback(text);
});
} else {
setStatus("Copie automatique indisponible : utilise la zone ci-dessous.");
showFallback(text);
}
});
document.getElementById("reset").addEventListener("click", function () {
if (!window.confirm("Effacer toutes les cases et tous les commentaires ?")) return;
state = { meta: { tester: "", version: "", os: "" }, steps: {} };
try { window.localStorage.removeItem(STORAGE_KEY); } catch (e) { /* nothing to remove */ }
document.getElementById("fallback").hidden = true;
setStatus("Checklist réinitialisée.");
render();
});
render();
})();
</script>
</body>
</html>