"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 (
);
}
// 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}
);
}
// 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 (
);
})}
);
}
export default function WaterFieldClient() {
return (
Loading...}>
);
}