Files
EpicNext-Cms/src/components/admin/studio/furniture-inspector.tsx
T
Simo c389c3893d
CI / check (push) Successful in 52s
CI / deploy (push) Successful in 2m10s
CI / publish-container (push) Failing after 1m18s
feat(cms): improve catalog, editorial recovery and operations
2026-09-09 19:36:15 +02:00

169 lines
4.6 KiB
TypeScript

"use client";
import { Button } from "@/components/ui/button";
import {
compareFurniture,
type FurnitureInspection,
furnitureHealth,
} from "@/lib/furni/studio-inspection";
import { FurnitureCompleteness } from "./furniture-completeness";
import type { FurniItem } from "./studio-types";
import { useFurnitureInspection } from "./use-furniture-inspection";
export function FurnitureComparison({
item,
local,
}: {
item: FurniItem;
local: FurnitureInspection;
}) {
const comparison = compareFurniture(item, local);
return (
<div className="space-y-3 text-sm">
<FurnitureCompleteness local={local} />
{!local.furnidataReadable && (
<p className="text-[var(--admin-warning)]">
Local furnidata could not be read. Its values are not verified.
</p>
)}
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr>
<th className="p-2 text-left">Field</th>
<th className="p-2 text-left">Local now</th>
<th className="p-2 text-left">Source</th>
</tr>
</thead>
<tbody>
{comparison.fields.map((field) => (
<tr
key={field.label}
className={field.changed ? "bg-[var(--admin-warning)]/10" : ""}
>
<th className="p-2 text-left">
{field.label}
{field.changed ? " · Changed" : ""}
</th>
<td className="max-w-48 break-words p-2">
{field.before ?? "—"}
</td>
<td className="max-w-48 break-words p-2">
{field.after ?? "Not supplied"}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-xs text-[var(--admin-text-muted)]">
Source values are compared with local data. Import may remap IDs and
translate names. Existing furniture keeps its local IDs and SQL
settings; new furniture receives a free local ID when the source ID is
occupied.
</p>
<details>
<summary className="cursor-pointer font-medium">
SQL records and catalog offers
</summary>
<div className="mt-2 space-y-2 text-xs">
{local.sql.length ? (
local.sql.map((row) => (
<p key={row.id}>
Item #{row.id} · Sprite #{row.spriteId} · {row.name}
</p>
))
) : (
<p>No local SQL item.</p>
)}
{local.catalog.map((row) => (
<p key={row.id}>
Offer #{row.id} · Page #{row.pageId} · {row.credits} credits +{" "}
{row.points} points
</p>
))}
</div>
</details>
</div>
);
}
export function FurnitureInspector({
item,
onRepair,
busy = false,
}: {
item: FurniItem;
onRepair?: () => Promise<unknown>;
busy?: boolean;
}) {
const inspection = useFurnitureInspection([item.classname]);
if (inspection.loading)
return (
<p role="status" className="py-3 text-sm">
Checking local data and assets…
</p>
);
if (inspection.error)
return (
<div role="alert">
<p>{inspection.error}</p>
<Button variant="outline" onClick={inspection.retry}>
Retry inspection
</Button>
</div>
);
const local = inspection.items.find(
(entry) => entry.classname === item.classname,
);
const health = local ? furnitureHealth(local) : null;
return local && health ? (
<div className="space-y-3">
<section className="rounded-md border border-[var(--admin-border)] p-3 space-y-2">
<strong>
{
{
complete: "Complete",
incomplete: "Needs repair",
new: "Not imported",
conflict: "Local conflict",
unknown: "Not verified",
}[health.state]
}
</strong>
{health.missing.length > 0 && (
<p className="text-sm">Missing: {health.missing.join(", ")}</p>
)}
{health.state === "conflict" && (
<p className="text-sm">
Duplicate records or mismatched IDs/types require review before
repair.
</p>
)}
{health.state === "unknown" && (
<p className="text-sm">
Some local data could not be checked. Retry verification before
repair.
</p>
)}
{health.state === "incomplete" && onRepair && (
<>
<p className="text-sm text-[var(--admin-text-muted)]">
Repair adds missing parts and preserves existing IDs, settings,
furnidata and catalog offers. Missing offers use the automatic
category and price shown below.
</p>
<Button disabled={busy} onClick={() => void onRepair()}>
{busy ? "Repair queued or running…" : "Repair furni"}
</Button>
</>
)}
<Button variant="outline" disabled={busy} onClick={inspection.retry}>
Recheck local state
</Button>
</section>
<FurnitureComparison item={item} local={local} />
</div>
) : (
<p>Local inspection unavailable.</p>
);
}