Release v1.1.1 demo advisor mode

This commit is contained in:
Franz Rolfsvaag 2026-05-28 11:13:35 +02:00
parent 82da79cc4a
commit 76ec48bfef
9 changed files with 314 additions and 139 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "arbeidspuls",
"version": "1.1.0",
"version": "1.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "arbeidspuls",
"version": "1.1.0",
"version": "1.1.1",
"dependencies": {
"lucide-react": "^0.475.0",
"react": "^19.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "arbeidspuls",
"version": "1.1.0",
"version": "1.1.1",
"private": true,
"type": "module",
"scripts": {

View File

@ -1,4 +1,4 @@
const CACHE_NAME = "arbeidspuls-v5";
const CACHE_NAME = "arbeidspuls-v6";
const ASSETS = ["/", "/index.html", "/manifest.webmanifest", "/icon.svg"];
self.addEventListener("install", (event) => {

View File

@ -37,6 +37,7 @@ import {
import {
deleteEvaluatorNote,
deleteEvaluatorProfile,
ensureDemoEvaluatorProfiles,
exportEvaluatorBackup,
importEvaluatorBackup,
importExportPayload,
@ -153,20 +154,14 @@ function AppRoutes() {
return (
<div className={demoMode ? "app-shell demo-active" : "app-shell"}>
{demoMode && <DemoBanner onExit={() => changeDemoMode(false)} />}
<header className="topbar">
<div>
<p className="eyebrow">{t("app.eyebrow")}</p>
<h1>{t("app.title")}</h1>
</div>
<div className="topbar-actions">
<button className={demoMode ? "text-link active" : "text-link"} onClick={() => changeDemoMode(!demoMode)} type="button">
{demoMode ? t("demo.exit") : t("demo.enter")}
</button>
<a className="text-link" href="/veileder">
{t("app.evaluator")}
</a>
</div>
</header>
<nav className="main-nav" aria-label={t("app.navLabel")}>
@ -206,7 +201,7 @@ function AppRoutes() {
)}
{view === "personvern" && <PrivacyPanel demoMode={demoMode} entries={entries} onDeleted={refresh} />}
</main>
<AppFooter />
<AppFooter demoMode={demoMode} onDemoModeChange={changeDemoMode} />
<ConfirmDeleteModal
itemName={deleteRequest?.name ?? ""}
open={Boolean(deleteRequest)}
@ -242,21 +237,7 @@ function NavButton({
);
}
function DemoBanner({ onExit }: { onExit: () => void }) {
const { t } = useI18n();
return (
<div className="demo-banner" role="status">
<span>DEMO</span>
<span aria-hidden="true">[</span>
<button aria-label={t("demo.exit")} onClick={onExit} type="button">
x
</button>
<span aria-hidden="true">]</span>
</div>
);
}
function AppFooter() {
function AppFooter({ demoMode, onDemoModeChange }: { demoMode: boolean; onDemoModeChange: (enabled: boolean) => void }) {
const { language, setLanguage, t } = useI18n();
const baseYear = 2026;
const currentYear = new Date().getFullYear();
@ -292,6 +273,9 @@ function AppFooter() {
<Github aria-hidden="true" />
<span>{t("footer.sourceCode", { version: APP_VERSION })}</span>
</a>
<button className={demoMode ? "footer-link active" : "footer-link"} onClick={() => onDemoModeChange(!demoMode)} type="button">
{demoMode ? t("demo.exit") : t("demo.enter")}
</button>
</div>
</footer>
);
@ -1687,8 +1671,9 @@ function PrivacyPanel({ demoMode, entries, onDeleted }: { demoMode: boolean; ent
function EvaluatorApp() {
const { labels, t } = useI18n();
const [profiles, setProfiles] = useState<EvaluatorProfile[]>(() => listEvaluatorProfiles());
const [selectedId, setSelectedId] = useState<string | null>(() => listEvaluatorProfiles()[0]?.id ?? null);
const [demoMode, setDemoMode] = useState(() => isDemoModeEnabled());
const [profiles, setProfiles] = useState<EvaluatorProfile[]>(() => listEvaluatorProfiles(isDemoModeEnabled()));
const [selectedId, setSelectedId] = useState<string | null>(() => listEvaluatorProfiles(isDemoModeEnabled())[0]?.id ?? null);
const [displayName, setDisplayName] = useState("");
const [targetId, setTargetId] = useState("");
const [message, setMessage] = useState("");
@ -1705,10 +1690,27 @@ function EvaluatorApp() {
if (selected) setPersonNote(getPersonNote(selected)?.text ?? "");
}, [selected?.id]);
const refresh = (nextSelectedId?: string) => {
const next = listEvaluatorProfiles();
useEffect(() => {
const next = listEvaluatorProfiles(demoMode);
setProfiles(next);
if (nextSelectedId) setSelectedId(nextSelectedId);
setSelectedId((current) => (current && next.some((profile) => profile.id === current) ? current : next[0]?.id ?? null));
setSelectedEntryId(null);
}, [demoMode]);
const refresh = (nextSelectedId?: string) => {
const next = listEvaluatorProfiles(demoMode);
setProfiles(next);
setSelectedId((current) => nextSelectedId ?? (current && next.some((profile) => profile.id === current) ? current : next[0]?.id ?? null));
};
const changeDemoMode = (enabled: boolean) => {
setDemoModeEnabled(enabled);
if (enabled) {
ensureDemoData();
ensureDemoEvaluatorProfiles();
}
setDemoMode(enabled);
setMessage(enabled ? t("demo.notice") : "");
};
const importFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
@ -1716,7 +1718,7 @@ function EvaluatorApp() {
if (!file) return;
try {
const result = importExportPayload(await file.text(), displayName, targetId || undefined);
const result = importExportPayload(await file.text(), displayName, targetId || undefined, demoMode);
const flagged = result.verification.modified + result.verification.missing + result.verification.unsupported + result.verification.error;
const actionText =
result.target_resolution === "matched_existing_person"
@ -1739,8 +1741,10 @@ function EvaluatorApp() {
const file = event.target.files?.[0];
if (!file) return;
try {
importEvaluatorBackup(await file.text());
setMessage(t("evaluator.importedBackup"));
const text = await file.text();
const demoEntries = countEvaluatorBackupDemoEntries(text);
importEvaluatorBackup(text, demoMode);
setMessage(`${t("evaluator.importedBackup")}${demoEntries > 0 ? ` ADVARSEL: Backupen inneholder ${demoEntries} demodata-oppføringer.` : ""}`);
refresh();
} catch (error) {
setMessage(error instanceof Error ? error.message : "Backup kunne ikke importeres.");
@ -1750,32 +1754,32 @@ function EvaluatorApp() {
};
const downloadBackup = () => {
const blob = new Blob([exportEvaluatorBackup()], { type: "application/json" });
const blob = new Blob([exportEvaluatorBackup(demoMode)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `arbeidsevne-veilederdata-${new Date().toISOString().slice(0, 10)}.json`;
link.download = `${demoMode ? "arbeidspuls-demo" : "arbeidspuls"}-veilederdata-${new Date().toISOString().slice(0, 10)}.json`;
link.click();
URL.revokeObjectURL(url);
};
const saveName = () => {
if (!selected) return;
renameEvaluatorProfile(selected.id, selected.display_name);
renameEvaluatorProfile(selected.id, selected.display_name, demoMode);
refresh(selected.id);
};
const savePersonNote = () => {
if (!selected) return;
upsertEvaluatorNote(selected.id, personNote, { note_type: "person" });
upsertEvaluatorNote(selected.id, personNote, { note_type: "person" }, demoMode);
setPersonNote("");
refresh(selected.id);
};
const deleteSelectedPerson = () => {
if (!deletePersonRequest) return;
deleteEvaluatorProfile(deletePersonRequest.id);
const remaining = listEvaluatorProfiles();
deleteEvaluatorProfile(deletePersonRequest.id, demoMode);
const remaining = listEvaluatorProfiles(demoMode);
setProfiles(remaining);
setSelectedId(remaining[0]?.id ?? null);
setSelectedEntryId(null);
@ -1801,7 +1805,7 @@ function EvaluatorApp() {
const followupsByParent = useMemo(() => (selected ? groupFollowups(selected.reports) : new Map<string, DelayedFollowup[]>()), [selected]);
return (
<div className="app-shell evaluator-shell">
<div className={demoMode ? "app-shell evaluator-shell demo-active" : "app-shell evaluator-shell"}>
<header className="topbar">
<div>
<p className="eyebrow">{t("evaluator.eyebrow")}</p>
@ -1812,6 +1816,13 @@ function EvaluatorApp() {
</a>
</header>
{demoMode && (
<div className="notice warning">
<ShieldAlert aria-hidden="true" />
<span>{t("demo.notice")}</span>
</div>
)}
<main className="evaluator-layout">
<section className="evaluator-sidebar">
<div className="field-card">
@ -1968,6 +1979,7 @@ function EvaluatorApp() {
followups={selectedEntry.type === "work_report" ? followupsByParent.get(selectedEntry.id) ?? [] : []}
notes={selected.evaluator_notes}
profileId={selected.id}
demoMode={demoMode}
onSaved={() => refresh(selected.id)}
onClose={() => setSelectedEntryId(null)}
/>
@ -1982,7 +1994,7 @@ function EvaluatorApp() {
)}
</section>
</main>
<AppFooter />
<AppFooter demoMode={demoMode} onDemoModeChange={changeDemoMode} />
<ConfirmDeleteModal
open={Boolean(deletePersonRequest)}
itemName={
@ -1997,6 +2009,20 @@ function EvaluatorApp() {
);
}
function countEvaluatorBackupDemoEntries(jsonText: string) {
try {
const parsed = JSON.parse(jsonText) as { data_mode?: string; profiles?: EvaluatorProfile[] };
if (!Array.isArray(parsed.profiles)) return 0;
const count = parsed.profiles.reduce(
(total, profile) => total + (profile.contains_demo_data ? 1 : 0) + profile.reports.filter((entry) => entry.demo || entry.id.startsWith("demo-arbeidspuls")).length,
0
);
return parsed.data_mode === "demo" && count === 0 ? 1 : count;
} catch {
return 0;
}
}
function ReviewReportCard({
report,
followups,
@ -2130,6 +2156,7 @@ function ReviewDetailPane({
followups,
notes,
profileId,
demoMode,
onSaved,
onClose
}: {
@ -2137,6 +2164,7 @@ function ReviewDetailPane({
followups: DelayedFollowup[];
notes: EvaluatorProfile["evaluator_notes"];
profileId: string;
demoMode: boolean;
onSaved: () => void;
onClose: () => void;
}) {
@ -2151,22 +2179,22 @@ function ReviewDetailPane({
const save = () => {
if (entry.type === "work_report") {
upsertEvaluatorNote(profileId, noteText, { note_type: "report", related_report_id: entry.id });
upsertEvaluatorNote(profileId, noteText, { note_type: "report", related_report_id: entry.id }, demoMode);
} else {
upsertEvaluatorNote(profileId, noteText, {
note_type: "followup",
related_followup_id: entry.id,
parent_report_id: findParentId(entry)
});
}, demoMode);
}
onSaved();
};
const remove = () => {
if (entry.type === "work_report") {
deleteEvaluatorNote(profileId, { note_type: "report", related_report_id: entry.id });
deleteEvaluatorNote(profileId, { note_type: "report", related_report_id: entry.id }, demoMode);
} else {
deleteEvaluatorNote(profileId, { note_type: "followup", related_followup_id: entry.id });
deleteEvaluatorNote(profileId, { note_type: "followup", related_followup_id: entry.id }, demoMode);
}
setNoteText("");
onSaved();

View File

@ -1,4 +1,4 @@
import type { StoredEntry } from "./types";
import type { EvaluatorProfile, StoredEntry } from "./types";
export const demoEntries: StoredEntry[] = [
{
@ -182,3 +182,154 @@ export const demoEntries: StoredEntry[] = [
}
}
];
const sharedDemoProfiles = {
created_at: "2026-05-28T08:00:00.000Z",
updated_at: "2026-05-28T08:00:00.000Z",
contains_demo_data: true,
evaluator_notes: []
};
export const demoEvaluatorProfiles: EvaluatorProfile[] = [
{
...sharedDemoProfiles,
id: "demo-advisor-profile-01",
display_name: "Demo: Nora Hjemmekontor",
source_fingerprint: "demo-nora",
reports: demoEntries
},
{
...sharedDemoProfiles,
id: "demo-advisor-profile-02",
display_name: "Demo: Amir Kontor",
source_fingerprint: "demo-amir",
reports: [
{
id: "demo-arbeidspuls-amir-001",
created_at: "2026-05-18T07:20:00.000Z",
report_date: "2026-05-18",
report_time: "09:20:00",
type: "work_report",
workplace: "Kontor",
work_start_time: "08:30",
work_end_time: "11:30",
work_ability: 3,
energy_level: 3,
mental_clarity: 4,
symptom_burden: 2,
effort_strain: 3,
status: "kan_fortsette",
physical_energy: 3,
mental_energy: 4,
perceived_productivity: 3,
task_completion: "delvis",
task_types: ["sitting", "computer_work", "time_pressure"],
main_limitations: ["fatigue"],
helpful_accommodations: ["extra_breaks"],
note: "Testdata: moderat dag med noen pauser.",
total_score_percent: 68,
demo: true
},
{
id: "demo-arbeidspuls-amir-002",
created_at: "2026-05-23T10:45:00.000Z",
report_date: "2026-05-23",
report_time: "12:45:00",
type: "work_report",
workplace: "Kontor",
work_start_time: "10:00",
work_end_time: "12:30",
work_ability: 2,
energy_level: 2,
mental_clarity: 2,
symptom_burden: 4,
effort_strain: 5,
status: "trenger_pause",
physical_energy: 2,
mental_energy: 2,
perceived_productivity: 2,
task_completion: "litt",
task_types: ["sitting", "talking_social", "noise"],
main_limitations: ["fatigue", "sensory_overload", "brain_fog"],
helpful_accommodations: ["quiet_room", "shorter_task"],
note: "Testdata: støy og sosial belastning ga tydelig fall.",
total_score_percent: 24,
demo: true
},
{
id: "demo-arbeidspuls-amir-followup-002",
created_at: "2026-05-24T08:30:00.000Z",
report_date: "2026-05-24",
report_time: "10:30:00",
type: "delayed_followup",
parent_report_id: "demo-arbeidspuls-amir-002",
related_report_id: "demo-arbeidspuls-amir-002",
worse_than_before: "moderat",
delayed_symptoms: ["fatigue", "brain_fog", "headache"],
recovery_status: "to_tre_dager",
note: "Testdata: oppfølging etter krevende kontordag.",
demo: true
}
]
},
{
...sharedDemoProfiles,
id: "demo-advisor-profile-03",
display_name: "Demo: Liv Butikk",
source_fingerprint: "demo-liv",
reports: [
{
id: "demo-arbeidspuls-liv-001",
created_at: "2026-05-19T12:15:00.000Z",
report_date: "2026-05-19",
report_time: "14:15:00",
type: "work_report",
workplace: "Butikk",
work_start_time: "12:00",
work_end_time: "14:00",
work_ability: 3,
energy_level: 3,
mental_clarity: 3,
symptom_burden: 3,
effort_strain: 3,
status: "trenger_enklere_oppgave",
physical_energy: 3,
mental_energy: 3,
perceived_productivity: 3,
task_completion: "delvis",
task_types: ["standing", "walking", "talking_social"],
main_limitations: ["pain", "fatigue"],
helpful_accommodations: ["sitting_down", "help_from_others"],
note: "Testdata: fysisk aktivitet krevde tilrettelegging.",
total_score_percent: 50,
demo: true
},
{
id: "demo-arbeidspuls-liv-002",
created_at: "2026-05-26T07:50:00.000Z",
report_date: "2026-05-26",
report_time: "09:50:00",
type: "work_report",
workplace: "Butikk",
work_start_time: "09:00",
work_end_time: "09:45",
work_ability: 4,
energy_level: 4,
mental_clarity: 4,
symptom_burden: 2,
effort_strain: 2,
status: "kan_fortsette",
physical_energy: 4,
mental_energy: 4,
perceived_productivity: 4,
task_completion: "som_forventet",
task_types: ["standing", "walking"],
main_limitations: [],
helpful_accommodations: ["shorter_task", "sitting_down"],
note: "Testdata: kortere oppgave fungerte bedre.",
total_score_percent: 78,
demo: true
}
]
}
];

View File

@ -123,4 +123,12 @@ describe("evaluator-import", () => {
expect(result.profile.contains_demo_data).toBe(true);
expect(listEvaluatorProfiles()[0].contains_demo_data).toBe(true);
});
it("holder veilederens demobrukere separat fra normale brukere", () => {
const demoProfiles = listEvaluatorProfiles(true);
expect(demoProfiles.length).toBeGreaterThanOrEqual(3);
expect(demoProfiles.every((profile) => profile.contains_demo_data)).toBe(true);
expect(listEvaluatorProfiles()).toHaveLength(0);
});
});

View File

@ -1,8 +1,11 @@
import { calculateReportScore, getEntryTime } from "./scoring";
import { summarizeVerification, verifyEntryIntegrity } from "./integrity";
import { demoEvaluatorProfiles } from "./demoData";
import type { EvaluatorNote, EvaluatorProfile, ExportPayload, ImportTargetResolution, Report, StoredEntry } from "./types";
const EVALUATOR_KEY = "arbeidsevne-egenvurdering:evaluator-profiles";
const DEMO_EVALUATOR_KEY = "arbeidspuls:demo-evaluator-profiles";
const DEMO_EVALUATOR_SEEDED_KEY = "arbeidspuls:demo-evaluator-seeded";
export type ImportResult = {
profile: EvaluatorProfile;
@ -15,8 +18,22 @@ export type ImportResult = {
matched_report_ids: string[];
};
function readProfiles(): EvaluatorProfile[] {
const raw = localStorage.getItem(EVALUATOR_KEY);
function evaluatorKey(demoMode = false) {
return demoMode ? DEMO_EVALUATOR_KEY : EVALUATOR_KEY;
}
export function ensureDemoEvaluatorProfiles() {
const existing = readProfiles(true);
if (existing.length > 0) return existing;
if (localStorage.getItem(DEMO_EVALUATOR_SEEDED_KEY) === "true") return [];
const seeded = demoEvaluatorProfiles.map(normalizeProfile);
writeProfiles(seeded, true);
localStorage.setItem(DEMO_EVALUATOR_SEEDED_KEY, "true");
return seeded;
}
function readProfiles(demoMode = false): EvaluatorProfile[] {
const raw = localStorage.getItem(evaluatorKey(demoMode));
if (!raw) return [];
try {
@ -27,32 +44,33 @@ function readProfiles(): EvaluatorProfile[] {
}
}
function writeProfiles(profiles: EvaluatorProfile[]) {
localStorage.setItem(EVALUATOR_KEY, JSON.stringify(profiles));
function writeProfiles(profiles: EvaluatorProfile[], demoMode = false) {
localStorage.setItem(evaluatorKey(demoMode), JSON.stringify(profiles));
}
export function listEvaluatorProfiles() {
return readProfiles().map(normalizeProfile).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
export function listEvaluatorProfiles(demoMode = false) {
if (demoMode) ensureDemoEvaluatorProfiles();
return readProfiles(demoMode).map(normalizeProfile).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
}
export function updateEvaluatorProfile(profile: EvaluatorProfile) {
const profiles = readProfiles();
export function updateEvaluatorProfile(profile: EvaluatorProfile, demoMode = false) {
const profiles = readProfiles(demoMode);
const next = profiles.some((item) => item.id === profile.id)
? profiles.map((item) => (item.id === profile.id ? profile : item))
: [profile, ...profiles];
writeProfiles(next);
writeProfiles(next, demoMode);
}
export function renameEvaluatorProfile(profileId: string, displayName: string) {
export function renameEvaluatorProfile(profileId: string, displayName: string, demoMode = false) {
const now = new Date().toISOString();
const profiles = readProfiles().map((profile) =>
const profiles = readProfiles(demoMode).map((profile) =>
profile.id === profileId ? { ...profile, display_name: displayName.trim() || profile.display_name, updated_at: now } : profile
);
writeProfiles(profiles);
writeProfiles(profiles, demoMode);
}
export function deleteEvaluatorProfile(profileId: string) {
writeProfiles(readProfiles().filter((profile) => profile.id !== profileId));
export function deleteEvaluatorProfile(profileId: string, demoMode = false) {
writeProfiles(readProfiles(demoMode).filter((profile) => profile.id !== profileId), demoMode);
}
export function addEvaluatorNote(profileId: string, text: string, relatedReportId?: string) {
@ -62,22 +80,24 @@ export function addEvaluatorNote(profileId: string, text: string, relatedReportI
export function upsertEvaluatorNote(
profileId: string,
text: string,
noteTarget: Pick<EvaluatorNote, "note_type" | "related_report_id" | "related_followup_id" | "parent_report_id">
noteTarget: Pick<EvaluatorNote, "note_type" | "related_report_id" | "related_followup_id" | "parent_report_id">,
demoMode = false
) {
const now = new Date().toISOString();
const trimmed = text.trim();
const profiles = readProfiles().map((profile) =>
const profiles = readProfiles(demoMode).map((profile) =>
profile.id === profileId ? upsertNoteInProfile(normalizeProfile(profile), trimmed, noteTarget, now) : profile
);
writeProfiles(profiles);
writeProfiles(profiles, demoMode);
}
export function deleteEvaluatorNote(
profileId: string,
noteTarget: Pick<EvaluatorNote, "note_type" | "related_report_id" | "related_followup_id">
noteTarget: Pick<EvaluatorNote, "note_type" | "related_report_id" | "related_followup_id">,
demoMode = false
) {
const now = new Date().toISOString();
const profiles = readProfiles().map((profile) =>
const profiles = readProfiles(demoMode).map((profile) =>
profile.id === profileId
? {
...normalizeProfile(profile),
@ -86,14 +106,14 @@ export function deleteEvaluatorNote(
}
: profile
);
writeProfiles(profiles);
writeProfiles(profiles, demoMode);
}
export function importExportPayload(jsonText: string, displayName: string, targetProfileId?: string): ImportResult {
export function importExportPayload(jsonText: string, displayName: string, targetProfileId?: string, demoMode = false): ImportResult {
const parsed = JSON.parse(jsonText);
const reports = extractReports(parsed).map(verifyEntryIntegrity);
const demoEntriesDetected = countDemoEntries(reports, parsed);
const profiles = readProfiles().map(normalizeProfile);
const profiles = readProfiles(demoMode).map(normalizeProfile);
const fingerprint = makeFingerprint(reports);
const target = resolveImportTarget(reports, profiles, targetProfileId);
if (target.action === "conflict_requires_manual_resolution") {
@ -141,7 +161,7 @@ export function importExportPayload(jsonText: string, displayName: string, targe
evaluator_notes: []
};
updateEvaluatorProfile(profile);
updateEvaluatorProfile(profile, demoMode);
return {
profile,
added,
@ -154,25 +174,26 @@ export function importExportPayload(jsonText: string, displayName: string, targe
};
}
export function exportEvaluatorBackup() {
export function exportEvaluatorBackup(demoMode = false) {
return JSON.stringify(
{
backup_type: "arbeidsevne-veilederdata",
schema_version: 1,
exported_at: new Date().toISOString(),
profiles: listEvaluatorProfiles()
data_mode: demoMode ? "demo" : "normal",
profiles: listEvaluatorProfiles(demoMode)
},
null,
2
);
}
export function importEvaluatorBackup(jsonText: string) {
export function importEvaluatorBackup(jsonText: string, demoMode = false) {
const parsed = JSON.parse(jsonText);
if (parsed?.backup_type !== "arbeidsevne-veilederdata" || !Array.isArray(parsed.profiles)) {
throw new Error("Filen er ikke en gyldig veilederbackup.");
}
const existing = readProfiles();
const existing = readProfiles(demoMode);
const merged = [...existing];
for (const profile of parsed.profiles as EvaluatorProfile[]) {
const index = merged.findIndex((item) => item.id === profile.id);
@ -186,7 +207,7 @@ export function importEvaluatorBackup(jsonText: string) {
merged.push(normalizeProfile(profile));
}
}
writeProfiles(merged);
writeProfiles(merged, demoMode);
}
export function extractReports(payload: ExportPayload | StoredEntry[] | { reports?: StoredEntry[] }) {

View File

@ -334,7 +334,7 @@ const en: TranslationTree = {
notCalculated: "Not calculated",
unknownWorkplace: "Unknown workplace"
},
app: { eyebrow: "Local self-assessment", title: "Arbeidspuls", evaluator: "Guide", navLabel: "Main navigation" },
app: { eyebrow: "Local self-assessment", title: "Arbeidspuls", evaluator: "Advisor", navLabel: "Main navigation" },
nav: { register: "Register", followup: "Follow-up", overview: "Overview", privacy: "Privacy" },
demo: {
enter: "Demo",
@ -442,9 +442,9 @@ const en: TranslationTree = {
backupTitle: "Backup and device changes",
backupBody:
"Because the data is stored locally, it may be lost if you change device, delete browser data, use another browser or reset the device. Export a JSON file regularly if you want to keep the reports.",
sharingTitle: "Sharing with a guide",
sharingTitle: "Sharing with an advisor",
sharingBody:
"If a guide or another person will review the reports, export a JSON file and share it with them, for example by email. The recipient can import the JSON file in the guide view.",
"If an advisor or another person will review the reports, export a JSON file and share it with them, for example by email. The recipient can import the JSON file in the advisor view.",
sensitiveTitle: "Sensitive information",
sensitiveBody:
"Reports may contain health and function-related information. Share the JSON file in a way suitable for sensitive information, and only with people who should have access.",
@ -452,8 +452,8 @@ const en: TranslationTree = {
deleteAllName: "all local reports"
},
evaluator: {
eyebrow: "Guide",
title: "Guide review",
eyebrow: "Advisor",
title: "Advisor review",
backToApp: "Back to app",
importJson: "Import JSON",
importHelp: "Choose an export from the app. Data is stored locally in this browser.",
@ -462,14 +462,14 @@ const en: TranslationTree = {
connectExisting: "Connect to existing person",
autoMatch: "Create new or use automatic match",
chooseJson: "Choose JSON file",
exportBackup: "Export guide data",
importBackup: "Import guide backup",
importedBackup: "Guide backup has been imported.",
exportBackup: "Export advisor data",
importBackup: "Import advisor backup",
importedBackup: "Advisor backup has been imported.",
demoDataWarning: "This import contains demo data. Do not use it as real user or health information.",
importStart: "Import a JSON file to start reviewing.",
saveName: "Save name",
deletePerson: "Delete user and reports",
deletePersonTitle: "Deletes this user, imported reports, follow-ups and guide notes after confirmation.",
deletePersonTitle: "Deletes this user, imported reports, follow-ups and advisor notes after confirmation.",
reportCount: "{count} reports",
personNote: "General note for user",
personNoteHelp: "This note applies to the person as a whole, not one specific report.",
@ -486,15 +486,15 @@ const en: TranslationTree = {
closeDetails: "Close details",
reportNote: "Report note",
followupNote: "Follow-up note",
noteHelp: "The guide note is stored locally and does not change imported report data.",
noteHelp: "The advisor note is stored locally and does not change imported report data.",
deleteNote: "Delete note",
validationError: "Validation error",
comment: "Comment",
evaluatorNote: "Guide note",
evaluatorNote: "Advisor note",
followupNoteBadge: "Follow-up note",
hasFollowup: "Has follow-up",
hasComment: "Has user comment",
hasEvaluatorNote: "Has guide note",
hasEvaluatorNote: "Has advisor note",
scoreValues: "Score values"
},
details: {

View File

@ -51,14 +51,6 @@ button {
padding: 10px 2px 14px;
}
.topbar-actions {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-end;
}
.topbar h1,
.field-head h2,
.filters h2,
@ -688,47 +680,6 @@ textarea {
white-space: nowrap;
}
.text-link.active {
background: #235b5e;
border-color: #235b5e;
color: #fff;
}
.demo-banner {
align-items: center;
background: #1f4f51;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 999px;
box-shadow: 0 12px 30px rgba(31, 79, 81, 0.22);
color: #fff;
display: inline-flex;
font-size: 0.78rem;
font-weight: 950;
gap: 0.5rem;
letter-spacing: 0;
padding: 0.42rem 0.46rem 0.42rem 0.74rem;
position: fixed;
right: max(12px, env(safe-area-inset-right));
top: max(12px, env(safe-area-inset-top));
z-index: 40;
}
.demo-banner button {
align-items: center;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.42);
border-radius: 999px;
color: #fff;
cursor: pointer;
display: inline-flex;
font: inherit;
height: 1.35rem;
justify-content: center;
line-height: 1;
padding: 0;
width: 1.35rem;
}
.demo-meta-pill {
background: #fbecd7;
color: #7d4118;
@ -1633,7 +1584,10 @@ textarea {
border: 1px solid #ccd7d4;
border-radius: 999px;
color: #235b5e;
cursor: pointer;
display: inline-flex;
font: inherit;
font-weight: 850;
gap: 0.4rem;
justify-content: center;
padding: 0.44rem 0.7rem;
@ -1653,6 +1607,19 @@ textarea {
transform: translateY(-1px);
}
.footer-link.active {
background: #235b5e;
border-color: #235b5e;
color: #fff;
}
.footer-link.active:hover,
.footer-link.active:focus-visible {
background: #1f4f51;
border-color: #1f4f51;
color: #fff;
}
.app-footer svg {
width: 14px;
height: 14px;