"use client"; import { useState } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { getWaterHeadgatesAdmin, createWaterHeadgate, regenerateHeadgateToken, updateWaterHeadgate, deleteWaterHeadgate, } from "@/actions/water-log/admin"; import { AdminButton } from "@/components/admin/design-system"; type Headgate = { id: string; name: string; active: boolean; unit: string; created_at: string; deleted_at?: string | null; headgate_token?: string | null; last_used_at?: string | null; high_threshold?: number | null; low_threshold?: number | null; }; type Props = { initialHeadgates: Headgate[]; brandId: string; }; const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"; export default function HeadgatesManager({ initialHeadgates, brandId }: Props) { const router = useRouter(); const [headgates, setHeadgates] = useState(initialHeadgates); const [showAdd, setShowAdd] = useState(false); const [newName, setNewName] = useState(""); const [newUnit, setNewUnit] = useState("CFS"); const [saving, setSaving] = useState(false); const [qrModal, setQrModal] = useState(null); const [selected, setSelected] = useState>(new Set()); const [printLoading, setPrintLoading] = useState(false); const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null); // Edit modal state const [editHg, setEditHg] = useState(null); const [editName, setEditName] = useState(""); const [editUnit, setEditUnit] = useState("CFS"); const [editActive, setEditActive] = useState(true); const [editHigh, setEditHigh] = useState(""); const [editLow, setEditLow] = useState(""); const [editSaving, setEditSaving] = useState(false); function showToast(msg: string, ok = true) { setToast({ msg, ok }); setTimeout(() => setToast(null), 2500); } function openEdit(hg: Headgate) { setEditHg(hg); setEditName(hg.name); setEditUnit(hg.unit); setEditActive(hg.active); setEditHigh(hg.high_threshold != null ? String(hg.high_threshold) : ""); setEditLow(hg.low_threshold != null ? String(hg.low_threshold) : ""); } async function handleEditSave(e: React.FormEvent) { e.preventDefault(); if (!editHg) return; setEditSaving(true); const res = await updateWaterHeadgate( editHg.id, editName, editActive, editUnit, editHigh ? parseFloat(editHigh) : null, editLow ? parseFloat(editLow) : null ); if (res.success) { const refreshed = await getWaterHeadgatesAdmin(brandId); setHeadgates(refreshed); setEditHg(null); showToast("Headgate updated"); } else { showToast(res.error ?? "Failed", false); } setEditSaving(false); } async function handleAdd(e: React.FormEvent) { e.preventDefault(); if (!newName.trim()) return; setSaving(true); const res = await createWaterHeadgate(brandId, newName.trim(), newUnit); if (res.success) { const refreshed = await getWaterHeadgatesAdmin(brandId); setHeadgates(refreshed); setNewName(""); setShowAdd(false); showToast("Headgate added"); } else { showToast(res.error ?? "Failed", false); } setSaving(false); } async function handleRegenerate(hg: Headgate) { if (!confirm(`Regenerate token for "${hg.name}"? Existing QR codes will stop working.`)) return; const res = await regenerateHeadgateToken(hg.id); if (res.success) { setHeadgates((prev) => prev.map((h) => (h.id === hg.id ? { ...h, headgate_token: res.token! } : h)) ); showToast("Token regenerated"); } else { showToast(res.error ?? "Failed", false); } } const [deletingId, setDeletingId] = useState(null); async function handleDelete(hg: Headgate) { if (!window.confirm(`Delete headgate "${hg.name}"? Existing log entries will be preserved.`)) return; setDeletingId(hg.id); const res = await deleteWaterHeadgate(hg.id); if (res.success) { // Optimistic update + refresh setHeadgates((prev) => prev.filter((h) => h.id !== hg.id)); const refreshed = await getWaterHeadgatesAdmin(brandId); setHeadgates(refreshed); showToast("Headgate deleted"); } else { showToast(res.error ?? "Failed to delete headgate", false); } setDeletingId(null); } function toggleSelect(id: string) { setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); } function toggleAll() { if (selected.size === headgates.length) { setSelected(new Set()); } else { setSelected(new Set(headgates.map((h) => h.id))); } } async function printSelected() { if (selected.size === 0) return; setPrintLoading(true); const hgs = headgates.filter((h) => selected.has(h.id)); try { const res = await fetch("/api/water-qr-sheet", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ headgates: hgs, baseUrl: BASE_URL }), }); const html = await res.text(); openPrintWindow(html); } finally { setPrintLoading(false); } } async function openPrintWindow(html: string) { 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) { // 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); } } return (
{/* Header */}

Headgates & QR Codes

Manage headgates and generate QR codes for field access

setShowAdd(true)}> + Add Headgate
{/* Bulk actions bar */} {headgates.length > 0 && (
| {selected.size} selected
Print {selected.size > 0 ? `(${selected.size})` : ""} QR Codes
)} {/* Add form */} {showAdd && (
setNewName(e.target.value)} placeholder="e.g. North Field Gate 1" className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]" required aria-required="true" />
Create setShowAdd(false)}> Cancel
)} {/* Table */} {headgates.length === 0 ? (
No headgates yet. Create one to get started.
) : (
{headgates.map((hg) => ( ))}
Name Token Unit Last Used Status QR Code
toggleSelect(hg.id)} />
{hg.name} {hg.high_threshold != null && ( High: {hg.high_threshold} )} {hg.low_threshold != null && ( Low: {hg.low_threshold} )}
{hg.headgate_token?.slice(0, 8)}… {hg.unit} {hg.last_used_at ? new Date(hg.last_used_at).toLocaleDateString("en-US", { month: "short", day: "numeric" }) : "Never"} {hg.active ? "Active" : "Inactive"}
openEdit(hg)}> Edit {/* QR preview thumbnail */} setQrModal(hg)}> Print handleDelete(hg)} disabled={deletingId === hg.id} isLoading={deletingId === hg.id} > Delete
)}
{/* Toast */} {toast && (
{toast.msg}
)} {/* Edit Modal */} {editHg && (
setEditHg(null)}>
e.stopPropagation()}>

Edit Headgate

setEditName(e.target.value)} className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]" required aria-required="true" />
setEditHigh(e.target.value)} placeholder="e.g. 15.0" className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]" />
setEditLow(e.target.value)} placeholder="e.g. 5.0" className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]" />

Leave thresholds blank to disable alerts for this headgate.

Save Changes setEditHg(null)}> Cancel
)} {/* QR Modal */} {qrModal && ( setQrModal(null)} onRegenerate={handleRegenerate} /> )}
); } function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => void; onRegenerate: (hg: Headgate) => void }) { const [tab, setTab] = useState<"preview" | "print" | "download">("preview"); const [loading, setLoading] = useState(false); const code = hg.name.replace(/\s+/g, "-").toUpperCase().slice(0, 6) + "-1"; const qrUrl = `/api/water-qr?token=${hg.headgate_token}&size=360`; const waterUrl = `${process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"}/water?h=${hg.headgate_token}`; async function handlePrintLabel() { setLoading(true); try { const res = await fetch("/api/water-qr-label", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: hg.headgate_token, name: hg.name }), }); const html = await res.text(); 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); } } async function handleDownload() { setLoading(true); try { const res = await fetch(`/api/water-qr?token=${hg.headgate_token}&size=400`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${hg.name.replace(/\s+/g, "-").toLowerCase()}-qr.png`; a.click(); URL.revokeObjectURL(url); } finally { setLoading(false); } } return (
e.stopPropagation()}> {/* Header */}

{hg.name}

QR Label Preview

{/* ── Simplified label preview ── */}
{/* Mini version of the new label design */}
{hg.name}
{code}
QR
{waterUrl}
{/* Tab toggle */}
{(["preview", "print", "download"] as const).map((t) => ( ))}
{tab === "preview" ? (

This label is optimized for physical signs and thermal printing:

  • Large name (24pt bold) — scannable from distance
  • Short code (e.g. {code}) — field reference
  • High-contrast QR — error correction H for outdoor use
  • Tiny URL at bottom — reference only
setTab("print")} fullWidth> 🖨️ Print This Label
) : tab === "print" ? ( 🖨️ Print Label ) : ( 💾 Download PNG )} { onRegenerate(hg); onClose(); }} variant="secondary" fullWidth > 🔄 Regenerate Token

Token

{hg.headgate_token}
); }