+88
-1
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Badge,
|
||||
@@ -222,6 +222,7 @@ function OperationsSection({
|
||||
}) {
|
||||
const [newOpen, setNewOpen] = useState(false);
|
||||
const [edit, setEdit] = useState<OperationRow | null>(null);
|
||||
const [qrFor, setQrFor] = useState<OperationRow | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -324,6 +325,9 @@ function OperationsSection({
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setQrFor(op)} disabled={busyId !== null}>
|
||||
QR
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setEdit(op)} disabled={busyId !== null}>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -374,10 +378,93 @@ function OperationsSection({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{qrFor && <QrModal operation={qrFor} onClose={() => setQrFor(null)} />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function QrModal({ operation, onClose }: { operation: OperationRow; onClose: () => void }) {
|
||||
const [data, setData] = useState<
|
||||
{ dataUrl: string; scanUrl: string; token: string } | null
|
||||
>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch lazily so we don't pre-render QRs for every op on the page. The
|
||||
// data URL is ~1 KB so this is cheap, but it does require a server hop.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
apiFetch<{ dataUrl: string; scanUrl: string; token: string }>(
|
||||
`/api/v1/operations/${operation.id}/qr`,
|
||||
)
|
||||
.then((d) => {
|
||||
if (!cancelled) setData(d);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) setError(err instanceof ApiClientError ? err.message : "Load failed");
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [operation.id]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onClose={onClose}
|
||||
title={`QR: step ${operation.sequence}. ${operation.name}`}
|
||||
footer={
|
||||
<>
|
||||
<div className="flex-1" />
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
{data ? (
|
||||
<a
|
||||
href={data.dataUrl}
|
||||
download={`op-${operation.sequence}-${operation.id}.png`}
|
||||
className="inline-flex items-center rounded-md bg-slate-900 text-white text-sm px-3 py-1.5 hover:bg-slate-800"
|
||||
>
|
||||
Download PNG
|
||||
</a>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{error ? (
|
||||
<ErrorBanner message={error} />
|
||||
) : data ? (
|
||||
<>
|
||||
<div className="flex justify-center bg-white border border-slate-200 rounded-md p-4">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={data.dataUrl}
|
||||
alt={`QR for ${operation.name}`}
|
||||
width={256}
|
||||
height={256}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-slate-600 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">Scan URL:</span>{" "}
|
||||
<a href={data.scanUrl} className="text-blue-600 hover:underline break-all">
|
||||
{data.scanUrl}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Token:</span>{" "}
|
||||
<code className="text-slate-500">{data.token}</code>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-slate-500 text-sm py-10">Rendering QR…</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function OperationModal({
|
||||
partId,
|
||||
operation,
|
||||
|
||||
Reference in New Issue
Block a user