"use client" import React, { useEffect, useState } from "react" import { Loader2, Info } from "lucide-react" import { Button } from "@/components/ui/button" import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { cn } from "@/lib/utils" import { type SensorDevice, type SensorCreate, type DeviceType, type Protocol, createSensor, updateSensor, } from "@/lib/api" // ── Option maps ─────────────────────────────────────────────────────────────── const DEVICE_TYPE_OPTIONS: { value: DeviceType; label: string }[] = [ { value: "ups", label: "UPS" }, { value: "generator", label: "Generator" }, { value: "crac", label: "CRAC Unit" }, { value: "chiller", label: "Chiller" }, { value: "ats", label: "Transfer Switch (ATS)" }, { value: "rack", label: "Rack PDU" }, { value: "network_switch", label: "Network Switch" }, { value: "leak", label: "Leak Sensor" }, { value: "fire_zone", label: "Fire / VESDA Zone" }, { value: "custom", label: "Custom" }, ] const PROTOCOL_OPTIONS: { value: Protocol; label: string }[] = [ { value: "mqtt", label: "MQTT" }, { value: "modbus_tcp", label: "Modbus TCP" }, { value: "modbus_rtu", label: "Modbus RTU" }, { value: "snmp", label: "SNMP" }, { value: "bacnet", label: "BACnet" }, { value: "http", label: "HTTP Poll" }, ] // ── Props ───────────────────────────────────────────────────────────────────── interface Props { siteId: string sensor: SensorDevice | null // null = add mode open: boolean onClose: () => void onSaved: (s: SensorDevice) => void } // ── Shared input / select class ─────────────────────────────────────────────── const inputCls = "border border-border rounded-md px-3 py-1.5 text-sm bg-background text-foreground w-full focus:outline-none focus:ring-1 focus:ring-primary" // ── Protocol-specific config state types ────────────────────────────────────── type MqttConfig = { topic: string } type ModbusTcpConfig = { host: string; port: number; unit_id: number } type ModbusRtuConfig = { serial_port: string; baud_rate: number; unit_id: number } type SnmpConfig = { host: string; community: string; version: "v1" | "v2c" | "v3" } type BacnetConfig = { host: string; device_id: number } type HttpConfig = { url: string; poll_interval_s: number; json_path: string } type ProtocolConfig = | MqttConfig | ModbusTcpConfig | ModbusRtuConfig | SnmpConfig | BacnetConfig | HttpConfig function defaultProtocolConfig(protocol: Protocol): ProtocolConfig { switch (protocol) { case "mqtt": return { topic: "" } case "modbus_tcp": return { host: "", port: 502, unit_id: 1 } case "modbus_rtu": return { serial_port: "", baud_rate: 9600, unit_id: 1 } case "snmp": return { host: "", community: "public", version: "v2c" } case "bacnet": return { host: "", device_id: 0 } case "http": return { url: "", poll_interval_s: 30, json_path: "$.value" } } } function configFromSensor(sensor: SensorDevice): ProtocolConfig { const raw = sensor.protocol_config ?? {} switch (sensor.protocol) { case "mqtt": return { topic: (raw.topic as string) ?? "" } case "modbus_tcp": return { host: (raw.host as string) ?? "", port: (raw.port as number) ?? 502, unit_id: (raw.unit_id as number) ?? 1, } case "modbus_rtu": return { serial_port: (raw.serial_port as string) ?? "", baud_rate: (raw.baud_rate as number) ?? 9600, unit_id: (raw.unit_id as number) ?? 1, } case "snmp": return { host: (raw.host as string) ?? "", community: (raw.community as string) ?? "public", version: (raw.version as "v1" | "v2c" | "v3") ?? "v2c", } case "bacnet": return { host: (raw.host as string) ?? "", device_id: (raw.device_id as number) ?? 0, } case "http": return { url: (raw.url as string) ?? "", poll_interval_s: (raw.poll_interval_s as number) ?? 30, json_path: (raw.json_path as string) ?? "$.value", } } } // ── Protocol-specific field editors ────────────────────────────────────────── interface ConfigEditorProps { config: T onChange: (next: T) => void } function MqttEditor({ config, onChange }: ConfigEditorProps) { return ( onChange({ ...config, topic: e.target.value })} /> ) } function ModbusTcpEditor({ config, onChange }: ConfigEditorProps) { return ( <> onChange({ ...config, host: e.target.value })} /> onChange({ ...config, port: Number(e.target.value) })} /> onChange({ ...config, unit_id: Number(e.target.value) })} /> ) } function ModbusRtuEditor({ config, onChange }: ConfigEditorProps) { return ( <> onChange({ ...config, serial_port: e.target.value })} /> onChange({ ...config, baud_rate: Number(e.target.value) })} /> onChange({ ...config, unit_id: Number(e.target.value) })} /> ) } function SnmpEditor({ config, onChange }: ConfigEditorProps) { return ( <> onChange({ ...config, host: e.target.value })} /> onChange({ ...config, community: e.target.value })} /> ) } function BacnetEditor({ config, onChange }: ConfigEditorProps) { return ( <> onChange({ ...config, host: e.target.value })} /> onChange({ ...config, device_id: Number(e.target.value) })} /> ) } function HttpEditor({ config, onChange }: ConfigEditorProps) { return ( <> onChange({ ...config, url: e.target.value })} /> onChange({ ...config, poll_interval_s: Number(e.target.value) })} /> onChange({ ...config, json_path: e.target.value })} /> ) } // ── Reusable field wrapper ──────────────────────────────────────────────────── function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{label} {children}
) } // ── Toggle switch ───────────────────────────────────────────────────────────── function Toggle({ checked, onChange, }: { checked: boolean onChange: (v: boolean) => void }) { return ( ) } // ── Main component ──────────────────────────────────────────────────────────── export function SensorSheet({ siteId, sensor, open, onClose, onSaved }: Props) { const isEdit = sensor !== null // ── Form state ──────────────────────────────────────────────────────────── const [deviceId, setDeviceId] = useState("") const [name, setName] = useState("") const [deviceType, setDeviceType] = useState("ups") const [room, setRoom] = useState("") const [enabled, setEnabled] = useState(true) const [protocol, setProtocol] = useState("mqtt") const [protoConfig, setProtoConfig] = useState(defaultProtocolConfig("mqtt")) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) // ── Populate form when sheet opens / sensor changes ─────────────────────── useEffect(() => { if (!open) return setError(null) setSaving(false) if (sensor) { setDeviceId(sensor.device_id) setName(sensor.name) setDeviceType(sensor.device_type) setRoom(sensor.room_id ?? "") setEnabled(sensor.enabled) setProtocol(sensor.protocol) setProtoConfig(configFromSensor(sensor)) } else { setDeviceId("") setName("") setDeviceType("ups") setRoom("") setEnabled(true) setProtocol("mqtt") setProtoConfig(defaultProtocolConfig("mqtt")) } }, [open, sensor]) // ── Protocol change — reset config to defaults ──────────────────────────── function handleProtocolChange(p: Protocol) { setProtocol(p) setProtoConfig(defaultProtocolConfig(p)) } // ── Submit ──────────────────────────────────────────────────────────────── async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError(null) setSaving(true) try { const body: SensorCreate = { device_id: deviceId.trim(), name: name.trim(), device_type: deviceType, room_id: room.trim() || null, protocol, protocol_config: protoConfig as Record, enabled, } const saved = isEdit ? await updateSensor(sensor!.id, body) : await createSensor(siteId, body) onSaved(saved) } catch (err) { setError(err instanceof Error ? err.message : "Save failed") } finally { setSaving(false) } } // ── Protocol config editor ──────────────────────────────────────────────── function renderProtoFields() { switch (protocol) { case "mqtt": return ( setProtoConfig(c)} /> ) case "modbus_tcp": return ( setProtoConfig(c)} /> ) case "modbus_rtu": return ( setProtoConfig(c)} /> ) case "snmp": return ( setProtoConfig(c)} /> ) case "bacnet": return ( setProtoConfig(c)} /> ) case "http": return ( setProtoConfig(c)} /> ) } } // ── Render ──────────────────────────────────────────────────────────────── return ( { if (!v) onClose() }}> {isEdit ? "Edit Sensor" : "Add Sensor"}
{/* ── Device ID ── */} setDeviceId(e.target.value)} /> {/* ── Name ── */} setName(e.target.value)} /> {/* ── Device Type ── */} {/* ── Room ── */} setRoom(e.target.value)} /> {/* ── Enabled toggle ── */}
Enabled
{/* ── Divider ── */}
{/* ── Protocol ── */} {/* ── Protocol-specific fields ── */} {renderProtoFields()} {/* ── Non-MQTT collector notice ── */} {protocol !== "mqtt" && (
Protocol config stored — active polling not yet implemented. Data will appear once the collector is enabled.
)} {/* ── Error ── */} {error && (

{error}

)} {/* ── Actions ── */}
) }