kirche-einfach/frontend/src/components/ApprovalsTab.tsx
Jonas Miederer 0a2b73f3b4 feat: minimal frontend (run view, approvals, admin) with E2E smoke
- Tabs: Vorgaenge (run flows + history), Freigaben (decide tasks),
  Administration (Langflow link, import, deploy)
- Typed API client over vite /api proxy; tsc + vite build clean
- scripts/smoke_e2e.py: real LFX + API end-to-end, all green
- 34 backend tests green
2026-09-06 23:13:05 +00:00

95 lines
2.8 KiB
TypeScript
Raw 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.

import { useCallback, useEffect, useState } from "react";
import { ApiError, api, type ApprovalTask } from "../api";
export default function ApprovalsTab() {
const [tasks, setTasks] = useState<ApprovalTask[]>([]);
const [approver, setApprover] = useState("");
const [note, setNote] = useState("");
const [error, setError] = useState("");
const [busyId, setBusyId] = useState("");
const load = useCallback(async () => {
try {
setTasks(await api.listApprovals());
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
}
}, []);
useEffect(() => {
void load();
}, [load]);
async function decide(taskId: string, decision: "approve" | "reject") {
setBusyId(taskId);
setError("");
try {
const result = await api.decide(taskId, decision, approver, note);
setTasks((prev) =>
prev.map((t) => (t.id === taskId ? result.task : t)),
);
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
} finally {
setBusyId("");
}
}
const open = tasks.filter((t) => t.status === "open");
const done = tasks.filter((t) => t.status !== "open");
return (
<div>
<h2>Freigaben</h2>
{error && <p style={{ color: "crimson" }}>{error}</p>}
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<input
value={approver}
onChange={(e) => setApprover(e.target.value)}
placeholder="Name (Bearbeiter)"
/>
<input
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Notiz"
style={{ flex: 1 }}
/>
<button onClick={() => void load()}>Aktualisieren</button>
</div>
<h3>Offen ({open.length})</h3>
{open.length === 0 && <p>Keine offenen Freigaben.</p>}
<ul>
{open.map((t) => (
<li key={t.id} style={{ marginBottom: 8 }}>
<strong>{t.title || t.id}</strong> · {t.tenant_id}
<br />
<code>{JSON.stringify(t.payload)}</code>
<br />
<button
onClick={() => void decide(t.id, "approve")}
disabled={busyId === t.id}
>
Freigeben
</button>{" "}
<button
onClick={() => void decide(t.id, "reject")}
disabled={busyId === t.id}
>
Ablehnen
</button>
</li>
))}
</ul>
<h3>Entschieden ({done.length})</h3>
<ul>
{done.map((t) => (
<li key={t.id}>
<strong>{t.status}</strong> · {t.title || t.id} ·{" "}
{t.approver ?? "–"}
{t.decision_note ? ` · „${t.decision_note}“` : ""}
</li>
))}
</ul>
</div>
);
}