import { apiFetch } from "./api.js"; import { openChart } from "./chart.js"; import { dom } from "./dom.js"; import { loadEquipments, renderBatchBindingDefaults, renderBindingEquipmentOptions, } from "./equipment.js"; import { renderRoleOptions } from "./roles.js"; import { state } from "./state.js"; function updatePointSourceNodeCount() { const count = dom.nodeTree.querySelectorAll("details, summary").length; dom.pointSourceNodeCount.textContent = `Nodes: ${count}`; } export function formatValue(monitor) { if (!monitor) { return "--"; } if (monitor.value_text) { return monitor.value_text; } if (monitor.value === null || monitor.value === undefined) { return "--"; } return typeof monitor.value === "string" ? monitor.value : JSON.stringify(monitor.value); } export function renderSelectedNodes() { dom.selectedCount.textContent = `Selected ${state.selectedNodeIds.size} nodes`; } export function updateSelectedPointSummary() { const count = state.selectedPointIds.size; dom.selectedPointCount.textContent = `Selected ${count} points`; dom.batchBindingSummary.textContent = `Selected ${count} points`; dom.openBatchBindingBtn.disabled = count === 0; } export function updatePointFilterSummary() { const filters = []; if (state.selectedEquipmentId) { const equipment = state.equipmentMap.get(state.selectedEquipmentId); filters.push(`Equipment:${equipment?.name || equipment?.code || "Unknown"}`); } if (state.selectedSourceId) { const source = state.sources.find((item) => item.id === state.selectedSourceId); filters.push(`Source:${source?.name || "Unknown"}`); } dom.pointFilterSummary.textContent = filters.length ? `Current filter: ${filters.join(" / ")}` : "Current filter: All points"; } export function clearSelectedPoints() { state.selectedPointIds.clear(); dom.toggleAllPoints.checked = false; dom.pointList .querySelectorAll('input[data-point-select="true"]') .forEach((input) => (input.checked = false)); updateSelectedPointSummary(); } function renderNode(node) { const details = document.createElement("details"); const summary = document.createElement("summary"); if (node.children?.length) { summary.classList.add("has-children"); } const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = state.selectedNodeIds.has(node.id); checkbox.addEventListener("change", () => { if (checkbox.checked) { state.selectedNodeIds.add(node.id); } else { state.selectedNodeIds.delete(node.id); } renderSelectedNodes(); }); const label = document.createElement("span"); label.className = "node-label"; label.textContent = `${node.display_name || node.browse_name} (${node.node_class})`; summary.append(checkbox, label); details.appendChild(summary); (node.children || []).forEach((child) => { details.appendChild(renderNode(child)); }); return details; } export function openPointCreateModal() { dom.pointModal.classList.remove("hidden"); if (dom.pointSourceSelect) { dom.pointSourceSelect.value = state.selectedSourceId || ""; } dom.nodeTree.innerHTML = '
Select a source and load nodes
'; dom.pointSourceNodeCount.textContent = "Nodes: 0"; state.selectedNodeIds.clear(); renderSelectedNodes(); } export async function loadTree() { const sourceId = dom.pointSourceSelect.value || state.selectedSourceId; if (!sourceId) { dom.nodeTree.innerHTML = '
Select a source
'; dom.pointSourceNodeCount.textContent = "Nodes: 0"; return; } state.selectedSourceId = sourceId; const data = await apiFetch(`/api/source/${sourceId}/node-tree`); dom.nodeTree.innerHTML = ""; (data || []).forEach((node) => dom.nodeTree.appendChild(renderNode(node))); updatePointSourceNodeCount(); } export async function browseAndLoadTree() { const sourceId = dom.pointSourceSelect.value || state.selectedSourceId; if (!sourceId) { throw new Error("Select a source first"); } state.selectedSourceId = sourceId; await apiFetch(`/api/source/${sourceId}/browse`, { method: "POST" }); await loadTree(); } export async function createPoints() { if (!state.selectedNodeIds.size) { return; } await apiFetch("/api/point/batch", { method: "POST", body: JSON.stringify({ node_ids: Array.from(state.selectedNodeIds) }), }); state.selectedNodeIds.clear(); renderSelectedNodes(); dom.pointModal.classList.add("hidden"); await loadPoints(); } function setPointSelected(pointId, checked) { if (checked) { state.selectedPointIds.add(pointId); } else { state.selectedPointIds.delete(pointId); } updateSelectedPointSummary(); } export async function loadPoints() { const params = new URLSearchParams({ page: String(state.pointsPage), page_size: String(state.pointsPageSize), }); if (state.selectedSourceId) { params.set("source_id", state.selectedSourceId); } if (state.selectedEquipmentId) { params.set("equipment_id", state.selectedEquipmentId); } const data = await apiFetch(`/api/point?${params.toString()}`); const items = data.data || []; state.pointsTotal = typeof data.total === "number" ? data.total : items.length; state.pointEls.clear(); dom.pointList.innerHTML = ""; if (!items.length) { dom.pointList.innerHTML = 'No points'; dom.pointsPageInfo.textContent = `${state.pointsPage} / 1`; clearSelectedPoints(); updatePointFilterSummary(); return; } items.forEach((item) => { const point = item.point || item; const monitor = item.point_monitor || null; const equipment = point.equipment_id ? state.equipmentMap.get(point.equipment_id) : null; const tr = document.createElement("tr"); tr.addEventListener("click", () => { openChart(point.id, point.name).catch((error) => { dom.statusText.textContent = error.message; }); }); tr.innerHTML = `
${point.name}
${point.node_id}
${formatValue(monitor)} ${(monitor?.quality || "unknown").toUpperCase()}
${equipment ? equipment.name : 'Unbound'}
${point.signal_role || "--"}
${monitor?.timestamp || "--"} `; const selectCell = tr.children[0]; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.pointSelect = "true"; checkbox.checked = state.selectedPointIds.has(point.id); checkbox.addEventListener("click", (event) => event.stopPropagation()); checkbox.addEventListener("change", () => setPointSelected(point.id, checkbox.checked)); selectCell.appendChild(checkbox); const actionCell = tr.lastElementChild; const bindBtn = document.createElement("button"); bindBtn.className = "secondary"; bindBtn.textContent = "Bind"; bindBtn.addEventListener("click", (event) => { event.stopPropagation(); openPointBinding(point); }); const deleteBtn = document.createElement("button"); deleteBtn.className = "danger"; deleteBtn.textContent = "Delete"; deleteBtn.addEventListener("click", (event) => { event.stopPropagation(); deletePoint(point.id).catch((error) => { dom.statusText.textContent = error.message; }); }); actionCell.append(bindBtn, deleteBtn); dom.pointList.appendChild(tr); state.pointEls.set(point.id, { row: tr, value: tr.querySelector(".point-value"), quality: tr.querySelector(".badge"), time: tr.querySelector("td:nth-child(6) .muted"), }); }); const totalPages = Math.max(1, Math.ceil(state.pointsTotal / state.pointsPageSize)); dom.pointsPageInfo.textContent = `${state.pointsPage} / ${totalPages}`; const pageCheckboxes = dom.pointList.querySelectorAll('input[data-point-select="true"]'); dom.toggleAllPoints.checked = pageCheckboxes.length > 0 && Array.from(pageCheckboxes).every((input) => input.checked); updateSelectedPointSummary(); updatePointFilterSummary(); } export function openPointBinding(point) { dom.bindingPointId.value = point.id; dom.bindingPointName.value = point.name || ""; renderBindingEquipmentOptions(point.equipment_id || ""); dom.bindingSignalRole.innerHTML = renderRoleOptions(point.signal_role || ""); dom.pointBindingModal.classList.remove("hidden"); } export async function savePointBinding(event) { event.preventDefault(); await apiFetch(`/api/point/${dom.bindingPointId.value}`, { method: "PUT", body: JSON.stringify({ equipment_id: dom.bindingEquipmentId.value || null, signal_role: dom.bindingSignalRole.value || null, }), }); dom.pointBindingModal.classList.add("hidden"); await loadEquipments(); await loadPoints(); } export function openBatchBinding() { if (!state.selectedPointIds.size) { return; } renderBatchBindingDefaults(); updateSelectedPointSummary(); dom.batchBindingModal.classList.remove("hidden"); } export async function saveBatchBinding(event) { event.preventDefault(); if (!state.selectedPointIds.size) { return; } await apiFetch("/api/point/batch/set-equipment", { method: "PUT", body: JSON.stringify({ point_ids: Array.from(state.selectedPointIds), equipment_id: dom.batchBindingEquipmentId.value || null, signal_role: dom.batchBindingSignalRole.value || null, }), }); dom.batchBindingModal.classList.add("hidden"); clearSelectedPoints(); await loadEquipments(); await loadPoints(); } export async function clearBatchBinding() { if (!state.selectedPointIds.size) { return; } await apiFetch("/api/point/batch/set-equipment", { method: "PUT", body: JSON.stringify({ point_ids: Array.from(state.selectedPointIds), equipment_id: null, signal_role: null, }), }); dom.batchBindingModal.classList.add("hidden"); clearSelectedPoints(); await loadEquipments(); await loadPoints(); } export async function deletePoint(pointId) { if (!window.confirm("Delete this point?")) { return; } await apiFetch(`/api/point/${pointId}`, { method: "DELETE" }); state.selectedPointIds.delete(pointId); await loadPoints(); }