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:
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
+104
-15
@@ -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>
|
||||||
|
|||||||
+6
@@ -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"),
|
||||||
|
|||||||
+130
-34
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user