import { useCallback, useEffect, useRef, useState } from 'react' import { Thermometer, CheckSquare, X } from 'lucide-react' import { kioskLogin, kioskFetchStatus, KioskSessionError } from '../api' import type { DeviceSummary, ZoneStatus } from '../types' import { KIOSK_DEVICE_TYPES } from '../types' import KioskZoneBlock from '../components/KioskZoneBlock' import KioskZonePanel from '../components/KioskZonePanel' import KioskModal from '../components/KioskModal' import KioskBulkPanel from '../components/KioskBulkPanel' // Control surface, not a passive display — far shorter than weather's 5-minute // precedent, but not so aggressive it hammers the backend given the underlying // data is already push/poll-cached (MQTT for TRV/MHI, a 60s poller for Daikin). const POLL_MS = 12_000 function liveDevicesOf(zone: ZoneStatus): DeviceSummary[] { return zone.devices.filter(d => KIOSK_DEVICE_TYPES.includes(d.device_type)) } // The wall-panel kiosk view — a dedicated route/login, deliberately outside the // normal staff AuthGate/Layout (see App.tsx). One master panel: one tile per // ROOM/ZONE (same as the Dashboard's ZoneCard — a room's TRV and AC show // together, not as separate tiles) in a scrollable grid; tap a tile for that // zone's full control modal, or toggle Select for type-aware bulk editing // across zones. See project_hvac_kiosk memory for the design decisions behind // this (access model, layout, bulk-edit mechanism). export default function Kiosk() { const [authState, setAuthState] = useState<'checking' | 'login' | 'authed'>('checking') const [password, setPassword] = useState('') const [loginError, setLoginError] = useState('') const [loginLoading, setLoginLoading] = useState(false) const [zones, setZones] = useState([]) const [loadError, setLoadError] = useState('') const [selectionMode, setSelectionMode] = useState(false) const [selectedZoneIds, setSelectedZoneIds] = useState>(new Set()) const [editingZone, setEditingZone] = useState(null) const [bulkOpen, setBulkOpen] = useState(false) const pollRef = useRef | null>(null) const load = useCallback(() => { kioskFetchStatus() .then(s => { setZones(s.zones.filter(z => liveDevicesOf(z).length > 0)) setLoadError('') setAuthState('authed') }) .catch(e => { if (e instanceof KioskSessionError) { setAuthState('login'); return } setLoadError(e instanceof Error ? e.message : 'Failed to load device status') }) }, []) useEffect(() => { load() }, [load]) useEffect(() => { if (authState !== 'authed') { if (pollRef.current) clearInterval(pollRef.current) return } pollRef.current = setInterval(load, POLL_MS) return () => { if (pollRef.current) clearInterval(pollRef.current) } }, [authState, load]) async function handleLogin(e: React.FormEvent) { e.preventDefault() setLoginLoading(true); setLoginError('') try { await kioskLogin(password) setPassword('') load() } catch (e) { setLoginError(e instanceof Error ? e.message : 'Login failed') } finally { setLoginLoading(false) } } function toggleSelectionMode() { setSelectionMode(m => !m) setSelectedZoneIds(new Set()) } function tapZone(zone: ZoneStatus) { if (selectionMode) { setSelectedZoneIds(prev => { const next = new Set(prev) if (next.has(zone.id)) next.delete(zone.id); else next.add(zone.id) return next }) } else { setEditingZone(zone) } } function closeBulk() { setBulkOpen(false) setSelectionMode(false) setSelectedZoneIds(new Set()) load() } // Bulk edit works across whichever zones are selected — gather every live // device from all of them, KioskBulkPanel sections by device type from there. const selectedDevices = zones.filter(z => selectedZoneIds.has(z.id)).flatMap(liveDevicesOf) if (authState === 'checking') { return

Loading…

} if (authState === 'login') { return (

HVAC Kiosk

setPassword(e.target.value)} placeholder="Kiosk password" autoFocus required autoComplete="off" /> {loginError &&
{loginError}
}
) } return (
HVAC Kiosk
{loadError &&
{loadError}
}
{zones.map(zone => ( tapZone(zone)} /> ))} {zones.length === 0 && !loadError && (

No live-controllable devices assigned to a zone yet.

)}
{selectionMode && selectedZoneIds.size > 0 && (
{selectedZoneIds.size} zone{selectedZoneIds.size === 1 ? '' : 's'} selected
)} {editingZone && ( { setEditingZone(null); load() }}> )} {bulkOpen && ( )}
) }