import { ref, computed, watch } from "vue";
import { defineStore } from "pinia";
import { areasApi } from "../api/modules/areas";
import { useAsyncRequest } from "../composables/useAsyncRequest";
const EXPANDED_NODES_KEY = "sh:areas:expandedNodes";
function loadExpandedNodes() {
try {
const raw = localStorage.getItem(EXPANDED_NODES_KEY);
if (raw) {
const ids = JSON.parse(raw);
if (Array.isArray(ids)) {
return new Set(ids);
}
}
} catch {
// ignore corrupt storage
}
return new Set();
}
function saveExpandedNodes(set) {
try {
localStorage.setItem(EXPANDED_NODES_KEY, JSON.stringify([...set]));
} catch {
// ignore storage errors
}
}
function buildAreaTree(areas) {
const map = {};
const roots = [];
for (const area of areas) {
map[area.id] = { ...area, children: [] };
}
for (const area of areas) {
const node = map[area.id];
const isSelfReference = area.parent_id && area.parent_id == area.id;
const parentExists = area.parent_id && map[area.parent_id];
if (!isSelfReference && parentExists) {
map[area.parent_id].children.push(node);
} else {
roots.push(node);
}
}
if (roots.length === 0 && areas.length > 0) {
return Object.values(map);
}
return roots;
}
export const useAreasStore = defineStore("areas", () => {
const areas = ref([]);
const currentArea = ref(null);
const currentAreaDevices = ref([]);
const currentAreaScripts = ref([]);
const listRequest = useAsyncRequest();
const detailRequest = useAsyncRequest();
const areasById = computed(() =>
Object.fromEntries(areas.value.map((area) => [String(area.id), area]))
);
const areaTree = computed(() => buildAreaTree(areas.value));
const expandedNodeIds = ref(loadExpandedNodes());
watch(
expandedNodeIds,
(next) => {
saveExpandedNodes(next);
},
{ deep: true }
);
function toggleNode(id) {
const next = new Set(expandedNodeIds.value);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
expandedNodeIds.value = next;
}
function isNodeExpanded(id) {
return expandedNodeIds.value.has(id);
}
async function loadAreas() {
return listRequest.execute(async (signal) => {
const result = await areasApi.list({ signal });
if (result.ok) {
areas.value = result.data?.data?.areas || [];
}
return result;
});
}
async function loadAreaDetail(areaId) {
detailRequest.abort();
const controller = new AbortController();
detailRequest.abortController.value = controller;
detailRequest.isLoading.value = true;
detailRequest.error.value = null;
currentArea.value = areasById.value[String(areaId)] || null;
currentAreaDevices.value = [];
currentAreaScripts.value = [];
try {
const [devicesResult, scriptsResult] = await Promise.all([
areasApi.devices(areaId, { signal: controller.signal }),
areasApi.scripts(areaId, { signal: controller.signal }),
]);
detailRequest.abortController.value = null;
detailRequest.isLoading.value = false;
if (!devicesResult.ok) {
if (devicesResult.error?.type !== "timeout") {
detailRequest.error.value = devicesResult.error;
}
return { ok: false, error: detailRequest.error.value };
}
if (!scriptsResult.ok) {
if (scriptsResult.error?.type !== "timeout") {
detailRequest.error.value = scriptsResult.error;
}
return { ok: false, error: detailRequest.error.value };
}
currentAreaDevices.value = devicesResult.data?.data?.devices || [];
currentAreaScripts.value = scriptsResult.data?.data?.scripts || [];
return { ok: true };
} catch (err) {
detailRequest.abortController.value = null;
detailRequest.isLoading.value = false;
detailRequest.error.value = {
type: "network_error",
message: err?.message || "Network error",
};
return { ok: false, error: detailRequest.error.value };
}
}
function clearAreaDetail() {
currentArea.value = null;
currentAreaDevices.value = [];
currentAreaScripts.value = [];
detailRequest.clear();
}
async function loadAreaDevices(areaId) {
const result = await areasApi.devices(areaId);
if (result.ok) {
currentAreaDevices.value = result.data?.data?.devices || [];
}
return result;
}
async function loadAreaScripts(areaId) {
const result = await areasApi.scripts(areaId);
if (result.ok) {
currentAreaScripts.value = result.data?.data?.scripts || [];
}
return result;
}
async function createArea(payload) {
const result = await areasApi.newArea(payload);
if (result.ok) {
const newArea = result.data?.data?.area;
if (newArea) {
areas.value.push(newArea);
}
}
return result;
}
async function renameArea(areaId, displayName) {
const result = await areasApi.updateDisplayName({
area_id: areaId,
display_name: displayName,
});
if (result.ok) {
const idx = areas.value.findIndex((a) => a.id === areaId);
if (idx !== -1) {
areas.value[idx] = { ...areas.value[idx], display_name: displayName };
}
}
return result;
}
async function removeArea(areaId) {
const result = await areasApi.remove(areaId);
if (result.ok) {
areas.value = areas.value.filter((a) => a.id !== areaId);
}
return result;
}
function isRootArea(area) {
return !area?.parent_id || area.parent_id <= 0;
}
async function assignToArea(areaId, parentAreaId) {
const target = areas.value.find((a) => a.id === areaId);
const rootCount = areas.value.filter(isRootArea).length;
if (target && isRootArea(target) && rootCount === 1) {
return {
ok: false,
error: { message: "Cannot assign the last root area as a child." },
};
}
const result = await areasApi.placeInArea({
target_id: areaId,
place_in_area_id: parentAreaId,
});
if (result.ok) {
const idx = areas.value.findIndex((a) => a.id === areaId);
if (idx !== -1) {
areas.value.splice(idx, 1, {
...areas.value[idx],
parent_id: Number(parentAreaId),
});
}
}
return result;
}
async function unassignArea(areaId) {
const result = await areasApi.unassign(areaId);
if (result.ok) {
const idx = areas.value.findIndex((a) => a.id === areaId);
if (idx !== -1) {
areas.value.splice(idx, 1, { ...areas.value[idx], parent_id: 0 });
}
}
return result;
}
return {
areas,
isLoading: listRequest.isLoading,
error: listRequest.error,
currentArea,
currentAreaDevices,
currentAreaScripts,
isLoadingAreaDetail: detailRequest.isLoading,
errorAreaDetail: detailRequest.error,
areasById,
areaTree,
expandedNodeIds,
toggleNode,
isNodeExpanded,
loadAreas,
loadAreaDetail,
clearAreaDetail,
loadAreaDevices,
loadAreaScripts,
createArea,
renameArea,
removeArea,
assignToArea,
unassignArea,
};
});