Fix self-serve settings state and contract (#229)

Ensure department-scoped self-serve settings load and save safely across route transitions, document the API contract, and cover stale in-flight state.
This commit is contained in:
Jeppe B
2026-07-28 18:34:35 +02:00
committed by GitHub
parent 7a84cd9162
commit 5204536f92
9 changed files with 385 additions and 58 deletions
+28
View File
@@ -3534,6 +3534,19 @@ paths:
properties: properties:
enabled: enabled:
type: boolean type: boolean
auto_deactivation:
type: object
required: [at, timezone, label]
properties:
at:
type: string
format: date-time
nullable: true
timezone:
type: string
example: Europe/Copenhagen
label:
type: string
'404': '404':
$ref: '#/components/responses/NotFound' $ref: '#/components/responses/NotFound'
put: put:
@@ -3566,6 +3579,21 @@ paths:
properties: properties:
message: message:
type: string type: string
enabled:
type: boolean
auto_deactivation:
type: object
required: [at, timezone, label]
properties:
at:
type: string
format: date-time
nullable: true
timezone:
type: string
example: Europe/Copenhagen
label:
type: string
'404': '404':
$ref: '#/components/responses/NotFound' $ref: '#/components/responses/NotFound'
+37 -6
View File
@@ -43,17 +43,40 @@ const extractEnabledValue = (response) => {
return undefined; return undefined;
}; };
export const getDepartmentSelfServeEnabled = async (departmentId) => { const extractSelfServeStatus = (response, fallbackEnabled = false) => {
const payload = response?.data?.data ?? response?.data ?? response;
const enabled = Object.prototype.hasOwnProperty.call(payload || {}, "enabled")
? normalizeEnabledValue(payload.enabled)
: normalizeEnabledValue(fallbackEnabled);
const autoDeactivation = payload?.auto_deactivation || payload?.autoDeactivation || null;
return {
enabled,
hasAutoDeactivation: autoDeactivation !== null,
autoDeactivation: {
at: autoDeactivation?.at || null,
timezone: autoDeactivation?.timezone || "Europe/Copenhagen",
label: autoDeactivation?.label || (autoDeactivation?.at ? String(autoDeactivation.at) : "NEVER"),
},
};
};
export const getDepartmentSelfServeStatus = async (departmentId) => {
const normalizedDepartmentId = normalizeDepartmentId(departmentId); const normalizedDepartmentId = normalizeDepartmentId(departmentId);
const response = await authenticatedRequest( const response = await authenticatedRequest(
`/departments/self-serve/enabled?id=${normalizedDepartmentId}`, `/departments/self-serve/enabled?id=${normalizedDepartmentId}`,
"GET" "GET"
); );
return normalizeEnabledValue(extractEnabledValue(response)); return extractSelfServeStatus(response);
}; };
export const setDepartmentSelfServeEnabled = async (departmentId, enabled) => { export const getDepartmentSelfServeEnabled = async (departmentId) => {
const status = await getDepartmentSelfServeStatus(departmentId);
return status.enabled;
};
export const setDepartmentSelfServeStatus = async (departmentId, enabled) => {
const normalizedDepartmentId = normalizeDepartmentId(departmentId); const normalizedDepartmentId = normalizeDepartmentId(departmentId);
const normalizedEnabled = normalizeEnabledValue(enabled); const normalizedEnabled = normalizeEnabledValue(enabled);
@@ -63,12 +86,20 @@ export const setDepartmentSelfServeEnabled = async (departmentId, enabled) => {
); );
const responseEnabled = extractEnabledValue(response); const responseEnabled = extractEnabledValue(response);
return responseEnabled === undefined return extractSelfServeStatus(
? normalizedEnabled response,
: normalizeEnabledValue(responseEnabled); responseEnabled === undefined ? normalizedEnabled : normalizeEnabledValue(responseEnabled)
);
};
export const setDepartmentSelfServeEnabled = async (departmentId, enabled) => {
const status = await setDepartmentSelfServeStatus(departmentId, enabled);
return status.enabled;
}; };
export default { export default {
getDepartmentSelfServeEnabled, getDepartmentSelfServeEnabled,
getDepartmentSelfServeStatus,
setDepartmentSelfServeEnabled, setDepartmentSelfServeEnabled,
setDepartmentSelfServeStatus,
}; };
+6
View File
@@ -965,6 +965,12 @@ export const router = createRouter({
component: DepartmentModulesSetup, component: DepartmentModulesSetup,
meta: { middleware: superUserMiddleware } meta: { middleware: superUserMiddleware }
}, },
{
name: 'departmentsopeninghours',
path: '/superuser/departments/:departmentId/opening-hours',
component: DepartmentTimeBookingsOpeningHours,
meta: { middleware: superUserMiddleware }
},
{ {
name: 'departmentsgateways', name: 'departmentsgateways',
path: '/superuser/departments/:departmentId/gateways', path: '/superuser/departments/:departmentId/gateways',
@@ -2,10 +2,27 @@
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue"; import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import {SessionUser} from "@/components/session/token/SessionUser.vue"; import {SessionUser} from "@/components/session/token/SessionUser.vue";
import DepartmentDashboardPageWrapper from "@/views/dashboards/departmentDashboard/DepartmentDashboardPageWrapper.vue"; import DepartmentDashboardPageWrapper from "@/views/dashboards/departmentDashboard/DepartmentDashboardPageWrapper.vue";
import DepartmentSubPageWrapper from "@/views/dashboards/superUserDashboard/department/DepartmentSubPageWrapper.vue";
import { setDepartment as setSelectedDepartment } from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
import NotFoundFallBackPageWrapper from "@/components/page/wrappers/NotFoundFallBackPageWrapper.vue"; import NotFoundFallBackPageWrapper from "@/components/page/wrappers/NotFoundFallBackPageWrapper.vue";
import {ref} from 'vue'; import PageTitle from "@/components/global/PageTitle.vue";
import {computed, ref, watch} from 'vue';
import {useRoute} from "vue-router";
const opening_hours = ref(null); const opening_hours = ref(null);
const route = useRoute();
const departmentId = computed(() => route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl());
const isSuperuserDepartmentRoute = computed(() => String(route.path || "").startsWith("/superuser/departments/"));
const pageTitle = computed(() => SessionUser.objects.global.language.opening_hours);
const pageSubtitle = "Afdelingens åbningstider";
const hasPagePermission = computed(() => (
isSuperuserDepartmentRoute.value
? SessionUser.canAccessSuperUser()
: SessionUser.canAccessAdmin()
));
const hasDepartmentAccess = computed(() => (
Boolean(departmentId.value) && SessionUser.canAccessDepartment(departmentId.value)
));
const isOpeningHoursLoaded = () => { return opening_hours.value !== null; } const isOpeningHoursLoaded = () => { return opening_hours.value !== null; }
@@ -54,30 +71,50 @@ const getControlClass = () => {
return ''; return '';
} }
const getOpeningHours = (departmentId) => { let openingHoursRequest = 0;
const getOpeningHours = (selectedDepartmentId) => {
const requestId = ++openingHoursRequest;
opening_hours.value = null;
// Fetch the opening hours for the department // Fetch the opening hours for the department
SessionUser.request( return SessionUser.request(
SessionUser.objects.department_time_bookings_opening_hours.meta.endpoint, SessionUser.objects.department_time_bookings_opening_hours.meta.endpoint,
'GET', 'GET',
{ {
department: departmentId department: selectedDepartmentId
} }
).then((response) => { ).then((response) => {
if (requestId !== openingHoursRequest) {
return;
}
console.log('response', response.data.data); console.log('response', response.data.data);
// Check if there's any object in the response // Check if there's any object in the response
opening_hours.value = response.data.data; opening_hours.value = response.data.data;
}) })
.catch((error) => { .catch((error) => {
console.error('Error fetching opening hours:', error); if (requestId === openingHoursRequest) {
console.error('Error fetching opening hours:', error);
}
}); });
} }
const departmentId = SessionUser.functions.getDepartmentIdFromUrl(); watch(
if (departmentId) { () => [departmentId.value, isSuperuserDepartmentRoute.value],
getOpeningHours(departmentId); ([selectedDepartmentId, isSuperuserRoute], [previousDepartmentId] = []) => {
} else { if (selectedDepartmentId) {
if (isSuperuserRoute) {
setSelectedDepartment(selectedDepartmentId);
}
if (selectedDepartmentId !== previousDepartmentId) {
getOpeningHours(selectedDepartmentId);
}
return;
}
openingHoursRequest++;
opening_hours.value = null;
console.error('Department ID not found in URL'); console.error('Department ID not found in URL');
} },
{ immediate: true }
);
const getOpeningHoursValue = (name) => { const getOpeningHoursValue = (name) => {
if (opening_hours.value === null) { if (opening_hours.value === null) {
@@ -88,12 +125,64 @@ const getOpeningHoursValue = (name) => {
</script> </script>
<template> <template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessAdmin()"> <RestrictedPageWrapper :hasPermission="hasPagePermission">
<DepartmentSubPageWrapper v-if="isSuperuserDepartmentRoute">
<template #title>
<PageTitle :title="pageTitle" :subtitle="pageSubtitle" />
</template>
<NotFoundFallBackPageWrapper :exists="hasDepartmentAccess" :error="$t('admin.errors.select_department')">
<table class="table is-striped is-bordered is-hoverable is-fullwidth">
<thead>
<tr>
<th>{{SessionUser.objects.global.language.weekday}}</th>
<th>{{SessionUser.objects.global.language.opening_hour}}</th>
<th>{{SessionUser.objects.global.language.closing_hour}}</th>
</tr>
</thead>
<tbody>
<tr v-for="(weekday, index) in weekdays" :key="index">
<td>{{ weekday.label }}</td>
<td>
<div class="field has-addons">
<div class="control is-expanded" :class="getControlClass(weekday, 'start')">
<input type="time" class="input is-fullwidth" @change="onChangeTime(weekday.name + '_start', $event.target.value)" v-if="isOpeningHoursLoaded()" :value="getOpeningHoursValue(weekday.name + '_start')" :id="weekday.name + '_start'" />
<input type="time" class="input is-skeleton is-fullwidth" v-else disabled/>
</div>
<div class="control">
<button class="button is-danger" @click="onChangeTime(weekday.name + '_start', null)" v-bind:disabled="!getOpeningHoursValue(weekday.name + '_start')">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
</button>
</div>
</div>
</td>
<td>
<div class="field has-addons">
<div class="control is-expanded" :class="getControlClass(weekday, 'end')">
<input type="time" class="input is-fullwidth" @change="onChangeTime(weekday.name + '_end', $event.target.value)" v-if="isOpeningHoursLoaded()" :value="getOpeningHoursValue(weekday.name + '_end')" :id="weekday.name + '_end'" />
<input type="time" class="input is-skeleton is-fullwidth" v-else disabled/>
</div>
<div class="control">
<button class="button is-danger" @click="onChangeTime(weekday.name + '_end', null)" v-bind:disabled="!getOpeningHoursValue(weekday.name + '_end')">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</NotFoundFallBackPageWrapper>
</DepartmentSubPageWrapper>
<DepartmentDashboardPageWrapper <DepartmentDashboardPageWrapper
:title="SessionUser.objects.global.language.opening_hours" v-else
subtitle="Afdelingens åbningstider" :title="pageTitle"
:subtitle="pageSubtitle"
> >
<NotFoundFallBackPageWrapper :exists="SessionUser.functions.getDepartmentIdFromUrl() && SessionUser.canAccessDepartment(SessionUser.functions.getDepartmentIdFromUrl())" :error="$t('admin.errors.select_department')"> <NotFoundFallBackPageWrapper :exists="hasDepartmentAccess" :error="$t('admin.errors.select_department')">
<table class="table is-striped is-bordered is-hoverable is-fullwidth"> <table class="table is-striped is-bordered is-hoverable is-fullwidth">
<thead> <thead>
<tr> <tr>
@@ -146,4 +235,4 @@ const getOpeningHoursValue = (name) => {
</template> </template>
<style scoped> <style scoped>
</style> </style>
@@ -26,6 +26,12 @@ const tabs = computed(() =>
to: `${departmentPath.value}/modules`, to: `${departmentPath.value}/modules`,
active: (path) => path.startsWith(`${departmentPath.value}/modules`), active: (path) => path.startsWith(`${departmentPath.value}/modules`),
}, },
{
key: "opening-hours",
label: "Opening hours",
to: `${departmentPath.value}/opening-hours`,
active: (path) => path.startsWith(`${departmentPath.value}/opening-hours`),
},
{ {
key: "branding", key: "branding",
label: t("superuser_dashboard.department_navigation.branding"), label: t("superuser_dashboard.department_navigation.branding"),
@@ -5,7 +5,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
import DepartmentSubPageWrapper from "@/views/dashboards/superUserDashboard/department/DepartmentSubPageWrapper.vue"; import DepartmentSubPageWrapper from "@/views/dashboards/superUserDashboard/department/DepartmentSubPageWrapper.vue";
import { departmentAdvanced, setDepartment, departmentId } from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue"; import { departmentAdvanced, setDepartment, departmentId } from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { computed, ref } from "vue"; import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import ConfigurationSelect from "@/components/displays/superuser/configuration/ConfigurationSelect.vue"; import ConfigurationSelect from "@/components/displays/superuser/configuration/ConfigurationSelect.vue";
import SuperuserOverviewPanel from "@/components/displays/superuser/overview/SuperuserOverviewPanel.vue"; import SuperuserOverviewPanel from "@/components/displays/superuser/overview/SuperuserOverviewPanel.vue";
@@ -14,14 +14,13 @@ import SuperUserDashboardDepartmentModulesNavigation
import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue"; import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue";
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue"; import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
import { import {
getDepartmentSelfServeEnabled, getDepartmentSelfServeStatus,
setDepartmentSelfServeEnabled, setDepartmentSelfServeStatus,
} from "@/composables/departmentSelfServeEnabled.js"; } from "@/composables/departmentSelfServeEnabled.js";
// Get the department from the route // Get the department from the route
const router = useRouter(); const router = useRouter();
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
setDepartment(router.currentRoute.value.params.departmentId);
const departmentTitle = computed(() => departmentAdvanced.value.name || t("superuser_dashboard.department_navigation.modules")); const departmentTitle = computed(() => departmentAdvanced.value.name || t("superuser_dashboard.department_navigation.modules"));
const pageSubtitle = computed(() => t("superuser_dashboard.department_pages.modules.subtitle")); const pageSubtitle = computed(() => t("superuser_dashboard.department_pages.modules.subtitle"));
@@ -30,9 +29,13 @@ const workfeedDepartmentOptions = ref([{ value: "", label: "No Workfeed departme
const isLoadingWorkfeedDepartments = ref(false); const isLoadingWorkfeedDepartments = ref(false);
const workfeedDepartmentOptionsError = ref(""); const workfeedDepartmentOptionsError = ref("");
const selfServeEnabled = ref(false); const selfServeEnabled = ref(false);
const selfServeAutoDeactivation = ref({ at: null, timezone: "Europe/Copenhagen", label: "NEVER" });
const isLoadingSelfServeEnabled = ref(false); const isLoadingSelfServeEnabled = ref(false);
const isSavingSelfServeEnabled = ref(false); const isSavingSelfServeEnabled = ref(false);
const selfServeEnabledError = ref(""); const selfServeEnabledError = ref("");
let departmentVariablesRequestSequence = 0;
let selfServeStatusRequestSequence = 0;
let selfServeSaveRequestSequence = 0;
const departmentVariableDescriptions = computed(() => ({ const departmentVariableDescriptions = computed(() => ({
bookingsystem_enabled: t("superuser_dashboard.department_pages.modules.variables.bookingsystem_enabled"), bookingsystem_enabled: t("superuser_dashboard.department_pages.modules.variables.bookingsystem_enabled"),
bookingsystem_time_based_enabled: t("superuser_dashboard.department_pages.modules.variables.bookingsystem_time_based_enabled"), bookingsystem_time_based_enabled: t("superuser_dashboard.department_pages.modules.variables.bookingsystem_time_based_enabled"),
@@ -47,20 +50,44 @@ const departmentVariableRows = computed(() => departmentVariables.value.map((var
value: variable.value, value: variable.value,
}))); })));
const getDepartmentVariables = async () => { const selfServeAutoDeactivationText = computed(() => {
await SessionUser.request( if (!selfServeEnabled.value) {
SessionUser.objects.department_variables.meta.endpoint, return "Automatic deactivation: not scheduled while self-serve is disabled.";
"GET", }
{
filters: "department_id:" + departmentId.value, const label = String(selfServeAutoDeactivation.value?.label || "").trim();
return `Automatic deactivation: ${label || "NEVER"}`;
});
const getDepartmentVariables = async (
requestedDepartmentId = String(departmentId.value)
) => {
const requestSequence = ++departmentVariablesRequestSequence;
try {
const response = await SessionUser.request(
SessionUser.objects.department_variables.meta.endpoint,
"GET",
{
filters: "department_id:" + requestedDepartmentId,
}
);
if (
requestSequence !== departmentVariablesRequestSequence
|| String(departmentId.value) !== requestedDepartmentId
) {
return false;
} }
) departmentVariables.value = response.data.data;
.then((response) => { return true;
departmentVariables.value = response.data.data; } catch (error) {
}) if (
.catch((error) => { requestSequence === departmentVariablesRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
console.log(error); console.log(error);
}); }
return false;
}
}; };
const getVariableValue = (variable) => { const getVariableValue = (variable) => {
@@ -153,40 +180,107 @@ const setWorkfeedDepartmentId = async (selectedDepartmentId) => {
}); });
}; };
const loadSelfServeEnabled = async () => { const loadSelfServeEnabled = async ({
preserveEnabledOnError = false,
requestedDepartmentId = String(departmentId.value),
} = {}) => {
const requestSequence = ++selfServeStatusRequestSequence;
isLoadingSelfServeEnabled.value = true; isLoadingSelfServeEnabled.value = true;
selfServeEnabledError.value = ""; selfServeEnabledError.value = "";
try { try {
selfServeEnabled.value = await getDepartmentSelfServeEnabled(departmentId.value); const status = await getDepartmentSelfServeStatus(requestedDepartmentId);
if (
requestSequence !== selfServeStatusRequestSequence
|| String(departmentId.value) !== requestedDepartmentId
) {
return false;
}
selfServeEnabled.value = status.enabled;
selfServeAutoDeactivation.value = status.autoDeactivation;
return true;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
selfServeEnabledError.value = "Unable to load self-serve status."; if (
selfServeEnabled.value = false; requestSequence === selfServeStatusRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
selfServeEnabledError.value = "Unable to load self-serve status.";
if (!preserveEnabledOnError) {
selfServeEnabled.value = false;
}
}
return false;
} finally { } finally {
isLoadingSelfServeEnabled.value = false; if (requestSequence === selfServeStatusRequestSequence) {
isLoadingSelfServeEnabled.value = false;
}
} }
}; };
const updateSelfServeEnabled = async (enabled) => { const updateSelfServeEnabled = async (enabled) => {
const requestedDepartmentId = String(departmentId.value);
const requestSequence = ++selfServeSaveRequestSequence;
const previousValue = selfServeEnabled.value; const previousValue = selfServeEnabled.value;
selfServeEnabled.value = Boolean(enabled); selfServeEnabled.value = Boolean(enabled);
isSavingSelfServeEnabled.value = true; isSavingSelfServeEnabled.value = true;
selfServeEnabledError.value = ""; selfServeEnabledError.value = "";
try { try {
selfServeEnabled.value = await setDepartmentSelfServeEnabled(departmentId.value, enabled); const status = await setDepartmentSelfServeStatus(requestedDepartmentId, enabled);
if (
requestSequence !== selfServeSaveRequestSequence
|| String(departmentId.value) !== requestedDepartmentId
) {
return;
}
selfServeEnabled.value = status.enabled;
if (status.hasAutoDeactivation) {
selfServeAutoDeactivation.value = status.autoDeactivation;
} else {
await loadSelfServeEnabled({
preserveEnabledOnError: true,
requestedDepartmentId,
});
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
selfServeEnabled.value = previousValue; if (
selfServeEnabledError.value = "Unable to update self-serve status."; requestSequence === selfServeSaveRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
selfServeEnabled.value = previousValue;
selfServeEnabledError.value = "Unable to update self-serve status.";
}
} finally { } finally {
isSavingSelfServeEnabled.value = false; if (
requestSequence === selfServeSaveRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
isSavingSelfServeEnabled.value = false;
}
} }
}; };
getDepartmentVariables();
getWorkfeedDepartmentOptions(); getWorkfeedDepartmentOptions();
loadSelfServeEnabled(); watch(
() => router.currentRoute.value.params.departmentId,
(nextDepartmentId) => {
selfServeSaveRequestSequence++;
isSavingSelfServeEnabled.value = false;
setDepartment(nextDepartmentId);
departmentVariables.value = [];
selfServeEnabled.value = false;
selfServeAutoDeactivation.value = {
at: null,
timezone: "Europe/Copenhagen",
label: "NEVER",
};
selfServeEnabledError.value = "";
getDepartmentVariables(String(nextDepartmentId));
loadSelfServeEnabled();
},
{ immediate: true }
);
</script> </script>
<template> <template>
@@ -287,13 +381,15 @@ loadSelfServeEnabled();
:subtitle="$t('superuser_dashboard.department_pages.modules.subtitle')" :subtitle="$t('superuser_dashboard.department_pages.modules.subtitle')"
> >
<template #default> <template #default>
<ConfigurationSwitch <div :title="selfServeAutoDeactivationText">
title="Selvvask" <ConfigurationSwitch
description="Aktiver selvvask i denne afdeling." title="Selvvask"
:value="selfServeEnabled" :description="selfServeAutoDeactivationText"
:disabled="isLoadingSelfServeEnabled || isSavingSelfServeEnabled" :value="selfServeEnabled"
:on-switch="updateSelfServeEnabled" :disabled="isLoadingSelfServeEnabled || isSavingSelfServeEnabled"
></ConfigurationSwitch> :on-switch="updateSelfServeEnabled"
></ConfigurationSwitch>
</div>
<p class="help is-danger" v-if="selfServeEnabledError"> <p class="help is-danger" v-if="selfServeEnabledError">
{{ selfServeEnabledError }} {{ selfServeEnabledError }}
</p> </p>
@@ -84,6 +84,7 @@ import { __pushMock, __routeRef } from "vue-router";
const flushMicrotasks = async () => { const flushMicrotasks = async () => {
await Promise.resolve(); await Promise.resolve();
await Promise.resolve(); await Promise.resolve();
await nextTick();
}; };
const createDeferred = () => { const createDeferred = () => {
@@ -9,14 +9,47 @@ const source = readFileSync(
describe("DepartmentModulesSetup self-serve management regression", () => { describe("DepartmentModulesSetup self-serve management regression", () => {
it("loads and updates department self-serve status through shared helpers", () => { it("loads and updates department self-serve status through shared helpers", () => {
expect(source).toContain("getDepartmentSelfServeEnabled"); expect(source).toContain("getDepartmentSelfServeStatus");
expect(source).toContain("setDepartmentSelfServeEnabled"); expect(source).toContain("setDepartmentSelfServeStatus");
expect(source).toContain("loadSelfServeEnabled()"); expect(source).toContain("loadSelfServeEnabled()");
expect(source).toContain("updateSelfServeEnabled"); expect(source).toContain("updateSelfServeEnabled");
}); });
it("renders a dedicated self-serve module switch in setup UI", () => { it("renders a dedicated self-serve module switch in setup UI", () => {
expect(source).toContain('title="Selvvask"'); expect(source).toContain('title="Selvvask"');
expect(source).toContain("Aktiver selvvask i denne afdeling."); expect(source).toContain(':title="selfServeAutoDeactivationText"');
expect(source).toContain(':description="selfServeAutoDeactivationText"');
expect(source).toContain("NEVER");
});
it("refetches scheduling metadata for legacy message-only update responses", () => {
expect(source).toContain("if (status.hasAutoDeactivation)");
expect(source).toContain("preserveEnabledOnError: true");
expect(source).toContain("requestedDepartmentId");
expect(source).toContain("if (!preserveEnabledOnError)");
});
it("reloads self-serve status safely when the route department changes", () => {
expect(source).toContain("watch(");
expect(source).toContain("router.currentRoute.value.params.departmentId");
expect(source).toContain("selfServeStatusRequestSequence");
expect(source).toContain("requestSequence !== selfServeStatusRequestSequence");
expect(source).toContain("{ immediate: true }");
});
it("invalidates an in-flight save when the route department changes", () => {
expect(source).toContain("selfServeSaveRequestSequence");
expect(source).toContain("const requestSequence = ++selfServeSaveRequestSequence");
expect(source).toContain("requestSequence !== selfServeSaveRequestSequence");
expect(source).toContain("selfServeSaveRequestSequence++");
expect(source).toContain("isSavingSelfServeEnabled.value = false");
});
it("ignores department-variable responses from a previously selected route department", () => {
expect(source).toContain("departmentVariablesRequestSequence");
expect(source).toContain("const requestSequence = ++departmentVariablesRequestSequence");
expect(source).toContain("requestSequence !== departmentVariablesRequestSequence");
expect(source).toContain("String(departmentId.value) !== requestedDepartmentId");
expect(source).toContain("getDepartmentVariables(String(nextDepartmentId))");
}); });
}); });
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const source = readFileSync(
join(
process.cwd(),
"src/views/dashboards/departmentDashboard/modules/time-bookings/DepartmentTimeBookingsOpeningHours.vue"
),
"utf8"
);
describe("Department opening-hours route synchronization", () => {
it("clears and reloads opening hours when the selected department changes", () => {
expect(source).toContain("departmentId.value, isSuperuserDepartmentRoute.value");
expect(source).toContain("{ immediate: true }");
expect(source).toContain("opening_hours.value = null");
expect(source).toContain("department: selectedDepartmentId");
});
it("ignores a slower response from the previously selected department", () => {
expect(source).toContain("const requestId = ++openingHoursRequest");
expect(source).toContain("requestId !== openingHoursRequest");
});
it("initializes the selected superuser department for direct and reloaded routes", () => {
expect(source).toContain("setDepartment as setSelectedDepartment");
expect(source).toContain("if (isSuperuserRoute)");
expect(source).toContain("setSelectedDepartment(selectedDepartmentId)");
});
it("initializes superuser state when only the route context changes", () => {
expect(source).toContain("isSuperuserDepartmentRoute.value");
expect(source).toContain("[selectedDepartmentId, isSuperuserRoute]");
expect(source).toContain("selectedDepartmentId !== previousDepartmentId");
});
});