release 1.1.2 graph layout and work time
This commit is contained in:
parent
76ec48bfef
commit
4c351d7242
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "arbeidspuls",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "arbeidspuls",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.475.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "arbeidspuls",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = "arbeidspuls-v6";
|
||||
const CACHE_NAME = "arbeidspuls-v7";
|
||||
const ASSETS = ["/", "/index.html", "/manifest.webmanifest", "/icon.svg"];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
|
||||
638
src/App.tsx
638
src/App.tsx
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createLineSegments } from "./App";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildFollowupResponsePoints, buildWorkTimeBands, calculateFollowupEffectScore, createLineSegments, formatDateTime } from "./App";
|
||||
import { datePlaceholderForLanguage } from "./i18n";
|
||||
import type { DelayedFollowup, Report } from "./types";
|
||||
|
||||
const store = new Map<string, string>();
|
||||
|
||||
beforeEach(() => {
|
||||
store.clear();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => store.set(key, value),
|
||||
removeItem: (key: string) => store.delete(key),
|
||||
clear: () => store.clear()
|
||||
});
|
||||
});
|
||||
|
||||
describe("grafsegmenter", () => {
|
||||
it("lager striplet horisontal startlinje og stripler hull på minst tre dager", () => {
|
||||
@ -19,3 +33,80 @@ describe("grafsegmenter", () => {
|
||||
expect(segments[2]).toMatchObject({ x1: 40, y1: 40, x2: 100, y2: 20 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("oppfølgingsrespons", () => {
|
||||
const report: Report = {
|
||||
id: "rapport-1",
|
||||
report_time: "08:00:00",
|
||||
report_date: "2026-05-27",
|
||||
created_at: "2026-05-27T06:00:00.000Z",
|
||||
type: "work_report",
|
||||
work_ability: 4,
|
||||
energy_level: 4,
|
||||
mental_clarity: 4,
|
||||
symptom_burden: 2,
|
||||
effort_strain: 2,
|
||||
status: "kan_fortsette",
|
||||
physical_energy: 4,
|
||||
mental_energy: 4,
|
||||
total_score_percent: 80,
|
||||
task_types: [],
|
||||
main_limitations: [],
|
||||
helpful_accommodations: []
|
||||
};
|
||||
|
||||
const followup: DelayedFollowup = {
|
||||
id: "oppfolging-1",
|
||||
report_time: "10:00:00",
|
||||
report_date: "2026-05-28",
|
||||
created_at: "2026-05-28T08:00:00.000Z",
|
||||
type: "delayed_followup",
|
||||
parent_report_id: "rapport-1",
|
||||
worse_than_before: "mye",
|
||||
delayed_symptoms: [],
|
||||
recovery_status: "ikke_tilbake_til_baseline"
|
||||
};
|
||||
|
||||
it("beregner visualisert ettereffekt uten å endre parent-score", () => {
|
||||
expect(calculateFollowupEffectScore(80, "mye", "ikke_tilbake_til_baseline")).toBe(30);
|
||||
expect(calculateFollowupEffectScore(80, "nei", "tilbake_til_baseline")).toBe(85);
|
||||
});
|
||||
|
||||
it("lager responsmodell på faktisk oppfølgingstidspunkt med parent-kobling", () => {
|
||||
const [point] = buildFollowupResponsePoints([report, followup]);
|
||||
|
||||
expect(point.parent.id).toBe("rapport-1");
|
||||
expect(point.followup.id).toBe("oppfolging-1");
|
||||
expect(point.parentScore).toBe(80);
|
||||
expect(point.effectScore).toBe(30);
|
||||
expect(point.offsetHours).toBe(26);
|
||||
});
|
||||
|
||||
it("bruker engelsk ukedag når språk er engelsk", () => {
|
||||
localStorage.setItem("arbeidspuls.language", "en");
|
||||
|
||||
expect(formatDateTime(report)).toContain("Wednesday");
|
||||
|
||||
localStorage.setItem("arbeidspuls.language", "nb-NO");
|
||||
expect(formatDateTime(report)).toContain("Onsdag");
|
||||
});
|
||||
|
||||
it("lager arbeidstidsband og håndterer arbeidstid over midnatt", () => {
|
||||
const daytime = { ...report, id: "rapport-dag", work_start_time: "08:00", work_end_time: "10:00" };
|
||||
const overnight = { ...report, id: "rapport-natt", work_start_time: "23:00", work_end_time: "01:00" };
|
||||
const missing = { ...report, id: "rapport-uten", work_start_time: undefined, work_end_time: "12:00" };
|
||||
|
||||
const bands = buildWorkTimeBands([daytime, overnight, missing]);
|
||||
|
||||
expect(bands).toHaveLength(2);
|
||||
expect(bands[0].reportId).toBe("rapport-dag");
|
||||
expect(bands[0].endAt - bands[0].startAt).toBe(2 * 60 * 60 * 1000);
|
||||
expect(bands[1].reportId).toBe("rapport-natt");
|
||||
expect(bands[1].endAt - bands[1].startAt).toBe(2 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("har språktilpassede datoplaceholdere for graffilter", () => {
|
||||
expect(datePlaceholderForLanguage("nb-NO")).toBe("dd/mm/åååå");
|
||||
expect(datePlaceholderForLanguage("en")).toBe("dd/mm/yyyy");
|
||||
});
|
||||
});
|
||||
|
||||
58
src/i18n.tsx
58
src/i18n.tsx
@ -118,7 +118,17 @@ const nb = {
|
||||
saved: "Oppfølging lagret lokalt.",
|
||||
saveFollowup: "Lagre oppfølging",
|
||||
orphan: "Oppfølging uten valgt hovedrapport",
|
||||
worsePrefix: "verre"
|
||||
worsePrefix: "verre",
|
||||
latestOnly: "Oppfølging kan bare opprettes for nyeste rapport."
|
||||
},
|
||||
edit: {
|
||||
edit: "Rediger",
|
||||
reportIntro: "Rediger rapporten. Redigering er bare mulig innen 24 timer etter opprettelse.",
|
||||
followupIntro: "Rediger oppfølgingen. Redigering er bare mulig innen 24 timer etter opprettelse.",
|
||||
saveChanges: "Lagre endringer",
|
||||
saved: "Endringer lagret lokalt.",
|
||||
cancelEdit: "Avbryt redigering",
|
||||
expired: "Redigering er bare mulig innen 24 timer etter opprettelse."
|
||||
},
|
||||
overview: {
|
||||
noReports: "Ingen rapporter er lagret ennå.",
|
||||
@ -134,6 +144,9 @@ const nb = {
|
||||
dashedLine: "Striplet linje betyr at det er 3 dager eller mer mellom rapportene.",
|
||||
from: "Fra",
|
||||
to: "Til",
|
||||
fromDate: "Fra dato",
|
||||
toDate: "Til dato",
|
||||
datePlaceholder: "dd/mm/åååå",
|
||||
all: "Alt",
|
||||
week: "1 uke",
|
||||
month: "1 måned",
|
||||
@ -258,6 +271,7 @@ const nb = {
|
||||
followupId: "Oppfølgings-ID",
|
||||
connectedReport: "Tilknyttet rapport",
|
||||
created: "Opprettet",
|
||||
updated: "Oppdatert",
|
||||
reportDate: "Rapportdato",
|
||||
reportTime: "Rapporttid",
|
||||
integrityStatus: "Integritetsstatus",
|
||||
@ -292,7 +306,16 @@ const nb = {
|
||||
noPoint: "Ingen rapportpunkt valgt",
|
||||
noPointHelp: "Hold musepekeren nær et punkt for å se faktiske rapportverdier.",
|
||||
nearestReport: "Nærmeste rapport",
|
||||
chartLabel: "Fullskjerm trendgraf for rapporter"
|
||||
chartLabel: "Fullskjerm trendgraf for rapporter",
|
||||
workTime: "Arbeidstid",
|
||||
workTimeLegend: "Skyggefelt viser registrert arbeidstid",
|
||||
workTimeTooltip: "Registrert arbeidstid",
|
||||
followupResponse: "Oppfølgingsrespons",
|
||||
followupResponseHelp: "Viser oppfølginger på faktisk tidspunkt og visualiserer ettereffekt etter arbeidsøkten.",
|
||||
followupResponseDisclaimer: "Dette er en visuell ettereffektindikator, ikke en ny totalscore.",
|
||||
noFollowups: "Ingen oppfølginger for valgt rapport.",
|
||||
followupDelay: "Forsinkelse: {hours} timer",
|
||||
followupEffect: "Ettereffekt: {score}% fra rapportens {parent}%"
|
||||
},
|
||||
scoring: {
|
||||
disclaimer:
|
||||
@ -397,7 +420,17 @@ const en: TranslationTree = {
|
||||
saved: "Follow-up saved locally.",
|
||||
saveFollowup: "Save follow-up",
|
||||
orphan: "Follow-up without selected main report",
|
||||
worsePrefix: "worse"
|
||||
worsePrefix: "worse",
|
||||
latestOnly: "Follow-up can only be created for the latest report."
|
||||
},
|
||||
edit: {
|
||||
edit: "Edit",
|
||||
reportIntro: "Edit the report. Editing is only available within 24 hours after creation.",
|
||||
followupIntro: "Edit the follow-up. Editing is only available within 24 hours after creation.",
|
||||
saveChanges: "Save changes",
|
||||
saved: "Changes saved locally.",
|
||||
cancelEdit: "Cancel editing",
|
||||
expired: "Editing is only available within 24 hours after creation."
|
||||
},
|
||||
overview: {
|
||||
noReports: "No reports have been saved yet.",
|
||||
@ -413,6 +446,9 @@ const en: TranslationTree = {
|
||||
dashedLine: "A dashed line means there are 3 days or more between reports.",
|
||||
from: "From",
|
||||
to: "To",
|
||||
fromDate: "From date",
|
||||
toDate: "To date",
|
||||
datePlaceholder: "dd/mm/yyyy",
|
||||
all: "All",
|
||||
week: "1 week",
|
||||
month: "1 month",
|
||||
@ -536,6 +572,7 @@ const en: TranslationTree = {
|
||||
followupId: "Follow-up ID",
|
||||
connectedReport: "Connected report",
|
||||
created: "Created",
|
||||
updated: "Updated",
|
||||
reportDate: "Report date",
|
||||
reportTime: "Report time",
|
||||
integrityStatus: "Integrity status",
|
||||
@ -570,7 +607,16 @@ const en: TranslationTree = {
|
||||
noPoint: "No report point selected",
|
||||
noPointHelp: "Hover near a point to see actual report values.",
|
||||
nearestReport: "Nearest report",
|
||||
chartLabel: "Fullscreen trend chart for reports"
|
||||
chartLabel: "Fullscreen trend chart for reports",
|
||||
workTime: "Work time",
|
||||
workTimeLegend: "Shaded areas show registered work time",
|
||||
workTimeTooltip: "Registered work time",
|
||||
followupResponse: "Follow-up response",
|
||||
followupResponseHelp: "Shows follow-ups at their actual time and visualizes after-effect from the work session.",
|
||||
followupResponseDisclaimer: "This is a visual after-effect indicator, not a new total score.",
|
||||
noFollowups: "No follow-ups for the selected report.",
|
||||
followupDelay: "Delay: {hours} hours",
|
||||
followupEffect: "After-effect: {score}% from report {parent}%"
|
||||
},
|
||||
scoring: {
|
||||
disclaimer:
|
||||
@ -795,6 +841,10 @@ export function translateScoreBandDescription(band: { label: string; description
|
||||
return key ? descriptions[key] : band.description;
|
||||
}
|
||||
|
||||
export function datePlaceholderForLanguage(language: Language) {
|
||||
return readTranslation(language, "overview.datePlaceholder");
|
||||
}
|
||||
|
||||
function readStoredLanguage(): Language {
|
||||
const stored = localStorage.getItem(LANGUAGE_KEY);
|
||||
return stored === "en" || stored === "nb-NO" ? stored : "nb-NO";
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { exportReports, importReportsFromJson, listReports, previewImportReports, setDemoModeEnabled } from "./storage";
|
||||
import type { ExportPayload, Report } from "./types";
|
||||
import {
|
||||
createFollowup,
|
||||
exportReports,
|
||||
importReportsFromJson,
|
||||
listReports,
|
||||
previewImportReports,
|
||||
setDemoModeEnabled,
|
||||
updateFollowup,
|
||||
updateReport
|
||||
} from "./storage";
|
||||
import type { DelayedFollowup, ExportPayload, Report } from "./types";
|
||||
|
||||
const store = new Map<string, string>();
|
||||
|
||||
@ -88,4 +97,105 @@ describe("ordinær brukerimport", () => {
|
||||
expect(listReports(true).some((entry) => entry.id === "demo-arbeidspuls-test" && entry.demo)).toBe(true);
|
||||
expect(listReports()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("tillater redigering innen 24 timer og beholder ID", () => {
|
||||
const fresh = { ...report, created_at: new Date().toISOString() };
|
||||
importReportsFromJson(JSON.stringify(payload([fresh])));
|
||||
|
||||
const updated = updateReport(fresh.id, {
|
||||
workplace: "Nytt sted",
|
||||
work_ability: 5,
|
||||
energy_level: 3,
|
||||
mental_clarity: 4,
|
||||
symptom_burden: 2,
|
||||
effort_strain: 3,
|
||||
status: "kan_fortsette",
|
||||
task_types: [],
|
||||
main_limitations: [],
|
||||
helpful_accommodations: [],
|
||||
note: "Oppdatert"
|
||||
});
|
||||
|
||||
expect(updated.id).toBe(fresh.id);
|
||||
expect(updated.created_at).toBe(fresh.created_at);
|
||||
expect(updated.updated_at).toBeTruthy();
|
||||
expect(updated.integrity?.verification_status).toBe("valid");
|
||||
const stored = listReports()[0];
|
||||
expect(stored.type).toBe("work_report");
|
||||
expect(stored.type === "work_report" ? stored.work_ability : null).toBe(5);
|
||||
});
|
||||
|
||||
it("blokkerer redigering etter 24 timer", () => {
|
||||
const old = { ...report, created_at: "2020-01-01T00:00:00.000Z" };
|
||||
importReportsFromJson(JSON.stringify(payload([old])));
|
||||
|
||||
expect(() =>
|
||||
updateReport(old.id, {
|
||||
work_ability: 5,
|
||||
energy_level: 3,
|
||||
mental_clarity: 4,
|
||||
symptom_burden: 2,
|
||||
effort_strain: 3,
|
||||
status: "kan_fortsette",
|
||||
task_types: [],
|
||||
main_limitations: [],
|
||||
helpful_accommodations: [],
|
||||
note: ""
|
||||
})
|
||||
).toThrow("24 timer");
|
||||
});
|
||||
|
||||
it("tillater bare nyeste rapport som parent for ny oppfølging", () => {
|
||||
const older = { ...report, id: "rapport-eldre", created_at: "2026-05-26T06:00:00.000Z", report_date: "2026-05-26" };
|
||||
const newer = { ...report, id: "rapport-nyere", created_at: "2026-05-28T06:00:00.000Z", report_date: "2026-05-28" };
|
||||
importReportsFromJson(JSON.stringify(payload([older, newer])));
|
||||
|
||||
expect(() =>
|
||||
createFollowup({
|
||||
parent_report_id: older.id,
|
||||
related_report_id: older.id,
|
||||
worse_than_before: "litt",
|
||||
delayed_symptoms: [],
|
||||
note: ""
|
||||
})
|
||||
).toThrow("nyeste rapport");
|
||||
|
||||
const saved = createFollowup({
|
||||
parent_report_id: newer.id,
|
||||
related_report_id: newer.id,
|
||||
worse_than_before: "litt",
|
||||
delayed_symptoms: [],
|
||||
note: ""
|
||||
});
|
||||
expect(saved.parent_report_id).toBe(newer.id);
|
||||
});
|
||||
|
||||
it("redigerer oppfølging innen 24 timer uten å endre ID", () => {
|
||||
const freshReport = { ...report, id: "rapport-fersk", created_at: new Date().toISOString(), report_date: "2026-05-29" };
|
||||
const followup: DelayedFollowup = {
|
||||
id: "oppfolging-fersk",
|
||||
created_at: new Date().toISOString(),
|
||||
report_date: "2026-05-29",
|
||||
report_time: "10:00:00",
|
||||
type: "delayed_followup",
|
||||
parent_report_id: freshReport.id,
|
||||
worse_than_before: "litt",
|
||||
delayed_symptoms: [],
|
||||
note: "før"
|
||||
};
|
||||
importReportsFromJson(JSON.stringify({ ...payload([freshReport]), reports: [freshReport, followup] }));
|
||||
|
||||
const updated = updateFollowup(followup.id, {
|
||||
parent_report_id: freshReport.id,
|
||||
related_report_id: freshReport.id,
|
||||
worse_than_before: "mye",
|
||||
delayed_symptoms: ["fatigue"],
|
||||
note: "etter"
|
||||
});
|
||||
|
||||
expect(updated.id).toBe(followup.id);
|
||||
expect(updated.created_at).toBe(followup.created_at);
|
||||
expect(updated.worse_than_before).toBe("mye");
|
||||
expect(updated.integrity?.verification_status).toBe("valid");
|
||||
});
|
||||
});
|
||||
|
||||
@ -127,6 +127,7 @@ export function createFollowup(draft: FollowupDraft, demoMode = false): DelayedF
|
||||
if (!draft.worse_than_before) {
|
||||
throw new Error("Oppfølgingen mangler svar på om du er verre enn før økten.");
|
||||
}
|
||||
assertLatestParentIfProvided(draft.parent_report_id || draft.related_report_id, demoMode);
|
||||
|
||||
const followup: DelayedFollowup = {
|
||||
...baseFields(),
|
||||
@ -145,6 +146,83 @@ export function createFollowup(draft: FollowupDraft, demoMode = false): DelayedF
|
||||
return signedFollowup;
|
||||
}
|
||||
|
||||
export function updateReport(id: string, draft: ReportDraft, demoMode = false): Report {
|
||||
const mode = modeFromDemoFlag(demoMode);
|
||||
const entries = readEntries(mode);
|
||||
const existing = entries.find((entry): entry is Report => entry.id === id && entry.type === "work_report");
|
||||
if (!existing) throw new Error("Rapporten finnes ikke.");
|
||||
assertEditable(existing);
|
||||
if (
|
||||
!draft.work_ability ||
|
||||
!draft.energy_level ||
|
||||
!draft.mental_clarity ||
|
||||
!draft.symptom_burden ||
|
||||
!draft.effort_strain ||
|
||||
!draft.status
|
||||
) {
|
||||
throw new Error("Rapporten mangler påkrevde felt.");
|
||||
}
|
||||
const updated: Report = {
|
||||
...existing,
|
||||
workplace: draft.workplace?.trim() || undefined,
|
||||
work_start_time: draft.work_start_time || undefined,
|
||||
work_end_time: draft.work_end_time || undefined,
|
||||
work_ability: draft.work_ability,
|
||||
energy_level: draft.energy_level,
|
||||
mental_clarity: draft.mental_clarity,
|
||||
symptom_burden: draft.symptom_burden,
|
||||
effort_strain: draft.effort_strain,
|
||||
status: draft.status,
|
||||
physical_energy: draft.physical_energy_detail ?? draft.energy_level,
|
||||
mental_energy: draft.mental_energy_detail ?? draft.energy_level,
|
||||
physical_energy_detail: draft.physical_energy_detail,
|
||||
mental_energy_detail: draft.mental_energy_detail,
|
||||
perceived_productivity: draft.perceived_productivity,
|
||||
task_completion: draft.task_completion,
|
||||
task_types: draft.task_types,
|
||||
main_limitations: draft.main_limitations,
|
||||
helpful_accommodations: draft.helpful_accommodations,
|
||||
note: draft.note.trim() || undefined,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
updated.total_score_percent = calculateReportScore(updated) ?? undefined;
|
||||
const signed = demoMode ? markDemoEntry(attachIntegrity(updated)) as Report : attachIntegrity(stripDemoMarker(updated));
|
||||
writeEntries(entries.map((entry) => (entry.id === id ? signed : entry)).sort((a, b) => getEntryTime(b) - getEntryTime(a)), mode);
|
||||
return signed;
|
||||
}
|
||||
|
||||
export function updateFollowup(id: string, draft: FollowupDraft, demoMode = false): DelayedFollowup {
|
||||
const mode = modeFromDemoFlag(demoMode);
|
||||
const entries = readEntries(mode);
|
||||
const existing = entries.find((entry): entry is DelayedFollowup => entry.id === id && entry.type === "delayed_followup");
|
||||
if (!existing) throw new Error("Oppfølgingen finnes ikke.");
|
||||
assertEditable(existing);
|
||||
if (!draft.worse_than_before) throw new Error("Oppfølgingen mangler svar på om du er verre enn før økten.");
|
||||
const parentId = draft.parent_report_id || draft.related_report_id || undefined;
|
||||
if (parentId !== (existing.parent_report_id ?? existing.related_report_id)) {
|
||||
assertLatestParentIfProvided(parentId, demoMode);
|
||||
}
|
||||
const updated: DelayedFollowup = {
|
||||
...existing,
|
||||
parent_report_id: parentId,
|
||||
related_report_id: parentId,
|
||||
worse_than_before: draft.worse_than_before,
|
||||
delayed_symptoms: draft.delayed_symptoms,
|
||||
recovery_status: draft.recovery_status,
|
||||
note: draft.note.trim() || undefined,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
const signed = demoMode ? markDemoEntry(attachIntegrity(updated)) as DelayedFollowup : attachIntegrity(stripDemoMarker(updated));
|
||||
writeEntries(entries.map((entry) => (entry.id === id ? signed : entry)).sort((a, b) => getEntryTime(b) - getEntryTime(a)), mode);
|
||||
return signed;
|
||||
}
|
||||
|
||||
export function isEditableWithin24Hours(entry: StoredEntry, now = new Date()) {
|
||||
const created = new Date(entry.created_at).getTime();
|
||||
if (!Number.isFinite(created)) return false;
|
||||
return now.getTime() - created <= 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
export function listReports(demoMode = false): StoredEntry[] {
|
||||
if (demoMode) ensureDemoData();
|
||||
return readEntries(modeFromDemoFlag(demoMode)).sort((a, b) => getEntryTime(b) - getEntryTime(a));
|
||||
@ -154,6 +232,12 @@ export function getLastWorkReport(demoMode = false): Report | undefined {
|
||||
return listReports(demoMode).find((entry): entry is Report => entry.type === "work_report");
|
||||
}
|
||||
|
||||
export function getLatestWorkReport(demoMode = false): Report | undefined {
|
||||
return listReports(demoMode)
|
||||
.filter((entry): entry is Report => entry.type === "work_report")
|
||||
.sort((a, b) => getEntryTime(b) - getEntryTime(a))[0];
|
||||
}
|
||||
|
||||
export function getReport(id: string, demoMode = false): StoredEntry | undefined {
|
||||
return readEntries(modeFromDemoFlag(demoMode)).find((entry) => entry.id === id);
|
||||
}
|
||||
@ -312,6 +396,20 @@ function normalizeStoredEntry(entry: StoredEntry): StoredEntry {
|
||||
};
|
||||
}
|
||||
|
||||
function assertEditable(entry: StoredEntry) {
|
||||
if (!isEditableWithin24Hours(entry)) {
|
||||
throw new Error("Redigering er bare mulig innen 24 timer etter opprettelse.");
|
||||
}
|
||||
}
|
||||
|
||||
function assertLatestParentIfProvided(parentId: string | undefined, demoMode: boolean) {
|
||||
if (!parentId) return;
|
||||
const latest = getLatestWorkReport(demoMode);
|
||||
if (latest && latest.id !== parentId) {
|
||||
throw new Error("Oppfølging kan bare opprettes for nyeste rapport.");
|
||||
}
|
||||
}
|
||||
|
||||
export function isDemoEntry(entry: Partial<StoredEntry>) {
|
||||
return entry.demo === true || typeof entry.id === "string" && entry.id.startsWith("demo-arbeidspuls");
|
||||
}
|
||||
|
||||
155
src/styles.css
155
src/styles.css
@ -788,6 +788,12 @@ textarea {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.date-range small {
|
||||
color: #65706e;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.trend-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@ -1055,17 +1061,41 @@ textarea {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.fullscreen-graph-body {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.fullscreen-graph-help {
|
||||
min-height: 1.4rem;
|
||||
}
|
||||
|
||||
.graph-inspection-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 240px;
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.graph-workspace {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-rows: minmax(420px, min(58vh, 620px)) minmax(96px, auto);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fullscreen-graph-chart-shell {
|
||||
min-height: 420px;
|
||||
height: min(58vh, 620px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fullscreen-chart {
|
||||
width: 100%;
|
||||
min-height: 380px;
|
||||
max-height: calc(100vh - 310px);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
border: 1px solid #e4e8e5;
|
||||
border-radius: 8px;
|
||||
background: #fffefa;
|
||||
@ -1125,6 +1155,103 @@ textarea {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.graph-followups {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-top: 1px solid #d9ded8;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.graph-followups h4,
|
||||
.graph-followups p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.graph-followups h4 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.graph-followups div {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
border: 1px solid #d9ded8;
|
||||
border-radius: 8px;
|
||||
background: #fffefa;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.graph-followups span {
|
||||
color: #4d5b58;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.graph-selected-pills {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-height: 96px;
|
||||
contain: layout;
|
||||
}
|
||||
|
||||
.graph-selected-pills div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.graph-selected-pills strong {
|
||||
color: #35413f;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.graph-selected-pills span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.graph-status-pill {
|
||||
align-items: center;
|
||||
border: 1px solid #cdd8d5;
|
||||
border-radius: 999px;
|
||||
background: #fffefa;
|
||||
color: #235b5e;
|
||||
display: inline-flex;
|
||||
font-style: normal;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 850;
|
||||
justify-content: center;
|
||||
line-height: 1.15;
|
||||
min-height: 1.6rem;
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.followup-link {
|
||||
stroke: #b2642a;
|
||||
stroke-dasharray: 5 5;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.followup-effect-bar {
|
||||
stroke: #b2642a;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.followup-response-point {
|
||||
fill: #fffefa;
|
||||
stroke: #b2642a;
|
||||
stroke-width: 2.4;
|
||||
}
|
||||
|
||||
.work-time-band {
|
||||
fill: #b2642a;
|
||||
opacity: 0.12;
|
||||
stroke: #b2642a;
|
||||
stroke-opacity: 0.16;
|
||||
}
|
||||
|
||||
.confirm-modal {
|
||||
width: min(420px, 100%);
|
||||
display: grid;
|
||||
@ -1162,6 +1289,16 @@ textarea {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.edit-zone {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
border: 1px solid #cdd8d5;
|
||||
border-radius: 8px;
|
||||
background: #f7faf8;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.danger-zone h3,
|
||||
.danger-zone p {
|
||||
margin: 0;
|
||||
@ -1786,8 +1923,16 @@ textarea {
|
||||
}
|
||||
|
||||
.fullscreen-chart {
|
||||
min-height: 300px;
|
||||
max-height: none;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.graph-workspace,
|
||||
.fullscreen-graph-chart-shell {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.graph-workspace {
|
||||
grid-template-rows: minmax(320px, 48vh) minmax(96px, auto);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +77,7 @@ export interface Report {
|
||||
report_time: string;
|
||||
report_date: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
type: "work_report";
|
||||
workplace?: string;
|
||||
work_start_time?: string;
|
||||
@ -127,6 +128,7 @@ export interface DelayedFollowup {
|
||||
report_time: string;
|
||||
report_date: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
type: "delayed_followup";
|
||||
parent_report_id?: string;
|
||||
related_report_id?: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user