/** * HeadgatesTab — CRUD surface for water headgates. * * Lists every headgate in a card grid, with add/regenerate-token/delete * controls. Owns its own form state via the shell reducer. */ "use client"; import * as React from "react"; import AdminButton from "@/components/admin/design-system/AdminButton"; import { createWaterHeadgate, deleteWaterHeadgate, regenerateHeadgateToken, type AdminEntry, type AdminHeadgate, } from "@/actions/water-log/admin"; import { WaterGauge } from "@/components/water/icons"; import { formatDateTime } from "@/lib/format-date"; import { EmptyState, Input, SectionHeader, Select, StatusPill, } from "../shared/Primitives"; import type { Action, NotifyFn } from "../types"; export type HeadgatesTabProps = { brandId: string; headgates: AdminHeadgate[]; lastEntryByHeadgate: Map; canManage: boolean; showAddHg: boolean; newHg: { name: string; unit: string; notes: string }; savingHg: boolean; notify: NotifyFn; dispatch: React.Dispatch; onEdit: (h: AdminHeadgate) => void; }; export function HeadgatesTab({ brandId, headgates, lastEntryByHeadgate, canManage, showAddHg, newHg, savingHg, notify, dispatch, onEdit, }: HeadgatesTabProps) { async function handleAdd(e: React.FormEvent) { e.preventDefault(); const trimmedName = newHg.name.trim(); if (!trimmedName) return; dispatch({ type: "SET_SAVING_HG", value: true }); const res = await createWaterHeadgate(brandId, trimmedName, newHg.unit, { notes: newHg.notes.trim() || null, }); if (res.success && res.headgate) { dispatch({ type: "ADD_HEADGATE", headgate: res.headgate }); dispatch({ type: "RESET_NEW_HG" }); notify("Headgate added"); } else { notify(res.error ?? "Failed", "err"); } dispatch({ type: "SET_SAVING_HG", value: false }); } async function handleDelete(h: AdminHeadgate) { if ( !window.confirm( `Delete "${h.name}"? Existing log entries will be preserved.`, ) ) return; const res = await deleteWaterHeadgate(h.id); if (res.success) { dispatch({ type: "REMOVE_HEADGATE", id: h.id }); notify("Headgate removed"); } else { notify(res.error ?? "Failed", "err"); } } async function handleRegenerateToken(h: AdminHeadgate) { if ( !window.confirm( `Regenerate QR token for "${h.name}"? Existing printed QR codes will stop working.`, ) ) return; const res = await regenerateHeadgateToken(h.id); if (res.success && res.token) { dispatch({ type: "UPDATE_HEADGATE_TOKEN", id: h.id, token: res.token, }); notify("Token regenerated"); } else { notify(res.error ?? "Failed", "err"); } } return ( <> dispatch({ type: "TOGGLE_ADD_HG" })}> {showAddHg ? "Cancel" : "+ Add Headgate"} ) : null } /> {showAddHg && canManage && (
dispatch({ type: "SET_NEW_HG_NAME", value: v })} placeholder="e.g. North Field Gate 1" required /> dispatch({ type: "SET_NEW_HG_NOTES", value: v })} placeholder="Location, GPS, contact…" />
Add
)} {headgates.length === 0 ? ( } title="No headgates yet" body="Add a headgate to start logging. Each one gets a unique QR code you can print and post at the gate." /> ) : (
    {headgates.map((h) => { const last = lastEntryByHeadgate.get(h.id); const level = last && h.high_threshold ? Math.min(1, last.measurement / Number(h.high_threshold)) : null; return (
  • {h.name}

    Unit: {h.unit} · Token: {h.headgate_token.slice(0, 8)}…

    {last ? (

    Last:{" "} {last.measurement} {h.unit} {" "} by {last.user_name} · {formatDateTime(last.logged_at)}

    ) : (

    No readings yet

    )} {(h.high_threshold != null || h.low_threshold != null) && (

    {h.high_threshold != null && ( <>Hi ≥ {h.high_threshold}   )} {h.low_threshold != null && ( <>Lo ≤ {h.low_threshold} )}

    )}
    {canManage && (
    ·
    )}
  • ); })}
)} ); }