diff --git a/src/components/displays/department/moduleNavigation/DepartmentModulesDisplay.vue b/src/components/displays/department/moduleNavigation/DepartmentModulesDisplay.vue index aecadba2..2dfc659a 100644 --- a/src/components/displays/department/moduleNavigation/DepartmentModulesDisplay.vue +++ b/src/components/displays/department/moduleNavigation/DepartmentModulesDisplay.vue @@ -5,12 +5,18 @@ import { authenticatedRequest } from "@/components/session/authenticatedRequest. import {BSwitch} from "buefy"; import {useToast} from 'vue-toast-notification'; import {useI18n} from 'vue-i18n'; +import { + getDepartmentSelfServeEnabled, + setDepartmentSelfServeEnabled, +} from "@/composables/departmentSelfServeEnabled.js"; const router = useRouter(); const toast = useToast(); const { t } = useI18n(); const selfServeEnabled = ref(false); +const isLoadingSelfServeEnabled = ref(false); +const isSavingSelfServeEnabled = ref(false); const bookingsCount = ref(0); const modules = computed(() => [ @@ -98,19 +104,24 @@ const modules = computed(() => [ count: 0, hasSwitch: true, switchValue: selfServeEnabled, + switchDisabled: isLoadingSelfServeEnabled.value || isSavingSelfServeEnabled.value, onToggle: async (newValue) => { const departmentId = getDepartmentId(); if (!departmentId) return; - // Get the current self-serve status, and set it to the opposite of the new value + const currentStatus = selfServeEnabled.value; + selfServeEnabled.value = newValue; + isSavingSelfServeEnabled.value = true; + try { - await authenticatedRequest(`/departments/self-serve/enabled?id=${departmentId}&enabled=${newValue}`, 'PUT'); + selfServeEnabled.value = await setDepartmentSelfServeEnabled(departmentId, newValue); toast.success(newValue ? t('admin.department_modules.self_wash.enabled') : t('admin.department_modules.self_wash.disabled')); } catch (error) { console.error("Failed to update self-serve status", error); - // Revert switch value on error selfServeEnabled.value = currentStatus; toast.error(t('admin.department_modules.self_wash.update_error')); + } finally { + isSavingSelfServeEnabled.value = false; } } } @@ -179,11 +190,13 @@ const getSelfServeStatus = async () => { const departmentId = getDepartmentId(); if (!departmentId) return; + isLoadingSelfServeEnabled.value = true; try { - const response = await authenticatedRequest(`/departments/self-serve/enabled?id=${departmentId}`, 'GET'); - selfServeEnabled.value = response.data.data.enabled; + selfServeEnabled.value = await getDepartmentSelfServeEnabled(departmentId); } catch (error) { console.error("Failed to fetch self-serve status", error); + } finally { + isLoadingSelfServeEnabled.value = false; } }; @@ -217,7 +230,13 @@ setInterval(() => {
{{ module.count }} - +
@@ -232,4 +251,4 @@ setInterval(() => { \ No newline at end of file + diff --git a/src/composables/departmentSelfServeEnabled.js b/src/composables/departmentSelfServeEnabled.js new file mode 100644 index 00000000..846e226c --- /dev/null +++ b/src/composables/departmentSelfServeEnabled.js @@ -0,0 +1,74 @@ +import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue"; + +const normalizeDepartmentId = (departmentId) => { + const parsedDepartmentId = Number.parseInt(String(departmentId), 10); + if (!Number.isInteger(parsedDepartmentId) || parsedDepartmentId <= 0) { + throw new Error(`Invalid department id: ${departmentId}`); + } + return parsedDepartmentId; +}; + +const normalizeEnabledValue = (value) => { + if (typeof value === "boolean") { + return value; + } + + if (typeof value === "number") { + return value > 0; + } + + if (typeof value === "string") { + const normalizedValue = value.trim().toLowerCase(); + if (["true", "1", "yes", "on", "enabled"].includes(normalizedValue)) { + return true; + } + if (["false", "0", "no", "off", "disabled", ""].includes(normalizedValue)) { + return false; + } + } + + return Boolean(value); +}; + +const extractEnabledValue = (response) => { + const payload = response?.data?.data ?? response?.data ?? response; + if (!payload || typeof payload !== "object") { + return undefined; + } + + if (Object.prototype.hasOwnProperty.call(payload, "enabled")) { + return payload.enabled; + } + + return undefined; +}; + +export const getDepartmentSelfServeEnabled = async (departmentId) => { + const normalizedDepartmentId = normalizeDepartmentId(departmentId); + const response = await authenticatedRequest( + `/departments/self-serve/enabled?id=${normalizedDepartmentId}`, + "GET" + ); + + return normalizeEnabledValue(extractEnabledValue(response)); +}; + +export const setDepartmentSelfServeEnabled = async (departmentId, enabled) => { + const normalizedDepartmentId = normalizeDepartmentId(departmentId); + const normalizedEnabled = normalizeEnabledValue(enabled); + + const response = await authenticatedRequest( + `/departments/self-serve/enabled?id=${normalizedDepartmentId}&enabled=${normalizedEnabled ? "true" : "false"}`, + "PUT" + ); + + const responseEnabled = extractEnabledValue(response); + return responseEnabled === undefined + ? normalizedEnabled + : normalizeEnabledValue(responseEnabled); +}; + +export default { + getDepartmentSelfServeEnabled, + setDepartmentSelfServeEnabled, +}; diff --git a/src/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue b/src/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue index 782cd517..165af3e5 100644 --- a/src/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue +++ b/src/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue @@ -12,6 +12,10 @@ import SuperUserDashboardDepartmentModulesNavigation from "@/views/dashboards/superUserDashboard/department/modules/SuperUserDashboardDepartmentModulesNavigation.vue"; import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue"; import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue"; +import { + getDepartmentSelfServeEnabled, + setDepartmentSelfServeEnabled, +} from "@/composables/departmentSelfServeEnabled.js"; // Get the department from the route const router = useRouter(); @@ -21,6 +25,10 @@ const departmentVariables = ref([]); const workfeedDepartmentOptions = ref([{ value: "", label: "No Workfeed department" }]); const isLoadingWorkfeedDepartments = ref(false); const workfeedDepartmentOptionsError = ref(""); +const selfServeEnabled = ref(false); +const isLoadingSelfServeEnabled = ref(false); +const isSavingSelfServeEnabled = ref(false); +const selfServeEnabledError = ref(""); const getDepartmentVariables = async () => { await SessionUser.request( @@ -129,8 +137,40 @@ const setWorkfeedDepartmentId = async (selectedDepartmentId) => { }); }; +const loadSelfServeEnabled = async () => { + isLoadingSelfServeEnabled.value = true; + selfServeEnabledError.value = ""; + try { + selfServeEnabled.value = await getDepartmentSelfServeEnabled(departmentId.value); + } catch (error) { + console.log(error); + selfServeEnabledError.value = "Unable to load self-serve status."; + selfServeEnabled.value = false; + } finally { + isLoadingSelfServeEnabled.value = false; + } +}; + +const updateSelfServeEnabled = async (enabled) => { + const previousValue = selfServeEnabled.value; + selfServeEnabled.value = Boolean(enabled); + isSavingSelfServeEnabled.value = true; + selfServeEnabledError.value = ""; + + try { + selfServeEnabled.value = await setDepartmentSelfServeEnabled(departmentId.value, enabled); + } catch (error) { + console.log(error); + selfServeEnabled.value = previousValue; + selfServeEnabledError.value = "Unable to update self-serve status."; + } finally { + isSavingSelfServeEnabled.value = false; + } +}; + getDepartmentVariables(); getWorkfeedDepartmentOptions(); +loadSelfServeEnabled(); + + + executeLaneCommand(laneId, "RESERVE", payload), RELEASE: (laneId: number, payload: LaneCommandPayload = {}) => executeLaneCommand(laneId, "RELEASE", payload), }, + wash: { + inProgress: (laneId: number) => SessionUser.request( + "/modules/self-serve/lane/wash/in-progress", + "GET", + { + lane_id: normalizeLaneId(laneId), + } + ), + }, gate: { open: (laneId: number, gate: LaneGate) => SessionUser.request( "/modules/self-serve/lane/gate/open", diff --git a/src/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue b/src/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue index c76a670d..6cb31b41 100644 --- a/src/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue +++ b/src/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue @@ -29,6 +29,13 @@ type ForceEnableOptions = { duration?: number | null; licensePlate?: string | null; }; +type InProgressWashDetails = { + lane_id: number; + in_progress: boolean; + session: Record | null; + customer: Record | null; + vehicle: Record | null; +}; const DEFAULT_POLL_INTERVAL_MS = 7000; @@ -37,6 +44,8 @@ const laneRelayLoading = reactive>({}); const laneCommandLoading = reactive>({}); const laneErrors = reactive>({}); const laneLastUpdatedAt = reactive>({}); +const laneInProgressDetails = reactive>({}); +const laneInProgressLoading = reactive>({}); const lanePollSubscribers = reactive>({}); const lanePollIntervals = new Map>(); @@ -84,6 +93,12 @@ const ensureLaneState = (laneId: number): void => { if (!Object.prototype.hasOwnProperty.call(laneLastUpdatedAt, laneId)) { laneLastUpdatedAt[laneId] = null; } + if (!Object.prototype.hasOwnProperty.call(laneInProgressDetails, laneId)) { + laneInProgressDetails[laneId] = null; + } + if (!Object.prototype.hasOwnProperty.call(laneInProgressLoading, laneId)) { + laneInProgressLoading[laneId] = false; + } if (!Object.prototype.hasOwnProperty.call(lanePollSubscribers, laneId)) { lanePollSubscribers[laneId] = 0; } @@ -191,6 +206,40 @@ const updateLaneLastUpdatedAt = (laneId: number): void => { laneLastUpdatedAt[laneId] = Date.now(); }; +const normalizeInProgressDetails = ( + laneId: number, + payload: unknown +): InProgressWashDetails => { + if (!payload || typeof payload !== "object") { + return { + lane_id: laneId, + in_progress: false, + session: null, + customer: null, + vehicle: null, + }; + } + + const source = payload as Record; + const session = source.session && typeof source.session === "object" + ? source.session as Record + : null; + const customer = source.customer && typeof source.customer === "object" + ? source.customer as Record + : null; + const vehicle = source.vehicle && typeof source.vehicle === "object" + ? source.vehicle as Record + : null; + + return { + lane_id: Number.isFinite(Number(source.lane_id)) ? Number(source.lane_id) : laneId, + in_progress: Boolean(source.in_progress), + session, + customer, + vehicle, + }; +}; + export const parseRelayStatus = (status: RelayStatus | null): RelayDisplayStatus => { if (!status) { return "MAINTENANCE"; @@ -304,6 +353,32 @@ export const setRelayState = async ( } }; +export const fetchInProgressDetails = async ( + laneId: number +): Promise => { + const normalizedLaneId = toLaneId(laneId); + ensureLaneState(normalizedLaneId); + + laneInProgressLoading[normalizedLaneId] = true; + + try { + const response = await lane.wash.inProgress(normalizedLaneId); + const details = normalizeInProgressDetails( + normalizedLaneId, + extractPayload(response) + ); + laneInProgressDetails[normalizedLaneId] = details; + laneErrors[normalizedLaneId] = null; + updateLaneLastUpdatedAt(normalizedLaneId); + return details; + } catch (error) { + laneErrors[normalizedLaneId] = parseErrorMessage(error); + throw error; + } finally { + laneInProgressLoading[normalizedLaneId] = false; + } +}; + type CommandLoadingKey = keyof LaneCommandLoadingState; const runLaneAction = async ( @@ -394,13 +469,17 @@ export const startPolling = async ( } const pollInterval = Math.max(1000, Math.round(intervalMs)); + const refreshLane = async () => { + await Promise.allSettled([ + fetchAllRelayStatuses(normalizedLaneId), + fetchInProgressDetails(normalizedLaneId), + ]); + }; - await fetchAllRelayStatuses(normalizedLaneId).catch(() => { - // Polling should continue even if one fetch iteration fails. - }); + await refreshLane(); const intervalRef = setInterval(() => { - fetchAllRelayStatuses(normalizedLaneId).catch(() => { + refreshLane().catch(() => { // Keep polling resilient to temporary backend/network failures. }); }, pollInterval); @@ -439,6 +518,8 @@ export const useMachineConnectivity = (laneId: number) => { const commandLoading = computed(() => laneCommandLoading[normalizedLaneId]); const error = computed(() => laneErrors[normalizedLaneId]); const lastUpdatedAt = computed(() => laneLastUpdatedAt[normalizedLaneId]); + const inProgressDetails = computed(() => laneInProgressDetails[normalizedLaneId]); + const inProgressLoading = computed(() => laneInProgressLoading[normalizedLaneId]); const isPolling = computed(() => lanePollIntervals.has(normalizedLaneId)); return { @@ -449,9 +530,12 @@ export const useMachineConnectivity = (laneId: number) => { commandLoading, error, lastUpdatedAt, + inProgressDetails, + inProgressLoading, isPolling, fetchRelayStatus: (relay: RelayKind) => fetchRelayStatus(normalizedLaneId, relay), fetchAllRelayStatuses: () => fetchAllRelayStatuses(normalizedLaneId), + fetchInProgressDetails: () => fetchInProgressDetails(normalizedLaneId), toggleRelay: (relay: RelayKind, on: boolean) => setRelayState(normalizedLaneId, relay, on), executeLaneCommand: (command: LaneCommand, payload: LaneCommandPayload = {}) => executeLaneCommand(normalizedLaneId, command, payload), stopWash: () => stopWash(normalizedLaneId), @@ -474,11 +558,14 @@ export const machineConnectivityStore = { laneCommandLoading, laneErrors, laneLastUpdatedAt, + laneInProgressDetails, + laneInProgressLoading, lanePollSubscribers, }, functions: { fetchRelayStatus, fetchAllRelayStatuses, + fetchInProgressDetails, setRelayState, executeLaneCommand, forceEnableMachine, @@ -508,6 +595,8 @@ export const __resetMachineConnectivityStoreForTests = (): void => { resetReactiveObject(laneCommandLoading as unknown as Record); resetReactiveObject(laneErrors as unknown as Record); resetReactiveObject(laneLastUpdatedAt as unknown as Record); + resetReactiveObject(laneInProgressDetails as unknown as Record); + resetReactiveObject(laneInProgressLoading as unknown as Record); resetReactiveObject(lanePollSubscribers as unknown as Record); }; diff --git a/src/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineRelayControls.vue b/src/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineRelayControls.vue index 6938db19..47da973c 100644 --- a/src/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineRelayControls.vue +++ b/src/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineRelayControls.vue @@ -1,5 +1,5 @@