Arbetsledarvyn: organisationsöversikt med status och statistik
Rollen arbetsledare får sin vy enligt Master Prompt ('se alla ärenden,
följa status, skapa statistik'):
- Ny behörighetsstyrd endpoint GET /api/oversikt (arbetsledare/admin):
organisationens alla ärenden med status, antal händelser, deltagande
tekniker, objekt och felbeskrivning — allt härlett ur händelseloggen
i en SQL-fråga, aldrig lagrat separat. Tekniker nekas med 403.
- Ny sida /felsokning/oversikt: statistikpanel (totalt/pågående/
avslutade + genomsnittlig ledtid), ärendelista med status och
'Öppna ärendet' som hämtar händelserna till enheten — konfliktfri
flätning om en lokal kopia redan finns. Länkas från kontopanelen
för arbetsledare/admin.
- Integrationstestet utökat: tekniker nekas översikten, arbetsledaren
ser organisationens ärenden och härledningarna (felbeskrivning,
status, händelseantal) stämmer.
Ej implementerat (medvetet): omfördelning av ärenden — överlämnings-
händelsen täcker handover tills en ansvarig-modell införs.
Verifierat: integrationstestets 17 kontroller gröna mot Postgres 16,
28 vitest-tester gröna, produktionsbygge ok, Playwright-röktest grönt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
+1
-1
@@ -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. |
|
| 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. |
|
| 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. |
|
| 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. Integrationstestat mot riktig Postgres (isolering, rollstyrning, append-only). |
|
| 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). |
|
||||||
| 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. |
|
| 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. |
|
| 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 (kund/intern/partner) i nästa fas. |
|
| 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 (kund/intern/partner) i nästa fas. |
|
||||||
|
|||||||
@@ -99,4 +99,17 @@ kontroll "org A har två användare" "$ANTAL_ANV" "2"
|
|||||||
ANTAL=$(curl -s "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_J" | falt .handelser.length)
|
ANTAL=$(curl -s "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_J" | falt .handelser.length)
|
||||||
kontroll "tekniker i samma org når ärendet" "$ANTAL" "2"
|
kontroll "tekniker i samma org når ärendet" "$ANTAL" "2"
|
||||||
|
|
||||||
|
# 9. Organisationsöversikten: tekniker nekas; arbetsledare får status ur loggen
|
||||||
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/oversikt" -H "Authorization: Bearer $TOKEN_J")
|
||||||
|
kontroll "tekniker nekas översikten" "$KOD" "403"
|
||||||
|
curl -s -X POST "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"epost":"lisa@a.se","losenord":"hemligt123","namn":"Lisa","roll":"arbetsledare"}' >/dev/null
|
||||||
|
TOKEN_L=$(curl -s -X POST "$BAS/api/auth/logga-in" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"epost":"lisa@a.se","losenord":"hemligt123"}' | falt .token)
|
||||||
|
OVERSIKT=$(curl -s "$BAS/api/oversikt" -H "Authorization: Bearer $TOKEN_L")
|
||||||
|
kontroll "arbetsledaren ser organisationens ärenden" "$(echo "$OVERSIKT" | falt .arenden.length)" "1"
|
||||||
|
kontroll "översikten härleder felbeskrivning" "$(echo "$OVERSIKT" | falt '.arenden[0].felbeskrivning')" "Startar inte"
|
||||||
|
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"
|
||||||
|
|
||||||
echo "Integrationstest: allt grönt"
|
echo "Integrationstest: allt grönt"
|
||||||
|
|||||||
@@ -220,6 +220,35 @@ export function skapaServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Organisationsöversikt för arbetsledare/admin: alla ärenden med
|
||||||
|
// status, deltagande tekniker och sammanfattning — härlett ur
|
||||||
|
// händelseloggen, aldrig lagrat separat.
|
||||||
|
if (req.method === "GET" && vag === "/api/oversikt") {
|
||||||
|
if (anspr.roll !== "arbetsledare" && anspr.roll !== "admin") {
|
||||||
|
return svara(res, 403, { error: "Kräver arbetsledar- eller administratörsbehörighet." });
|
||||||
|
}
|
||||||
|
const rader = await pool.query(
|
||||||
|
`select a.id, a.nummer, a.skapad, a.delningskod, a.metodik_id,
|
||||||
|
count(h.id)::int as antal_handelser,
|
||||||
|
min(h.tidpunkt) as forsta,
|
||||||
|
max(h.tidpunkt) as senaste,
|
||||||
|
coalesce(bool_or(h.handelse->>'typ' = 'arende_avslutat'), false) as avslutat,
|
||||||
|
(array_agg(h.handelse->'objekt'->>'beskrivning')
|
||||||
|
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(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
|
||||||
|
where a.organisation_id = $1
|
||||||
|
group by a.id
|
||||||
|
order by max(h.tidpunkt) desc nulls last
|
||||||
|
limit 200`,
|
||||||
|
[anspr.org],
|
||||||
|
);
|
||||||
|
return svara(res, 200, { arenden: rader.rows });
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && vag === "/api/arenden") {
|
if (req.method === "GET" && vag === "/api/arenden") {
|
||||||
const rader = await pool.query(
|
const rader = await pool.query(
|
||||||
`select id, nummer, skapad, delningskod, metodik_id from felsokning_arenden
|
`select id, nummer, skapad, delningskod, metodik_id from felsokning_arenden
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import NyttArende from "./pages/felsokning/NyttArende";
|
|||||||
import ArendeSida from "./pages/felsokning/ArendeSida";
|
import ArendeSida from "./pages/felsokning/ArendeSida";
|
||||||
import DelatArende from "./pages/felsokning/DelatArende";
|
import DelatArende from "./pages/felsokning/DelatArende";
|
||||||
import PublikDelning from "./pages/felsokning/PublikDelning";
|
import PublikDelning from "./pages/felsokning/PublikDelning";
|
||||||
|
import Oversikt from "./pages/felsokning/Oversikt";
|
||||||
import { PaymentTestModeBanner } from "@/components/PaymentTestModeBanner";
|
import { PaymentTestModeBanner } from "@/components/PaymentTestModeBanner";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -43,6 +44,7 @@ function AppContent() {
|
|||||||
<Route path="/felsokning/arende/:id" element={<ArendeSida />} />
|
<Route path="/felsokning/arende/:id" element={<ArendeSida />} />
|
||||||
<Route path="/felsokning/dela/:id" element={<DelatArende />} />
|
<Route path="/felsokning/dela/:id" element={<DelatArende />} />
|
||||||
<Route path="/felsokning/delad/:kod" element={<PublikDelning />} />
|
<Route path="/felsokning/delad/:kod" element={<PublikDelning />} />
|
||||||
|
<Route path="/felsokning/oversikt" element={<Oversikt />} />
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -98,6 +98,28 @@ export async function skapaAnvandare(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Organisationsöversikt (arbetsledare/admin; servern verifierar).
|
||||||
|
export interface OversiktsRad {
|
||||||
|
id: string;
|
||||||
|
nummer: number;
|
||||||
|
skapad: string;
|
||||||
|
delningskod: string | null;
|
||||||
|
metodik_id: string | null;
|
||||||
|
antal_handelser: number;
|
||||||
|
forsta: string | null;
|
||||||
|
senaste: string | null;
|
||||||
|
avslutat: boolean;
|
||||||
|
objekt: string | null;
|
||||||
|
felbeskrivning: string | null;
|
||||||
|
tekniker: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hamtaOversikt(): Promise<OversiktsRad[]> {
|
||||||
|
const res = await plattformFetch("/api/oversikt");
|
||||||
|
if (!res.ok) throw new Error(`Fel ${res.status}`);
|
||||||
|
return ((await res.json()) as { arenden: OversiktsRad[] }).arenden;
|
||||||
|
}
|
||||||
|
|
||||||
// Autentiserat anrop mot plattformen. En utgången token rensas (401) så
|
// Autentiserat anrop mot plattformen. En utgången token rensas (401) så
|
||||||
// att appen faller tillbaka till lokalt läge tills nästa inloggning.
|
// att appen faller tillbaka till lokalt läge tills nästa inloggning.
|
||||||
export async function plattformFetch(vag: string, init?: RequestInit): Promise<Response> {
|
export async function plattformFetch(vag: string, init?: RequestInit): Promise<Response> {
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ function PlattformInloggning() {
|
|||||||
Logga ut
|
Logga ut
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
||||||
|
<Link to="/felsokning/oversikt" className="mt-3 block">
|
||||||
|
<StorKnapp variant="sekundar">📊 Organisationsöversikt</StorKnapp>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
{konto.roll === "admin" && <AnvandarAdmin />}
|
{konto.roll === "admin" && <AnvandarAdmin />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// Arbetsledarvyn: organisationens alla ärenden med status och statistik.
|
||||||
|
// Endast arbetsledare/admin (servern verifierar behörigheten). Ett ärende
|
||||||
|
// kan hämtas till den här enheten — händelserna flätas ihop konfliktfritt
|
||||||
|
// med eventuell lokal kopia.
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import type { Arende } from "@/felsokning/domain";
|
||||||
|
import {
|
||||||
|
hamtaOversikt,
|
||||||
|
plattformAktiv,
|
||||||
|
plattformFetch,
|
||||||
|
plattformKonto,
|
||||||
|
type OversiktsRad,
|
||||||
|
} from "@/felsokning/plattform";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default function Oversikt() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { arenden, laggInArende, sammanfoga } = useFelsokning();
|
||||||
|
const [rader, setRader] = useState<OversiktsRad[] | null>(null);
|
||||||
|
const [fel, setFel] = useState("");
|
||||||
|
const konto = plattformKonto();
|
||||||
|
const behorig = plattformAktiv() && (konto?.roll === "arbetsledare" || konto?.roll === "admin");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!behorig) return;
|
||||||
|
hamtaOversikt()
|
||||||
|
.then(setRader)
|
||||||
|
.catch(() => setFel("Kunde inte hämta översikten — kontrollera inloggningen."));
|
||||||
|
}, [behorig]);
|
||||||
|
|
||||||
|
if (!behorig) {
|
||||||
|
return (
|
||||||
|
<FelsokningSkal rubrik="Organisationsöversikt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||||
|
<Panel>
|
||||||
|
<p className="text-lg text-zinc-300">
|
||||||
|
Översikten kräver arbetsledar- eller administratörsbehörighet på plattformen.
|
||||||
|
</p>
|
||||||
|
</Panel>
|
||||||
|
</FelsokningSkal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hämtar ärendets händelser och materialiserar det lokalt (flätning om
|
||||||
|
// en lokal kopia redan finns), sedan öppnas arbetsytan.
|
||||||
|
const oppna = async (rad: OversiktsRad) => {
|
||||||
|
try {
|
||||||
|
const res = await plattformFetch(`/api/arenden/${rad.id}/handelser`);
|
||||||
|
if (!res.ok) throw new Error(`Fel ${res.status}`);
|
||||||
|
const { handelser } = (await res.json()) as {
|
||||||
|
handelser: { id: string; tidpunkt: string; anvandare: string; handelse: Arende["handelser"][number]["handelse"] }[];
|
||||||
|
};
|
||||||
|
const fjarr = handelser.map((h) => ({ ...h, tidpunkt: new Date(h.tidpunkt).toISOString() }));
|
||||||
|
const lokal = arenden[rad.id];
|
||||||
|
if (lokal) {
|
||||||
|
sammanfoga(rad.id, flataIhop(lokal.handelser, fjarr));
|
||||||
|
} else {
|
||||||
|
laggInArende({
|
||||||
|
id: rad.id,
|
||||||
|
nummer: rad.nummer,
|
||||||
|
skapad: new Date(rad.skapad).toISOString(),
|
||||||
|
delningskod: rad.delningskod ?? undefined,
|
||||||
|
metodikId: rad.metodik_id ?? undefined,
|
||||||
|
handelser: fjarr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
navigate(`/felsokning/arende/${rad.id}`);
|
||||||
|
} catch {
|
||||||
|
setFel("Kunde inte hämta ärendet.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pagaende = rader?.filter((r) => !r.avslutat) ?? [];
|
||||||
|
const avslutade = rader?.filter((r) => r.avslutat) ?? [];
|
||||||
|
const medelLedtidMs =
|
||||||
|
avslutade.length > 0
|
||||||
|
? avslutade.reduce(
|
||||||
|
(summa, r) =>
|
||||||
|
summa + (r.forsta && r.senaste ? new Date(r.senaste).getTime() - new Date(r.forsta).getTime() : 0),
|
||||||
|
0,
|
||||||
|
) / avslutade.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FelsokningSkal
|
||||||
|
rubrik="Organisationsöversikt"
|
||||||
|
tillbaka={{ till: "/felsokning", text: "Ärenden" }}
|
||||||
|
hoger={<span className="text-sm font-bold text-zinc-400">{konto?.organisation}</span>}
|
||||||
|
>
|
||||||
|
{fel && <p className="mb-3 font-bold text-red-400">{fel}</p>}
|
||||||
|
{!rader && !fel && <p className="animate-pulse text-lg text-zinc-400">Hämtar …</p>}
|
||||||
|
|
||||||
|
{rader && (
|
||||||
|
<>
|
||||||
|
<Panel rubrik="Statistik">
|
||||||
|
<div className="grid grid-cols-3 gap-3 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-extrabold">{rader.length}</p>
|
||||||
|
<p className="text-sm font-bold uppercase text-zinc-400">Totalt</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-extrabold text-amber-400">{pagaende.length}</p>
|
||||||
|
<p className="text-sm font-bold uppercase text-zinc-400">Pågående</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-extrabold">{avslutade.length}</p>
|
||||||
|
<p className="text-sm font-bold uppercase text-zinc-400">Avslutade</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{avslutade.length > 0 && (
|
||||||
|
<p className="mt-3 text-center text-zinc-300">
|
||||||
|
Genomsnittlig ledtid för avslutade: <span className="font-extrabold">{formateraTid(medelLedtidMs)}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
{rader.length === 0 && <p className="text-center text-lg text-zinc-400">Inga ärenden i organisationen ännu.</p>}
|
||||||
|
|
||||||
|
{rader.map((rad) => (
|
||||||
|
<div key={rad.id} className="mb-3 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-4">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-sm font-extrabold uppercase tracking-widest text-zinc-400">
|
||||||
|
Ärende #{rad.nummer}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`rounded px-2 py-0.5 text-xs font-extrabold uppercase ${
|
||||||
|
rad.avslutat ? "bg-zinc-700 text-zinc-300" : "bg-amber-400 text-zinc-950"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{rad.avslutat ? "Avslutat" : "Pågående"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<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(", ")}` : ""}
|
||||||
|
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||||
|
</p>
|
||||||
|
<StorKnapp variant="sekundar" className="mt-2" onClick={() => oppna(rad)}>
|
||||||
|
Öppna ärendet
|
||||||
|
</StorKnapp>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FelsokningSkal>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user