import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import api from "../api/client";
import { useAuth } from "../auth/AuthContext";
import SearchBar from "../components/SearchBar";
import ConfirmDialog from "../components/ConfirmDialog";
const TIER_OPTIONS = ["", "basic", "small", "mini", "premium", "vip", "custom"];
// All available columns with their defaults
const ALL_COLUMNS = [
{ key: "status", label: "Status", defaultOn: true },
{ key: "name", label: "Name", defaultOn: true, alwaysOn: true },
{ key: "serialNumber", label: "Serial Number", defaultOn: true },
{ key: "location", label: "Location", defaultOn: true },
{ key: "subscrTier", label: "Subscr Tier", defaultOn: true },
{ key: "maxOutputs", label: "Max Outputs", defaultOn: false },
{ key: "hasClock", label: "Has Clock", defaultOn: false },
{ key: "hasBells", label: "Has Bells", defaultOn: false },
{ key: "totalBells", label: "Total Bells", defaultOn: true },
{ key: "bellGuard", label: "Bell Guard", defaultOn: false },
{ key: "warningsOn", label: "Warnings On", defaultOn: false },
{ key: "serialLogLevel", label: "Serial Log Level", defaultOn: false },
{ key: "sdLogLevel", label: "SD Log Level", defaultOn: false },
{ key: "bellOutputs", label: "Bell Outputs", defaultOn: false },
{ key: "hammerTimings", label: "Hammer Timings", defaultOn: false },
{ key: "ringAlertsMaster", label: "Ring Alerts Master", defaultOn: false },
{ key: "totalPlaybacks", label: "Total Playbacks", defaultOn: false },
{ key: "totalHammerStrikes", label: "Total Hammer Strikes", defaultOn: false },
{ key: "totalWarnings", label: "Total Warnings", defaultOn: false },
{ key: "warrantyActive", label: "Warranty Active", defaultOn: false },
{ key: "totalMelodies", label: "Total Melodies", defaultOn: false },
{ key: "assignedUsers", label: "Assigned Users", defaultOn: true },
];
function getDefaultVisibleColumns() {
const saved = localStorage.getItem("deviceListColumns");
if (saved) {
try {
return JSON.parse(saved);
} catch {
// ignore
}
}
return ALL_COLUMNS.filter((c) => c.defaultOn).map((c) => c.key);
}
function BoolBadge({ value, yesLabel = "Yes", noLabel = "No" }) {
return (
{value ? yesLabel : noLabel}
);
}
function isSubscriptionActive(device) {
const sub = device.device_subscription;
if (!sub?.subscrStart || !sub?.subscrDuration) return false;
try {
const start = parseFirestoreDate(sub.subscrStart);
if (!start) return false;
const end = new Date(start.getTime() + sub.subscrDuration * 86400000);
return end > new Date();
} catch {
return false;
}
}
function isWarrantyActive(device) {
const stats = device.device_stats;
if (!stats?.warrantyStart || !stats?.warrantyPeriod) return !!stats?.warrantyActive;
try {
const start = parseFirestoreDate(stats.warrantyStart);
if (!start) return !!stats?.warrantyActive;
const end = new Date(start.getTime() + stats.warrantyPeriod * 86400000);
return end > new Date();
} catch {
return !!stats?.warrantyActive;
}
}
function parseFirestoreDate(str) {
if (!str) return null;
// Handle format like "22 December 2025 at 16:35:56 UTC+0000"
const cleaned = str.replace(" at ", " ").replace("UTC+0000", "UTC").replace(/UTC\+(\d{4})/, "UTC");
const d = new Date(cleaned);
return isNaN(d.getTime()) ? null : d;
}
export default function DeviceList() {
const [devices, setDevices] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [search, setSearch] = useState("");
const [onlineFilter, setOnlineFilter] = useState("");
const [tierFilter, setTierFilter] = useState("");
const [subscrStatusFilter, setSubscrStatusFilter] = useState("");
const [warrantyStatusFilter, setWarrantyStatusFilter] = useState("");
const [hasClockFilter, setHasClockFilter] = useState("");
const [hasBellsFilter, setHasBellsFilter] = useState("");
const [deleteTarget, setDeleteTarget] = useState(null);
const [visibleColumns, setVisibleColumns] = useState(getDefaultVisibleColumns);
const [showColumnPicker, setShowColumnPicker] = useState(false);
const [mqttStatusMap, setMqttStatusMap] = useState({});
const columnPickerRef = useRef(null);
const navigate = useNavigate();
const { hasPermission } = useAuth();
const canEdit = hasPermission("devices", "edit");
// Close column picker on outside click
useEffect(() => {
const handleClick = (e) => {
if (columnPickerRef.current && !columnPickerRef.current.contains(e.target)) {
setShowColumnPicker(false);
}
};
if (showColumnPicker) {
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}
}, [showColumnPicker]);
const fetchDevices = async () => {
setLoading(true);
setError("");
try {
const params = new URLSearchParams();
if (search) params.set("search", search);
if (onlineFilter === "true") params.set("online", "true");
if (onlineFilter === "false") params.set("online", "false");
if (tierFilter) params.set("tier", tierFilter);
const qs = params.toString();
// Phase 1: load devices from DB immediately
const data = await api.get(`/devices${qs ? `?${qs}` : ""}`);
setDevices(data.devices);
setLoading(false);
// Phase 2: fetch MQTT status in background and update online indicators
api.get("/mqtt/status").then((mqttData) => {
if (mqttData?.devices) {
const map = {};
for (const s of mqttData.devices) {
map[s.device_serial] = s;
}
setMqttStatusMap(map);
}
}).catch(() => {});
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDevices();
}, [search, onlineFilter, tierFilter]);
const handleDelete = async () => {
if (!deleteTarget) return;
try {
await api.delete(`/devices/${deleteTarget.id}`);
setDeleteTarget(null);
fetchDevices();
} catch (err) {
setError(err.message);
setDeleteTarget(null);
}
};
const toggleColumn = (key) => {
const col = ALL_COLUMNS.find((c) => c.key === key);
if (col?.alwaysOn) return;
setVisibleColumns((prev) => {
const next = prev.includes(key)
? prev.filter((k) => k !== key)
: [...prev, key];
localStorage.setItem("deviceListColumns", JSON.stringify(next));
return next;
});
};
const isVisible = (key) => visibleColumns.includes(key);
const renderCellValue = (key, device) => {
const attr = device.device_attributes || {};
const clock = attr.clockSettings || {};
const sub = device.device_subscription || {};
const stats = device.device_stats || {};
switch (key) {
case "status": {
const mqtt = mqttStatusMap[device.device_id];
const isOnline = mqtt ? mqtt.online : device.is_Online;
return (
);
}
case "name":
return (
{device.device_name || "Unnamed Device"}
);
case "serialNumber":
return {device.device_id || "-"};
case "location":
return device.device_location || "-";
case "subscrTier":
return (
{sub.subscrTier || "basic"}
);
case "maxOutputs":
return sub.maxOutputs ?? "-";
case "hasClock":
return
| {col.key === "status" ? "" : col.label} | ))} {canEdit && ()} |
|---|---|
| {renderCellValue(col.key, device)} | ))} {canEdit && (
e.stopPropagation()}
>
|
)}