fcded6bd43
- Supabase-migration: felsokning_arenden + felsokning_handelser med RLS för inloggade och append-only garanterat i databasen (inga update/delete-rättigheter). Live Share förberedd via security definer-funktionen hamta_delat_arende(delningskod) så att anonyma läsare aldrig når tabellerna direkt; kategoribyten filtreras bort. Realtime-publicering aktiverad för händelsetabellen. - Synklager i klienten (synk.ts): deterministisk, konfliktfri ihopflätning av händelser per id (testad: dedupe, tidsordning, idempotens), push av lokala och pull av kollegors händelser var 15:e sekund. Utan inloggning: lokalt läge, status i ärendehuvudet. - Live Share-vy (/felsokning/dela/:id): skrivskyddad livevy med status ✔/🔄/⏳, bilder, mätvärdestabell, tidslinje och rekommenderat nästa steg. Varje ärende får slumpad delningskod vid skapande. - Ny metodik: Elsystem/strömförsörjning enligt relä-exemplet i visionen, med mätvärdeskrav på alla mätpunkter. Verifierat: 17 vitest-tester gröna, produktionsbygge ok, Playwright- röktest inkl. Live Share-vyn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
119 lines
4.0 KiB
TypeScript
119 lines
4.0 KiB
TypeScript
// Domänmodell för Guidad Felsökning.
|
|
// Händelseloggen är append-only och är ärendets enda sanningskälla.
|
|
// Allt annat (brief, tidrapport, kundrapport) är projektioner av loggen.
|
|
|
|
export type Tillforlitlighet = "hog" | "medel" | "lag";
|
|
|
|
export const TILLFORLITLIGHET_LABEL: Record<Tillforlitlighet, string> = {
|
|
hog: "🟢 Hög",
|
|
medel: "🟡 Medel",
|
|
lag: "🔴 Låg",
|
|
};
|
|
|
|
export type TidKategori =
|
|
| "aktiv_felsokning"
|
|
| "vantetid"
|
|
| "administration"
|
|
| "reservdelssokning"
|
|
| "provkorning"
|
|
| "kundkontakt"
|
|
| "paus";
|
|
|
|
export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
|
|
aktiv_felsokning: "Aktiv felsökning",
|
|
vantetid: "Väntetid",
|
|
administration: "Administration",
|
|
reservdelssokning: "Reservdelssökning",
|
|
provkorning: "Provkörning",
|
|
kundkontakt: "Kundkontakt",
|
|
paus: "Paus",
|
|
};
|
|
|
|
export interface Objekt {
|
|
typ: string; // t.ex. "Fordon", "Industrimaskin"
|
|
identifierare: string; // reg.nr, VIN, serienummer …
|
|
identifieringsmetod: string; // "Registreringsnummer", "VIN", "Serienummer", "Manuell inmatning"
|
|
beskrivning: string; // t.ex. "Volvo XC60 D4 2019"
|
|
kund?: string;
|
|
}
|
|
|
|
// Varje händelse är en av dessa typer. Ingen händelse ändras eller tas bort.
|
|
export type Handelse =
|
|
| { typ: "objekt_identifierat"; objekt: Objekt }
|
|
| { typ: "felbeskrivning"; text: string }
|
|
| { typ: "fraga_besvarad"; stegId: string; frageId: string; fraga: string; svar: string }
|
|
| { typ: "kontroll_utford"; stegId: string; kontrollId: string; text: string; resultat?: string }
|
|
| { typ: "observation"; text: string }
|
|
| { typ: "matvarde"; beskrivning: string; varde: string; enhet?: string }
|
|
| { typ: "hypotes"; text: string; niva: Exclude<Tillforlitlighet, "hog"> }
|
|
| { typ: "foto"; beskrivning: string; dataUrl: string }
|
|
| { typ: "kommentar"; text: string }
|
|
| { typ: "kategori_byte"; kategori: TidKategori }
|
|
| { typ: "inaktivitet_forklarad"; text: string; minuter: number }
|
|
| { typ: "overlamning"; fran: string; till?: string }
|
|
| { typ: "export_skapad"; format: string; version: number }
|
|
| { typ: "arende_avslutat" };
|
|
|
|
export interface LoggPost {
|
|
id: string;
|
|
tidpunkt: string; // ISO 8601
|
|
anvandare: string;
|
|
handelse: Handelse;
|
|
}
|
|
|
|
export interface Arende {
|
|
id: string;
|
|
nummer: number;
|
|
skapad: string; // ISO 8601
|
|
// Slumpad kod för Live Share-länken; sätts vid skapande och kan aldrig
|
|
// ändras (ärenderaden är append-only även i databasen).
|
|
delningskod?: string;
|
|
handelser: LoggPost[];
|
|
}
|
|
|
|
let raknare = 0;
|
|
|
|
export function nyLoggPost(anvandare: string, handelse: Handelse, tidpunkt?: string): LoggPost {
|
|
raknare += 1;
|
|
return {
|
|
id: `${Date.now().toString(36)}-${raknare.toString(36)}`,
|
|
tidpunkt: tidpunkt ?? new Date().toISOString(),
|
|
anvandare,
|
|
handelse,
|
|
};
|
|
}
|
|
|
|
export function handelseRubrik(post: LoggPost): string {
|
|
const h = post.handelse;
|
|
switch (h.typ) {
|
|
case "objekt_identifierat":
|
|
return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`;
|
|
case "felbeskrivning":
|
|
return `Felbeskrivning registrerad`;
|
|
case "fraga_besvarad":
|
|
return `${h.fraga} — ${h.svar}`;
|
|
case "kontroll_utford":
|
|
return h.resultat ? `${h.text} — ${h.resultat}` : `${h.text} — utförd`;
|
|
case "observation":
|
|
return `Observation: ${h.text}`;
|
|
case "matvarde":
|
|
return `Mätvärde: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`;
|
|
case "hypotes":
|
|
return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`;
|
|
case "foto":
|
|
return `Foto: ${h.beskrivning}`;
|
|
case "kommentar":
|
|
return h.text;
|
|
case "kategori_byte":
|
|
return `Arbetskategori: ${TIDKATEGORI_LABEL[h.kategori]}`;
|
|
case "inaktivitet_forklarad":
|
|
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
|
|
case "overlamning":
|
|
return h.till ? `Arbete överlämnat från ${h.fran} till ${h.till}` : `Arbete överlämnat av ${h.fran}`;
|
|
case "export_skapad":
|
|
return `Export skapad: ${h.format}, version ${h.version}`;
|
|
case "arende_avslutat":
|
|
return "Felsökning avslutad";
|
|
}
|
|
}
|