feat(onboarding): dashboard empty state, getting-started checklist and re-show button (#352) #378

Merged
maximus merged 2 commits from issue-352-dashboard-empty-state into main 2026-10-04 12:51:19 +00:00
Owner

Fixes #352

Stacked on PR #377 (issue-351-onboarding-core, issue #351, not merged): this PR's base is that branch and its diff only shows the #352 work.

What

A profile without any transaction opens a dashboard that says what to do: a welcome card (DashboardEmptyState) with the expanded getting-started checklist and one primary action towards /import, instead of widgets showing 0 $. With transactions, the compact checklist sits between the title and the filter panel until it is complete or dismissed. Paramètres → Utilisateurs → Guide gets a "Afficher la liste de démarrage" button, visible only when the dismissal preference is set.

Why

Onboarding phase 1 (planned-2026-10-03-onboarding-phase1): the analysis in docs/analyse-onboarding-2026-09.md found that a new profile lands on a page of empty charts with no next step.

How

  • src/components/shared/EmptyState.tsx — pure; dashed card from SourceList; a to action renders a react-router <Link> (never useNavigate), an onClick action renders a <button> with disabled.
  • src/components/dashboard/OnboardingChecklist.tsx — pure (only useTranslation); props items/done/total/mode/budgetReady/onHide; undone rows link to their to, done rows have no link; the locked Budget row shows a lock, the "Base et plus" label and links to /settings/users; while budgetReady is false the budget row is neutral and the counter is hidden; compact mode = one labelled icon per row on a line, counter, hide button, no sentences.
  • src/components/dashboard/DashboardEmptyState.tsx — title, sentence, expanded checklist (omitted when hidden), primary <Link to="/import">.
  • src/pages/DashboardPage.tsx — branches on the hook status: loading → title + PageHelp + banner only; error → v0.15.0 page without the list + logWarn; ready and counters.transactions === 0 → empty state rendered outside the opacity-50 pointer-events-none wrapper; ready and > 0 → current page with the compact checklist unless hidden or complete.
  • src/hooks/useOnboardingChecklist.ts — the return gains counters and error (additive; the issue has the page read counters.transactions and log the error).
  • src/pages/settings/UsersSettingsPage.tsx — reads onboarding_checklist_hidden at mount; button → showChecklist() then an inline confirmation replaces it; logError on failure.
  • fr.json / en.json — 16 keys: dashboard.empty.*, onboarding.checklist.* (title, counter, hide, lockedBudget, items.*), settings.users.onboarding.*.

No DB migration. CHANGELOG untouched (centralised in #357).

Decisions (autopilot)

  • Empty state with the checklist dismissed: the welcome card and the import CTA stay, only the list goes (otherwise "Masquer" would do nothing visible there).
  • Budget row while the entitlement is unknown: neutral and without link (its destination depends on the lock that is not known yet).

Tests

  • EmptyState.test.tsx (7) and OnboardingChecklist.test.tsx (9) by direct call, FilterPanel pattern: to variant renders a Link, onClick variant a button with disabled; lock in Free with counter 1 / 5; neutral row and absent counter when budgetReady=false; compact mode without sentences; one link per undone row in both modes.
  • npm test: 74 files, 1401 tests green. npm run build green.
  • grep of the new components: no hard-coded string; FR/EN key sets identical.

Generated autonomously by /autopilot run of 2026-10-03

🤖 Generated with Claude Code

Fixes #352 **Stacked on PR #377** (`issue-351-onboarding-core`, issue #351, not merged): this PR's base is that branch and its diff only shows the #352 work. ## What A profile without any transaction opens a dashboard that says what to do: a welcome card (`DashboardEmptyState`) with the expanded getting-started checklist and one primary action towards `/import`, instead of widgets showing 0 $. With transactions, the compact checklist sits between the title and the filter panel until it is complete or dismissed. Paramètres → Utilisateurs → Guide gets a "Afficher la liste de démarrage" button, visible only when the dismissal preference is set. ## Why Onboarding phase 1 (`planned-2026-10-03-onboarding-phase1`): the analysis in `docs/analyse-onboarding-2026-09.md` found that a new profile lands on a page of empty charts with no next step. ## How - `src/components/shared/EmptyState.tsx` — pure; dashed card from `SourceList`; a `to` action renders a react-router `<Link>` (never `useNavigate`), an `onClick` action renders a `<button>` with `disabled`. - `src/components/dashboard/OnboardingChecklist.tsx` — pure (only `useTranslation`); props `items/done/total/mode/budgetReady/onHide`; undone rows link to their `to`, done rows have no link; the locked Budget row shows a lock, the "Base et plus" label and links to `/settings/users`; while `budgetReady` is false the budget row is neutral and the counter is hidden; compact mode = one labelled icon per row on a line, counter, hide button, no sentences. - `src/components/dashboard/DashboardEmptyState.tsx` — title, sentence, expanded checklist (omitted when `hidden`), primary `<Link to="/import">`. - `src/pages/DashboardPage.tsx` — branches on the hook `status`: `loading` → title + `PageHelp` + banner only; `error` → v0.15.0 page without the list + `logWarn`; `ready` and `counters.transactions === 0` → empty state rendered outside the `opacity-50 pointer-events-none` wrapper; `ready` and > 0 → current page with the compact checklist unless `hidden` or `complete`. - `src/hooks/useOnboardingChecklist.ts` — the return gains `counters` and `error` (additive; the issue has the page read `counters.transactions` and log the error). - `src/pages/settings/UsersSettingsPage.tsx` — reads `onboarding_checklist_hidden` at mount; button → `showChecklist()` then an inline confirmation replaces it; `logError` on failure. - `fr.json` / `en.json` — 16 keys: `dashboard.empty.*`, `onboarding.checklist.*` (title, counter, hide, lockedBudget, items.*), `settings.users.onboarding.*`. No DB migration. CHANGELOG untouched (centralised in #357). ## Decisions (autopilot) - Empty state with the checklist dismissed: the welcome card and the import CTA stay, only the list goes (otherwise "Masquer" would do nothing visible there). - Budget row while the entitlement is unknown: neutral and without link (its destination depends on the lock that is not known yet). ## Tests - `EmptyState.test.tsx` (7) and `OnboardingChecklist.test.tsx` (9) by direct call, FilterPanel pattern: `to` variant renders a `Link`, `onClick` variant a button with `disabled`; lock in Free with counter 1 / 5; neutral row and absent counter when `budgetReady=false`; compact mode without sentences; one link per undone row in both modes. - `npm test`: 74 files, 1401 tests green. `npm run build` green. - `grep` of the new components: no hard-coded string; FR/EN key sets identical. Generated autonomously by /autopilot run of 2026-10-03 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-10-03 23:33:16 +00:00
feat(onboarding): dashboard empty state, getting-started checklist and re-show button (#352)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m46s
a060f0e0e0
A profile without any transaction now opens a dashboard that says what to
do: a welcome card with the expanded getting-started checklist and one
primary action towards the import page, instead of widgets showing 0 $.
With transactions, the compact checklist sits between the title and the
filter panel until it is complete or dismissed.

- `EmptyState` (pure, dashed card from the import source list; a `to`
  action renders a react-router `<Link>`, an `onClick` action a button)
- `OnboardingChecklist` (pure, expanded and compact modes; undone rows
  link to their route, the locked Budget row shows a lock, the "Base and
  up" label and links to the license card; neutral budget row and hidden
  counter until the entitlement is known)
- `DashboardEmptyState` composes both
- `DashboardPage` branches on the hook `status`: loading renders the title,
  help and banner only; error falls back to the v0.15.0 page without the
  list and logs a warning; ready with zero transactions renders the empty
  state outside the `isLoading` veil
- `useOnboardingChecklist` exposes `counters` and `error` for the page
- Users settings: "Show the getting-started checklist" button in the Guide
  section, visible only when the preference is set, with an inline
  confirmation after the click
- FR + EN keys `dashboard.empty.*`, `onboarding.checklist.*`,
  `settings.users.onboarding.*`

Tests: `EmptyState.test.tsx` and `OnboardingChecklist.test.tsx` by direct
call (FilterPanel pattern). No DB migration. CHANGELOG left to #357.

Resolves #352

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
maximus added the
autopilot:pending-human
label 2026-10-03 23:33:31 +00:00
maximus added 1 commit 2026-10-03 23:34:27 +00:00
refactor(onboarding): name the checklist preference states after what they hold (#352)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m42s
3f3531392e
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
Owner

Self-review (autopilot, 2026-10-03) — Verdict: APPROVE

Reviewed GET /pulls/378.diff (vs issue-351-onboarding-core, 10 files, +~900 lines), on the final head 3f35313.

Summary. The page branches on the hook status, never on a bare counter; the empty state is returned before the opacity-50 pointer-events-none wrapper, so its <Link to="/import"> is clickable; loading renders only the banner, title and PageHelp; error renders the v0.15.0 page without the list and emits one logWarn. The two new components are pure and tested by direct call. One quality finding (settings state names that read backwards: "visible" meant "no button") was fixed in 3f35313 before this verdict.

Critère d'acceptation Preuve
Profil neuf : aucun KPI ni graphique, liste 1/6 (1/5 en Free), lien /import sans voile DashboardPage.tsx early return checklistReady && counters.transactions === 0 → <div>{header}<DashboardEmptyState/></div>, no veil class; counter asserted done=1,total=6 and done=1,total=5 in OnboardingChecklist.test.tsx
loading : ni widgets ni état vide ; error : page actuelle sans liste if (status === "loading") return <div>{header}</div>; checklistReady requires status === "ready", so no list on error; logWarn in a useEffect keyed on status/error
Transactions + liste masquée ou complète : rendu identique à v0.15.0 showCompactChecklist = checklistReady && !hidden && !checklist.complete; the rest of the tree is byte-identical to the base (header moved into a fragment, same DOM)
OnboardingChecklist.test.tsx : cadenas en Free, case neutre + compteur absent si budgetReady=false, mode compact, liens par case 9 tests: lock + lockedBudget + link /settings/users; neutral Circle, no Lock/CheckCircle2, no link, no counter; compact without items.* text, 3 done / 3 todo icons, aria-label per icon; links === undone rows' to in both modes
EmptyState.test.tsx : variantes to (Link) et onClick (bouton, disabled) 7 tests: type === Link with props.to, type === "button" with type="button", disabled false/true, handler called, both actions ordered, children between description and actions
Bouton « Afficher la liste de démarrage » absent sans préférence, présent et fonctionnel sinon UsersSettingsPage: preference read at mount, row rendered only for dismissed/restored; click → showChecklist() then the confirmation line; logError on failure, button kept
npm test, npm run build verts ; grep sans chaîne en dur 74 files / 1401 tests; build green; tsc --noEmit green after the rename; grep over the five touched components hits only one code comment; FR/EN flattened key sets identical (16 new keys)

Security. No user input is rendered; every to comes from computeChecklist constants; no new network call, no DB write beyond the existing showChecklist() (preference delete). No migration.

i18n. Same 16 keys in fr.json and en.json, both files valid JSON and byte-stable under the project's indent=2 dump. Counter uses {{done}} / {{total}} interpolation.

Accessibility. Actions are <button type="button"> or <Link>; compact icons carry aria-label/title, decorative icons aria-hidden; both checklist modes are role="region" with a label; the confirmation line is role="status".

Concerns (non-blocking).

  • The hook already logErrors a failed load; the page adds the logWarn the issue asks for, so one failure writes two Journal lines. Kept as specified.
  • When the user hides the checklist on an empty profile, the welcome card and import CTA stay and only the list goes (decision logged; alternative was a "Masquer" that does nothing visible there).
  • useDashboard still fires its queries on an empty database (accepted in the issue's planning decisions).

Generated autonomously by /autopilot run of 2026-10-03

## Self-review (autopilot, 2026-10-03) — Verdict: **APPROVE** Reviewed `GET /pulls/378.diff` (vs `issue-351-onboarding-core`, 10 files, +~900 lines), on the final head `3f35313`. **Summary.** The page branches on the hook `status`, never on a bare counter; the empty state is returned before the `opacity-50 pointer-events-none` wrapper, so its `<Link to="/import">` is clickable; `loading` renders only the banner, title and `PageHelp`; `error` renders the v0.15.0 page without the list and emits one `logWarn`. The two new components are pure and tested by direct call. One quality finding (settings state names that read backwards: `"visible"` meant "no button") was fixed in `3f35313` before this verdict. | Critère d'acceptation | Preuve | |---|---| | Profil neuf : aucun KPI ni graphique, liste 1/6 (1/5 en Free), lien `/import` sans voile | `DashboardPage.tsx` early return `checklistReady && counters.transactions === 0` → `<div>{header}<DashboardEmptyState/></div>`, no veil class; counter asserted `done=1,total=6` and `done=1,total=5` in `OnboardingChecklist.test.tsx` | | `loading` : ni widgets ni état vide ; `error` : page actuelle sans liste | `if (status === "loading") return <div>{header}</div>`; `checklistReady` requires `status === "ready"`, so no list on `error`; `logWarn` in a `useEffect` keyed on `status`/`error` | | Transactions + liste masquée ou complète : rendu identique à v0.15.0 | `showCompactChecklist = checklistReady && !hidden && !checklist.complete`; the rest of the tree is byte-identical to the base (header moved into a fragment, same DOM) | | `OnboardingChecklist.test.tsx` : cadenas en Free, case neutre + compteur absent si `budgetReady=false`, mode compact, liens par case | 9 tests: lock + `lockedBudget` + link `/settings/users`; neutral `Circle`, no `Lock`/`CheckCircle2`, no link, no counter; compact without `items.*` text, 3 done / 3 todo icons, `aria-label` per icon; links `===` undone rows' `to` in both modes | | `EmptyState.test.tsx` : variantes `to` (Link) et `onClick` (bouton, `disabled`) | 7 tests: `type === Link` with `props.to`, `type === "button"` with `type="button"`, `disabled` false/true, handler called, both actions ordered, children between description and actions | | Bouton « Afficher la liste de démarrage » absent sans préférence, présent et fonctionnel sinon | `UsersSettingsPage`: preference read at mount, row rendered only for `dismissed`/`restored`; click → `showChecklist()` then the confirmation line; `logError` on failure, button kept | | `npm test`, `npm run build` verts ; `grep` sans chaîne en dur | 74 files / 1401 tests; build green; `tsc --noEmit` green after the rename; grep over the five touched components hits only one code comment; FR/EN flattened key sets identical (16 new keys) | **Security.** No user input is rendered; every `to` comes from `computeChecklist` constants; no new network call, no DB write beyond the existing `showChecklist()` (preference delete). No migration. **i18n.** Same 16 keys in `fr.json` and `en.json`, both files valid JSON and byte-stable under the project's `indent=2` dump. Counter uses `{{done}} / {{total}}` interpolation. **Accessibility.** Actions are `<button type="button">` or `<Link>`; compact icons carry `aria-label`/`title`, decorative icons `aria-hidden`; both checklist modes are `role="region"` with a label; the confirmation line is `role="status"`. **Concerns (non-blocking).** - The hook already `logError`s a failed load; the page adds the `logWarn` the issue asks for, so one failure writes two Journal lines. Kept as specified. - When the user hides the checklist on an empty profile, the welcome card and import CTA stay and only the list goes (decision logged; alternative was a "Masquer" that does nothing visible there). - `useDashboard` still fires its queries on an empty database (accepted in the issue's planning decisions). Generated autonomously by /autopilot run of 2026-10-03
Author
Owner

Revue indépendante (/pr-review, 2026-10-03) — Verdict : APPROVE

Première passe indépendante : le commentaire précédent est l'auto-revue de l'autopilot. Diff issue-352-dashboard-empty-state (3f35313) contre issue-351-onboarding-core (76fd8d3), 10 fichiers.

Ce qui a été exécuté

  • Copie hors dépôt (git archive de la tête, node_modules lié) : EmptyState.test.tsx + OnboardingChecklist.test.tsx + useOnboardingChecklist.test.ts → 26/26 verts ; tsc nu (celui du script build) → exit 0. CI PR Check — Frontend verte sur la tête (1m42s), donc la PR stackée a bien eu sa CI.
  • Six mutations du relecteur, chacune suivie d'une restauration prouvée identique à la tête (diff -q) :
Mutation Tests rouges
Lignes faites rendues avec un Link (expanded) 2 / 16
Compteur rendu même quand budgetReady=false 2 / 16
État locked jamais produit 1 / 16
Mode compact rend les phrases 1 / 16
Action to rendue en <button> 2 / 16
disabled non propagé 1 / 16
  • Parité FR/EN : 16 clés nouvelles, fr-only = en-only = ∅ ; aucune clé dupliquée au même niveau (object_pairs_hook), ce que JSON.parse ne détecte pas ; aucune chaîne en dur dans les trois composants ; aucun skip/only.
  • Timeline de #352 : un seul closes, venant de #378. Aucune migration, aucun secret, pas de CHANGELOG (centralisé en #357, conforme aux règles de la vague).

Critères d'acceptation revérifiés dans le code

  • Branche sur status, jamais sur un compteur nu ; tous les hooks (useDashboard, useOnboardingChecklist, useEffect, 2 useState, 3 useCallback, useMemo) sont appelés avant le premier return anticipé — pas de violation des règles des hooks.
  • État vide retourné hors du voile opacity-50 pointer-events-none : le <Link to="/import"> reste cliquable.
  • loading → header seul ; error → checklistReady faux, page v0.15.0 sans liste, un logWarn par transition (useEffect sur [status, error]).
  • complete ne peut pas être vrai à 0 transaction (manualCategorized compte des transactions), donc l'état vide n'a pas besoin de le lire.
  • Paramètres : bouton visible sur dismissed | restored seulement, lecture au montage avec garde cancelled, logError sur échec ; le tableau de bord remonte à chaque navigation (et BrowserRouter key={refreshKey} au changement de profil), donc la préférence supprimée est relue sans état partagé.

Suggestions (non bloquantes)

  1. DashboardPage.tsx:803 — complete pendant le chargement de la licence. useEntitlement().allowed est fail-closed tant que ready est faux, donc computeChecklist compte Budget comme verrouillée et total vaut 5. Au boot à froid, un profil Base+ avec 5 lignes faites et aucun budget voit la liste compacte masquée (5/5) puis réapparaître (5/6) quand la licence arrive. const showCompactChecklist = checklistReady && !onboarding.hidden && !(onboarding.budgetReady && checklist.complete); ferme la fenêtre. Elle est étroite : LicenseProvider est dans main.tsx, au-dessus du remount du routeur, donc absente au changement de profil.
  2. Accessibilité de l'état. L'icône est aria-hidden et aria-label/title ne portent que le libellé : un lecteur d'écran n'entend ni « fait » ni « verrouillé », en compact comme en étendu (le line-through n'est pas annoncé). Deux clés i18n (state.done, state.locked) ajoutées au libellé suffiraient.
  3. Le branchement de DashboardPage n'a aucun test. Cohérent avec le projet (pages non testées sans jsdom), mais c'est la seule logique de la PR qu'aucune de mes mutations ne pouvait atteindre ; un === 0 muté en <= 1 passerait vert.
  4. onboarding.counters?.transactions : le ?. est mort — checklist !== null implique counters !== null dans le hook.
  5. La liste compacte vit dans le voile isLoading du tableau de bord : ses liens sont inertes pendant un rechargement des widgets. Conforme à l'issue (qui ne sort que l'état vide du voile), signalé pour mémoire.

Actions faites

  • Verdict posté une fois ; l'issue #352 porte déjà status:approved (posé par l'auto-revue), inchangé.
  • Copie hors dépôt supprimée (scratchpad/pr378), aucun fichier du dépôt touché (git status propre avant et après).
## Revue indépendante (`/pr-review`, 2026-10-03) — Verdict : **APPROVE** Première passe indépendante : le commentaire précédent est l'auto-revue de l'autopilot. Diff `issue-352-dashboard-empty-state` (`3f35313`) contre `issue-351-onboarding-core` (`76fd8d3`), 10 fichiers. ### Ce qui a été exécuté - Copie hors dépôt (`git archive` de la tête, `node_modules` lié) : `EmptyState.test.tsx` + `OnboardingChecklist.test.tsx` + `useOnboardingChecklist.test.ts` → **26/26 verts** ; `tsc` nu (celui du script `build`) → exit 0. CI `PR Check — Frontend` **verte** sur la tête (1m42s), donc la PR stackée a bien eu sa CI. - **Six mutations du relecteur**, chacune suivie d'une restauration prouvée identique à la tête (`diff -q`) : | Mutation | Tests rouges | |---|---| | Lignes faites rendues avec un `Link` (expanded) | 2 / 16 | | Compteur rendu même quand `budgetReady=false` | 2 / 16 | | État `locked` jamais produit | 1 / 16 | | Mode compact rend les phrases | 1 / 16 | | Action `to` rendue en `<button>` | 2 / 16 | | `disabled` non propagé | 1 / 16 | - Parité FR/EN : **16 clés** nouvelles, `fr-only` = `en-only` = ∅ ; **aucune clé dupliquée** au même niveau (`object_pairs_hook`), ce que `JSON.parse` ne détecte pas ; aucune chaîne en dur dans les trois composants ; aucun `skip`/`only`. - Timeline de #352 : un seul `closes`, venant de #378. Aucune migration, aucun secret, pas de CHANGELOG (centralisé en #357, conforme aux règles de la vague). ### Critères d'acceptation revérifiés dans le code - Branche sur `status`, jamais sur un compteur nu ; tous les hooks (`useDashboard`, `useOnboardingChecklist`, `useEffect`, 2 `useState`, 3 `useCallback`, `useMemo`) sont appelés **avant** le premier `return` anticipé — pas de violation des règles des hooks. - État vide retourné hors du voile `opacity-50 pointer-events-none` : le `<Link to="/import">` reste cliquable. - `loading` → header seul ; `error` → `checklistReady` faux, page v0.15.0 sans liste, un `logWarn` par transition (`useEffect` sur `[status, error]`). - `complete` ne peut pas être vrai à 0 transaction (`manualCategorized` compte des transactions), donc l'état vide n'a pas besoin de le lire. - Paramètres : bouton visible sur `dismissed | restored` seulement, lecture au montage avec garde `cancelled`, `logError` sur échec ; le tableau de bord remonte à chaque navigation (et `BrowserRouter key={refreshKey}` au changement de profil), donc la préférence supprimée est relue sans état partagé. ### Suggestions (non bloquantes) 1. **`DashboardPage.tsx:803` — `complete` pendant le chargement de la licence.** `useEntitlement().allowed` est fail-closed tant que `ready` est faux, donc `computeChecklist` compte Budget comme verrouillée et `total` vaut 5. Au boot à froid, un profil Base+ avec 5 lignes faites et aucun budget voit la liste compacte **masquée** (5/5) puis **réapparaître** (5/6) quand la licence arrive. `const showCompactChecklist = checklistReady && !onboarding.hidden && !(onboarding.budgetReady && checklist.complete);` ferme la fenêtre. Elle est étroite : `LicenseProvider` est dans `main.tsx`, au-dessus du remount du routeur, donc absente au changement de profil. 2. **Accessibilité de l'état.** L'icône est `aria-hidden` et `aria-label`/`title` ne portent que le libellé : un lecteur d'écran n'entend ni « fait » ni « verrouillé », en compact comme en étendu (le `line-through` n'est pas annoncé). Deux clés i18n (`state.done`, `state.locked`) ajoutées au libellé suffiraient. 3. **Le branchement de `DashboardPage` n'a aucun test.** Cohérent avec le projet (pages non testées sans jsdom), mais c'est la seule logique de la PR qu'aucune de mes mutations ne pouvait atteindre ; un `=== 0` muté en `<= 1` passerait vert. 4. `onboarding.counters?.transactions` : le `?.` est mort — `checklist !== null` implique `counters !== null` dans le hook. 5. La liste compacte vit **dans** le voile `isLoading` du tableau de bord : ses liens sont inertes pendant un rechargement des widgets. Conforme à l'issue (qui ne sort que l'état vide du voile), signalé pour mémoire. ### Actions faites - Verdict posté une fois ; l'issue #352 porte déjà `status:approved` (posé par l'auto-revue), inchangé. - Copie hors dépôt supprimée (`scratchpad/pr378`), aucun fichier du dépôt touché (`git status` propre avant et après).
maximus changed target branch from issue-351-onboarding-core to main 2026-10-04 12:51:14 +00:00
maximus merged commit 3f3531392e into main 2026-10-04 12:51:19 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#378
No description provided.