Files
Bernt 04e0a1e8ec
Guidad Felsökning / test-och-bygg (push) Has been cancelled
Guidad Felsökning / tjanster (push) Has been cancelled
Guidad Felsökning / terraform (push) Has been cancelled
Guidad Felsökning / leverantorskedja (push) Has been cancelled
Guidad Felsökning / aterstallning (push) Has been cancelled
Guidad Felsökning / publicera (push) Has been cancelled
Guidad Felsökning / driftsatt (push) Has been cancelled
ALVA deployment config
2026-08-08 13:12:13 +00:00

209 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ALVA-REP-0100 · Operational metrics.
//
// Vilka siffror som visas avgör vad organisationen optimerar. Det är
// därför det här är ett produktbeslut och inte ett rapportjobb.
//
// Uteslutna med avsikt:
//
// Ärenden per tekniker Belönar snabbhet över noggrannhet. Den som
// hoppar över kontroller vinner.
// Genomsnittlig ledtid Samma sak, en gång till. Ett svårt fel
// SKA ta längre tid.
// Inloggningar, aktivitet Mäter användning, inte nytta.
//
// Medtagna, och varför var och en är handlingsbar:
//
// VERIFIERINGSGRAD Andel avslut med fastställd orsak. Sjunker den
// gissar organisationen mer. Det här är det enda
// måttet ett försäkringsbolag egentligen bryr sig
// om.
// REPRODUKTIONSGRAD Andel symptom som återskapats. Låg siffra
// förutsäger återkommande fordon — man kan inte
// åtgärda det man inte sett.
// OMARBETNING Samma fordon tillbaka med samma orsakskategori.
// Det dyraste felet i en verkstad, och det enda
// ingen mäter.
// UNDANTAGSFREKVENS Vilka kontroller hoppas över, per steg. Ett
// steg med hög frekvens är antingen fel skrivet
// eller kräver utrustning som saknas — bägge är
// åtgärdbara, ingetdera syns i dag.
// EVIDENSPROFIL Fördelning E0E6. Visar om underlaget vilar på
// mätningar eller på observationer.
// FASFÖRDELNING Var tiden går i ALVA-termer.
//
// Alla funktioner är rena över (ärenden). Ingen databasåtkomst, ingen
// klocka — samma underlag ger samma siffra, vilket är förutsättningen
// för att en kvartalsrapport ska gå att granska i efterhand.
import { fasFor } from "./faser.mjs";
const handelserI = (arende) => (arende.handelser ?? []).map((p) => p.handelse ?? p);
const av = (handelser, typ) => handelser.filter((h) => h?.typ === typ);
const arAvslutat = (arende) => handelserI(arende).some((h) => h?.typ === "arende_avslutat");
/** Andel med en decimal, eller null när underlaget saknas. */
function andel(traffar, av_) {
return av_ === 0 ? null : Math.round((traffar / av_) * 1000) / 10;
}
/**
* Verifieringsgrad — andel avslutade ärenden där orsaken fastställdes.
*
* Att den inte är 100 % är friskt. En organisation som aldrig skriver
* "orsaken kunde inte fastställas" ljuger antingen, eller får bara enkla
* fel. Talet är intressant över tid, inte som absolut nivå.
*/
export function verifieringsgrad(arenden) {
const avslutade = arenden.filter(arAvslutat);
const fastställda = avslutade.filter((a) => {
const s = av(handelserI(a), "slutsats").at(-1);
return s && s.orsakFastställd !== false;
});
return { antal: avslutade.length, fastställda: fastställda.length, andel: andel(fastställda.length, avslutade.length) };
}
/** Reproduktionsgrad — andel ärenden där symptomet faktiskt återskapades. */
export function reproduktionsgrad(arenden) {
const medSvar = arenden.filter((a) => av(handelserI(a), "reproducering").length > 0);
const fördelning = { ja: 0, delvis: 0, nej: 0 };
for (const a of medSvar) {
const r = av(handelserI(a), "reproducering").at(-1);
if (r?.status in fördelning) fördelning[r.status] += 1;
}
return { antal: medSvar.length, ...fördelning, andel: andel(fördelning.ja, medSvar.length) };
}
/**
* Omarbetning — samma fordon tillbaka med samma orsakskategori.
*
* Det dyraste felet i en verkstad och det enda ingen mäter, eftersom det
* kräver att man kopplar ihop två ärenden som ser fristående ut. Här är
* kopplingen gratis: loggen bär fordonets identifierare och orsakens
* kategori.
*
* @param inomDagar fönster; 90 dagar är en rimlig utgångspunkt men
* varje verkstad har sin egen uppfattning.
*/
export function omarbetning(arenden, inomDagar = 90) {
const perFordon = new Map();
for (const a of arenden) {
if (!arAvslutat(a)) continue;
const ident = av(handelserI(a), "objekt_identifierat").at(-1)?.objekt?.identifierare;
if (!ident) continue;
const orsak = av(handelserI(a), "felorsak").at(-1);
const avslut = (a.handelser ?? []).find((p) => (p.handelse ?? p)?.typ === "arende_avslutat")?.tidpunkt;
if (!orsak || !avslut) continue;
if (!perFordon.has(ident)) perFordon.set(ident, []);
perFordon.get(ident).push({ tid: Date.parse(avslut), kategorier: orsak.orsaker ?? [], arende: a.id });
}
const fall = [];
const fönster = inomDagar * 86400000;
for (const [ident, poster] of perFordon) {
poster.sort((a, b) => a.tid - b.tid);
for (let i = 1; i < poster.length; i += 1) {
const föregående = poster[i - 1];
const nu = poster[i];
const delad = nu.kategorier.filter((k) => föregående.kategorier.includes(k));
if (delad.length > 0 && nu.tid - föregående.tid <= fönster) {
fall.push({ identifierare: ident, kategorier: delad, dagar: Math.round((nu.tid - föregående.tid) / 86400000) });
}
}
}
const avslutade = arenden.filter(arAvslutat).length;
return { antal: fall.length, andel: andel(fall.length, avslutade), fall: fall.slice(0, 20) };
}
/**
* Undantagsfrekvens per metodiksteg.
*
* Ett steg som ofta hoppas över är antingen felskrivet eller kräver
* utrustning verkstaden inte har. Bägge går att åtgärda — men bara om
* man vet vilket steg det gäller, vilket ingen gör i dag.
*/
export function undantagsfrekvens(arenden) {
const per = new Map();
for (const a of arenden) {
for (const h of av(handelserI(a), "kontroll_utford")) {
const nyckel = `${h.stegId}/${h.kontrollId}`;
if (!per.has(nyckel)) per.set(nyckel, { steg: h.stegId, kontroll: h.kontrollId, text: h.text, utforda: 0, undantag: 0 });
const rad = per.get(nyckel);
rad.utforda += 1;
if ((h.undantag ?? "").trim()) rad.undantag += 1;
}
}
return [...per.values()]
.map((r) => ({ ...r, fas: fasFor(r.steg), andel: andel(r.undantag, r.utforda) }))
.filter((r) => r.undantag > 0)
.sort((a, b) => b.andel - a.andel || b.undantag - a.undantag);
}
/** Evidensprofil — fördelning av starkaste evidenstyp per ärende. */
export function evidensprofil(arenden) {
const nivå = { E0: 0, E1: 0, E2: 0, E3: 0, E4: 0, E5: 0, E6: 0 };
for (const a of arenden) {
const h = handelserI(a);
const har = {
E5: av(h, "arbetsorder_skannad").length > 0,
// Ett mätvärde räknas som E4 bara med spårbart mätdon; utan det är
// det teknikerns observation av en siffra (QUALITY-AUDIT M-1).
E4: av(h, "matvarde").some((m) => m.matdonId),
E3: av(h, "video").length > 0,
E2: av(h, "foto").length > 0,
E1: av(h, "observation").length + av(h, "kontroll_utford").length > 0,
};
const källor = ["E2", "E3", "E4", "E5"].filter((n) => har[n]).length;
const högsta = källor >= 2 ? "E6" : (["E5", "E4", "E3", "E2", "E1"].find((n) => har[n]) ?? "E0");
nivå[högsta] += 1;
}
return nivå;
}
/** Fasfördelning — var tiden går, i ALVA-termer. */
export function fasfordelning(arenden) {
const per = { analysis: 0, localization: 0, verification: 0, action: 0 };
for (const a of arenden) {
for (const h of handelserI(a)) {
if (h?.stegId) per[fasFor(h.stegId)] += 1;
}
}
const summa = Object.values(per).reduce((s, v) => s + v, 0);
return Object.fromEntries(
Object.entries(per).map(([fas, antal]) => [fas, { antal, andel: andel(antal, summa) }]),
);
}
/** Orsakskategorier över flottan. */
export function orsakskategorier(arenden) {
const per = new Map();
for (const a of arenden) {
for (const h of av(handelserI(a), "felorsak")) {
for (const k of h.orsaker ?? []) per.set(k, (per.get(k) ?? 0) + 1);
}
}
return [...per.entries()]
.map(([kategori, antal]) => ({ kategori, antal }))
.sort((a, b) => b.antal - a.antal);
}
/**
* Hela underlaget för portalens analysvy och kvartalsrapporten.
*
* En enda funktion, så att skärmen och rapporten aldrig kan visa olika
* siffror för samma period — det är den vanligaste orsaken till att
* ingen litar på rapporten.
*/
export function oversikt(arenden, { omarbetningsfonster = 90 } = {}) {
return {
version: "ALVA-REP-0100",
antal: { totalt: arenden.length, avslutade: arenden.filter(arAvslutat).length },
verifiering: verifieringsgrad(arenden),
reproduktion: reproduktionsgrad(arenden),
omarbetning: omarbetning(arenden, omarbetningsfonster),
undantag: undantagsfrekvens(arenden).slice(0, 10),
evidens: evidensprofil(arenden),
faser: fasfordelning(arenden),
orsaker: orsakskategorier(arenden).slice(0, 12),
};
}