"use client"; import Image from "next/image"; import { useState, useEffect, Suspense, useMemo } from "react"; import { verifyWaterPin, submitWaterEntry, getWaterHeadgates, logoutWater, setWaterLang, } from "@/actions/water-log/field"; import { useSearchParams } from "next/navigation"; type Headgate = { id: string; name: string; active: boolean; high_threshold?: number | null; low_threshold?: number | null; }; type Language = "en" | "es"; const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; const UNITS = ["CFS", "GPM", "Inches", "AF/Day"]; const LABELS = { en: { title: "Water Log", selectLang: "Select Language", whoAreYou: "Who are you?", enterPin: "Enter your 4-digit PIN", pinPlaceholder: "PIN", submit: "Submit Reading", loggingIn: "Verifying...", selectHeadgate: "Select Headgate", measurement: "Measurement", notes: "Notes (optional)", notesPlaceholder: "Notes...", unit: "Unit", loggedInAs: "Logged in as", logout: "Logout", submitting: "Submitting...", success: "Reading submitted!", error: "Error", noHeadgates: "No headgates available", invalidPin: "Invalid PIN", sessionExpired: "Session expired", back: "Back", captureGps: "Capture GPS", capturingGps: "Capturing...", gpsSuccess: "GPS Captured", gpsError: "GPS unavailable", gpsDenied: "Location denied. Tap to retry.", photoLabel: "Photo (optional)", addPhoto: "Add Photo", removePhoto: "Remove", headgateLocked: "QR-locked", locked: "Locked", highAlert: "High", lowAlert: "Low", }, es: { title: "Registro de Agua", selectLang: "Seleccionar Idioma", whoAreYou: "¿Quién eres?", enterPin: "Ingrese su PIN de 4 dígitos", pinPlaceholder: "PIN", submit: "Enviar Lectura", loggingIn: "Verificando...", selectHeadgate: "Seleccionar Compuerta", measurement: "Medición", notes: "Notas (opcional)", notesPlaceholder: "Notas...", unit: "Unidad", loggedInAs: "Conectado como", logout: "Salir", submitting: "Enviando...", success: "¡Lectura registrada!", error: "Error", noHeadgates: "No hay compuertas disponibles", invalidPin: "PIN inválido", sessionExpired: "Sesión expirada", back: "Volver", captureGps: "Capturar GPS", capturingGps: "Capturando...", gpsSuccess: "GPS Capturado", gpsError: "GPS no disponible", gpsDenied: "Ubicación denegada. Toca para reintentar.", photoLabel: "Foto (opcional)", addPhoto: "Agregar Foto", removePhoto: "Quitar", headgateLocked: "Bloqueada por QR", locked: "Bloqueada", highAlert: "Alto", lowAlert: "Bajo", }, }; function ThresholdBadge({ high, low, t }: { high?: number | null; low?: number | null; t: typeof LABELS.en }) { if (!high && !low) return null; return (
{high != null && ( ⚠️ {t.highAlert}: {high} )} {low != null && ( ⚠️ {t.lowAlert}: {low} )}
); } function WaterFieldInner() { const searchParams = useSearchParams(); const qrHeadgateToken = searchParams.get("h"); // Read cookie preferences on first render. Component is client-only via // , so accessing document.cookie in the lazy initializer is safe. const [lang, setLang] = useState(() => { if (typeof document === "undefined") return "en"; const saved = document.cookie.match(/wl_lang=(en|es)/)?.[1]; return (saved as Language) || "en"; }); const [step, setStep] = useState<"loading" | "lang" | "role" | "pin" | "form">(() => { if (typeof document === "undefined") return "loading"; return document.cookie.match(/wl_session=([^;]+)/) ? "form" : "lang"; }); const [pin, setPin] = useState(""); const [irrigatorName, setIrrigatorName] = useState(""); const [headgates, setHeadgates] = useState([]); const [selectedHeadgate, setSelectedHeadgate] = useState(""); const [headgateLocked, setHeadgateLocked] = useState(false); const [measurement, setMeasurement] = useState(""); const [unit, setUnit] = useState("CFS"); const [notes, setNotes] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(false); // Photo state const [photoFile, setPhotoFile] = useState(null); const [photoPreview, setPhotoPreview] = useState(null); // GPS state const [latitude, setLatitude] = useState(null); const [longitude, setLongitude] = useState(null); const [gpsStatus, setGpsStatus] = useState<"idle" | "capturing" | "success" | "error" | "denied">("idle"); const t = LABELS[lang]; // Restore headgates on first render if user is already logged in // (wl_session cookie present). Done in an effect so the initial step // state can be set synchronously and headgates load asynchronously. useEffect(() => { if (step === "form" && headgates.length === 0) { loadHeadgates(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // QR-locked headgate: derive during render instead of syncing in an effect. // When qrHeadgateToken matches a loaded headgate, override the user's // selection and lock the dropdown. Otherwise fall back to whatever the // user picked (or none). const qrMatchedHeadgate = useMemo(() => { if (!qrHeadgateToken || headgates.length === 0) return null; return ( headgates.find((hg) => hg.id === qrHeadgateToken || hg.name === qrHeadgateToken) ?? null ); }, [qrHeadgateToken, headgates]); const effectiveSelectedHeadgate = qrMatchedHeadgate?.id ?? selectedHeadgate; const isHeadgateLocked = qrMatchedHeadgate ? true : headgateLocked; async function loadHeadgates() { const hgs = await getWaterHeadgates(TUXEDO_BRAND_ID, true); setHeadgates(hgs.filter((h) => h.active)); } // Show loading spinner next to headgate dropdown const isLoadingHeadgates = step === "form" && headgates.length === 0; async function handleLangSelect(lang: Language) { setLang(lang); await setWaterLang(lang); setStep("role"); setError(""); } async function handlePinSubmit(e: React.FormEvent) { e.preventDefault(); if (pin.length !== 4) return; setLoading(true); setError(""); const result = await verifyWaterPin(TUXEDO_BRAND_ID, pin); if (!result.success) { setError(result.error === "Invalid PIN" ? LABELS[lang].invalidPin : result.error); setPin(""); setLoading(false); return; } if (result.role === "water_admin") { window.location.href = "/water/admin"; return; } setIrrigatorName(result.name ?? ""); await loadHeadgates(); setStep("form"); setLoading(false); } async function handleCaptureGps() { if (!navigator.geolocation) { setGpsStatus("error"); return; } setGpsStatus("capturing"); navigator.geolocation.getCurrentPosition( (pos) => { setLatitude(pos.coords.latitude); setLongitude(pos.coords.longitude); setGpsStatus("success"); }, (err) => { if (err.code === err.PERMISSION_DENIED) { setGpsStatus("denied"); } else { setGpsStatus("error"); } }, { enableHighAccuracy: true, timeout: 10000 } ); } function handlePhotoChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { setError("Photo must be under 5MB"); return; } if (!["image/jpeg", "image/png"].includes(file.type)) { setError("Only JPG or PNG photos allowed"); return; } setPhotoFile(file); const reader = new FileReader(); reader.onload = (ev) => setPhotoPreview(ev.target?.result as string); reader.readAsDataURL(file); } function removePhoto() { setPhotoFile(null); setPhotoPreview(null); } async function handleSubmitEntry(e: React.FormEvent) { e.preventDefault(); if (!effectiveSelectedHeadgate || !measurement) return; setLoading(true); setError(""); let photoUrl: string | undefined; if (photoFile) { const formData = new FormData(); formData.append("file", photoFile); formData.append("bucket", "water-log-photos"); try { const uploadRes = await fetch("/api/water-photo-upload", { method: "POST", body: formData, }); const uploadData = await uploadRes.json(); if (uploadData.url) { photoUrl = uploadData.url; } } catch { // photo upload failed — submit without photo } } const result = await submitWaterEntry( effectiveSelectedHeadgate, parseFloat(measurement), unit, notes, photoUrl, latitude ?? undefined, longitude ?? undefined, isHeadgateLocked ); if (!result.success) { if (result.error === "Session expired or invalid" || result.error === "Not logged in") { setStep("lang"); setPin(""); setError(LABELS[lang].sessionExpired); } else { setError(result.error); } setLoading(false); return; } setSuccess(true); setMeasurement(""); setNotes(""); setPhotoFile(null); setPhotoPreview(null); setLatitude(null); setLongitude(null); setGpsStatus("idle"); setLoading(false); setTimeout(() => setSuccess(false), 3000); } async function handleLogout() { await logoutWater(); setStep("lang"); setPin(""); setIrrigatorName(""); setHeadgates([]); setSelectedHeadgate(""); setMeasurement(""); setNotes(""); setPhotoFile(null); setPhotoPreview(null); setLatitude(null); setLongitude(null); setGpsStatus("idle"); setHeadgateLocked(false); } // Language selection screen if (step === "loading") { return (

Loading...

); } // Language selection screen if (step === "lang") { return (

{t.title}

{t.selectLang}

); } // Role selection screen if (step === "role") { return (

{t.title}

{t.whoAreYou}

); } // PIN entry screen if (step === "pin") { return (

{t.title}

{t.enterPin}

setPin(e.target.value.replace(/\D/g, "").slice(0, 4))} placeholder={t.pinPlaceholder} className="w-full rounded-xl border-2 border-stone-300 px-6 py-6 text-center text-4xl font-bold tracking-widest outline-none focus:border-stone-900 mb-4" style={{ letterSpacing: "0.3em" }} autoFocus /> {error && (

{error}

)}
); } // Main log form const selectedHg = headgates.find((hg) => hg.id === effectiveSelectedHeadgate); return (
{/* Header */}

{t.title}

{t.loggedInAs}: {irrigatorName}

{/* Step progress indicator */}
{["lang", "role", "pin", "form"].map((s, i) => { const stepIndex = ["lang", "role", "pin", "form"].indexOf(step as string); const isActive = step === s; const isPast = stepIndex > i; return (
{i < 3 &&
}
); })}
{/* Success banner */} {success && (
✓ {t.success}
)} {/* Error banner */} {error && !success && (
{t.error}: {error}
)} {/* Headgate selector */}
{isHeadgateLocked ? (
{selectedHg?.name ?? effectiveSelectedHeadgate} 🔒 {t.locked}
{selectedHg && ( )}
) : isLoadingHeadgates ? (
Loading headgates...
) : ( )} {selectedHg && !isHeadgateLocked && ( )}
{/* Measurement + Unit row */}
setMeasurement(e.target.value)} required placeholder="0.00" className="w-full rounded-xl border-2 border-stone-300 px-4 py-4 text-2xl font-bold text-stone-900 outline-none focus:border-stone-900 min-h-[60px]" inputMode="decimal" />
{/* GPS Capture */}
{/* Photo Upload */}
{photoPreview ? (
Preview
) : ( )}
{/* Notes */}