Ansvarig tekniker och omfördelning från arbetsledarvyn
Ansvarig per ärende härleds ur händelseloggen: skaparen är ansvarig tills en överlämning eller den nya händelsen ansvarig_satt pekar ut någon annan. Arbetsledaren kan omfördela pågående ärenden direkt i översikten; händelsen är organisationsintern och filtreras bort ur kund- och partnerdelningar i klient, plattformstjänst och Supabase-RPC. - Ny händelsetyp ansvarig_satt + projektion ansvarig() (testad) - Briefen visar ansvarig tekniker; översikten visar ansvarig/skapare - Arbetsledare får lista användare (skapa kräver fortfarande admin) - Översikts-SQL:en härleder ansvarig och skapare ur loggen - OpenAPI-specen uppdaterad; integrationstestet utökat (omfördelning, behörigheter, delningsnivåernas filtrering) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
+1
-2
@@ -27,7 +27,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
|
||||
| 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. |
|
||||
| Multi-tenant & roller | ✅ I självhostat läge: registrering skapar organisation + systemadministratör; admin hanterar användare (tekniker/arbetsledare/admin) via UI; all ärendedata organisationsisolerad i API:t; roll + organisation i JWT:n. **Arbetsledarvy** (`/felsokning/oversikt`): organisationens alla ärenden med status, deltagande tekniker och statistik (pågående/avslutade/ledtid) — härlett ur händelseloggen; ärenden kan hämtas till enheten med konfliktfri flätning. Integrationstestat mot riktig Postgres (isolering, rollstyrning, append-only, översiktens behörighet och härledningar). |
|
||||
| Multi-tenant & roller | ✅ I självhostat läge: registrering skapar organisation + systemadministratör; admin hanterar användare (tekniker/arbetsledare/admin) via UI; all ärendedata organisationsisolerad i API:t; roll + organisation i JWT:n. **Arbetsledarvy** (`/felsokning/oversikt`): organisationens alla ärenden med status, deltagande tekniker och statistik (pågående/avslutade/ledtid) — härlett ur händelseloggen; ärenden kan hämtas till enheten med konfliktfri flätning. **Ansvarig tekniker** per ärende härleds ur loggen (skapare → överlämning → omfördelning) och arbetsledaren kan omfördela pågående ärenden — loggat som den organisationsinterna händelsen `ansvarig_satt`, aldrig synlig i kund-/partnerdelningar. Integrationstestat mot riktig Postgres (isolering, rollstyrning, append-only, översiktens behörighet och härledningar). |
|
||||
| Backend & synk | ✅ Databas-migration (`supabase/migrations/20260802230000_guidad_felsokning.sql`): ärenden + händelser med RLS, append-only även i databasen (inga update/delete-rättigheter). Synklager i klienten: konfliktfri ihopflätning av händelser per id (testad), push av lokala + pull av kollegors händelser var 15:e sekund. Utan inloggning arbetar appen i lokalt läge; status visas i ärendehuvudet. |
|
||||
| Metodiker | ✅ Tre: vibration, elsystem/strömförsörjning (relä-exemplet ur visionen) och generisk — vald automatiskt utifrån felbeskrivningen. |
|
||||
| Live Share | ✅ Skrivskyddad livevy per ärende (`/felsokning/dela/:id`): status ✔/🔄/⏳, bilder, mätvärdestabell, tidslinje, rekommenderat nästa steg. Uppdateras automatiskt, interna poster filtreras bort. Publik delningssida (`/felsokning/delad/:kod`) läser via `hamta_delat_arende` utan inloggning och pollar för liveuppdatering; "Kopiera delningslänk" finns i rapportfliken. **Behörighetsnivåer**: återkallbara delningslänkar per nivå — kund (det kunddelbara), extern partner (även hypoteser, märkta ej verifierade), intern (full insyn) — med serverstyrd filtrering, hanterade från rapportfliken i självhostat läge. |
|
||||
@@ -44,5 +44,4 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
|
||||
|
||||
## Medvetna avgränsningar
|
||||
|
||||
- Omfördelning av ärenden (arbetsledare) är inte implementerad — överlämningshändelsen täcker handover tills en ansvarig-modell införs.
|
||||
- QR/streckkod/OCR för objektidentifiering, bildanalys och tillverkarintegrationer ingår inte ännu.
|
||||
|
||||
@@ -112,6 +112,16 @@ kontroll "översikten härleder felbeskrivning" "$(echo "$OVERSIKT" | falt '.are
|
||||
kontroll "översikten härleder status" "$(echo "$OVERSIKT" | falt '.arenden[0].avslutat')" "false"
|
||||
kontroll "översikten räknar händelser" "$(echo "$OVERSIKT" | falt '.arenden[0].antal_handelser')" "2"
|
||||
|
||||
# 9b. Omfördelning: arbetsledaren listar användare och sätter ny ansvarig
|
||||
ANTAL_ANV=$(curl -s "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_L" | falt .anvandare.length)
|
||||
kontroll "arbetsledaren kan lista användare" "$ANTAL_ANV" "3"
|
||||
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_L" \
|
||||
-H 'Content-Type: application/json' -d '{"epost":"y@a.se","losenord":"hemligt123","namn":"Y","roll":"tekniker"}')
|
||||
kontroll "arbetsledaren nekas skapa användare" "$KOD" "403"
|
||||
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_L" -H 'Content-Type: application/json' \
|
||||
-d '{"handelser":[{"id":"h-omf","tidpunkt":"2026-08-03T08:05:00Z","anvandare":"Lisa","handelse":{"typ":"ansvarig_satt","ansvarig":"Johan"}}]}' >/dev/null
|
||||
kontroll "översikten visar ny ansvarig" "$(curl -s "$BAS/api/oversikt" -H "Authorization: Bearer $TOKEN_L" | falt '.arenden[0].ansvarig')" "Johan"
|
||||
|
||||
# 10. Live Share-behörighetsnivåer: kund/partner/intern + återkallelse
|
||||
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
||||
-d '{"handelser":[{"id":"h3","tidpunkt":"2026-08-03T08:03:00Z","anvandare":"Anna","handelse":{"typ":"hypotes","text":"Trasigt relä","niva":"lag"}}]}' >/dev/null
|
||||
@@ -126,7 +136,7 @@ kontroll "nivån följer med svaret" "$(echo "$PARTNER" | falt .niva)" "partner"
|
||||
|
||||
INTERNKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
|
||||
-H 'Content-Type: application/json' -d '{"niva":"intern"}' | falt .kod)
|
||||
kontroll "internnivån visar allt" "$(curl -s "$BAS/api/delad/$INTERNKOD" | falt .handelser.length)" "3"
|
||||
kontroll "internnivån visar allt" "$(curl -s "$BAS/api/delad/$INTERNKOD" | falt .handelser.length)" "4"
|
||||
|
||||
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-test1/delningar" \
|
||||
-H "Authorization: Bearer $TOKEN_B" -H 'Content-Type: application/json' -d '{"niva":"intern"}')
|
||||
|
||||
@@ -106,7 +106,7 @@ paths:
|
||||
get:
|
||||
tags: [Användare]
|
||||
summary: Lista organisationens användare
|
||||
description: Kräver rollen `admin`.
|
||||
description: Kräver rollen `admin` eller `arbetsledare`.
|
||||
responses:
|
||||
"200":
|
||||
description: Användare i den egna organisationen.
|
||||
@@ -512,6 +512,7 @@ components:
|
||||
- kategori_byte
|
||||
- inaktivitet_forklarad
|
||||
- overlamning
|
||||
- ansvarig_satt
|
||||
- export_skapad
|
||||
- ai_svar
|
||||
- arende_avslutat
|
||||
|
||||
@@ -222,7 +222,7 @@ export function skapaServer() {
|
||||
[arendeId],
|
||||
);
|
||||
const bortfiltrerat =
|
||||
niva === "intern" ? [] : niva === "partner" ? ["kategori_byte", "ai_svar"] : ["kategori_byte", "hypotes", "ai_svar"];
|
||||
niva === "intern" ? [] : niva === "partner" ? ["kategori_byte", "ai_svar", "ansvarig_satt"] : ["kategori_byte", "hypotes", "ai_svar", "ansvarig_satt"];
|
||||
const handelser = await pool.query(
|
||||
`select id, tidpunkt, anvandare, handelse from felsokning_handelser
|
||||
where arende_id = $1 and not (handelse->>'typ' = any($2))
|
||||
@@ -238,7 +238,9 @@ export function skapaServer() {
|
||||
|
||||
// Användarhantering: endast systemadministratör, endast egen org.
|
||||
if (vag === "/api/anvandare") {
|
||||
if (anspr.roll !== "admin") return svara(res, 403, { error: "Kräver administratörsbehörighet." });
|
||||
// Läsning: admin + arbetsledare (behövs för omfördelning).
|
||||
// Skapande: endast admin.
|
||||
if (anspr.roll === "tekniker") return svara(res, 403, { error: "Kräver arbetsledar- eller administratörsbehörighet." });
|
||||
if (req.method === "GET") {
|
||||
const rader = await pool.query(
|
||||
`select id, epost, namn, roll from anvandare where organisation_id = $1 order by namn`,
|
||||
@@ -247,6 +249,7 @@ export function skapaServer() {
|
||||
return svara(res, 200, { anvandare: rader.rows });
|
||||
}
|
||||
if (req.method === "POST") {
|
||||
if (anspr.roll !== "admin") return svara(res, 403, { error: "Kräver administratörsbehörighet." });
|
||||
const { epost, losenord, namn, roll } = await lasKropp(req);
|
||||
if (!epost?.includes("@") || !losenord || losenord.length < 8 || !namn?.trim() || !ROLLER.includes(roll)) {
|
||||
return svara(res, 400, { error: "Ange namn, e-post, roll och lösenord (minst 8 tecken)." });
|
||||
@@ -280,6 +283,10 @@ export function skapaServer() {
|
||||
filter (where h.handelse->>'typ' = 'objekt_identifierat'))[1] as objekt,
|
||||
(array_agg(h.handelse->>'text')
|
||||
filter (where h.handelse->>'typ' = 'felbeskrivning'))[1] as felbeskrivning,
|
||||
(array_agg(coalesce(h.handelse->>'ansvarig', h.handelse->>'till') order by h.tidpunkt desc)
|
||||
filter (where h.handelse->>'typ' in ('ansvarig_satt', 'overlamning')
|
||||
and coalesce(h.handelse->>'ansvarig', h.handelse->>'till') is not null))[1] as ansvarig,
|
||||
(array_agg(h.anvandare order by h.tidpunkt asc))[1] as skapare,
|
||||
array_agg(distinct h.anvandare) filter (where h.anvandare is not null) as tekniker
|
||||
from felsokning_arenden a
|
||||
left join felsokning_handelser h on h.arende_id = a.id
|
||||
|
||||
@@ -32,7 +32,7 @@ export function DelatArendeVy({
|
||||
const matvarden = arende.handelser.filter((p) => p.handelse.typ === "matvarde");
|
||||
const kundposter = redanFiltrerad
|
||||
? arende.handelser
|
||||
: arende.handelser.filter((p) => !["kategori_byte", "hypotes", "ai_svar"].includes(p.handelse.typ));
|
||||
: arende.handelser.filter((p) => !["kategori_byte", "hypotes", "ai_svar", "ansvarig_satt"].includes(p.handelse.typ));
|
||||
const pagaende = b.rekommenderatNastaSteg[0];
|
||||
|
||||
return (
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("AI-orkestern", () => {
|
||||
// … och AI-endpointen + händelsetyperna är dokumenterade.
|
||||
expect(spec).toContain("/api/ai:");
|
||||
expect(spec).toContain("append-only");
|
||||
for (const typ of ["objekt_identifierat", "kontroll_utford", "ai_svar", "arende_avslutat"]) {
|
||||
for (const typ of ["objekt_identifierat", "kontroll_utford", "ai_svar", "ansvarig_satt", "arende_avslutat"]) {
|
||||
expect(spec).toContain(typ);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -139,6 +139,27 @@ describe("tidsfordelning", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ansvarig", () => {
|
||||
it("skaparen är ansvarig tills överlämning eller omfördelning pekar ut någon annan", async () => {
|
||||
const { ansvarig } = await import("../projektioner");
|
||||
const bas: [string, Handelse][] = [["2026-08-03T08:00:00Z", OBJEKT]];
|
||||
expect(ansvarig(byggArende(bas))).toBe("Anna");
|
||||
|
||||
const efterOverlamning = byggArende([
|
||||
...bas,
|
||||
["2026-08-03T09:00:00Z", { typ: "overlamning", fran: "Anna", till: "Johan" }],
|
||||
]);
|
||||
expect(ansvarig(efterOverlamning)).toBe("Johan");
|
||||
|
||||
const efterOmfordelning = byggArende([
|
||||
...bas,
|
||||
["2026-08-03T09:00:00Z", { typ: "overlamning", fran: "Anna", till: "Johan" }],
|
||||
["2026-08-03T10:00:00Z", { typ: "ansvarig_satt", ansvarig: "Lisa" }],
|
||||
]);
|
||||
expect(ansvarig(efterOmfordelning)).toBe("Lisa");
|
||||
});
|
||||
});
|
||||
|
||||
describe("brief", () => {
|
||||
const arende = byggArende([
|
||||
["2026-08-02T08:03:00Z", OBJEKT],
|
||||
|
||||
@@ -51,6 +51,7 @@ export type Handelse =
|
||||
| { typ: "kategori_byte"; kategori: TidKategori }
|
||||
| { typ: "inaktivitet_forklarad"; text: string; minuter: number }
|
||||
| { typ: "overlamning"; fran: string; till?: string }
|
||||
| { typ: "ansvarig_satt"; ansvarig: string }
|
||||
| { typ: "export_skapad"; format: string; version: number }
|
||||
| {
|
||||
typ: "ai_svar";
|
||||
@@ -119,6 +120,8 @@ export function handelseRubrik(post: LoggPost): string {
|
||||
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 "ansvarig_satt":
|
||||
return `Ansvarig tekniker: ${h.ansvarig}`;
|
||||
case "export_skapad":
|
||||
return `Export skapad: ${h.format}, version ${h.version}`;
|
||||
case "ai_svar": {
|
||||
|
||||
@@ -141,6 +141,8 @@ export interface OversiktsRad {
|
||||
avslutat: boolean;
|
||||
objekt: string | null;
|
||||
felbeskrivning: string | null;
|
||||
ansvarig: string | null;
|
||||
skapare: string | null;
|
||||
tekniker: string[] | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,18 @@ export function felbeskrivning(arende: Arende): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Ansvarig tekniker härleds ur loggen: skaparen, tills en överlämning
|
||||
// med mottagare eller en omfördelning (ansvarig_satt) pekar ut någon annan.
|
||||
export function ansvarig(arende: Arende): string | undefined {
|
||||
let namn = arende.handelser[0]?.anvandare;
|
||||
for (const post of arende.handelser) {
|
||||
const h = post.handelse;
|
||||
if (h.typ === "ansvarig_satt") namn = h.ansvarig;
|
||||
if (h.typ === "overlamning" && h.till) namn = h.till;
|
||||
}
|
||||
return namn;
|
||||
}
|
||||
|
||||
export function arAvslutat(arende: Arende): boolean {
|
||||
return arende.handelser.some((p) => p.handelse.typ === "arende_avslutat");
|
||||
}
|
||||
@@ -161,6 +173,7 @@ export function tillforlitlighet(arende: Arende): TillforlitlighetsRad[] {
|
||||
|
||||
export interface Brief {
|
||||
objekt?: Objekt;
|
||||
ansvarig?: string;
|
||||
felbeskrivning?: string;
|
||||
utfordaKontroller: UtfordKontroll[];
|
||||
observationer: string[];
|
||||
@@ -190,6 +203,7 @@ export function brief(arende: Arende, metodik: Metodik, nu?: string): Brief {
|
||||
}
|
||||
return {
|
||||
objekt: objekt(arende),
|
||||
ansvarig: ansvarig(arende),
|
||||
felbeskrivning: felbeskrivning(arende),
|
||||
utfordaKontroller: utfordaKontroller(arende),
|
||||
observationer: observationer(arende),
|
||||
|
||||
@@ -796,6 +796,7 @@ function BriefFlik({
|
||||
<p className="text-xl font-extrabold">{b.objekt.beskrivning}</p>
|
||||
<p className="font-bold text-amber-400">{b.objekt.identifierare}</p>
|
||||
{b.objekt.kund && <p className="text-zinc-300">Kund: {b.objekt.kund}</p>}
|
||||
{b.ansvarig && <p className="text-zinc-300">Ansvarig tekniker: {b.ansvarig}</p>}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-zinc-400">Ej identifierat</p>
|
||||
@@ -875,7 +876,7 @@ function RapportFlik({
|
||||
// Kategoribyten är interna; hypoteser och AI-dialogen är arbetsmaterial
|
||||
// och ingår inte i det som delas med kund.
|
||||
const kundposter = arende.handelser.filter(
|
||||
(p) => !["kategori_byte", "hypotes", "ai_svar"].includes(p.handelse.typ),
|
||||
(p) => !["kategori_byte", "hypotes", "ai_svar", "ansvarig_satt"].includes(p.handelse.typ),
|
||||
);
|
||||
|
||||
// Alla exporter bygger på samma händelselogg och versionsmärks:
|
||||
|
||||
@@ -7,18 +7,77 @@ import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { Arende } from "@/felsokning/domain";
|
||||
import {
|
||||
hamtaAnvandare,
|
||||
hamtaOversikt,
|
||||
plattformAktiv,
|
||||
plattformFetch,
|
||||
plattformKonto,
|
||||
type OversiktsRad,
|
||||
type PlattformAnvandare,
|
||||
} from "@/felsokning/plattform";
|
||||
import { nyLoggPost } from "@/felsokning/domain";
|
||||
import { flataIhop } from "@/felsokning/synk";
|
||||
import { useFelsokning } from "@/felsokning/store";
|
||||
import { formateraTid } from "@/felsokning/projektioner";
|
||||
import { FelsokningSkal, Panel, StorKnapp } from "@/felsokning/ui";
|
||||
import { tidDatum, tidKlockslag } from "@/felsokning/format";
|
||||
|
||||
// Omfördelning: arbetsledaren pekar ut ny ansvarig tekniker — loggas som
|
||||
// append-only-händelsen ansvarig_satt i ärendets logg (organisationsintern,
|
||||
// delas aldrig i kund-/partnervyer).
|
||||
function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void }) {
|
||||
const [oppen, setOppen] = useState(false);
|
||||
const [personer, setPersoner] = useState<PlattformAnvandare[]>([]);
|
||||
const konto = plattformKonto();
|
||||
|
||||
if (!oppen) {
|
||||
return (
|
||||
<StorKnapp
|
||||
variant="sekundar"
|
||||
onClick={() => {
|
||||
setOppen(true);
|
||||
hamtaAnvandare().then(setPersoner).catch(() => setPersoner([]));
|
||||
}}
|
||||
>
|
||||
Omfördela
|
||||
</StorKnapp>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="col-span-2 rounded-lg border-2 border-zinc-700 bg-zinc-950 p-2">
|
||||
<p className="mb-2 text-sm font-bold uppercase text-zinc-400">Ny ansvarig tekniker</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{personer.map((person) => (
|
||||
<button
|
||||
key={person.id}
|
||||
className="min-h-12 rounded-lg border-2 border-zinc-600 font-extrabold text-zinc-200 hover:border-amber-400"
|
||||
onClick={async () => {
|
||||
const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
|
||||
typ: "ansvarig_satt",
|
||||
ansvarig: person.namn,
|
||||
});
|
||||
await plattformFetch(`/api/arenden/${rad.id}/handelser`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ handelser: [post] }),
|
||||
});
|
||||
setOppen(false);
|
||||
vidKlar();
|
||||
}}
|
||||
>
|
||||
{person.namn}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className="min-h-12 rounded-lg border-2 border-zinc-600 font-bold text-zinc-400"
|
||||
onClick={() => setOppen(false)}
|
||||
>
|
||||
Avbryt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Oversikt() {
|
||||
const navigate = useNavigate();
|
||||
const { arenden, laggInArende, sammanfoga } = useFelsokning();
|
||||
@@ -138,13 +197,16 @@ export default function Oversikt() {
|
||||
<p className="mt-1 text-xl font-extrabold">{rad.objekt ?? "Okänt objekt"}</p>
|
||||
{rad.felbeskrivning && <p className="text-lg text-zinc-200">”{rad.felbeskrivning}”</p>}
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
{rad.antal_handelser} händelser
|
||||
{rad.tekniker?.length ? ` · ${rad.tekniker.join(", ")}` : ""}
|
||||
Ansvarig: <span className="font-bold text-zinc-300">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
||||
{` · ${rad.antal_handelser} händelser`}
|
||||
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||
</p>
|
||||
<StorKnapp variant="sekundar" className="mt-2" onClick={() => oppna(rad)}>
|
||||
Öppna ärendet
|
||||
</StorKnapp>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => oppna(rad)}>
|
||||
Öppna ärendet
|
||||
</StorKnapp>
|
||||
{!rad.avslutat && <Omfordela rad={rad} vidKlar={() => hamtaOversikt().then(setRader)} />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -59,7 +59,7 @@ as $$
|
||||
select jsonb_agg(to_jsonb(h) order by h.tidpunkt, h.id)
|
||||
from felsokning_handelser h
|
||||
where h.arende_id = a.id
|
||||
and h.handelse->>'typ' not in ('kategori_byte', 'hypotes', 'ai_svar')
|
||||
and h.handelse->>'typ' not in ('kategori_byte', 'hypotes', 'ai_svar', 'ansvarig_satt')
|
||||
), '[]'::jsonb)
|
||||
)
|
||||
from felsokning_arenden a
|
||||
|
||||
Reference in New Issue
Block a user