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(() => {
@@ -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();
@@ -217,6 +257,25 @@ getWorkfeedDepartmentOptions();
>
+
+
+
+
+ {{ selfServeEnabledError }}
+
+
+
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 @@