From 137b5e7ffebe2d6dbace0c86f28a1db6c733e05e Mon Sep 17 00:00:00 2001 From: Tyler Date: Thu, 2 Jul 2026 12:16:04 -0600 Subject: [PATCH] refactor(water-log): tone down report preview modal to production-appropriate GlassModal Drops the overdesigned 'Field Bulletin' treatment (perforated tear-strip, crop marks, rubber stamp, paper grain, side stats, signature) in favor of the shared GlassModal chrome with a clean data table, meta line, and a plain-text copy action. Matches the rest of the admin UI. --- src/components/admin/WaterLogAdminPanel.tsx | 781 ++++---------------- 1 file changed, 138 insertions(+), 643 deletions(-) diff --git a/src/components/admin/WaterLogAdminPanel.tsx b/src/components/admin/WaterLogAdminPanel.tsx index 560b580..ddf70fa 100644 --- a/src/components/admin/WaterLogAdminPanel.tsx +++ b/src/components/admin/WaterLogAdminPanel.tsx @@ -22,7 +22,7 @@ * `src/actions/water-log/admin.ts`. */ -import { useEffect, useMemo, useReducer, useRef, useState } from "react"; +import { useEffect, useMemo, useReducer, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { @@ -49,6 +49,7 @@ import { } from "@/lib/water-log-reporting"; import { WaterGauge, SeasonMark } from "@/components/water/icons"; import AdminButton from "./design-system/AdminButton"; +import GlassModal from "@/components/admin/GlassModal"; import { formatDate, formatDateTime } from "@/lib/format-date"; const DEFAULT_SEASON: IrrigationSeasonSettings = { @@ -76,7 +77,6 @@ type ReportPreviewData = { dateIso: string; recipientName?: string; inSeason: boolean; - bulletinNumber: number; }; type State = { @@ -493,13 +493,6 @@ export default function WaterLogAdminPanel({ const unit = rows.find((r) => r.unit)?.unit ?? state.headgates[0]?.unit ?? "CFS"; const now = new Date(); - // Deterministic-ish bulletin number from today's date + entry count so - // it changes per-day but feels like a real reference code. - const dayKey = - state.today.replace(/-/g, "") || - `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`; - const bulletinNumber = - (parseInt(dayKey, 10) % 1000) + (state.entries.length % 7) * 13 + 1; if (rows.length === 0) { notify( @@ -518,7 +511,6 @@ export default function WaterLogAdminPanel({ dateIso: state.today || now.toISOString().slice(0, 10), recipientName: state.recipientName.trim() || undefined, inSeason, - bulletinNumber, }, }); } @@ -1832,12 +1824,10 @@ function DayInput({ // ─── Report Preview Modal ───────────────────────────────────────────────── // -// A "Field Bulletin" — modeled after USDA Soil Conservation Service / USGS -// survey bulletins. Cream paper, deep forest ink, monospaced data with -// hairline rules, perforated tear-strip on the left, and a rotated -// PREVIEW rubber stamp to keep it clearly distinct from the actual -// deliverable. The composition is a single tactile sheet that opens with -// a soft paper-curl motion and staggers each entry row in. +// Simple, production-appropriate preview of the daily water report that +// would be sent by the cron. Built on the shared GlassModal so ESC, focus +// trapping, scroll-lock, and the standard admin header/close chrome all +// match the rest of /admin. function ReportPreviewModal({ data, @@ -1848,30 +1838,11 @@ function ReportPreviewModal({ onClose: () => void; notify: (msg: string, kind?: "ok" | "err") => void; }) { - const dialogRef = useRef(null); const [copied, setCopied] = useState(false); - // ESC closes; lock body scroll while open. - useEffect(() => { - if (!data) return; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("keydown", onKey); - const prevOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - // Move focus into the dialog for keyboard users. - const t = setTimeout(() => dialogRef.current?.focus(), 30); - return () => { - document.removeEventListener("keydown", onKey); - document.body.style.overflow = prevOverflow; - clearTimeout(t); - }; - }, [data, onClose]); - if (!data) return null; - const { rows, total, unit, dateLabel, dateIso, recipientName, inSeason, bulletinNumber } = data; + const { rows, total, unit, dateLabel, dateIso, recipientName, inSeason } = data; const totalUnit = rows[0]?.unit ?? unit ?? "CFS"; async function handleCopy() { @@ -1890,560 +1861,159 @@ function ReportPreviewModal({ } } - // Earliest entry time → "shift open", latest → "shift close" — a nice - // touch pulled straight from irrigation logbook conventions. - const times = rows - .map((r) => new Date(r.logged_at)) - .filter((d) => !Number.isNaN(d.getTime())); - const firstTime = times.length - ? times.reduce((a, b) => (a < b ? a : b)) - : null; - const lastTime = times.length - ? times.reduce((a, b) => (a > b ? a : b)) - : null; - const fmtTime = (d: Date | null) => - d - ? d.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - }) - : "—"; - - // Most active headgate (max sum of measurement) — useful color piece. - const byHeadgate = new Map(); - for (const r of rows) { - byHeadgate.set(r.headgate_name, (byHeadgate.get(r.headgate_name) ?? 0) + r.measurement); - } - const topHeadgate = Array.from(byHeadgate.entries()).sort((a, b) => b[1] - a[1])[0]; + const subtitle = + `${rows.length} ${rows.length === 1 ? "entry" : "entries"} · ` + + `${total.toFixed(2)} ${totalUnit} total · ` + + (inSeason ? "in season" : "out of season"); return ( -
{ - // Click on the backdrop (not the paper) closes. - if (e.target === e.currentTarget) onClose(); - }} + - {/* Backdrop wash — deep forest tint with subtle radial vignette. */} -
- - {/* The paper itself */} -
- {/* Paper grain overlay — extremely subtle */} -
- - {/* Perforated left tear-strip — like a printed form */} -
- - {/* Crop marks at the four corners — printer's registration marks */} - - - - - - {/* Header band — forest ink strip with cream mono caps */} -
-
-
- Tuxedo Ditch Co. · Water Log -
-
- Daily Field Bulletin -
-
-
-
Bulletin №
-
- {String(bulletinNumber).padStart(4, "0")} -
-
-
- - {/* Title row + PREVIEW stamp */} -
-

- Daily Water Report -

-

+

+ {/* Meta line */} +
+ + Date:{" "} {dateLabel} - / - - {dateIso.replace(/-/g, " · ")} - -

- - {/* Rotated rubber stamp — PREVIEW */} -
- PREVIEW - - ONLY - -
-
- - {/* Meta strip — date / season / total / count */} -
- - - - -
- - {/* Salutation */} -
+ + · + + {dateIso} + {recipientName ? ( -

- {recipientName}, -

- ) : ( -

- To the irrigator — -

- )} -

- Below is the day's record of measured flow at every headgate - in service. Times are local; volumes are reported in the unit - declared for each gate.{" "} - - This bulletin is a preview — no message has been sent. - -

+ <> + · + + + Recipient: + {" "} + {recipientName} + + + ) : null} +
+ + {/* Preview banner — keep it clear this hasn't been sent */} +
+ Preview only. No message has been sent. Daily-report delivery is + handled by the scheduled{" "} + api/cron/water-report endpoint.
{/* Data table */} -
-
- - - +
+ + + + + + + + + + + {rows.map((row, i) => { + const time = new Date(row.logged_at).toLocaleTimeString( + "en-US", + { hour: "numeric", minute: "2-digit" }, + ); + return ( + + + + + + + + ); + })} + + + + - - - - - - - - {rows.map((row, i) => { - const time = new Date(row.logged_at).toLocaleTimeString( - "en-US", - { hour: "numeric", minute: "2-digit" }, - ); - const isTop = - topHeadgate && - row.headgate_name === topHeadgate[0] && - row.measurement === topHeadgate[1]; - return ( - - - - - - - - ); - })} - - - - - - - - - - -
TimeHeadgateIrrigatorVolumeNote
+ {time} + + {row.headgate_name} + + {row.user_name} + + + {row.measurement.toFixed(2)} + {" "} + + {row.unit} + + + {row.notes ?? } +
- TimeHeadgateIrrigatorVolumeNote
{time} - - - {row.headgate_name} - - {row.user_name} - - {row.measurement.toFixed(2)} - - - {row.unit} - - - {row.notes ? ( - - “{row.notes}” - - ) : ( - · - )} -
- Day total - - · - - Σ - - - {total.toFixed(2)} - - - {totalUnit} - - - {rows.length} logs -
-
+ Total + + + + {total.toFixed(2)} + {" "} + + {totalUnit} + + + + + + + +
- {/* Side stats — first/last entry, top headgate */} -
- - - -
- - {/* Footer signature */} -
-
-

- — End of transmission — -

-

- Filed by Route Commerce Water Log -

-
- - {/* Action bar — sticky to the bottom of the paper */} -
- -
- - {/* Component-local animations */} - -
+ ); } -function MetaCell({ - label, - value, - mono = false, - emphasis = false, - accent, +function Th({ + children, + className = "", }: { - label: string; - value: string; - mono?: boolean; - emphasis?: boolean; - accent?: "green" | "amber"; + children: React.ReactNode; + className?: string; }) { - const accentColor = - accent === "green" - ? "#1a4d2e" - : accent === "amber" - ? "#a16207" - : undefined; - return ( -
- - {label} - - - {value} - -
- ); -} - -function Th({ children, className = "" }: { children: React.ReactNode; className?: string }) { return ( {children} @@ -2453,96 +2023,21 @@ function Th({ children, className = "" }: { children: React.ReactNode; className function Td({ children, className = "", + colSpan, }: { children: React.ReactNode; className?: string; + colSpan?: number; }) { return ( {children} ); } - -function SideStat({ - label, - value, - sub, -}: { - label: string; - value: string; - sub?: string; -}) { - return ( -
- - {label} - - - {value} - - {sub ? ( - - {sub} - - ) : null} -
- ); -} - -function CropMark({ className = "", rotate = 0 }: { className?: string; rotate?: number }) { - return ( - - - - ); -} - -function CopyMark() { - return ( - - - - - ); -} - -function CheckMark() { - return ( - - - - ); -} - // ─── Icons (16–20px, currentColor) ────────────────────────────────────── function QrIcon() {