);
}
@@ -559,8 +624,6 @@ function HeaderNav() {
}
type TodaySummarySectionProps = {
- brandId: string;
- today: string;
todayLabel: string;
todayEntries: AdminEntry[];
todayTotal: number;
@@ -570,15 +633,13 @@ type TodaySummarySectionProps = {
showReportSettings: boolean;
seasonStart: IrrigationSeasonSettings;
recipientName: string;
- notify: (msg: string, kind?: "ok" | "err") => void;
onToggleReportSettings: () => void;
onSeasonChange: (s: IrrigationSeasonSettings) => void;
onRecipientNameChange: (v: string) => void;
+ onPreviewReport: () => void;
};
function TodaySummarySection({
- brandId,
- today,
todayLabel,
todayEntries,
todayTotal,
@@ -588,30 +649,13 @@ function TodaySummarySection({
showReportSettings,
seasonStart,
recipientName,
- notify,
onToggleReportSettings,
onSeasonChange,
onRecipientNameChange,
+ onPreviewReport,
}: TodaySummarySectionProps) {
- async function handlePreviewReport() {
- const all = await getWaterEntries(brandId, 1000);
- const todayRows: WaterLogReportRow[] = [];
- for (const e of all) {
- if ((e.logged_date ?? e.logged_at.slice(0, 10)) === today) {
- todayRows.push(shapeWaterLogEntry(e));
- }
- }
- const report = formatDailyWaterReport(todayRows, {
- recipientName: recipientName || undefined,
- previewMode: true,
- });
- if (!report) {
- notify(`No entries today — report would be skipped (season: ${inSeason ? "in" : "out"}).`);
- return;
- }
- // The browser's `alert` blocks; the modal preview is the better UX in
- // a future iteration, but alert is dependency-free today.
- window.alert(`Daily Report — ${inSeason ? "in season" : "out of season"}\n\n${report}`);
+ function handlePreviewReport() {
+ onPreviewReport();
}
return (
@@ -1786,6 +1830,719 @@ 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.
+
+function ReportPreviewModal({
+ data,
+ onClose,
+ notify,
+}: {
+ data: ReportPreviewData | null;
+ 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 totalUnit = rows[0]?.unit ?? unit ?? "CFS";
+
+ async function handleCopy() {
+ const text = formatDailyWaterReport(rows, {
+ recipientName: recipientName,
+ previewMode: true,
+ });
+ if (!text) return;
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ notify("Report copied to clipboard");
+ setTimeout(() => setCopied(false), 1600);
+ } catch {
+ notify("Copy failed — your browser blocked clipboard access", "err");
+ }
+ }
+
+ // 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];
+
+ 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 */}
+
+
+ {/* Meta strip — date / season / total / count */}
+
+
+
+
+
+
+
+ {/* Salutation */}
+
+ {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.
+
+