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()} |
|
${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();
}