Introduce caching and workspace persistence for Edge Gateways, optimize API requests with in-flight request tracking, and implement UI layout adjustments in PosDepartmentStepMobile2Product.vue.

This commit is contained in:
Jeppe Bundgaard
2026-04-22 13:25:19 +02:00
parent c03603f3f3
commit 21c42fa443
2 changed files with 372 additions and 17 deletions
@@ -252,11 +252,7 @@ const noop = () => {};
>
<i class="fas fa-check"></i>
</span>
<span
v-else
class="icon vehicle-card__footer-chevron"
data-testid="pos-mobile-additional-items-header-icon"
>
<span v-else class="icon vehicle-card__footer-chevron" data-testid="pos-mobile-additional-items-header-icon">
<i class="fas fa-angle-right"></i>
</span>
</div>
@@ -484,6 +480,7 @@ const noop = () => {};
}
.vehicle-card__additional-items {
justify-content: flex-start;
min-height: 4.1rem;
}
@@ -512,6 +509,7 @@ const noop = () => {};
display: flex;
flex: 0 0 3.4rem;
justify-content: flex-end;
margin-left: auto;
min-width: 3.4rem;
position: relative;
z-index: 1;
+369 -12
View File
@@ -1,21 +1,372 @@
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
export const listEdgeGatewayDepartments = async () => authenticatedRequest("/departments", "GET", {});
export const EDGE_GATEWAY_WORKSPACE_CACHE_KEY = "truckwash.edgeGatewayWorkspace.cache.v1";
export const listEdgeGateways = async ({ departmentId = null, view = "summary" } = {}) =>
authenticatedRequest("/edge-gateways", "GET", {
...(departmentId ? { department_id: departmentId } : {}),
...(view ? { view } : {}),
const listRequestsInFlight = new Map();
const detailRequestsInFlight = new Map();
let departmentsRequestInFlight = null;
let edgeGatewayWorkspaceCache = null;
const cloneJson = (value) => (value === null || value === undefined ? value : JSON.parse(JSON.stringify(value)));
const normalizeDepartmentId = (departmentId) => (departmentId === null || departmentId === undefined || departmentId === "" ? null : Number(departmentId));
const normalizeGatewayId = (gatewayId) => (gatewayId === null || gatewayId === undefined || gatewayId === "" ? null : String(gatewayId));
const listCacheKey = ({ departmentId = null, view = "summary" } = {}) =>
JSON.stringify({ departmentId: normalizeDepartmentId(departmentId), view: String(view || "summary") });
const createEmptyWorkspaceCache = () => ({
departments: null,
lists: {},
details: {},
});
const storage = () => {
if (typeof window === "undefined" || !window.localStorage) {
return null;
}
return window.localStorage;
};
const loadWorkspaceCache = () => {
if (edgeGatewayWorkspaceCache !== null) {
return edgeGatewayWorkspaceCache;
}
const localStorageHandle = storage();
if (!localStorageHandle) {
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
return edgeGatewayWorkspaceCache;
}
try {
const decoded = JSON.parse(localStorageHandle.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY) || "null");
edgeGatewayWorkspaceCache = {
...createEmptyWorkspaceCache(),
...(decoded && typeof decoded === "object" ? decoded : {}),
lists: decoded?.lists && typeof decoded.lists === "object" ? decoded.lists : {},
details: decoded?.details && typeof decoded.details === "object" ? decoded.details : {},
};
} catch (_error) {
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
}
return edgeGatewayWorkspaceCache;
};
const persistWorkspaceCache = () => {
const localStorageHandle = storage();
if (!localStorageHandle || edgeGatewayWorkspaceCache === null) {
return;
}
localStorageHandle.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(edgeGatewayWorkspaceCache));
};
const mergeGatewaySnapshots = (currentGateway, nextGateway) => {
if (!currentGateway) {
return cloneJson(nextGateway);
}
return {
...cloneJson(currentGateway),
...cloneJson(nextGateway),
metadata: {
...(currentGateway?.metadata || {}),
...(nextGateway?.metadata || {}),
},
inventory: Array.isArray(nextGateway?.inventory)
? cloneJson(nextGateway.inventory)
: Array.isArray(currentGateway?.inventory)
? cloneJson(currentGateway.inventory)
: undefined,
bindings: Array.isArray(nextGateway?.bindings)
? cloneJson(nextGateway.bindings)
: Array.isArray(currentGateway?.bindings)
? cloneJson(currentGateway.bindings)
: undefined,
operations: Array.isArray(nextGateway?.operations)
? cloneJson(nextGateway.operations)
: Array.isArray(currentGateway?.operations)
? cloneJson(currentGateway.operations)
: undefined,
audit_logs: Array.isArray(nextGateway?.audit_logs)
? cloneJson(nextGateway.audit_logs)
: Array.isArray(currentGateway?.audit_logs)
? cloneJson(currentGateway.audit_logs)
: undefined,
};
};
const cacheGatewaySnapshot = (gateway) => {
if (!gateway || !gateway.id) {
return null;
}
const cache = loadWorkspaceCache();
const gatewayId = String(gateway.id);
const mergedGateway = mergeGatewaySnapshots(cache.details[gatewayId]?.data || null, gateway);
cache.details[gatewayId] = {
cachedAt: Date.now(),
data: mergedGateway,
};
Object.entries(cache.lists || {}).forEach(([cacheKey, entry]) => {
const listEntry = entry && typeof entry === "object" ? entry : { params: null, rows: [] };
const params = listEntry.params && typeof listEntry.params === "object" ? listEntry.params : {};
const rows = Array.isArray(listEntry.rows) ? [...listEntry.rows] : [];
const matchesDepartment =
normalizeDepartmentId(params.departmentId) === null || Number(params.departmentId) === Number(gateway.department_id);
const existingIndex = rows.findIndex((item) => Number(item?.id || 0) === Number(gateway.id));
if (existingIndex >= 0) {
rows[existingIndex] = mergeGatewaySnapshots(rows[existingIndex], gateway);
} else if (matchesDepartment) {
rows.unshift(cloneJson(gateway));
}
cache.lists[cacheKey] = {
...listEntry,
rows,
cachedAt: Date.now(),
};
});
export const getEdgeGateway = async (gatewayId) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}`, "GET", {});
persistWorkspaceCache();
return cloneJson(mergedGateway);
};
const cacheFleetSnapshot = ({ departmentId = null, view = "summary", rows = [], meta = {} } = {}) => {
const cache = loadWorkspaceCache();
const key = listCacheKey({ departmentId, view });
cache.lists[key] = {
params: {
departmentId: normalizeDepartmentId(departmentId),
view: String(view || "summary"),
},
rows: cloneJson(Array.isArray(rows) ? rows : []),
meta: cloneJson(meta || {}),
cachedAt: Date.now(),
};
rows.forEach((gateway) => {
if (!gateway?.id) {
return;
}
const gatewayId = String(gateway.id);
if (!cache.details[gatewayId]) {
cache.details[gatewayId] = {
cachedAt: Date.now(),
data: cloneJson(gateway),
};
return;
}
cache.details[gatewayId] = {
cachedAt: Date.now(),
data: mergeGatewaySnapshots(cache.details[gatewayId].data, gateway),
};
});
persistWorkspaceCache();
};
const cacheDepartmentsSnapshot = (departments) => {
const cache = loadWorkspaceCache();
cache.departments = {
cachedAt: Date.now(),
data: cloneJson(Array.isArray(departments) ? departments : []),
};
persistWorkspaceCache();
};
const cacheGatewayFromResponse = (response) => {
const gateway = response?.data?.data;
if (gateway?.id) {
cacheGatewaySnapshot(gateway);
}
return response;
};
export const peekCachedEdgeGatewayDepartments = () => {
const departments = loadWorkspaceCache().departments?.data;
return Array.isArray(departments) ? cloneJson(departments) : null;
};
export const peekCachedEdgeGatewayList = ({ departmentId = null, view = "summary" } = {}) => {
const entry = loadWorkspaceCache().lists[listCacheKey({ departmentId, view })];
if (!entry || !Array.isArray(entry.rows)) {
return null;
}
return {
cachedAt: Number(entry.cachedAt || 0),
rows: cloneJson(entry.rows),
meta: cloneJson(entry.meta || {}),
};
};
export const peekCachedEdgeGateway = (gatewayId) => {
const normalizedGatewayId = normalizeGatewayId(gatewayId);
if (!normalizedGatewayId) {
return null;
}
const entry = loadWorkspaceCache().details[normalizedGatewayId];
return entry?.data ? cloneJson(entry.data) : null;
};
export const patchEdgeGatewayCache = (gatewayId, patch) => {
const current = peekCachedEdgeGateway(gatewayId);
if (!current) {
return null;
}
return cacheGatewaySnapshot(mergeGatewaySnapshots(current, patch));
};
export const removeEdgeGatewayCache = (gatewayId) => {
const normalizedGatewayId = normalizeGatewayId(gatewayId);
if (!normalizedGatewayId) {
return;
}
const cache = loadWorkspaceCache();
delete cache.details[normalizedGatewayId];
Object.entries(cache.lists || {}).forEach(([cacheKey, entry]) => {
const rows = Array.isArray(entry?.rows) ? entry.rows.filter((item) => String(item?.id || "") !== normalizedGatewayId) : [];
cache.lists[cacheKey] = {
...(entry || {}),
rows,
cachedAt: Date.now(),
};
});
persistWorkspaceCache();
};
export const updateDepartmentGatewayCutoverCache = (departmentId, transportMode) => {
const normalizedDepartmentId = normalizeDepartmentId(departmentId);
if (normalizedDepartmentId === null) {
return;
}
const cache = loadWorkspaceCache();
Object.entries(cache.details || {}).forEach(([gatewayId, entry]) => {
if (Number(entry?.data?.department_id || 0) !== normalizedDepartmentId) {
return;
}
cache.details[gatewayId] = {
cachedAt: Date.now(),
data: mergeGatewaySnapshots(entry.data, {
department_transport_mode: transportMode,
}),
};
});
Object.entries(cache.lists || {}).forEach(([cacheKey, entry]) => {
const rows = Array.isArray(entry?.rows)
? entry.rows.map((gateway) =>
Number(gateway?.department_id || 0) === normalizedDepartmentId
? { ...gateway, department_transport_mode: transportMode }
: gateway
)
: [];
cache.lists[cacheKey] = {
...(entry || {}),
rows,
cachedAt: Date.now(),
};
});
persistWorkspaceCache();
};
export const listEdgeGatewayDepartments = async ({ forceRefresh = false } = {}) => {
if (!forceRefresh && departmentsRequestInFlight) {
return departmentsRequestInFlight;
}
const request = authenticatedRequest("/departments", "GET", {}).then((response) => {
cacheDepartmentsSnapshot(response?.data?.data || []);
return response;
});
departmentsRequestInFlight = request.finally(() => {
if (departmentsRequestInFlight === request) {
departmentsRequestInFlight = null;
}
});
return departmentsRequestInFlight;
};
export const listEdgeGateways = async ({ departmentId = null, view = "summary", forceRefresh = false } = {}) => {
const requestKey = listCacheKey({ departmentId, view });
if (!forceRefresh && listRequestsInFlight.has(requestKey)) {
return listRequestsInFlight.get(requestKey);
}
const request = authenticatedRequest("/edge-gateways", "GET", {
...(departmentId ? { department_id: departmentId } : {}),
...(view ? { view } : {}),
}).then((response) => {
cacheFleetSnapshot({
departmentId,
view,
rows: response?.data?.data || [],
meta: response?.data?.meta || {},
});
return response;
});
const trackedRequest = request.finally(() => {
if (listRequestsInFlight.get(requestKey) === trackedRequest) {
listRequestsInFlight.delete(requestKey);
}
});
listRequestsInFlight.set(requestKey, trackedRequest);
return trackedRequest;
};
export const getEdgeGateway = async (gatewayId, { forceRefresh = false } = {}) => {
const normalizedGatewayId = normalizeGatewayId(gatewayId);
if (!normalizedGatewayId) {
throw new Error("A gatewayId is required");
}
if (!forceRefresh && detailRequestsInFlight.has(normalizedGatewayId)) {
return detailRequestsInFlight.get(normalizedGatewayId);
}
const request = authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}`, "GET", {}).then((response) => {
const gateway = response?.data?.data;
if (gateway?.id) {
cacheGatewaySnapshot(gateway);
}
return response;
});
const trackedRequest = request.finally(() => {
if (detailRequestsInFlight.get(normalizedGatewayId) === trackedRequest) {
detailRequestsInFlight.delete(normalizedGatewayId);
}
});
detailRequestsInFlight.set(normalizedGatewayId, trackedRequest);
return trackedRequest;
};
export const listEdgeGatewayOperations = async (gatewayId) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/operations`, "GET", {});
export const createEdgeGatewayOperation = async (gatewayId, type, request = {}) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/operations`, "POST", { type, request });
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/operations`, "POST", { type, request }).then((response) => {
const gateway = response?.data?.data?.gateway;
if (gateway?.id) {
cacheGatewaySnapshot(gateway);
}
return response;
});
export const getEdgeGatewayOperationEvents = async (gatewayId, operationId) =>
authenticatedRequest(
@@ -31,18 +382,24 @@ export const createEdgeGatewayInstallToken = async ({ department_id, label }) =>
authenticatedRequest("/edge-gateways/install-token", "POST", { department_id, label });
export const updateEdgeGateway = async (gatewayId, payload) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}`, "PUT", payload);
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}`, "PUT", payload).then(cacheGatewayFromResponse);
export const triggerEdgeGatewayDiscovery = async (gatewayId) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/discovery`, "POST", {});
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/discovery`, "POST", {}).then(cacheGatewayFromResponse);
export const saveEdgeGatewayBindings = async (gatewayId, bindings) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/bindings`, "PUT", { bindings });
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/bindings`, "PUT", { bindings }).then(cacheGatewayFromResponse);
export const deleteEdgeGateway = async (gatewayId) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}`, "DELETE", {});
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}`, "DELETE", {}).then((response) => {
removeEdgeGatewayCache(gatewayId);
return response;
});
export const setDepartmentGatewayCutover = async (departmentId, transport_mode) =>
authenticatedRequest(`/departments/${encodeURIComponent(departmentId)}/gateway-cutover`, "POST", {
transport_mode,
}).then((response) => {
updateDepartmentGatewayCutoverCache(departmentId, transport_mode);
return response;
});