Simpl-Resultat/src/pages/settings/UsersSettingsPage.tsx
le king fu a060f0e0e0
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m46s
feat(onboarding): dashboard empty state, getting-started checklist and re-show button (#352)
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>
2026-10-03 19:32:43 -04:00

103 lines
3.9 KiB
TypeScript

import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { ArrowLeft, ListChecks } from "lucide-react";
import AccountCard from "../../components/settings/AccountCard";
import LicenseCard from "../../components/settings/LicenseCard";
import DocsContent from "../../components/settings/DocsContent";
import { getPreference } from "../../services/userPreferenceService";
import {
ONBOARDING_CHECKLIST_HIDDEN_KEY,
showChecklist,
} from "../../services/onboardingService";
import { logError } from "../../services/logService";
// Re-show button of the getting-started checklist (#352). `hidden` = the
// preference was "1" at mount; `shown` = the button was clicked and the
// preference removed, so a confirmation line replaces the button.
type ChecklistButtonState = "unknown" | "visible" | "hidden" | "shown";
export default function UsersSettingsPage() {
const { t } = useTranslation();
const [checklistButton, setChecklistButton] = useState<ChecklistButtonState>("unknown");
useEffect(() => {
let cancelled = false;
(async () => {
try {
const value = await getPreference(ONBOARDING_CHECKLIST_HIDDEN_KEY);
if (!cancelled) setChecklistButton(value === "1" ? "hidden" : "visible");
} catch (e) {
// Informational control: without the preference, keep it out of the way.
if (!cancelled) setChecklistButton("visible");
logError("Settings: failed to read the checklist preference", e);
}
})();
return () => {
cancelled = true;
};
}, []);
const handleShowChecklist = async () => {
try {
await showChecklist();
setChecklistButton("shown");
} catch (e) {
logError("Settings: failed to show the checklist", e);
}
};
return (
<div className="space-y-6">
<Link
to="/settings"
className="inline-flex items-center gap-2 text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
>
<ArrowLeft size={16} />
{t("settings.backToHome")}
</Link>
<h1 className="text-2xl font-bold">{t("settings.users.title")}</h1>
<section className="space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)]">
{t("settings.users.sections.accounts")}
</h2>
<AccountCard />
</section>
<section className="space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)]">
{t("settings.users.sections.licenses")}
</h2>
<LicenseCard />
</section>
<section className="space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)]">
{t("settings.users.sections.userGuide")}
</h2>
{(checklistButton === "hidden" || checklistButton === "shown") && (
<div className="flex flex-wrap items-center gap-3 rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 text-sm">
<ListChecks size={18} className="shrink-0 text-[var(--muted-foreground)]" aria-hidden="true" />
<span className="flex-1 min-w-[12rem]">{t("settings.users.onboarding.label")}</span>
{checklistButton === "hidden" ? (
<button
type="button"
onClick={handleShowChecklist}
className="inline-flex items-center justify-center px-3 py-1.5 rounded-md border border-[var(--border)] font-medium hover:bg-[var(--muted)] transition-colors"
>
{t("settings.users.onboarding.show")}
</button>
) : (
<span role="status" className="text-[var(--muted-foreground)]">
{t("settings.users.onboarding.confirmation")}
</span>
)}
</div>
)}
<DocsContent />
</section>
</div>
);
}