79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState } from "react";
|
||
|
|
import { Pencil } from "lucide-react";
|
||
|
|
type Asset = { id: string; code: string; qr_code?: string };
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The text on a vehicle's QR sticker. Double-click (or tap the pencil) to type it in; Enter
|
||
|
|
* or leaving the box saves, Escape cancels, and an empty box clears it. Saves on its own —
|
||
|
|
* it does not wait for the table's Edit mode, because it is one value, not a batch.
|
||
|
|
*/
|
||
|
|
export function QrTextCell({ asset, onSaved }: { asset: Asset; onSaved: (qr: string) => void }) {
|
||
|
|
const [editing, setEditing] = useState(false);
|
||
|
|
const [value, setValue] = useState("");
|
||
|
|
const [saving, setSaving] = useState(false);
|
||
|
|
const [error, setError] = useState("");
|
||
|
|
|
||
|
|
function begin() {
|
||
|
|
setValue(asset.qr_code || "");
|
||
|
|
setError("");
|
||
|
|
setEditing(true);
|
||
|
|
}
|
||
|
|
async function save() {
|
||
|
|
if (saving) return;
|
||
|
|
const next = value.trim();
|
||
|
|
if (next === (asset.qr_code || "")) { setEditing(false); return; }
|
||
|
|
setSaving(true);
|
||
|
|
setError("");
|
||
|
|
try {
|
||
|
|
const res = await fetch(`/api/assets/${asset.id}`, {
|
||
|
|
method: "PATCH",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({ qr_code: next }),
|
||
|
|
});
|
||
|
|
const j = await res.json().catch(() => ({}));
|
||
|
|
if (!res.ok || !j.ok) throw new Error(j.error || "Could not save");
|
||
|
|
onSaved(next);
|
||
|
|
setEditing(false);
|
||
|
|
} catch (e: any) {
|
||
|
|
setError(e.message);
|
||
|
|
} finally {
|
||
|
|
setSaving(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (editing) {
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<input
|
||
|
|
autoFocus
|
||
|
|
value={value}
|
||
|
|
disabled={saving}
|
||
|
|
maxLength={200}
|
||
|
|
aria-label={`QR code for ${asset.code}`}
|
||
|
|
onChange={(e) => setValue(e.target.value)}
|
||
|
|
onBlur={save}
|
||
|
|
onKeyDown={(e) => {
|
||
|
|
if (e.key === "Enter") { e.preventDefault(); save(); }
|
||
|
|
if (e.key === "Escape") setEditing(false);
|
||
|
|
}}
|
||
|
|
className="w-full sm:w-44 min-h-[44px] px-2 rounded-md text-sm font-mono border border-[#30638E] bg-white dark:bg-white/10 focus:outline-none"
|
||
|
|
/>
|
||
|
|
{error && <p role="alert" className="text-xs text-[#D1495B] mt-1 max-w-[11rem]">{error}</p>}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return (
|
||
|
|
<div className="flex items-center gap-1 min-h-[44px]" onDoubleClick={begin} title="Double-click to set the QR code">
|
||
|
|
<span className={asset.qr_code ? "font-mono text-gray-700 dark:text-gray-300 break-all" : "text-gray-400 dark:text-gray-500"}>
|
||
|
|
{asset.qr_code || "—"}
|
||
|
|
</span>
|
||
|
|
<button type="button" onClick={begin} aria-label={`Edit QR code for ${asset.code}`}
|
||
|
|
className="grid place-items-center min-h-[44px] min-w-[44px] text-gray-400 hover:text-[#30638E]">
|
||
|
|
<Pencil className="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|