Master Prompt v2.0 + röstinmatning, verifierade checklistor och export

Dokumentation:
- MASTER-PROMPT.md uppdaterad till v2.0 (SaaS/AI-wrapper, roller,
  multi-tenant, onboarding, beta-fokus).
- Nya moduler: kommunikationsmodell (tal in/text ut, Push-to-Talk,
  redigering före skick), Live Share (behörighetsstyrd realtidslänk,
  versionsmärkta exporter), verifierade checklistor (bevis och kontext
  per kontrolltyp).

Implementation i MVP:t:
- Push-to-Talk-röstinmatning via webbläsarens taligenkänning (sv-SE):
  lyssnar endast efter aktivt tryck, röd indikator med realtids-
  transkript, texten hamnar i redigerbart fält och skickas aldrig
  automatiskt. Döljs i webbläsare utan talstöd.
- Verifierade checklistor: varje kontroll i metodikerna har minimikrav
  (foto/mätvärde/kommentar); fotokontroller verifieras med bild och
  mätkontroller kan inte markeras verifierade utan värde.
- Versionsmärkt JSON-export ur händelseloggen; exporten loggas själv
  som händelse med format, version och användare.

Verifierat: 12 vitest-tester gröna, produktionsbygge ok, utökat
Playwright-röktest genom hela flödet inkl. fotokravet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-02 22:40:47 +00:00
parent 92fa8d6716
commit 7b6619133f
13 changed files with 600 additions and 210 deletions
+105 -171
View File
@@ -1,237 +1,171 @@
# Guidad Felsökning Master Prompt v1.0 # Guidad Felsökning Master Prompt v2.0
Det här är ett produktdirektiv, inte en teknisk specifikation ett styrdokument som ett utvecklingsteam kan använda från dag ett. Visionen och de resonemang som ligger bakom finns i [VISION.md](VISION.md). **Production Ready AI Wrapper Platform (MVP/Beta)**
Det här är ett produktdirektiv, inte en teknisk specifikation. Visionen och resonemangen bakom finns i [VISION.md](VISION.md); v1.0 finns i versionshistoriken. Detaljerade modulspecifikationer: [kommunikationsmodell (röst/PTT)](moduler/kommunikationsmodell.md), [Live Share](moduler/live-share.md), [verifierade checklistor](moduler/verifierade-checklistor.md), [ärendebrief](moduler/arendebrief.md), [arbetslogg & tidredovisning](moduler/arbetslogg-och-tidredovisning.md), [kundrapport](moduler/kundrapport.md).
--- ---
## Projektmål ## Projekt
Bygg en produktionsklar SaaS-plattform med namnet **Guidad Felsökning**. Bygg en produktionsredo SaaS-plattform med namnet **Guidad Felsökning**.
Plattformen är ett professionellt arbetsverktyg för mekaniker, servicetekniker och felsökare inom alla tekniska områden där strukturerad diagnostik används. Plattformen är en AI-wrapper ovanpå en LLM-leverantörs API och fungerar som ett professionellt arbetsverktyg för mekaniker och servicetekniker. Den ska inte ersätta teknisk kompetens eller tillverkarens dokumentation, utan vägleda användaren genom en strukturerad felsökningsprocess, dokumentera allt arbete och skapa full spårbarhet.
Exempel: Målet är att lansera en stabil, enkel och köpvärdig beta-version.
- Fordon
- Lastbilar
- Bussar
- Entreprenadmaskiner
- Lantbruksmaskiner
- Kompressorer
- Generatorer
- Industrimaskiner
- Marin utrustning
- Elsystem
- Hydraulik
- Pneumatik
- Automation
Systemet ska fungera som en digital diagnostikhandledare. Det ska inte ersätta teknikerens kompetens eller fatta beslut åt användaren, utan metodiskt vägleda genom en dokumenterad felsökningsprocess.
--- ---
## Designfilosofi ## Produktfilosofi
Designen ska kännas som ett professionellt fabriksverktyg. Produkten ska kännas som ett verktyg från en stor industrileverantör: enkel, stabil, extremt snabb, professionell, förutsägbar, tydlig, minimalistisk.
Målbild: Ingen "AI-leksak". Ingen onödig design. Inga experimentella funktioner. Allting ska kännas robust.
- enkel
- robust
- extremt tydlig
- inga onödiga animationer
- inga sociala funktioner
- inga distraktioner
- stora knappar
- hög kontrast
- optimerad för arbete med handskar
- fullt stöd för mobil, surfplatta och dator
Varje skärm ska ha ett tydligt syfte och minimera antalet val.
--- ---
## Arbetsflöde ## Roller
1. Identifiera objektet. ### Systemadministratör
2. Starta arbetsloggen.
3. Beskriv felet. Normalt en eller flera personer hos kunden. Behörigheter: hantera organisation, API-nycklar, integrationer, skapa/ta bort användare, roller, behörigheter, export, säkerhetsinställningar, fakturering, loggar.
4. AI leder användaren genom verifierbara kontroller.
5. Bilder, video och mätvärden dokumenteras. ### Tekniker
6. Alla aktiviteter tidsstämplas.
7. Rapport genereras. Kan skapa ärenden, fortsätta ärenden, ta över ärenden, skriva, prata, fotografera, filma, mäta, exportera rapport.
8. Rapport exporteras eller delas via API.
### Arbetsledare
Kan dessutom se alla ärenden, omfördela ärenden, följa status, läsa rapporter, skapa statistik.
--- ---
## Kärnfunktioner ## Multi-tenant
- Samtalsbaserad guidning (text och röst) Varje kund är en egen tenant med egna användare, API-nycklar, integrationer, ärenden, databaslogik och säkerhet. **Ingen data får blandas mellan kunder.**
- Bildanalys
- OCR
- Objektidentifiering
- Automatisk arbetslogg
- Tidrapportering
- Fotodokumentation
- Videostöd
- Mätvärdeslogg
- Checklista
- Kundrapport
- API-export
- Integration med affärssystem
- Integration med tillverkarinformation via användarens egna behörigheter
- Rollbaserad behörighet
- Offline-läge med synkronisering
--- ---
## AI-principer ## Enkel onboarding
AI får aldrig gissa. Första gången en kund loggar in:
Alla rekommendationer ska tydligt skilja mellan: 1. Skapa företag
2. Lägg till logotyp
3. Lägg till användare
4. Lägg till API-nyckel för AI-tjänsten
5. Lägg till eventuella integrationer
- observation Klart. Hela onboarding ska ta mindre än fem minuter.
- verifierat faktum
- hypotes
- rekommenderat nästa steg
Varje svar ska ha en tydlig tillförlitlighetsnivå.
AI ska vara konsekvent, kortfattad och metodisk.
--- ---
## Dokumentation ## Dashboard
Varje aktivitet ska lagras. Visa endast det viktigaste: Mina ärenden · Pågående · Väntar · Klara · Starta nytt ärende.
Exempel:
- tid
- användare
- GPS (om aktiverat)
- objekt
- bilder
- video
- mätvärden
- användarens kommentarer
- AI:s rekommendation
- resultat
Loggen ska vara revisionssäker, vilket innebär att historik bevaras och ändringar kan spåras.
--- ---
## API-first ## Nytt ärende
Hela plattformen byggs API-first. Identifiera objekt genom registreringsnummer, VIN, maskinnummer, QR, streckkod, OCR, foto eller manuell identifiering. Objektet verifieras innan felsökning startar.
Alla objekt ska kunna:
- hämtas
- uppdateras
- exporteras
- integreras
Dokumentationen ska vara komplett (exempelvis OpenAPI/Swagger) så att externa system enkelt kan anslutas.
--- ---
## Infrastruktur ## AI-guidning
Bygg för hög tillgänglighet och skalbarhet. Systemet arbetar stegvis. Inte långa svar. En kontroll åt gången:
Exempel på målarkitektur: > Kontrollera säkring F24. → Användaren svarar. → AI går vidare.
- AWS ### AI-regler
- Kubernetes
- Docker AI får aldrig hitta på fakta, låtsas veta eller gissa. Den ska skilja på **Observation**, **Verifierat**, **Hypotes** och **Rekommendation**. Alla svar ska ha tydlig tillförlitlighet.
- PostgreSQL
- Redis
- Objektlagring för bilder och dokument
- CDN
- Lastbalansering
- Automatisk skalning
- Övervakning och loggning
- Dagliga säkerhetskopior
- Katastrofåterställning
- Kryptering av data under överföring och lagring
--- ---
## Säkerhet ## Kommunikationsmodell
Systemet ska vara lämpligt för professionella verksamheter. **Tal in, text ut.** All röstinmatning sker via tal-till-text enligt Push-to-Talk — ingen bakgrundslyssning, ingen röstagent, aldrig automatiskt skick. Transkriberingen är alltid redigerbar innan den sparas i arbetsloggen. Se [modulen](moduler/kommunikationsmodell.md) för fullständig specifikation.
Prioritera: ---
- stark autentisering ## Kamerastöd
- rollbaserad åtkomst
- API-nycklar och OAuth där det passar Foto, video, OCR, bildanalys och objektidentifiering: däck, typskyltar, serienummer, skyltar, komponenter, mätinstrument.
- detaljerad revisionslogg
- principen om minsta behörighet ---
- säker nyckelhantering
- möjlighet att uppfylla krav enligt relevanta regelverk (t.ex. GDPR) ## Arbetslogg
Allt loggas: tid, användare, objekt, kommentar, foto, video, mätvärde, AI-fråga, AI-svar, resultat. Ingenting får försvinna. Loggen ska vara revisionssäker.
---
## Tidrapportering
När objektet identifierats startar arbetstiden. Vid längre inaktivitet ber systemet om en kort beskrivning av vad som gjorts. Slutrapporten visar total tid fördelad på moment (provkörning, diagnos, administration …).
---
## Ärendebrief
AI håller alltid en levande sammanfattning. När en annan tekniker öppnar ärendet visas automatiskt: vad kunden beskriver, vad som gjorts, vad som verifierats, vad som återstår, rekommenderade nästa steg och total arbetstid.
---
## Verifierade checklistor
En kontrollpunkt är inte slutförd enbart genom en kryssruta — varje kontroll samlar bevis och kontext (observation, mätvärde, foto) med minimikrav anpassade efter kontrolltyp. Se [modulen](moduler/verifierade-checklistor.md).
---
## Samarbete
Flera tekniker kan arbeta samtidigt. Alla ser bilder, filmer, mätningar, anteckningar, AI-sammanfattning, status och rekommendationer.
---
## Kundrapport och Live Share
Kundrapporten genereras automatiskt (objekt, felbeskrivning, bilder, tester, mätvärden, utförda kontroller, tid, rekommendation, nästa steg) och delas som PDF, länk eller API. Varje ärende kan dessutom publiceras via en säker, behörighetsstyrd delningslänk som uppdateras i realtid — se [Live Share-modulen](moduler/live-share.md). Alla exporter versionsmärks (version, datum, tid, vem, format).
---
## API First
Bygg hela systemet API-first. Alla resurser ska kunna skapas, läsas, uppdateras, exporteras och integreras. Dokumentera API:erna med OpenAPI/Swagger.
--- ---
## Integrationer ## Integrationer
Systemet ska kunna integreras med: Förbered integrationsramverk för DMS, ERP, CRM, elektroniska serviceböcker, tidredovisning, fakturering, reservdelssystem och tillverkarsystem via kundens egna behörigheter. Modulär integrationsarkitektur så att nya integrationer läggs till utan att påverka kärnplattformen.
- DMS-system
- ERP-system
- CRM-system
- elektroniska serviceböcker
- garantihantering
- fakturering
- tidredovisning
- reservdelssystem
- tillverkarnas informationssystem, när användaren har giltig behörighet och integration finns tillgänglig
--- ---
## Kundrapport ## Infrastruktur
Efter varje avslutat arbete ska systemet kunna generera en professionell rapport med: Bygg för produktion. Exempel på målarkitektur: AWS, Kubernetes, Docker, PostgreSQL, Redis, objektlagring, CDN, automatisk skalning, lastbalansering, backup, central loggning, övervakning, CI/CD, Infrastructure as Code.
- identifierat objekt
- felbeskrivning
- utförda kontroller
- bilder
- mätvärden
- provkörning
- rekommenderade nästa steg
- total tidsåtgång
- komplett arbetslogg
Rapporten ska kunna delas via länk, PDF eller API.
--- ---
## Affärsmål ## Säkerhet
Produkten ska ge värde på flera nivåer: Rollbaserad åtkomst, kryptering i vila och under överföring, säker API-autentisering, revisionsloggar, principen om minsta behörighet, säker hantering av API-nycklar och hemligheter. Utforma systemet så att det kan uppfylla relevanta krav, exempelvis GDPR, beroende på hur kunden använder tjänsten.
- Teknikern får metodstöd och mindre administration.
- Verkstaden får bättre kvalitet, spårbarhet och debiteringsunderlag.
- Kunden får insyn i vad som faktiskt utförts.
- Organisationen bygger över tid en sökbar kunskapsbas.
- I framtiden kan anonymiserade och aggregerade data användas för trendanalyser, förutsatt att detta sker i enlighet med avtal och tillämpliga regler.
--- ---
## MVP-avgränsning ## Beta-fokus
Det här är en produkt som lämpar sig väl för ett MVP. Den första versionen behöver inte stödja alla maskintyper eller alla integrationer. Prioritera ett litet antal funktioner med hög kvalitet framför många halvfärdiga funktioner.
Om man bygger en stabil kärna MVP ska innehålla: inloggning, organisation (tenant), användarhantering, starta ärende, objektidentifiering, AI-guidad felsökning, kamera och bildanalys, arbetslogg, tidrapportering, ärendebrief, kundrapport, API, administration.
1. objektidentifiering, All övrig funktionalitet planeras för senare versioner.
2. AI-guidad felsökning,
3. arbetslogg,
4. dokumentation,
5. ett öppet API
finns en plattform som sedan kan utökas bransch för bransch utan att behöva göras om i grunden. ---
## Slutmål
Bygg en plattform som känns lika självklar för en mekaniker eller servicetekniker som ett diagnosinstrument är idag. Fokus på snabbhet, tydlighet, metodisk vägledning och spårbar dokumentation. När en användare öppnar appen ska den upplevas som ett pålitligt professionellt verktyg — inte som en generell AI-chatt. Det ska vara enkelt att komma igång, enkelt att samarbeta och enkelt att visa kunden exakt hur felsökningen har genomförts.
+4
View File
@@ -22,7 +22,11 @@ npm test # projektionstester
| Ärendebrief | ✅ Regenereras ur loggen vid varje visning: utförda kontroller, observationer, **ej kontrollerat**, rekommenderat nästa steg, tillförlitlighet, total arbetstid. | | Ärendebrief | ✅ Regenereras ur loggen vid varje visning: utförda kontroller, observationer, **ej kontrollerat**, rekommenderat nästa steg, tillförlitlighet, total arbetstid. |
| Överlämning | ✅ ”Lämna över arbete” genererar överlämningsrapport ur briefen och loggar överlämningen. | | Överlämning | ✅ ”Lämna över arbete” genererar överlämningsrapport ur briefen och loggar överlämningen. |
| Kundrapport | ✅ Tidslinjevy utan interna poster, med bilder och tidsfördelning. Utskrift/PDF via webbläsaren, med påminnelse om granskning före delning. | | Kundrapport | ✅ Tidslinjevy utan interna poster, med bilder och tidsfördelning. Utskrift/PDF via webbläsaren, med påminnelse om granskning före delning. |
| Röstinmatning (tal in, text ut) | ✅ Push-to-Talk via webbläsarens taligenkänning (sv-SE): lyssnar bara efter aktivt tryck, röd indikator med realtidstranskript, texten hamnar i ett redigerbart fält och skickas aldrig automatiskt. Knappen visas bara i webbläsare med talstöd. Produktionsversionen byter motor till leverantörens Voice-to-Text bakom samma gränssnitt. |
| Verifierade checklistor | ✅ Varje kontroll i metodiken har ett minimikrav (foto, mätvärde eller kort observation). Foto-kontroller verifieras med bild; mätningar kan inte markeras verifierade utan värde. |
| Export | ✅ Versionsmärkt JSON-export (version = antal händelser vid exporttillfället, med användare och tidpunkt); exporten loggas själv som händelse. PDF via utskrift. CSV och API i backend-fasen. |
| Öppet API | 🔶 Datamodellen är API-klar (händelser som JSON), men MVP:t lagrar lokalt (localStorage). Nästa steg: backend med samma händelsemodell. | | Öppet API | 🔶 Datamodellen är API-klar (händelser som JSON), men MVP:t lagrar lokalt (localStorage). Nästa steg: backend med samma händelsemodell. |
| Live Share | 🔶 Kräver backend — specificerad i [moduler/live-share.md](moduler/live-share.md). Kundrapporten är förberedd som samma projektion. |
## Arkitekturprinciper i koden ## Arkitekturprinciper i koden
+4 -1
View File
@@ -1,6 +1,6 @@
# Guidad Felsökning # Guidad Felsökning
> Styrdokument för utveckling: [Master Prompt v1.0](MASTER-PROMPT.md) > Styrdokument för utveckling: [Master Prompt v2.0](MASTER-PROMPT.md)
## Vision ## Vision
@@ -124,6 +124,9 @@ Utöver grundprinciperna byggs plattformen upp av moduler som specificeras separ
- [Arbetslogg & Tidredovisning](moduler/arbetslogg-och-tidredovisning.md) tidsatt, spårbart arbete kopplat till konkreta aktiviteter; ett digitalt arbetsprotokoll där tid, aktivitet och tekniskt resonemang hänger ihop. - [Arbetslogg & Tidredovisning](moduler/arbetslogg-och-tidredovisning.md) tidsatt, spårbart arbete kopplat till konkreta aktiviteter; ett digitalt arbetsprotokoll där tid, aktivitet och tekniskt resonemang hänger ihop.
- [Delningsbar kundrapport (Kundvy)](moduler/kundrapport.md) en tydlig tidslinje med bilder, mätvärden och kommentarer som visar kunden vad de faktiskt betalat för. - [Delningsbar kundrapport (Kundvy)](moduler/kundrapport.md) en tydlig tidslinje med bilder, mätvärden och kommentarer som visar kunden vad de faktiskt betalat för.
- [Ärendebrief](moduler/arendebrief.md) en löpande uppdaterad arbetsbild av ärendet som gör att en ny tekniker blir produktiv på under en minut; fleranvändararbetsyta med överlämning med ett klick. - [Ärendebrief](moduler/arendebrief.md) en löpande uppdaterad arbetsbild av ärendet som gör att en ny tekniker blir produktiv på under en minut; fleranvändararbetsyta med överlämning med ett klick.
- [Kommunikationsmodell (röst)](moduler/kommunikationsmodell.md) tal in, text ut via Push-to-Talk; röst är ett inmatningssätt, inte ett separat gränssnitt, och inget skickas utan bekräftelse.
- [Verifierade checklistor](moduler/verifierade-checklistor.md) en kontrollpunkt är inte slutförd genom en kryssruta; varje kontroll samlar bevis och kontext med minimikrav per kontrolltyp.
- [Live Share](moduler/live-share.md) behörighetsstyrd delningslänk som visar ärendet i realtid; versionsmärkta exporter ur samma händelselogg.
Hur processen fungerar i praktiken illustreras i exempelflödet [”Bilen vibrerar runt 88 km/h”](exempel/vibration-vid-88-km-h.md). Hur processen fungerar i praktiken illustreras i exempelflödet [”Bilen vibrerar runt 88 km/h”](exempel/vibration-vid-88-km-h.md).
+78
View File
@@ -0,0 +1,78 @@
# Modul: Kommunikationsmodell (röst)
## Grundprincip
**Användaren pratar, systemet skriver.**
Systemet använder tal-till-text (Voice-to-Text) för all röstinmatning. Teknikern ska aldrig behöva skriva med tangentbord under ett pågående arbete.
Ingen röstagent: systemet för inte ett löpande röstsamtal, läser inte upp långa svar och försöker inte efterlikna en mänsklig konversation. Kommunikationen är **tal in, text ut**.
## Viktig designprincip
> All röst behandlas som ett inmatningssätt, inte som ett separat gränssnitt.
All logik i systemet bygger på text. Voice-to-Text är endast ett sätt att skapa den texten. Det gör lösningen enklare att underhålla, enklare att söka i, enklare att exportera och enklare att utveckla vidare med nya AI-modeller i framtiden.
## Arbetsflöde
1. Teknikern trycker på mikrofonen: *"Jag har mätt mellan stift 14 och jord. Jag får 12,4 volt."*
2. Voice-to-Text transkriberar talet.
3. Den transkriberade texten skickas till AI:n som en vanlig textförfrågan.
4. AI:n svarar alltid skriftligt: *Verifierat: Matningsspänning finns på stift 14. Nästa steg: Kontrollera jordanslutningen på stift 7.*
## Varför detta val?
- fungerar bättre i bullriga verkstäder,
- ger en permanent textlogg utan extra steg,
- gör det enkelt att söka i historiken,
- minskar risken för missförstånd jämfört med ett kontinuerligt röstsamtal,
- passar bättre när flera tekniker arbetar i samma ärende.
## Push-to-Talk (PTT)
Röstinmatning fungerar enligt Push-to-Talk. Appen lyssnar **endast** när användaren aktivt håller inne mikrofonknappen eller har startat en tydlig inspelning. Ingen bakgrundslyssning. Ingen automatisk aktivering.
### Flöde
1. Användaren håller inne mikrofonknappen (eller trycker på en tydlig "Spela in"-knapp beroende på plattform).
2. Inspelning startar omedelbart.
3. Appen visar tydligt att inspelning pågår: röd indikator, timer, ljudnivåmätare, texten "Inspelning pågår".
4. Talet transkriberas i realtid — användaren ser texten växa fram och får direkt återkoppling om talet uppfattats korrekt.
5. När inspelningen avslutas visas den transkriberade texten i ett **redigerbart** textfält.
6. Användaren kan godkänna, redigera eller spela in på nytt.
7. **Först när användaren bekräftar** skickas texten vidare till AI:n och sparas i arbetsloggen.
### Redigering före skick
Transkriberingen är alltid redigerbar. Vanliga korrigeringar: registreringsnummer, serienummer, komponentbeteckningar, personnamn, facktermer.
**"Skicka" sker aldrig automatiskt.** Teknikern får alltid en snabb chans att rätta transkriberingen innan den blir en del av den permanenta arbetsloggen. Det minskar risken för felaktiga registreringsnummer, komponentbeteckningar och mätvärden.
### Ingen dold funktionalitet
Användaren ska alltid kunna se:
- när inspelning pågår,
- när den är avslutad,
- vad som kommer att skickas,
- vad som faktiskt har sparats.
Det ska aldrig råda någon tvekan om när ljud spelas in eller när information skickas.
## Automatisk journalföring
Varje transkriberad mening blir automatiskt en del av arbetsloggen:
```
08:14 "Mätt spänning mellan stift 14 och jord. 12,4 volt."
08:14 AI: Matningsspänning verifierad.
08:15 "Relä klickar inte."
08:15 AI: Kontrollera styrsignal till relä.
```
Allt sparas utan att teknikern behöver skriva en enda rad.
## Handsfree-arbete
Appen är optimerad för upptagna eller smutsiga händer. Under ett normalt ärende ska användaren kunna identifiera objektet med kameran, fotografera komponenter, diktera observationer, få nästa steg presenterat och fortsätta arbetet — utan att skriva manuellt. Gränssnittet ska fungera med handskar, smutsiga händer, starkt solljus, buller och vibrationer; mikrofonknappen är stor, lätt att träffa och har tydlig visuell återkoppling.
+70
View File
@@ -0,0 +1,70 @@
# Modul: Live Share
## Syfte
Varje ärende kan publiceras via en unik säker delningslänk. Länken visar ärendets aktuella status i realtid och uppdateras automatiskt när ny information registreras. Ingen manuell export behövs.
En livevy ger stort värde för kunder, arbetsledare, försäkringsbolag och tillverkare — men den ska **alltid vara under verkstadens kontroll**, med tydliga behörigheter och säkerhetsnivåer.
## Exempel på kundvy
```
Ärende: Volvo XC60
Status: 🟢 Felsökning pågår
Kundens felbeskrivning
Bilen vibrerar vid cirka 88 km/h.
Aktuell status
✔ Objekt identifierat
✔ Provkörning utförd
✔ Däck dokumenterade
✔ Lufttryck kontrollerat
🔄 Hjulbalansering kontrolleras
⏳ Drivaxlar ej kontrollerade
Bilder · Mätvärden · Tidslinje
Rekommenderat nästa steg
Kontroll av radialkast.
```
## Liveuppdatering
När teknikern arbetar uppdateras sidan automatiskt, utan omladdning. Mottagaren ser direkt nya bilder, nya mätvärden, nya kommentarer och statusändringar.
## Behörighetsnivåer
Länkar kan skapas med olika åtkomstnivåer:
- **Kund** läsbehörighet till den information verkstaden valt att dela.
- **Intern** full insyn för kollegor och arbetsledare.
- **Extern partner** exempelvis försäkringsbolag eller tillverkare, med avgränsad information.
## Export
Från samma ärende ska det gå att exportera:
- PDF
- JSON
- CSV
- API
- Utskriftsvänlig HTML
Alla exporter bygger på samma datakälla (händelseloggen), vilket minskar risken för avvikelser.
## Versionshantering
Varje export märks med:
- versionsnummer,
- datum,
- tid,
- vem som exporterade,
- exportformat.
Det gör det möjligt att i efterhand se exakt vilken information som delades vid en viss tidpunkt.
## Produktvision
Ett felsökningsärende är inte bara en chatt eller en logg, utan en **levande digital arbetsjournal**. Den kan följas i realtid, tas över av en kollega, granskas av en arbetsledare, delas med kunden och avslutas med en komplett rapport — allt från samma datamodell. Det minskar dubbelarbete och gör att alla parter utgår från samma aktuella information.
+73
View File
@@ -0,0 +1,73 @@
# Modul: Verifierade checklistor
## Grundprincip
En kontrollpunkt är inte slutförd enbart genom att kryssa i en ruta.
Systemet registrerar inte bara *att* en ruta har kryssats i — det samlar in **bevis och kontext**. Varje kontroll ska innehålla ett eller flera av följande:
- ✔ Bekräftelse att kontrollen är utförd.
- 📝 Kort observation eller slutsats.
- 📷 Foto (när det är relevant).
- 📹 Video (vid behov).
- 🎤 Tal-till-text (för snabb dokumentation).
- 📏 Mätvärde (när tillämpligt).
På så sätt blir varje moment både spårbart och begripligt.
## Exempel
**Kontrollera batterispänning**
> Teknikern markerar "Utförd".
> Systemet: *Vilket värde uppmättes?* → **12,63 V**
> Systemet: *Hur mättes detta? (valfritt)* → **Direkt på batteripolerna.**
> Kontrollpunkten markeras som verifierad.
**Kontrollera säkring F24**
> ✔ Utförd
> Systemet: *Vad observerades?* → **Säkringen är hel och spänning finns på båda sidor.**
> Kontrollpunkten avslutas.
## AI:s roll
AI:n hjälper till att upptäcka när dokumentationen verkar ofullständig:
> "Du har markerat att hjulbalanseringen är kontrollerad, men ingen observation eller mätning har registrerats. Vill du lägga till en kort kommentar innan du går vidare?"
Det ska vara ett **stöd, inte ett hinder**.
## Anpassning efter kontrolltyp
Alla moment behöver inte samma nivå av dokumentation.
| Kontrolltyp | Minimikrav |
| --- | --- |
| Visuell kontroll | Bekräftelse + kort kommentar |
| Mätning | Mätvärde + kommentar |
| Demontering | Kommentar, foto vid behov |
| Provkörning | Sammanfattning av resultat |
| Bildbaserad kontroll | Foto + observation |
## Syfte
Målet är inte att "fånga" teknikern, utan att skapa ett arbetsunderlag som visar:
- vad som kontrollerades,
- hur det kontrollerades,
- vad resultatet blev,
- och vilka slutsatser som är rimliga att dra.
Det stärker kvaliteten i arbetet, gör överlämningar enklare och ger ett bättre underlag gentemot kund och arbetsledning.
## Viktig designprincip
Undvik att göra fritext obligatorisk överallt. Om varje kontroll kräver långa texter upplevs systemet snabbt som tungrott. Använd i stället en kombination av:
- förvalda svar där det passar,
- kort tal-till-text för observationer,
- mätvärdesfält,
- och foto eller video när det ger mest värde.
Då blir dokumentationen rik utan att arbetsflödet bromsas — och teknikerna använder systemet konsekvent i vardagen.
+65
View File
@@ -0,0 +1,65 @@
// Push-to-Talk-mikrofon: lyssnar ENDAST efter aktivt tryck, visar tydligt
// när inspelning pågår och skriver in transkriberingen i ett redigerbart
// fält. Skicka sker aldrig automatiskt.
import { useEffect, useRef, useState } from "react";
import { startaIgenkanning, stodRost } from "./rost";
export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
const [lyssnar, setLyssnar] = useState(false);
const [interim, setInterim] = useState("");
const igenkanningRef = useRef<ReturnType<typeof startaIgenkanning>>(undefined);
const paTextRef = useRef(paText);
paTextRef.current = paText;
useEffect(() => () => igenkanningRef.current?.abort(), []);
if (!stodRost()) return null;
const stoppa = () => {
igenkanningRef.current?.stop();
igenkanningRef.current = undefined;
setLyssnar(false);
setInterim("");
};
const starta = () => {
setLyssnar(true);
igenkanningRef.current = startaIgenkanning(
({ transkript, slutgiltigt }) => {
if (slutgiltigt) {
setInterim("");
paTextRef.current(transkript.trim());
} else {
setInterim(transkript);
}
},
() => {
setLyssnar(false);
setInterim("");
},
);
};
return (
<span className="inline-flex items-center gap-2">
<button
type="button"
onClick={() => (lyssnar ? stoppa() : starta())}
aria-label={lyssnar ? "Stoppa inspelning" : "Starta röstinmatning"}
className={`min-h-11 min-w-11 rounded-full border-2 px-3 text-lg font-bold transition-colors ${
lyssnar
? "animate-pulse border-red-500 bg-red-600 text-white"
: "border-zinc-600 bg-zinc-800 text-zinc-100 hover:border-amber-400"
}`}
>
{lyssnar ? "■" : "🎤"}
</button>
{lyssnar && (
<span className="text-sm font-bold text-red-400">
Inspelning pågår{interim ? ` — ”${interim}` : "…"}
</span>
)}
</span>
);
}
@@ -26,6 +26,19 @@ const OBJEKT: Handelse = {
}, },
}; };
describe("verifierade checklistor", () => {
it("varje kontroll i metodikerna har ett definierat minimikrav", async () => {
const { GENERISK_METODIK } = await import("../metodik");
for (const metodik of [VIBRATION_METODIK, GENERISK_METODIK]) {
for (const steg of metodik.steg) {
for (const kontroll of steg.kontroller ?? []) {
expect(kontroll.krav, `${metodik.id}/${steg.id}/${kontroll.id}`).toBeDefined();
}
}
}
});
});
describe("metodikval", () => { describe("metodikval", () => {
it("väljer vibrationsmetodiken när felbeskrivningen nämner vibration", () => { it("väljer vibrationsmetodiken när felbeskrivningen nämner vibration", () => {
expect(valjMetodik("Bilen vibrerar runt 88 km/h").id).toBe("vibration"); expect(valjMetodik("Bilen vibrerar runt 88 km/h").id).toBe("vibration");
+3
View File
@@ -51,6 +51,7 @@ export type Handelse =
| { typ: "kategori_byte"; kategori: TidKategori } | { typ: "kategori_byte"; kategori: TidKategori }
| { typ: "inaktivitet_forklarad"; text: string; minuter: number } | { typ: "inaktivitet_forklarad"; text: string; minuter: number }
| { typ: "overlamning"; fran: string; till?: string } | { typ: "overlamning"; fran: string; till?: string }
| { typ: "export_skapad"; format: string; version: number }
| { typ: "arende_avslutat" }; | { typ: "arende_avslutat" };
export interface LoggPost { export interface LoggPost {
@@ -106,6 +107,8 @@ export function handelseRubrik(post: LoggPost): string {
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`; return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
case "overlamning": case "overlamning":
return h.till ? `Arbete överlämnat från ${h.fran} till ${h.till}` : `Arbete överlämnat av ${h.fran}`; 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": case "arende_avslutat":
return "Felsökning avslutad"; return "Felsökning avslutad";
} }
+28 -23
View File
@@ -15,9 +15,14 @@ export interface Fraga {
val?: string[]; val?: string[];
} }
// Minimikrav enligt modulen Verifierade checklistor: en kontrollpunkt är
// inte slutförd enbart genom en kryssruta — den samlar bevis och kontext.
export type KontrollKrav = "matvarde" | "kommentar" | "foto";
export interface Kontroll { export interface Kontroll {
id: string; id: string;
text: string; text: string;
krav?: KontrollKrav;
} }
export interface MetodikSteg { export interface MetodikSteg {
@@ -67,23 +72,23 @@ export const VIBRATION_METODIK: Metodik = {
rubrik: "Visuell kontroll", rubrik: "Visuell kontroll",
beskrivning: "Fotografera samtliga hjul. Skilj på observation och slutsats.", beskrivning: "Fotografera samtliga hjul. Skilj på observation och slutsats.",
kontroller: [ kontroller: [
{ id: "foto_vf", text: "Fotografera vänster framhjul" }, { id: "foto_vf", text: "Fotografera vänster framhjul", krav: "foto" },
{ id: "foto_hf", text: "Fotografera höger framhjul" }, { id: "foto_hf", text: "Fotografera höger framhjul", krav: "foto" },
{ id: "foto_vb", text: "Fotografera vänster bakhjul" }, { id: "foto_vb", text: "Fotografera vänster bakhjul", krav: "foto" },
{ id: "foto_hb", text: "Fotografera höger bakhjul" }, { id: "foto_hb", text: "Fotografera höger bakhjul", krav: "foto" },
{ id: "dot", text: "Dokumentera DOT-/tillverkningsdatum" }, { id: "dot", text: "Dokumentera DOT-/tillverkningsdatum", krav: "kommentar" },
{ id: "slitage", text: "Kontrollera däckslitage" }, { id: "slitage", text: "Kontrollera däckslitage", krav: "kommentar" },
], ],
}, },
{ {
id: "kontroller", id: "kontroller",
rubrik: "Rekommenderade kontroller", rubrik: "Rekommenderade kontroller",
kontroller: [ kontroller: [
{ id: "lufttryck", text: "Kontrollera lufttryck" }, { id: "lufttryck", text: "Kontrollera lufttryck", krav: "matvarde" },
{ id: "hjulmoment", text: "Kontrollera hjulmoment" }, { id: "hjulmoment", text: "Kontrollera hjulmoment", krav: "matvarde" },
{ id: "kast", text: "Kontrollera radial- och sidokast" }, { id: "kast", text: "Kontrollera radial- och sidokast", krav: "matvarde" },
{ id: "balansering", text: "Kontrollera hjulbalansering" }, { id: "balansering", text: "Kontrollera hjulbalansering", krav: "kommentar" },
{ id: "bussningar", text: "Kontrollera bussningar och leder" }, { id: "bussningar", text: "Kontrollera bussningar och leder", krav: "kommentar" },
], ],
}, },
{ {
@@ -91,11 +96,11 @@ export const VIBRATION_METODIK: Metodik = {
rubrik: "Provkörning", rubrik: "Provkörning",
beskrivning: "Verifiera under provkörning:", beskrivning: "Verifiera under provkörning:",
kontroller: [ kontroller: [
{ id: "pk_hastighet", text: "Hastighet där vibration uppstår" }, { id: "pk_hastighet", text: "Hastighet där vibration uppstår", krav: "kommentar" },
{ id: "pk_acc", text: "Förändring vid acceleration" }, { id: "pk_acc", text: "Förändring vid acceleration", krav: "kommentar" },
{ id: "pk_motorbroms", text: "Förändring vid motorbroms" }, { id: "pk_motorbroms", text: "Förändring vid motorbroms", krav: "kommentar" },
{ id: "pk_kurva", text: "Förändring vid kurvtagning" }, { id: "pk_kurva", text: "Förändring vid kurvtagning", krav: "kommentar" },
{ id: "pk_var", text: "Om vibration känns i ratt eller kaross" }, { id: "pk_var", text: "Om vibration känns i ratt eller kaross", krav: "kommentar" },
], ],
}, },
], ],
@@ -124,25 +129,25 @@ export const GENERISK_METODIK: Metodik = {
id: "visuell", id: "visuell",
rubrik: "Visuell kontroll", rubrik: "Visuell kontroll",
kontroller: [ kontroller: [
{ id: "foto_objekt", text: "Fotografera objektet och det berörda området" }, { id: "foto_objekt", text: "Fotografera objektet och det berörda området", krav: "foto" },
{ id: "skador", text: "Kontrollera synliga skador, läckage eller lösa anslutningar" }, { id: "skador", text: "Kontrollera synliga skador, läckage eller lösa anslutningar", krav: "kommentar" },
{ id: "typskylt", text: "Dokumentera typskylt/märkning" }, { id: "typskylt", text: "Dokumentera typskylt/märkning", krav: "foto" },
], ],
}, },
{ {
id: "grundkontroller", id: "grundkontroller",
rubrik: "Grundkontroller", rubrik: "Grundkontroller",
kontroller: [ kontroller: [
{ id: "matning", text: "Utför grundläggande mätningar (spänning, tryck, temperatur — det som är relevant)" }, { id: "matning", text: "Utför grundläggande mätningar (spänning, tryck, temperatur — det som är relevant)", krav: "matvarde" },
{ id: "sakringar", text: "Kontrollera säkringar/skydd" }, { id: "sakringar", text: "Kontrollera säkringar/skydd", krav: "kommentar" },
{ id: "anslutningar", text: "Kontrollera kontaktdon och anslutningar" }, { id: "anslutningar", text: "Kontrollera kontaktdon och anslutningar", krav: "kommentar" },
], ],
}, },
{ {
id: "funktionstest", id: "funktionstest",
rubrik: "Funktionstest", rubrik: "Funktionstest",
kontroller: [ kontroller: [
{ id: "test", text: "Genomför funktionstest och dokumentera resultatet" }, { id: "test", text: "Genomför funktionstest och dokumentera resultatet", krav: "kommentar" },
], ],
}, },
], ],
+65
View File
@@ -0,0 +1,65 @@
// Tal-till-text enligt kommunikationsmodellen: rösten är ett inmatningssätt,
// inte ett separat gränssnitt. All logik bygger på text — transkriberingen
// hamnar i ett redigerbart fält och ingenting skickas utan bekräftelse.
//
// MVP:t använder webbläsarens SpeechRecognition (sv-SE). I produktions-
// versionen ersätts motorn av leverantörens Voice-to-Text-API bakom samma
// gränssnitt.
interface IgenkanningsResultat {
transkript: string;
slutgiltigt: boolean;
}
interface Igenkanning {
start(): void;
stop(): void;
abort(): void;
}
type SpeechRecognitionCtor = new () => {
lang: string;
continuous: boolean;
interimResults: boolean;
onresult: ((event: { resultIndex: number; results: ArrayLike<{ 0: { transcript: string }; isFinal: boolean }> }) => void) | null;
onend: (() => void) | null;
onerror: ((event: { error: string }) => void) | null;
start(): void;
stop(): void;
abort(): void;
};
function ctor(): SpeechRecognitionCtor | undefined {
if (typeof window === "undefined") return undefined;
const w = window as unknown as {
SpeechRecognition?: SpeechRecognitionCtor;
webkitSpeechRecognition?: SpeechRecognitionCtor;
};
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
}
export function stodRost(): boolean {
return ctor() !== undefined;
}
export function startaIgenkanning(
paResultat: (r: IgenkanningsResultat) => void,
paSlut: () => void,
): Igenkanning | undefined {
const Ctor = ctor();
if (!Ctor) return undefined;
const igenkanning = new Ctor();
igenkanning.lang = "sv-SE";
igenkanning.continuous = true;
igenkanning.interimResults = true;
igenkanning.onresult = (event) => {
for (let i = event.resultIndex; i < event.results.length; i++) {
const resultat = event.results[i];
paResultat({ transkript: resultat[0].transcript, slutgiltigt: resultat.isFinal });
}
};
igenkanning.onend = paSlut;
igenkanning.onerror = paSlut;
igenkanning.start();
return igenkanning;
}
+16 -2
View File
@@ -3,9 +3,10 @@
// träffytor (handskvänligt), få val per skärm, inga distraktioner. // träffytor (handskvänligt), få val per skärm, inga distraktioner.
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { ReactNode } from "react"; import { useRef, type ReactNode } from "react";
import type { Tillforlitlighet } from "./domain"; import type { Tillforlitlighet } from "./domain";
import { TILLFORLITLIGHET_LABEL } from "./domain"; import { TILLFORLITLIGHET_LABEL } from "./domain";
import { MikrofonKnapp } from "./Mikrofon";
export function FelsokningSkal({ export function FelsokningSkal({
rubrik, rubrik,
@@ -90,18 +91,31 @@ export function TextFalt({
satt, satt,
platshallare, platshallare,
flerRad, flerRad,
rost,
}: { }: {
label: string; label: string;
varde: string; varde: string;
satt: (v: string) => void; satt: (v: string) => void;
platshallare?: string; platshallare?: string;
flerRad?: boolean; flerRad?: boolean;
rost?: boolean;
}) { }) {
// Tal-till-text skriver in i samma redigerbara fält — användaren
// granskar och bekräftar alltid innan något sparas i loggen.
const vardeRef = useRef(varde);
vardeRef.current = varde;
const klass = const klass =
"w-full rounded-lg border-2 border-zinc-600 bg-zinc-950 px-4 py-3 text-lg text-zinc-50 placeholder-zinc-500 focus:border-amber-400 focus:outline-none"; "w-full rounded-lg border-2 border-zinc-600 bg-zinc-950 px-4 py-3 text-lg text-zinc-50 placeholder-zinc-500 focus:border-amber-400 focus:outline-none";
return ( return (
<label className="mb-3 block"> <label className="mb-3 block">
<span className="mb-1 block text-sm font-bold uppercase tracking-wide text-zinc-400">{label}</span> <span className="mb-1 flex min-h-11 items-center justify-between gap-2">
<span className="text-sm font-bold uppercase tracking-wide text-zinc-400">{label}</span>
{rost && (
<MikrofonKnapp
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
/>
)}
</span>
{flerRad ? ( {flerRad ? (
<textarea value={varde} onChange={(e) => satt(e.target.value)} placeholder={platshallare} rows={3} className={klass} /> <textarea value={varde} onChange={(e) => satt(e.target.value)} placeholder={platshallare} rows={3} className={klass} />
) : ( ) : (
+74 -11
View File
@@ -88,7 +88,7 @@ export default function ArendeSida() {
)} )}
{flik === "logg" && <LoggFlik arende={arende} />} {flik === "logg" && <LoggFlik arende={arende} />}
{flik === "brief" && <BriefFlik arende={arende} metodik={metodik} nu={nu} />} {flik === "brief" && <BriefFlik arende={arende} metodik={metodik} nu={nu} />}
{flik === "rapport" && <RapportFlik arende={arende} metodik={metodik} nu={nu} />} {flik === "rapport" && <RapportFlik arende={arende} metodik={metodik} nu={nu} skicka={skicka} />}
</FelsokningSkal> </FelsokningSkal>
); );
} }
@@ -114,7 +114,7 @@ function InaktivitetsBanner({
Ingen aktivitet har registrerats de senaste {minuter} minuterna. Ingen aktivitet har registrerats de senaste {minuter} minuterna.
</p> </p>
<p className="mb-3 text-zinc-300">Beskriv kort vad som gjorts under denna period.</p> <p className="mb-3 text-zinc-300">Beskriv kort vad som gjorts under denna period.</p>
<TextFalt label="Vad har gjorts?" varde={text} satt={setText} platshallare="T.ex. Demonterade instrumentpanelen för att komma åt kabelstammen." /> <TextFalt label="Vad har gjorts?" varde={text} satt={setText} platshallare="T.ex. Demonterade instrumentpanelen för att komma åt kabelstammen." rost />
<StorKnapp <StorKnapp
disabled={!text.trim()} disabled={!text.trim()}
onClick={() => { onClick={() => {
@@ -245,7 +245,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
)} )}
{fraga.svarstyp === "text" && ( {fraga.svarstyp === "text" && (
<> <>
<TextFalt label="Svar" varde={text} satt={setText} /> <TextFalt label="Svar" varde={text} satt={setText} rost />
<StorKnapp disabled={!text.trim()} onClick={() => svara(text.trim())}> <StorKnapp disabled={!text.trim()} onClick={() => svara(text.trim())}>
Spara svar Spara svar
</StorKnapp> </StorKnapp>
@@ -255,11 +255,14 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
); );
} }
// Verifierad checklista: en kontrollpunkt är inte slutförd enbart genom en
// kryssruta — minimikravet (foto, mätvärde eller kort observation) styrs av
// kontrolltypen i metodiken.
function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) => void }) { function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) => void }) {
const kontroll = steg.kontroll!; const kontroll = steg.kontroll!;
const [resultat, setResultat] = useState(""); const [resultat, setResultat] = useState("");
const filRef = useRef<HTMLInputElement>(null); const filRef = useRef<HTMLInputElement>(null);
const arFoto = /fotografera|foto/i.test(kontroll.text); const krav = kontroll.krav;
const utford = () => const utford = () =>
skicka({ skicka({
@@ -270,11 +273,13 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
resultat: resultat.trim() || undefined, resultat: resultat.trim() || undefined,
}); });
const kravUppfyllt = krav === "foto" || !krav || resultat.trim().length > 0;
return ( return (
<> <>
{steg.steg.beskrivning && <p className="mb-2 text-zinc-400">{steg.steg.beskrivning}</p>} {steg.steg.beskrivning && <p className="mb-2 text-zinc-400">{steg.steg.beskrivning}</p>}
<p className="mb-4 text-2xl font-extrabold leading-snug">{kontroll.text}</p> <p className="mb-4 text-2xl font-extrabold leading-snug">{kontroll.text}</p>
{arFoto && ( {krav === "foto" ? (
<> <>
<input <input
ref={filRef} ref={filRef}
@@ -290,13 +295,36 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
utford(); utford();
}} }}
/> />
<StorKnapp className="mb-2" onClick={() => filRef.current?.click()}> <TextFalt label="Observation (valfritt)" varde={resultat} satt={setResultat} rost />
📷 Ta / välj foto <StorKnapp onClick={() => filRef.current?.click()}>📷 Ta foto verifierar kontrollen</StorKnapp>
<p className="mt-2 text-sm text-zinc-500">Denna kontroll verifieras med foto.</p>
</>
) : (
<>
<TextFalt
label={
krav === "matvarde"
? "Uppmätt värde (krävs för verifiering)"
: krav === "kommentar"
? "Vad observerades? (krävs för verifiering)"
: "Resultat / avläst värde (valfritt)"
}
varde={resultat}
satt={setResultat}
platshallare={krav === "matvarde" ? "T.ex. 2,4 bar samtliga hjul" : "T.ex. Säkringen är hel, spänning på båda sidor"}
rost
/>
{!kravUppfyllt && (
<p className="mb-3 text-sm font-bold text-amber-400">
Ingen {krav === "matvarde" ? "mätning" : "observation"} har registrerats lägg till en kort{" "}
{krav === "matvarde" ? "mätuppgift" : "kommentar"} innan kontrollen kan verifieras.
</p>
)}
<StorKnapp disabled={!kravUppfyllt} onClick={utford}>
Markera verifierad
</StorKnapp> </StorKnapp>
</> </>
)} )}
<TextFalt label="Resultat / avläst värde (valfritt)" varde={resultat} satt={setResultat} platshallare="T.ex. 2,4 bar samtliga hjul" />
<StorKnapp onClick={utford}>Markera utförd</StorKnapp>
</> </>
); );
} }
@@ -375,6 +403,7 @@ function SnabbDokumentation({ skicka }: { skicka: (h: Handelse) => void }) {
varde={text} varde={text}
satt={setText} satt={setText}
platshallare={typ === "matvarde" ? "T.ex. Matningsspänning stift 30" : ""} platshallare={typ === "matvarde" ? "T.ex. Matningsspänning stift 30" : ""}
rost
/> />
{typ === "matvarde" && <TextFalt label="Värde (med enhet)" varde={varde} satt={setVarde} platshallare="T.ex. 13,9 V" />} {typ === "matvarde" && <TextFalt label="Värde (med enhet)" varde={varde} satt={setVarde} platshallare="T.ex. 13,9 V" />}
<StorKnapp disabled={!text.trim() || (typ === "matvarde" && !varde.trim())} onClick={spara}> <StorKnapp disabled={!text.trim() || (typ === "matvarde" && !varde.trim())} onClick={spara}>
@@ -521,12 +550,41 @@ function BriefFlik({ arende, metodik, nu }: { arende: Arende; metodik: Metodik;
// Kundrapporten: en tydlig tidslinje i stället för "Felsökning 2,5 timmar". // Kundrapporten: en tydlig tidslinje i stället för "Felsökning 2,5 timmar".
// Interna poster (kategoribyten) visas inte för kund. // Interna poster (kategoribyten) visas inte för kund.
function RapportFlik({ arende, metodik, nu }: { arende: Arende; metodik: Metodik; nu: string }) { function RapportFlik({
arende,
metodik,
nu,
skicka,
}: {
arende: Arende;
metodik: Metodik;
nu: string;
skicka: (h: Handelse) => void;
}) {
const anvandare = useFelsokning((s) => s.anvandare);
const b = brief(arende, metodik, nu); const b = brief(arende, metodik, nu);
const bilder = foton(arende); const bilder = foton(arende);
const fordelning = tidsfordelningsRader(arende, nu); const fordelning = tidsfordelningsRader(arende, nu);
const kundposter = arende.handelser.filter((p) => p.handelse.typ !== "kategori_byte"); const kundposter = arende.handelser.filter((p) => p.handelse.typ !== "kategori_byte");
// Alla exporter bygger på samma händelselogg och versionsmärks:
// version = antal händelser vid exporttillfället.
const exporteraJson = () => {
const version = arende.handelser.length;
const data = {
export: { format: "JSON", version, exporteradAv: anvandare, tidpunkt: new Date().toISOString() },
arende,
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const lank = document.createElement("a");
lank.href = url;
lank.download = `arende-${arende.nummer}-v${version}.json`;
lank.click();
URL.revokeObjectURL(url);
skicka({ typ: "export_skapad", format: "JSON", version });
};
return ( return (
<> <>
<Panel rubrik="Kundrapport"> <Panel rubrik="Kundrapport">
@@ -534,9 +592,14 @@ function RapportFlik({ arende, metodik, nu }: { arende: Arende; metodik: Metodik
Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas bilder kan Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas bilder kan
innehålla uppgifter om andra kunder. innehålla uppgifter om andra kunder.
</p> </p>
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => window.print()}> <StorKnapp variant="sekundar" onClick={() => window.print()}>
Skriv ut / spara som PDF Skriv ut / PDF
</StorKnapp> </StorKnapp>
<StorKnapp variant="sekundar" onClick={exporteraJson}>
Exportera JSON
</StorKnapp>
</div>
</Panel> </Panel>
<Panel rubrik={`Ärende #${arende.nummer}`}> <Panel rubrik={`Ärende #${arende.nummer}`}>
{b.objekt && ( {b.objekt && (