Files
ApiCore/ApiCore-Frontend/src/components/hive/hive-conflict-sheet.tsx
T
Nick SzaunigandClaude Opus 5.5 c608f5d405 feat: hive card with SVG, outline, edit sheets and catalog settings (Beute L2)
- lib/hive: frontend port of the hive domain (ops, validator, facts,
  layout, follow-ups, describe texts, reason texts) running the backend
  fixtures, API client
- hive card on the colony page (same height as Stammdaten): SVG view,
  accessible outline tree, frame/part sheets with optimistic save,
  conflict sheet, history with past states, init from template or
  another colony, inspection draft mode
- follow-up toasts prefilling feeding and treatment forms (treatment
  end only with an open treatment)
- settings page "Beutensysteme" (hive-systems:manage)
- NumberStepper decimal steps, reactive motion level hook, holo tokens
- spec: owner design decision 2026-10-16 (glass hologram, focus mode)

The SVG view is the fallback; the 3D scene follows in L3.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 09:12:03 +02:00

86 lines
3.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { SheetFooter } from "@/components/ui/sheet";
import { hiveReasonText } from "@/lib/hive/reason-text";
import { describeHiveOps } from "@/lib/hive/describe";
import type { HiveCatalog } from "@/lib/hive/types";
import { HiveSheetShell } from "./hive-sheet-shell";
import type { HiveConflict } from "./use-hive-mutation";
import { SAVE_FAILED_TEXT } from "./use-sheet-submit";
export interface HiveConflictSheetProps {
conflict: HiveConflict | null;
catalog: HiveCatalog;
onResolve: (action: "retry" | "discard") => Promise<void>;
}
/** Spec section 11: the hive changed meanwhile; the own ops are shown on the current state. */
export function HiveConflictSheet({ conflict, catalog, onResolve }: HiveConflictSheetProps) {
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
async function resolve(action: "retry" | "discard") {
setBusy(true);
setError(null);
try {
await onResolve(action);
} catch {
setError(SAVE_FAILED_TEXT);
} finally {
setBusy(false);
}
}
const preview = conflict?.preview;
const failedOp = conflict && preview && !preview.ok ? conflict.ops.slice(preview.opIndex, preview.opIndex + 1) : [];
return (
<HiveSheetShell
open={conflict !== null}
onOpenChange={(open) => {
// Closing counts as discard, but not while a request is running.
if (!open && !busy) void resolve("discard");
}}
title="Die Beute wurde inzwischen geändert"
description="Jemand anderes hat den Aufbau verändert. Deine Änderung wurde noch nicht gespeichert."
>
{conflict && preview ? (
<div className="flex flex-col gap-4 px-4 pb-4">
{preview.ok ? (
<p className="text-sm">
<span className="font-medium">Deine Änderung auf dem neuen Stand: </span>
{describeHiveOps(conflict.ops, conflict.current.state, catalog)}
</p>
) : (
<p role="alert" className="text-sm text-destructive">
Deine Änderung passt nicht mehr:{" "}
{describeHiveOps(failedOp, conflict.current.state, catalog) || "Schritt " + (preview.opIndex + 1)}.
<span className="mt-1 block text-xs text-muted-foreground">{hiveReasonText(preview.reason)}</span>
</p>
)}
{error ? (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
) : null}
<SheetFooter className="p-0">
<Button
type="button"
size="touch"
disabled={!preview.ok || busy}
onClick={() => void resolve("retry")}
>
Erneut speichern
</Button>
<Button type="button" variant="outline" size="touch" disabled={busy} onClick={() => void resolve("discard")}>
Verwerfen
</Button>
</SheetFooter>
</div>
) : null}
</HiveSheetShell>
);
}