- 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]>
86 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|