|
|
|
@@ -171,10 +171,16 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openPrintWindow(html: string) {
|
|
|
|
|
const w = window.open("", "_blank", "width=700,height=700");
|
|
|
|
|
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const w = window.open(url, "_blank", "width=700,height=700");
|
|
|
|
|
if (w) {
|
|
|
|
|
w.document.write(html);
|
|
|
|
|
w.document.close();
|
|
|
|
|
// Revoke the blob URL once the new window has had time to load the document.
|
|
|
|
|
w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
|
|
|
|
|
// Safety net: revoke after 60s even if the load event never fires.
|
|
|
|
|
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
|
|
|
} else {
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -185,7 +191,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
<div className="mx-auto max-w-4xl flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm">← Back</button>
|
|
|
|
|
<button type="button" onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm">← Back</button>
|
|
|
|
|
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">Headgates & QR Codes</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">Manage headgates and generate QR codes for field access</p>
|
|
|
|
@@ -201,7 +207,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
{/* Bulk actions bar */}
|
|
|
|
|
{headgates.length > 0 && (
|
|
|
|
|
<div className="flex items-center gap-3 rounded-xl border border-[var(--admin-border)] bg-white px-4 py-3">
|
|
|
|
|
<button onClick={toggleAll} className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
|
|
|
|
<button type="button" onClick={toggleAll} className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
|
|
|
|
{selected.size === headgates.length ? "Deselect all" : "Select all"}
|
|
|
|
|
</button>
|
|
|
|
|
<span className="text-xs text-[var(--admin-text-muted)]">|</span>
|
|
|
|
@@ -223,7 +229,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
<form onSubmit={handleAdd} className="flex gap-3 items-end">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label htmlFor="headgate-add-name" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Headgate Name</label>
|
|
|
|
|
<input
|
|
|
|
|
<input aria-label=". North Field Gate 1"
|
|
|
|
|
id="headgate-add-name"
|
|
|
|
|
autoFocus
|
|
|
|
|
type="text"
|
|
|
|
@@ -237,7 +243,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label htmlFor="headgate-add-unit" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
|
|
|
|
|
<select
|
|
|
|
|
<select aria-label="Headgate Add Unit"
|
|
|
|
|
id="headgate-add-unit"
|
|
|
|
|
value={newUnit}
|
|
|
|
|
onChange={(e) => setNewUnit(e.target.value)}
|
|
|
|
@@ -269,7 +275,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="border-b border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] text-left">
|
|
|
|
|
<th className="px-4 py-3 w-8">
|
|
|
|
|
<input type="checkbox" className="rounded" onChange={toggleAll} checked={selected.size === headgates.length} />
|
|
|
|
|
<input aria-label="Checkbox" type="checkbox" className="rounded" onChange={toggleAll} checked={selected.size === headgates.length} />
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Name</th>
|
|
|
|
|
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Token</th>
|
|
|
|
@@ -283,7 +289,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
{headgates.map((hg) => (
|
|
|
|
|
<tr key={hg.id} className={`border-b border-[var(--admin-border)] last:border-0 hover:bg-[var(--admin-bg-subtle)] ${selected.has(hg.id) ? "bg-blue-50" : ""}`}>
|
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
|
<input type="checkbox" className="rounded" checked={selected.has(hg.id)} onChange={() => toggleSelect(hg.id)} />
|
|
|
|
|
<input aria-label="Checkbox" type="checkbox" className="rounded" checked={selected.has(hg.id)} onChange={() => toggleSelect(hg.id)} />
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3">
|
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
|
|
@@ -324,7 +330,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
Edit
|
|
|
|
|
</AdminButton>
|
|
|
|
|
{/* QR preview thumbnail */}
|
|
|
|
|
<button onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR">
|
|
|
|
|
<button type="button" onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR">
|
|
|
|
|
<img
|
|
|
|
|
src={`/api/water-qr?token=${hg.headgate_token}&size=80`}
|
|
|
|
|
alt={`QR for ${hg.name}`}
|
|
|
|
@@ -366,7 +372,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
|
|
|
|
|
<p className="font-bold text-[var(--admin-text-primary)]">Edit Headgate</p>
|
|
|
|
|
<button onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
|
|
|
|
<button type="button" onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
|
|
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
|
|
|
</svg>
|
|
|
|
@@ -375,7 +381,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
<form onSubmit={handleEditSave} className="p-5 space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label htmlFor="headgate-edit-name" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Name</label>
|
|
|
|
|
<input
|
|
|
|
|
<input aria-label="Headgate Edit Name"
|
|
|
|
|
id="headgate-edit-name"
|
|
|
|
|
type="text"
|
|
|
|
|
value={editName}
|
|
|
|
@@ -388,7 +394,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label htmlFor="headgate-edit-unit" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
|
|
|
|
|
<select
|
|
|
|
|
<select aria-label="Headgate Edit Unit"
|
|
|
|
|
id="headgate-edit-unit"
|
|
|
|
|
value={editUnit}
|
|
|
|
|
onChange={(e) => setEditUnit(e.target.value)}
|
|
|
|
@@ -409,7 +415,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label htmlFor="headgate-edit-high" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">High Alert Threshold</label>
|
|
|
|
|
<input
|
|
|
|
|
<input aria-label=". 15.0"
|
|
|
|
|
id="headgate-edit-high"
|
|
|
|
|
type="number"
|
|
|
|
|
step="any"
|
|
|
|
@@ -421,7 +427,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label htmlFor="headgate-edit-low" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Low Alert Threshold</label>
|
|
|
|
|
<input
|
|
|
|
|
<input aria-label=". 5.0"
|
|
|
|
|
id="headgate-edit-low"
|
|
|
|
|
type="number"
|
|
|
|
|
step="any"
|
|
|
|
@@ -471,8 +477,15 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
|
|
|
|
body: JSON.stringify({ token: hg.headgate_token, name: hg.name }),
|
|
|
|
|
});
|
|
|
|
|
const html = await res.text();
|
|
|
|
|
const w = window.open("", "_blank", "width=500,height=600");
|
|
|
|
|
if (w) { w.document.write(html); w.document.close(); }
|
|
|
|
|
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const w = window.open(url, "_blank", "width=500,height=600");
|
|
|
|
|
if (w) {
|
|
|
|
|
w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
|
|
|
|
|
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
|
|
|
} else {
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
@@ -504,7 +517,7 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
|
|
|
|
<p className="font-bold text-[var(--admin-text-primary)]">{hg.name}</p>
|
|
|
|
|
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">QR Label Preview</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
|
|
|
|
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
|
|
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
|
|
|
</svg>
|
|
|
|
@@ -531,7 +544,7 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
|
|
|
|
{/* Tab toggle */}
|
|
|
|
|
<div className="flex border-b border-[var(--admin-border)]">
|
|
|
|
|
{(["preview", "print", "download"] as const).map((t) => (
|
|
|
|
|
<button
|
|
|
|
|
<button type="button"
|
|
|
|
|
key={t}
|
|
|
|
|
onClick={() => setTab(t)}
|
|
|
|
|
className={`flex-1 py-3 text-sm font-semibold transition-colors ${tab === t ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-accent)]" : "text-[var(--admin-text-muted)]"}`}
|
|
|
|
|