t-paliad-340 — B0 of edison's 7-slice train (PRD §7.1). DB-only: schema + RLS land, dev-only test route exercises the surface, no user-facing change. B1 wires the actual builder UI on top. Migration 157 (additive on the legacy mig-145 scenarios table — 0 rows in prod, safe to relax): - paliad.scenarios gets owner_id / status / origin_project_id / promoted_project_id / stichtag / notes. spec drops NOT NULL and the scenarios_unique_per_scope constraint drops (the builder allows multiple scratch + Unbenanntes Szenario rows per user). - New tables: scenario_proceedings, scenario_events, scenario_shares. - paliad.projects.origin_scenario_id for the promote-to-project audit trail (the FK lands now; the wizard ships in B5). - paliad.can_see_scenario(uuid) STABLE SECURITY DEFINER helper covering owner / share / global_admin / two legacy paths. - Replacement RLS on scenarios + RLS on the three new tables; legacy service + handlers stay live and unchanged. PRD §5.1 deviations called out in the migration header: - proceeding_type_id is integer (live schema), not uuid (PRD draft). - FK target is paliad.users, matching the rest of paliad's schema. Go surface: - ScenarioBuilderService — list/create/get-deep/patch scenarios, add/patch/delete proceedings, add/patch/delete events, add/delete shares. Writes wrap in transactions with set_config( paliad.audit_reason, ..., true) per event_choice_service.go pattern. - /api/builder/scenarios/* — handlers register under a builder/ prefix so the legacy /api/scenarios surface still works. - /dev/scenario-builder — single-page HTML form gated to PaliadinOwnerEmail, exercises the B0 surface without Postman. - Live-DB integration test (TEST_DATABASE_URL gated) covers create + list + deep-get + share + visibility negatives + patch. Audit-first: every DDL block ran clean via BEGIN/ROLLBACK against the live DB before commit; end-to-end sanity (insert chain + CHECK constraints + CASCADE-on-delete) verified via the Supabase MCP. bun build clean. go vet + go test -short ./... green.
590 lines
19 KiB
Go
590 lines
19 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/services"
|
|
)
|
|
|
|
// t-paliad-340 / m/paliad#153 B0 — REST endpoints over the new normalised
|
|
// scenario builder shape (paliad.scenarios with owner_id, +
|
|
// paliad.scenario_proceedings / scenario_events / scenario_shares).
|
|
//
|
|
// Endpoints live under /api/builder/scenarios/* to avoid clashing with
|
|
// the legacy /api/scenarios/* endpoints from m/paliad#124 Slice D. The
|
|
// B6 cleanup slice retires the legacy surface; until then both shapes
|
|
// coexist on the same paliad.scenarios table (the legacy paths require
|
|
// project_id IS NOT NULL OR an abstract created_by = caller; the builder
|
|
// paths require owner_id = caller).
|
|
//
|
|
// All handlers gate by requireScenarioBuilderService — 503 when the
|
|
// service is nil (DATABASE_URL unset). Auth is checked via requireUser;
|
|
// per-row visibility is enforced inside the service.
|
|
|
|
func requireScenarioBuilderService(w http.ResponseWriter) bool {
|
|
if dbSvc == nil || dbSvc.scenarioBuilder == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
|
|
"error": "Litigation-Builder ist vorübergehend nicht verfügbar (keine Datenbank).",
|
|
})
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// scenarioBuilderErrorToStatus maps service errors to HTTP statuses.
|
|
func scenarioBuilderErrorToStatus(err error) (int, string) {
|
|
switch {
|
|
case errors.Is(err, services.ErrScenarioBuilderNotVisible),
|
|
errors.Is(err, services.ErrNotVisible):
|
|
return http.StatusNotFound, "Szenario nicht gefunden"
|
|
case errors.Is(err, services.ErrInvalidInput):
|
|
return http.StatusBadRequest, err.Error()
|
|
}
|
|
return http.StatusInternalServerError, err.Error()
|
|
}
|
|
|
|
func writeBuilderError(w http.ResponseWriter, err error) {
|
|
status, msg := scenarioBuilderErrorToStatus(err)
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scenario CRUD
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleBuilderScenariosList — GET /api/builder/scenarios?status=<active|archived|promoted|all>
|
|
func handleBuilderScenariosList(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
status := r.URL.Query().Get("status")
|
|
out, err := dbSvc.scenarioBuilder.ListMyScenarios(r.Context(), uid, status)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// handleBuilderScenarioCreate — POST /api/builder/scenarios
|
|
func handleBuilderScenarioCreate(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var input services.CreateBuilderScenarioInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.CreateScenario(r.Context(), uid, input)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, out)
|
|
}
|
|
|
|
// handleBuilderScenarioGet — GET /api/builder/scenarios/{id}
|
|
func handleBuilderScenarioGet(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige ID"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.GetScenarioDeep(r.Context(), uid, id)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// handleBuilderScenarioPatch — PATCH /api/builder/scenarios/{id}
|
|
func handleBuilderScenarioPatch(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige ID"})
|
|
return
|
|
}
|
|
var input services.PatchBuilderScenarioInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.PatchScenario(r.Context(), uid, id, input)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Proceedings
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleBuilderProceedingCreate — POST /api/builder/scenarios/{id}/proceedings
|
|
func handleBuilderProceedingCreate(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
sid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
var input services.AddProceedingInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.AddProceeding(r.Context(), uid, sid, input)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, out)
|
|
}
|
|
|
|
// handleBuilderProceedingPatch — PATCH /api/builder/scenarios/{id}/proceedings/{pid}
|
|
func handleBuilderProceedingPatch(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
sid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
pid, err := uuid.Parse(r.PathValue("pid"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Proceeding-ID"})
|
|
return
|
|
}
|
|
var input services.PatchProceedingInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.PatchProceeding(r.Context(), uid, sid, pid, input)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// handleBuilderProceedingDelete — DELETE /api/builder/scenarios/{id}/proceedings/{pid}
|
|
func handleBuilderProceedingDelete(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
sid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
pid, err := uuid.Parse(r.PathValue("pid"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Proceeding-ID"})
|
|
return
|
|
}
|
|
if err := dbSvc.scenarioBuilder.DeleteProceeding(r.Context(), uid, sid, pid); err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Events
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleBuilderEventCreate — POST /api/builder/scenarios/{id}/proceedings/{pid}/events
|
|
func handleBuilderEventCreate(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
sid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
pid, err := uuid.Parse(r.PathValue("pid"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Proceeding-ID"})
|
|
return
|
|
}
|
|
var input services.AddEventInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.AddEvent(r.Context(), uid, sid, pid, input)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, out)
|
|
}
|
|
|
|
// handleBuilderEventPatch — PATCH /api/builder/scenarios/{id}/events/{eid}
|
|
func handleBuilderEventPatch(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
sid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
eid, err := uuid.Parse(r.PathValue("eid"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Event-ID"})
|
|
return
|
|
}
|
|
var input services.PatchEventInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.PatchEvent(r.Context(), uid, sid, eid, input)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// handleBuilderEventDelete — DELETE /api/builder/scenarios/{id}/events/{eid}
|
|
func handleBuilderEventDelete(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
sid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
eid, err := uuid.Parse(r.PathValue("eid"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Event-ID"})
|
|
return
|
|
}
|
|
if err := dbSvc.scenarioBuilder.DeleteEvent(r.Context(), uid, sid, eid); err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shares
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleBuilderShareCreate — POST /api/builder/scenarios/{id}/shares
|
|
// Body: {"shared_with_user_id": "<uuid>"}
|
|
func handleBuilderShareCreate(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
sid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
var body struct {
|
|
SharedWithUserID uuid.UUID `json:"shared_with_user_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"})
|
|
return
|
|
}
|
|
out, err := dbSvc.scenarioBuilder.AddShare(r.Context(), uid, sid, body.SharedWithUserID)
|
|
if err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, out)
|
|
}
|
|
|
|
// handleBuilderShareDelete — DELETE /api/builder/scenarios/{id}/shares/{sid}
|
|
func handleBuilderShareDelete(w http.ResponseWriter, r *http.Request) {
|
|
if !requireScenarioBuilderService(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
scid, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"})
|
|
return
|
|
}
|
|
shid, err := uuid.Parse(r.PathValue("sid"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Share-ID"})
|
|
return
|
|
}
|
|
if err := dbSvc.scenarioBuilder.DeleteShare(r.Context(), uid, scid, shid); err != nil {
|
|
writeBuilderError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dev-only test route
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleBuilderDevTestPage — GET /dev/scenario-builder
|
|
//
|
|
// Gated to services.PaliadinOwnerEmail (the same single-owner gate the
|
|
// /paliadin route uses). Every other authenticated user gets 404. Pure
|
|
// HTML — no JS bundle — so the page works even before B1 wires the real
|
|
// builder shell. Renders curl-equivalent forms for the B0 surface so the
|
|
// schema can be exercised end-to-end without Postman / shell scripts.
|
|
//
|
|
// This is the "dev-only test route" the head's task spec asked for. It
|
|
// disappears in B6 cleanup once the production builder UI ships at
|
|
// /tools/procedures.
|
|
func handleBuilderDevTestPage(w http.ResponseWriter, r *http.Request) {
|
|
if !requirePaliadinOwner(w, r) {
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
_, _ = w.Write([]byte(builderDevTestHTML))
|
|
}
|
|
|
|
const builderDevTestHTML = `<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Scenario Builder — Dev Test (B0)</title>
|
|
<style>
|
|
body { font-family: ui-monospace, Menlo, monospace; max-width: 880px; margin: 2em auto;
|
|
padding: 0 1em; color: #222; background: #fafaf7; }
|
|
h1, h2 { font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
h1 { border-bottom: 4px solid #c6f41c; padding-bottom: .2em; }
|
|
section { background: #fff; border: 1px solid #ddd; border-radius: 4px;
|
|
padding: 1em 1.2em; margin: 1em 0; }
|
|
label { display: block; margin: .4em 0 .15em; font-size: .85em; color: #555; }
|
|
input, textarea, select, button { font: inherit; padding: .35em .5em; box-sizing: border-box; }
|
|
input[type="text"], input[type="number"], textarea { width: 100%; }
|
|
button { background: #c6f41c; border: 1px solid #9ec61f; cursor: pointer;
|
|
padding: .4em 1em; border-radius: 3px; margin: .2em 0; }
|
|
button.secondary { background: #eee; border-color: #ccc; }
|
|
pre.out { background: #1e1e1e; color: #e6e6e6; padding: .8em 1em; border-radius: 4px;
|
|
overflow: auto; max-height: 30em; font-size: .85em; }
|
|
.note { color: #777; font-size: .9em; }
|
|
.row { display: flex; gap: .5em; }
|
|
.row > * { flex: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Scenario Builder — Dev Test (B0)</h1>
|
|
<p class="note">t-paliad-340 / m/paliad#153 — DB-only slice. Exercises
|
|
paliad.scenarios (builder rows), scenario_proceedings, scenario_events,
|
|
scenario_shares via /api/builder/scenarios/*. Gated to PaliadinOwnerEmail.</p>
|
|
|
|
<section>
|
|
<h2>1. Liste meine Szenarien</h2>
|
|
<label>Status filter</label>
|
|
<select id="list-status">
|
|
<option value="">(default: alle)</option>
|
|
<option value="active">active</option>
|
|
<option value="archived">archived</option>
|
|
<option value="promoted">promoted</option>
|
|
<option value="all">all (explicit)</option>
|
|
</select>
|
|
<button onclick="listScenarios()">GET /api/builder/scenarios</button>
|
|
<pre class="out" id="list-out"></pre>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>2. Szenario anlegen</h2>
|
|
<label>Name</label>
|
|
<input type="text" id="create-name" placeholder="(leer = Unbenanntes Szenario)">
|
|
<label>Notes (optional)</label>
|
|
<textarea id="create-notes" rows="2"></textarea>
|
|
<button onclick="createScenario()">POST /api/builder/scenarios</button>
|
|
<pre class="out" id="create-out"></pre>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>3. Szenario abrufen (deep)</h2>
|
|
<label>Scenario ID</label>
|
|
<input type="text" id="get-id">
|
|
<button onclick="getScenario()">GET /api/builder/scenarios/{id}</button>
|
|
<pre class="out" id="get-out"></pre>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>4. Verfahren hinzufügen</h2>
|
|
<label>Scenario ID</label>
|
|
<input type="text" id="proc-sid">
|
|
<label>proceeding_type_id (integer)</label>
|
|
<input type="number" id="proc-pt-id" placeholder="z.B. 7 für upc.inf.cfi">
|
|
<label>primary_party</label>
|
|
<select id="proc-party">
|
|
<option value="">(none)</option>
|
|
<option value="claimant">claimant</option>
|
|
<option value="defendant">defendant</option>
|
|
</select>
|
|
<button onclick="addProceeding()">POST .../proceedings</button>
|
|
<pre class="out" id="proc-out"></pre>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>5. Event-Karte hinzufügen</h2>
|
|
<label>Scenario ID</label>
|
|
<input type="text" id="ev-sid">
|
|
<label>Proceeding ID</label>
|
|
<input type="text" id="ev-pid">
|
|
<label>custom_label (oder sequencing_rule_id / procedural_event_id)</label>
|
|
<input type="text" id="ev-label" placeholder="freitext-Karte">
|
|
<label>state</label>
|
|
<select id="ev-state">
|
|
<option value="planned">planned</option>
|
|
<option value="filed">filed</option>
|
|
<option value="skipped">skipped</option>
|
|
</select>
|
|
<button onclick="addEvent()">POST .../proceedings/{pid}/events</button>
|
|
<pre class="out" id="ev-out"></pre>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>6. Status patchen (archive / restore)</h2>
|
|
<label>Scenario ID</label>
|
|
<input type="text" id="patch-sid">
|
|
<label>new status</label>
|
|
<select id="patch-status">
|
|
<option value="active">active</option>
|
|
<option value="archived">archived</option>
|
|
</select>
|
|
<button onclick="patchStatus()">PATCH /api/builder/scenarios/{id}</button>
|
|
<pre class="out" id="patch-out"></pre>
|
|
</section>
|
|
|
|
<script>
|
|
const j = (id, payload) =>
|
|
document.getElementById(id).textContent = JSON.stringify(payload, null, 2);
|
|
|
|
async function call(method, url, body) {
|
|
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
const r = await fetch(url, opts);
|
|
const text = await r.text();
|
|
let parsed = text;
|
|
try { parsed = JSON.parse(text); } catch (_) {}
|
|
return { status: r.status, body: parsed };
|
|
}
|
|
|
|
async function listScenarios() {
|
|
const status = document.getElementById('list-status').value;
|
|
const q = status ? '?status=' + encodeURIComponent(status) : '';
|
|
j('list-out', await call('GET', '/api/builder/scenarios' + q));
|
|
}
|
|
|
|
async function createScenario() {
|
|
const name = document.getElementById('create-name').value;
|
|
const notes = document.getElementById('create-notes').value;
|
|
const body = {};
|
|
if (name) body.name = name;
|
|
if (notes) body.notes = notes;
|
|
j('create-out', await call('POST', '/api/builder/scenarios', body));
|
|
}
|
|
|
|
async function getScenario() {
|
|
const id = document.getElementById('get-id').value.trim();
|
|
if (!id) return j('get-out', { error: 'ID erforderlich' });
|
|
j('get-out', await call('GET', '/api/builder/scenarios/' + id));
|
|
}
|
|
|
|
async function addProceeding() {
|
|
const sid = document.getElementById('proc-sid').value.trim();
|
|
const ptID = parseInt(document.getElementById('proc-pt-id').value, 10);
|
|
const party = document.getElementById('proc-party').value;
|
|
if (!sid || !ptID) return j('proc-out', { error: 'sid + proceeding_type_id erforderlich' });
|
|
const body = { proceeding_type_id: ptID };
|
|
if (party) body.primary_party = party;
|
|
j('proc-out', await call('POST', '/api/builder/scenarios/' + sid + '/proceedings', body));
|
|
}
|
|
|
|
async function addEvent() {
|
|
const sid = document.getElementById('ev-sid').value.trim();
|
|
const pid = document.getElementById('ev-pid').value.trim();
|
|
const label = document.getElementById('ev-label').value.trim();
|
|
const state = document.getElementById('ev-state').value;
|
|
if (!sid || !pid || !label) return j('ev-out', { error: 'sid + pid + custom_label erforderlich' });
|
|
j('ev-out', await call('POST',
|
|
'/api/builder/scenarios/' + sid + '/proceedings/' + pid + '/events',
|
|
{ custom_label: label, state }));
|
|
}
|
|
|
|
async function patchStatus() {
|
|
const sid = document.getElementById('patch-sid').value.trim();
|
|
const status = document.getElementById('patch-status').value;
|
|
if (!sid) return j('patch-out', { error: 'sid erforderlich' });
|
|
j('patch-out', await call('PATCH', '/api/builder/scenarios/' + sid, { status }));
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>`
|