205 lines
5.6 KiB
TypeScript
205 lines
5.6 KiB
TypeScript
"use client";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
compareFurniture,
|
|
type FurnitureInspection,
|
|
furnitureHealth,
|
|
} from "@/lib/furni/studio-inspection";
|
|
import type { FurniItem } from "./studio-types";
|
|
import { useFurnitureInspection } from "./use-furniture-inspection";
|
|
|
|
const status = (value: boolean | null | undefined) =>
|
|
value == null ? "Not verified" : value ? "Present" : "Missing";
|
|
export function FurnitureComparison({
|
|
item,
|
|
local,
|
|
}: {
|
|
item: FurniItem;
|
|
local: FurnitureInspection;
|
|
}) {
|
|
const comparison = compareFurniture(item, local);
|
|
return (
|
|
<div className="space-y-3 text-sm">
|
|
<dl className="grid grid-cols-2 gap-2">
|
|
{(
|
|
[
|
|
[".nitro", local.nitro.exists],
|
|
["Local icon", local.icon.exists],
|
|
[
|
|
"Furnidata",
|
|
local.furnidataReadable ? local.furnidata.length > 0 : null,
|
|
],
|
|
["SQL item", local.sql.length > 0],
|
|
["Catalog offer", local.catalog.length > 0],
|
|
] as const
|
|
).map(([label, present]) => (
|
|
<div
|
|
key={label}
|
|
className="rounded-md border border-[var(--admin-border)] p-2"
|
|
>
|
|
<dt className="text-xs text-[var(--admin-text-muted)]">{label}</dt>
|
|
<dd
|
|
className={
|
|
present
|
|
? "text-[var(--admin-success)]"
|
|
: "text-[var(--admin-warning)]"
|
|
}
|
|
>
|
|
{status(present)}
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
{comparison.state === "conflict" && (
|
|
<p role="alert" className="text-[var(--admin-warning)]">
|
|
Multiple local records use this classname. Review them in Catalog
|
|
audit before importing.
|
|
</p>
|
|
)}
|
|
{!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>
|
|
);
|
|
}
|