348 lines
14 KiB
TypeScript
348 lines
14 KiB
TypeScript
import type React from "react";
|
|
import { useEffect, useState } from "react";
|
|
import type { MeterInput } from "../../api/meters";
|
|
import { fetchConcentrators, type Concentrator } from "../../api/concentrators";
|
|
|
|
type Props = {
|
|
editingId: string | null;
|
|
selectedProject?: string;
|
|
|
|
form: MeterInput;
|
|
setForm: React.Dispatch<React.SetStateAction<MeterInput>>;
|
|
|
|
errors: Record<string, boolean>;
|
|
setErrors: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
|
|
|
onClose: () => void;
|
|
onSave: () => void | Promise<void>;
|
|
};
|
|
|
|
export default function MetersModal({
|
|
editingId,
|
|
selectedProject,
|
|
form,
|
|
setForm,
|
|
errors,
|
|
setErrors,
|
|
onClose,
|
|
onSave,
|
|
}: Props) {
|
|
const title = editingId ? "Editar Medidor" : "Agregar Medidor";
|
|
const [concentrators, setConcentrators] = useState<Concentrator[]>([]);
|
|
const [loadingConcentrators, setLoadingConcentrators] = useState(true);
|
|
const isPruebaProject = selectedProject === "PRUEBA";
|
|
|
|
// Load concentrators for the dropdown
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
const data = await fetchConcentrators();
|
|
setConcentrators(data);
|
|
} catch (error) {
|
|
console.error("Error loading concentrators:", error);
|
|
} finally {
|
|
setLoadingConcentrators(false);
|
|
}
|
|
};
|
|
load();
|
|
}, []);
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-xl p-6 w-[500px] max-h-[90vh] overflow-y-auto space-y-4">
|
|
<h2 className="text-lg font-semibold">{title}</h2>
|
|
|
|
{/* FORM */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-gray-700 border-b pb-2">
|
|
Información del Medidor
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Serial *</label>
|
|
<input
|
|
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
|
errors["serialNumber"] ? "border-red-500" : ""
|
|
}`}
|
|
placeholder="Número de serie"
|
|
value={form.serialNumber}
|
|
onChange={(e) => {
|
|
setForm({ ...form, serialNumber: e.target.value });
|
|
if (errors["serialNumber"]) setErrors({ ...errors, serialNumber: false });
|
|
}}
|
|
required
|
|
/>
|
|
{errors["serialNumber"] && (
|
|
<p className="text-red-500 text-xs mt-1">Campo requerido</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Meter ID</label>
|
|
<input
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ID del medidor (opcional)"
|
|
value={form.meterId ?? ""}
|
|
onChange={(e) => setForm({ ...form, meterId: e.target.value || undefined })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Nombre *</label>
|
|
<input
|
|
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
|
errors["name"] ? "border-red-500" : ""
|
|
}`}
|
|
placeholder="Nombre del medidor"
|
|
value={form.name}
|
|
onChange={(e) => {
|
|
setForm({ ...form, name: e.target.value });
|
|
if (errors["name"]) setErrors({ ...errors, name: false });
|
|
}}
|
|
required
|
|
/>
|
|
{errors["name"] && <p className="text-red-500 text-xs mt-1">Campo requerido</p>}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Concentrador *</label>
|
|
<select
|
|
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
|
errors["concentratorId"] ? "border-red-500" : ""
|
|
}`}
|
|
value={form.concentratorId}
|
|
onChange={(e) => {
|
|
setForm({ ...form, concentratorId: e.target.value });
|
|
if (errors["concentratorId"]) setErrors({ ...errors, concentratorId: false });
|
|
}}
|
|
disabled={loadingConcentrators}
|
|
required
|
|
>
|
|
<option value="">
|
|
{loadingConcentrators ? "Cargando..." : "Selecciona un concentrador"}
|
|
</option>
|
|
{concentrators.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.name} ({c.serialNumber})
|
|
</option>
|
|
))}
|
|
</select>
|
|
{errors["concentratorId"] && (
|
|
<p className="text-red-500 text-xs mt-1">Selecciona un concentrador</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Ubicación</label>
|
|
<input
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="Ubicación del medidor (opcional)"
|
|
value={form.location ?? ""}
|
|
onChange={(e) => setForm({ ...form, location: e.target.value || undefined })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Tipo</label>
|
|
<select
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
value={form.type ?? "LORA"}
|
|
onChange={(e) => setForm({ ...form, type: e.target.value })}
|
|
>
|
|
<option value="LORA">LoRa</option>
|
|
<option value="LORAWAN">LoRaWAN</option>
|
|
<option value="GRANDES">Grandes Consumidores</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Estado</label>
|
|
<select
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
value={form.status ?? "ACTIVE"}
|
|
onChange={(e) => setForm({ ...form, status: e.target.value })}
|
|
>
|
|
<option value="ACTIVE">Activo</option>
|
|
<option value="INACTIVE">Inactivo</option>
|
|
<option value="MAINTENANCE">Mantenimiento</option>
|
|
<option value="FAULTY">Averiado</option>
|
|
<option value="REPLACED">Reemplazado</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Fecha de Instalación</label>
|
|
<input
|
|
type="date"
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
value={form.installationDate?.split("T")[0] ?? ""}
|
|
onChange={(e) =>
|
|
setForm({
|
|
...form,
|
|
installationDate: e.target.value ? new Date(e.target.value).toISOString() : undefined,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{isPruebaProject && (
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-gray-700 border-b pb-2">
|
|
Información Técnica (Proyecto PRUEBA)
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Protocol</label>
|
|
<input
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: LoRaWAN"
|
|
value={form.protocol ?? ""}
|
|
onChange={(e) => setForm({ ...form, protocol: e.target.value || undefined })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Voltage (V)</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: 3.6"
|
|
value={form.voltage ?? ""}
|
|
onChange={(e) => setForm({ ...form, voltage: e.target.value ? parseFloat(e.target.value) : undefined })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Signal (dBm)</label>
|
|
<input
|
|
type="number"
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: -85"
|
|
value={form.signal ?? ""}
|
|
onChange={(e) => setForm({ ...form, signal: e.target.value ? parseInt(e.target.value) : undefined })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Current Flow</label>
|
|
<input
|
|
type="number"
|
|
step="0.0001"
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: 12.5"
|
|
value={form.currentFlow ?? ""}
|
|
onChange={(e) => setForm({ ...form, currentFlow: e.target.value ? parseFloat(e.target.value) : undefined })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Total Flow Reverse</label>
|
|
<input
|
|
type="number"
|
|
step="0.0001"
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: 0.0"
|
|
value={form.totalFlowReverse ?? ""}
|
|
onChange={(e) => setForm({ ...form, totalFlowReverse: e.target.value ? parseFloat(e.target.value) : undefined })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Leakage Status</label>
|
|
<select
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
value={form.leakageStatus ?? ""}
|
|
onChange={(e) => setForm({ ...form, leakageStatus: e.target.value || undefined })}
|
|
>
|
|
<option value="">Selecciona...</option>
|
|
<option value="OK">OK</option>
|
|
<option value="WARNING">Warning</option>
|
|
<option value="ALERT">Alert</option>
|
|
<option value="CRITICAL">Critical</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Burst Status</label>
|
|
<select
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
value={form.burstStatus ?? ""}
|
|
onChange={(e) => setForm({ ...form, burstStatus: e.target.value || undefined })}
|
|
>
|
|
<option value="">Selecciona...</option>
|
|
<option value="OK">OK</option>
|
|
<option value="WARNING">Warning</option>
|
|
<option value="ALERT">Alert</option>
|
|
<option value="CRITICAL">Critical</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Manufacturer</label>
|
|
<input
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: Kamstrup"
|
|
value={form.manufacturer ?? ""}
|
|
onChange={(e) => setForm({ ...form, manufacturer: e.target.value || undefined })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Latitude</label>
|
|
<input
|
|
type="number"
|
|
step="0.00000001"
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: 20.659698"
|
|
value={form.latitude ?? ""}
|
|
onChange={(e) => setForm({ ...form, latitude: e.target.value ? parseFloat(e.target.value) : undefined })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-gray-600 mb-1">Longitude</label>
|
|
<input
|
|
type="number"
|
|
step="0.00000001"
|
|
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ej: -103.349609"
|
|
value={form.longitude ?? ""}
|
|
onChange={(e) => setForm({ ...form, longitude: e.target.value ? parseFloat(e.target.value) : undefined })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ACTIONS */}
|
|
<div className="flex justify-end gap-2 pt-3 border-t">
|
|
<button onClick={onClose} className="px-4 py-2 rounded hover:bg-gray-100">
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
onClick={onSave}
|
|
className="bg-[#4c5f9e] text-white px-4 py-2 rounded hover:bg-[#3d4d7e]"
|
|
>
|
|
Guardar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|