"use client"; /** * /admin/water-log/settings * * Site-admin-facing settings page for the Water Log module. Controls: * - Whether `/water/admin` is enabled * - 4-digit admin PIN (regenerate on demand) * - Session duration (hours) * - Per-coarse-permission flags (edit / delete / export) * - High/low alert phone + enable flag * * Visual language: Field Almanac. Same cream + forest palette as * the main WaterLogAdminPanel, but trimmed to a single column for * a focused "form" feel. */ import { useReducer, useEffect, useCallback } from "react"; import { getWaterAdminSettings, saveWaterAdminSettings, regenerateAdminPin, type AdminSettings, } from "@/actions/water-log/settings"; import SmartsheetHub from "@/components/admin/water-log/SmartsheetHub"; import SmartsheetIntegrationCard from "@/components/admin/water-log/SmartsheetIntegrationCard"; import TimeTrackingSmartsheetCard from "@/components/admin/time-tracking/TimeTrackingSmartsheetCard"; const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; type Message = { type: "success" | "error"; text: string } | null; type State = { settings: AdminSettings | null; loading: boolean; saving: boolean; regenerating: boolean; revealedPin: string | null; message: Message; enabled: boolean; sessionDuration: number; canEdit: boolean; canDelete: boolean; canExport: boolean; alertPhone: string; alertsEnabled: boolean; }; type Action = | { type: "LOAD_START" } | { type: "LOAD_SUCCESS"; data: AdminSettings; } | { type: "LOAD_DONE" } | { type: "SAVE_START" } | { type: "SAVE_SUCCESS"; settings: AdminSettings | undefined } | { type: "SAVE_FAIL"; message: Message } | { type: "SAVE_DONE" } | { type: "REGEN_START" } | { type: "REGEN_SUCCESS"; pin: string; message: Message } | { type: "REGEN_FAIL"; message: Message } | { type: "REGEN_DONE" } | { type: "DISMISS_PIN" } | { type: "CLEAR_MESSAGE" } | { type: "SET_ENABLED"; value: boolean } | { type: "SET_SESSION_DURATION"; value: number } | { type: "SET_CAN_EDIT"; value: boolean } | { type: "SET_CAN_DELETE"; value: boolean } | { type: "SET_CAN_EXPORT"; value: boolean } | { type: "SET_ALERT_PHONE"; value: string } | { type: "SET_ALERTS_ENABLED"; value: boolean }; const initialState: State = { settings: null, loading: true, saving: false, regenerating: false, revealedPin: null, message: null, enabled: false, sessionDuration: 12, canEdit: true, canDelete: true, canExport: true, alertPhone: "", alertsEnabled: false, }; function reducer(state: State, action: Action): State { switch (action.type) { case "LOAD_START": return { ...state, loading: true }; case "LOAD_SUCCESS": return { ...state, settings: action.data, enabled: action.data.enabled, sessionDuration: action.data.sessionDurationHours, canEdit: action.data.canEditEntries, canDelete: action.data.canDeleteEntries, canExport: action.data.canExportCsv, alertPhone: action.data.alertPhone ?? "", alertsEnabled: action.data.alertsEnabled, }; case "LOAD_DONE": return { ...state, loading: false }; case "SAVE_START": return { ...state, saving: true, message: null }; case "SAVE_SUCCESS": return { ...state, message: { type: "success", text: "Settings saved" }, settings: action.settings ?? state.settings, }; case "SAVE_FAIL": return { ...state, message: action.message }; case "SAVE_DONE": return { ...state, saving: false }; case "REGEN_START": return { ...state, regenerating: true, message: null }; case "REGEN_SUCCESS": return { ...state, revealedPin: action.pin, message: action.message, }; case "REGEN_FAIL": return { ...state, message: action.message }; case "REGEN_DONE": return { ...state, regenerating: false }; case "DISMISS_PIN": return { ...state, revealedPin: null }; case "CLEAR_MESSAGE": return { ...state, message: null }; case "SET_ENABLED": return { ...state, enabled: action.value }; case "SET_SESSION_DURATION": return { ...state, sessionDuration: action.value }; case "SET_CAN_EDIT": return { ...state, canEdit: action.value }; case "SET_CAN_DELETE": return { ...state, canDelete: action.value }; case "SET_CAN_EXPORT": return { ...state, canExport: action.value }; case "SET_ALERT_PHONE": return { ...state, alertPhone: action.value }; case "SET_ALERTS_ENABLED": return { ...state, alertsEnabled: action.value }; default: return state; } } export default function WaterLogSettingsPage() { const [state, dispatch] = useReducer(reducer, initialState); const { settings, loading, saving, regenerating, revealedPin, message, enabled, sessionDuration, canEdit, canDelete, canExport, alertPhone, alertsEnabled, } = state; const loadSettings = useCallback(async () => { dispatch({ type: "LOAD_START" }); const data = await getWaterAdminSettings(TUXEDO_BRAND_ID); if (data) { dispatch({ type: "LOAD_SUCCESS", data }); } dispatch({ type: "LOAD_DONE" }); }, []); useEffect(() => { // Load on mount — setState-in-effect is intentional here // because we're hydrating from server data. void loadSettings(); }, [loadSettings]); async function handleSave(e: React.FormEvent) { e.preventDefault(); dispatch({ type: "SAVE_START" }); const result = await saveWaterAdminSettings(TUXEDO_BRAND_ID, { enabled, sessionDurationHours: sessionDuration, canEditEntries: canEdit, canDeleteEntries: canDelete, canExportCsv: canExport, alertPhone: alertPhone || null, alertsEnabled, }); if (result.success) { dispatch({ type: "SAVE_SUCCESS", settings: result.settings }); } else { dispatch({ type: "SAVE_FAIL", message: { type: "error", text: result.error ?? "Save failed" }, }); } dispatch({ type: "SAVE_DONE" }); } async function handleRegenerate() { if (!confirm("Regenerate the admin PIN? All current admin sessions will be signed out.")) { return; } dispatch({ type: "REGEN_START" }); const result = await regenerateAdminPin(TUXEDO_BRAND_ID); if (result.success && result.pin) { dispatch({ type: "REGEN_SUCCESS", pin: result.pin, message: { type: "success", text: "New PIN generated. Save it now — it will not be shown again.", }, }); } else { dispatch({ type: "REGEN_FAIL", message: { type: "error", text: result.error ?? "Failed to regenerate PIN" }, }); } dispatch({ type: "REGEN_DONE" }); } if (loading) { return (
Loading…
); } return (
{/* Header */}

§ 04 — Settings

Water Log · Admin Portal

Configure PIN access for the field admin portal at /water/admin. This is separate from the platform admin login.

{message && (
{message.type === "success" ? ( ) : ( )}

{message.text}

)} {/* Enable toggle */} dispatch({ type: "SET_ENABLED", value })} /> {enabled && ( <> {/* PIN */} {revealedPin ? (

New PIN — write it down

{revealedPin}
) : (

{settings?.pin === null ? "Generate a new PIN to give to your water admin." : "A PIN is already configured. Regenerate to issue a new one (this signs out existing admin sessions)."}

)}
{/* Session Duration */}
dispatch({ type: "SET_SESSION_DURATION", value: parseInt(e.target.value, 10), }) } className="flex-1 accent-[#1a4d2e]" aria-label="Session duration in hours" /> {sessionDuration}h
{/* Permissions */}
dispatch({ type: "SET_CAN_EDIT", value })} /> dispatch({ type: "SET_CAN_DELETE", value })} /> dispatch({ type: "SET_CAN_EXPORT", value })} />
{/* High/Low Alerts */} dispatch({ type: "SET_ALERTS_ENABLED", value })} /> {alertsEnabled && (
dispatch({ type: "SET_ALERT_PHONE", value: e.target.value }) } placeholder="+13035551234" className="mt-1 block min-h-[44px] w-full rounded-lg border border-[#d4d9d3] bg-white px-4 py-2.5 text-[14px] text-[#1d1d1f] placeholder:text-[#a3a5a0] outline-none motion-safe:transition-[border-color,box-shadow] motion-safe:duration-150 focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/15" autoComplete="tel" />

U.S. format recommended. Include country code (e.g. +1 for USA).

)}
)}
{/* Smartsheet Workbook Hub (Cycle 7). One token, many sheets. The hub card owns the brand-level workbook connection (token + test + enable); the per-feature cards below pick which sheet inside the workbook to write to. */}

§ 05 — Integrations

Smartsheet

Connect one Smartsheet workbook and pick which sheet each feature writes to. The token is encrypted at rest; per-feature cards below manage only their sheet mapping.

{/* Water Log sheet mapping — uses the workbook token from the hub card above. Configures which sheet inside the workbook receives water-log entries. */}
{/* Time Tracking sheet mapping — also uses the workbook token. Configures which sheet inside the workbook receives clock-out rows. */}
{/* § 06 removed in Cycle 7 — Time Tracking sheet mapping is now a sibling card under § 05 above (uses the workbook token). */}
); } function Card({ title, subtitle, children, }: { title: string; subtitle?: string; children: React.ReactNode; }) { return (

{title}

{subtitle && (

{subtitle}

)}
{children}
); } function ToggleRow({ label, description, value, onChange, }: { label: string; description?: string; value: boolean; onChange: (v: boolean) => void; }) { return (

{label}

{description &&

{description}

}
); }