From 82da79cc4a3f879026aaa21bce828379e58fec72 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Thu, 28 May 2026 10:58:54 +0200 Subject: [PATCH] Add demo mode and seeded demo data --- package-lock.json | 4 +- package.json | 2 +- public/service-worker.js | 2 +- src/App.tsx | 120 +++++++++++++++++------ src/demoData.ts | 184 +++++++++++++++++++++++++++++++++++ src/evaluatorStorage.test.ts | 12 +++ src/evaluatorStorage.ts | 25 ++++- src/i18n.tsx | 20 ++++ src/integrity.test.ts | 13 +++ src/integrity.ts | 5 +- src/storage.test.ts | 22 ++++- src/storage.ts | 157 +++++++++++++++++++++++------- src/styles.css | 61 ++++++++++++ src/types.ts | 6 ++ 14 files changed, 558 insertions(+), 75 deletions(-) create mode 100644 src/demoData.ts diff --git a/package-lock.json b/package-lock.json index 5d5c90f..b233376 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "arbeidspuls", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arbeidspuls", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { "lucide-react": "^0.475.0", "react": "^19.0.0", diff --git a/package.json b/package.json index bf7c364..e7ab4f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arbeidspuls", - "version": "1.0.0", + "version": "1.1.0", "private": true, "type": "module", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index bcbd3ec..92c7e58 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE_NAME = "arbeidspuls-v4"; +const CACHE_NAME = "arbeidspuls-v5"; const ASSETS = ["/", "/index.html", "/manifest.webmanifest", "/icon.svg"]; self.addEventListener("install", (event) => { diff --git a/src/App.tsx b/src/App.tsx index 6891c61..9064dcb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -66,11 +66,14 @@ import { createReport, deleteAllReports, deleteReport, + ensureDemoData, exportReports, getLastWorkReport, importReportsFromJson, + isDemoModeEnabled, listReports, - previewImportReports + previewImportReports, + setDemoModeEnabled } from "./storage"; import type { Accommodation, @@ -97,7 +100,7 @@ type LineVisibility = "normal" | "dim" | "hidden"; type TrendMetric = { key: keyof Report | "total_score_percent"; label: string; short: string; color: string }; const PERIOD_KEY = "arbeidsevne-egenvurdering:trend-period"; -const APP_VERSION = "1.0"; +const APP_VERSION = "1.1"; const taskTypeOptions = Object.keys(taskTypeLabels) as TaskType[]; const limitationOptions = Object.keys(limitationLabels) as Limitation[]; @@ -127,24 +130,43 @@ function AppRoutes() { return ; } + const [demoMode, setDemoMode] = useState(() => isDemoModeEnabled()); const [view, setView] = useState("registrer"); - const [entries, setEntries] = useState(() => listReports()); + const [entries, setEntries] = useState(() => listReports(isDemoModeEnabled())); const [selectedEntryId, setSelectedEntryId] = useState(null); const [deleteRequest, setDeleteRequest] = useState<{ id: string; name: string } | null>(null); - const refresh = () => setEntries(listReports()); + const refresh = () => setEntries(listReports(demoMode)); const selectedEntry = selectedEntryId ? entries.find((entry) => entry.id === selectedEntryId) : undefined; + const changeDemoMode = (enabled: boolean) => { + setDemoModeEnabled(enabled); + if (enabled) ensureDemoData(); + setDemoMode(enabled); + setSelectedEntryId(null); + setDeleteRequest(null); + setEntries(listReports(enabled)); + }; + + useEffect(() => { + setEntries(listReports(demoMode)); + }, [demoMode]); return ( -
+
+ {demoMode && changeDemoMode(false)} />}

{t("app.eyebrow")}

{t("app.title")}

- - {t("app.evaluator")} - +
+ + + {t("app.evaluator")} + +
- {view === "registrer" && } - {view === "oppfolging" && } + {demoMode && ( +
+
+ )} + {view === "registrer" && } + {view === "oppfolging" && } {view === "oversikt" && ( setDeleteRequest({ id: entry.id, name: entry.type === "work_report" ? entry.workplace || t("danger.reportName") : t("danger.followupName") })} /> )} - {view === "personvern" && } + {view === "personvern" && }
setDeleteRequest(null)} onConfirm={() => { if (!deleteRequest) return; - deleteReport(deleteRequest.id); + deleteReport(deleteRequest.id, demoMode); if (selectedEntryId === deleteRequest.id) setSelectedEntryId(null); setDeleteRequest(null); refresh(); @@ -214,6 +242,20 @@ function NavButton({ ); } +function DemoBanner({ onExit }: { onExit: () => void }) { + const { t } = useI18n(); + return ( +
+ DEMO + + + +
+ ); +} + function AppFooter() { const { language, setLanguage, t } = useI18n(); const baseYear = 2026; @@ -306,8 +348,8 @@ function ConfirmDeleteModal({ ); } -function createInitialReportDraft(): ReportDraft { - const last = getLastWorkReport(); +function createInitialReportDraft(demoMode = false): ReportDraft { + const last = getLastWorkReport(demoMode); return { workplace: last?.workplace ?? "", work_start_time: last?.work_start_time ?? "", @@ -319,9 +361,9 @@ function createInitialReportDraft(): ReportDraft { }; } -function ReportForm({ onSaved }: { onSaved: () => void }) { +function ReportForm({ demoMode, onSaved }: { demoMode: boolean; onSaved: () => void }) { const { labels, t } = useI18n(); - const [draft, setDraft] = useState(() => createInitialReportDraft()); + const [draft, setDraft] = useState(() => createInitialReportDraft(demoMode)); const [attempted, setAttempted] = useState(false); const [savedText, setSavedText] = useState(""); @@ -329,6 +371,12 @@ function ReportForm({ onSaved }: { onSaved: () => void }) { const missing = reportFields.filter((field) => !draft[field.key]); const canSubmit = missing.length === 0; + useEffect(() => { + setDraft(createInitialReportDraft(demoMode)); + setAttempted(false); + setSavedText(""); + }, [demoMode]); + const setScale = (key: keyof ReportDraft, value: ScaleValue) => { setDraft((current) => ({ ...current, [key]: value })); }; @@ -347,8 +395,8 @@ function ReportForm({ onSaved }: { onSaved: () => void }) { setAttempted(true); if (!canSubmit) return; - createReport(draft); - setDraft(createInitialReportDraft()); + createReport(draft, demoMode); + setDraft(createInitialReportDraft(demoMode)); setAttempted(false); setSavedText(t("register.saved")); onSaved(); @@ -666,7 +714,7 @@ function ChipGroup({ ); } -function FollowupForm({ reports, onSaved }: { reports: StoredEntry[]; onSaved: () => void }) { +function FollowupForm({ demoMode, reports, onSaved }: { demoMode: boolean; reports: StoredEntry[]; onSaved: () => void }) { const { labels, t } = useI18n(); const [draft, setDraft] = useState({ delayed_symptoms: [], note: "" }); const [attempted, setAttempted] = useState(false); @@ -679,7 +727,7 @@ function FollowupForm({ reports, onSaved }: { reports: StoredEntry[]; onSaved: ( setAttempted(true); if (!draft.worse_than_before) return; - createFollowup(draft); + createFollowup(draft, demoMode); setDraft({ delayed_symptoms: [], note: "" }); setAttempted(false); setSavedText(t("followup.saved")); @@ -1502,24 +1550,24 @@ function Detail({ label, value }: { label: string; value: React.ReactNode }) { ); } -function PrivacyPanel({ entries, onDeleted }: { entries: StoredEntry[]; onDeleted: () => void }) { +function PrivacyPanel({ demoMode, entries, onDeleted }: { demoMode: boolean; entries: StoredEntry[]; onDeleted: () => void }) { const { t } = useI18n(); const [confirmDeleteAll, setConfirmDeleteAll] = useState(false); const [pendingImport, setPendingImport] = useState<{ text: string; summary: ImportSummary; fileName: string } | null>(null); const [importMessage, setImportMessage] = useState(""); const download = () => { - const blob = new Blob([exportReports()], { type: "application/json" }); + const blob = new Blob([exportReports(demoMode)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = `arbeidsevne-rapporter-${new Date().toISOString().slice(0, 10)}.json`; + link.download = `${demoMode ? "arbeidspuls-demo" : "arbeidspuls"}-rapporter-${new Date().toISOString().slice(0, 10)}.json`; link.click(); URL.revokeObjectURL(url); }; const clear = () => { - deleteAllReports(); + deleteAllReports(demoMode); setConfirmDeleteAll(false); onDeleted(); }; @@ -1529,7 +1577,7 @@ function PrivacyPanel({ entries, onDeleted }: { entries: StoredEntry[]; onDelete if (!file) return; try { const text = await file.text(); - const summary = previewImportReports(text); + const summary = previewImportReports(text, demoMode); setPendingImport({ text, summary, fileName: file.name }); setImportMessage(""); } catch (error) { @@ -1542,13 +1590,13 @@ function PrivacyPanel({ entries, onDeleted }: { entries: StoredEntry[]; onDelete const confirmImport = () => { if (!pendingImport) return; - const summary = importReportsFromJson(pendingImport.text); + const summary = importReportsFromJson(pendingImport.text, demoMode); const flagged = summary.verification ? summary.verification.modified + summary.verification.missing + summary.verification.unsupported + summary.verification.error : 0; setPendingImport(null); setImportMessage( - `Import fullført: ${summary.reports_added} nye rapporter, ${summary.followups_added} nye oppfølginger, ${summary.duplicates_skipped} duplikater hoppet over.${flagged > 0 ? " Noen rapporter kunne ikke verifiseres." : ""}` + `Import fullført: ${summary.reports_added} nye rapporter, ${summary.followups_added} nye oppfølginger, ${summary.duplicates_skipped} duplikater hoppet over.${summary.demo_entries_detected ? " Demodata ble importert i demo-modus." : ""}${flagged > 0 ? " Noen rapporter kunne ikke verifiseres." : ""}` ); onDeleted(); }; @@ -1557,7 +1605,7 @@ function PrivacyPanel({ entries, onDeleted }: { entries: StoredEntry[]; onDelete
@@ -1594,6 +1642,9 @@ function PrivacyPanel({ entries, onDeleted }: { entries: StoredEntry[]; onDelete conflicts: pendingImport.summary.conflicts_detected })}

+ {Boolean(pendingImport.summary.demo_entries_detected) && ( +

{t("privacy.demoImportWarning", { count: pendingImport.summary.demo_entries_detected ?? 0 })}

+ )}