Files
pleno-vue/src/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue
T
Jeppe B 5204536f92 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.
2026-07-28 18:34:35 +02:00

429 lines
16 KiB
Vue

<script setup>
import PageTitle from "@/components/global/PageTitle.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import DepartmentSubPageWrapper from "@/views/dashboards/superUserDashboard/department/DepartmentSubPageWrapper.vue";
import { departmentAdvanced, setDepartment, departmentId } from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
import { useRouter } from "vue-router";
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import ConfigurationSelect from "@/components/displays/superuser/configuration/ConfigurationSelect.vue";
import SuperuserOverviewPanel from "@/components/displays/superuser/overview/SuperuserOverviewPanel.vue";
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 {
getDepartmentSelfServeStatus,
setDepartmentSelfServeStatus,
} from "@/composables/departmentSelfServeEnabled.js";
// Get the department from the route
const router = useRouter();
const { t } = useI18n({ useScope: "global" });
const departmentTitle = computed(() => departmentAdvanced.value.name || t("superuser_dashboard.department_navigation.modules"));
const pageSubtitle = computed(() => t("superuser_dashboard.department_pages.modules.subtitle"));
const departmentVariables = ref([]);
const workfeedDepartmentOptions = ref([{ value: "", label: "No Workfeed department" }]);
const isLoadingWorkfeedDepartments = ref(false);
const workfeedDepartmentOptionsError = ref("");
const selfServeEnabled = ref(false);
const selfServeAutoDeactivation = ref({ at: null, timezone: "Europe/Copenhagen", label: "NEVER" });
const isLoadingSelfServeEnabled = ref(false);
const isSavingSelfServeEnabled = ref(false);
const selfServeEnabledError = ref("");
let departmentVariablesRequestSequence = 0;
let selfServeStatusRequestSequence = 0;
let selfServeSaveRequestSequence = 0;
const departmentVariableDescriptions = computed(() => ({
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_password: t("superuser_dashboard.department_pages.modules.variables.bookingsystem_time_based_password"),
exclude_from_invoicing: t("superuser_dashboard.department_pages.modules.variables.exclude_from_invoicing"),
workfeed_department_id: t("superuser_dashboard.department_pages.modules.variables.workfeed_department_id"),
}));
const departmentVariableRows = computed(() => departmentVariables.value.map((variable) => ({
key: `${variable.variable}:${variable.id}`,
label: departmentVariableDescriptions.value[variable.variable] || variable.variable,
value: variable.value,
})));
const selfServeAutoDeactivationText = computed(() => {
if (!selfServeEnabled.value) {
return "Automatic deactivation: not scheduled while self-serve is disabled.";
}
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;
return true;
} catch (error) {
if (
requestSequence === departmentVariablesRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
console.log(error);
}
return false;
}
};
const getVariableValue = (variable) => {
if (departmentVariables.value.length === 0) {
return null;
}
// Find the variable in the department variables
const configuredVariable = departmentVariables.value.find((item) => item.variable === variable);
// If the variable is found, return the value
if (configuredVariable) {
return configuredVariable;
}
// If the variable is not found, return null
return null;
};
const getVariableStringValue = (variable) => {
const configuredVariable = getVariableValue(variable);
if (!configuredVariable || configuredVariable.value === undefined || configuredVariable.value === null) {
return "";
}
return String(configuredVariable.value);
};
const getBooleanValue = (variable) => {
if (departmentVariables.value.length === 0) {
return false;
}
// Find the variable in the department variables
if (getVariableValue(variable)) {
// If the variable is found, return the value
return getVariableValue(variable).value === "true";
}
return false;
};
const getWorkfeedItems = (payload) => {
if (Array.isArray(payload?.items)) {
return payload.items;
}
if (Array.isArray(payload?.results)) {
return payload.results;
}
if (Array.isArray(payload)) {
return payload;
}
return [];
};
const getWorkfeedDepartmentOptions = async () => {
isLoadingWorkfeedDepartments.value = true;
workfeedDepartmentOptionsError.value = "";
try {
const response = await SessionUser.superUser.modules.workfeed.functions.listDepartments();
const departments = getWorkfeedItems(response?.data?.data);
const options = departments
.map((workfeedDepartment) => {
const mappedDepartmentId = String(workfeedDepartment?.id ?? "").trim();
if (!mappedDepartmentId) {
return null;
}
const mappedDepartmentName = String(workfeedDepartment?.name ?? "").trim();
return {
value: mappedDepartmentId,
label: mappedDepartmentName || mappedDepartmentId,
};
})
.filter((option) => option !== null);
workfeedDepartmentOptions.value = [
{ value: "", label: "No Workfeed department" },
...options,
];
} catch (error) {
console.log(error);
workfeedDepartmentOptionsError.value = "Unable to load Workfeed departments.";
workfeedDepartmentOptions.value = [{ value: "", label: "No Workfeed department" }];
} finally {
isLoadingWorkfeedDepartments.value = false;
}
};
const setWorkfeedDepartmentId = async (selectedDepartmentId) => {
await SessionUser.objects.department_variables.add(
departmentId.value,
"workfeed_department_id",
selectedDepartmentId ?? ""
).then(() => {
getDepartmentVariables();
});
};
const loadSelfServeEnabled = async ({
preserveEnabledOnError = false,
requestedDepartmentId = String(departmentId.value),
} = {}) => {
const requestSequence = ++selfServeStatusRequestSequence;
isLoadingSelfServeEnabled.value = true;
selfServeEnabledError.value = "";
try {
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) {
console.log(error);
if (
requestSequence === selfServeStatusRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
selfServeEnabledError.value = "Unable to load self-serve status.";
if (!preserveEnabledOnError) {
selfServeEnabled.value = false;
}
}
return false;
} finally {
if (requestSequence === selfServeStatusRequestSequence) {
isLoadingSelfServeEnabled.value = false;
}
}
};
const updateSelfServeEnabled = async (enabled) => {
const requestedDepartmentId = String(departmentId.value);
const requestSequence = ++selfServeSaveRequestSequence;
const previousValue = selfServeEnabled.value;
selfServeEnabled.value = Boolean(enabled);
isSavingSelfServeEnabled.value = true;
selfServeEnabledError.value = "";
try {
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) {
console.log(error);
if (
requestSequence === selfServeSaveRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
selfServeEnabled.value = previousValue;
selfServeEnabledError.value = "Unable to update self-serve status.";
}
} finally {
if (
requestSequence === selfServeSaveRequestSequence
&& String(departmentId.value) === requestedDepartmentId
) {
isSavingSelfServeEnabled.value = false;
}
}
};
getWorkfeedDepartmentOptions();
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>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
<DepartmentSubPageWrapper>
<template #title>
<PageTitle :title="departmentTitle" :subtitle="pageSubtitle" />
</template>
<div class="department-modules">
<SuperUserDashboardDepartmentModulesNavigation />
<SuperuserOverviewPanel
:title="$t('objects.columns.value')"
:subtitle="$t('superuser_dashboard.department_pages.modules.subtitle')"
data-testid="department-module-variables"
>
<div v-if="departmentVariableRows.length === 0" class="has-text-grey">
{{ $t('common.none') }}
</div>
<dl v-else class="department-modules-variables">
<div v-for="row in departmentVariableRows" :key="row.key" class="department-modules-variables__row">
<dt>{{ row.label }}</dt>
<dd>{{ row.value || $t('common.none') }}</dd>
</div>
</dl>
</SuperuserOverviewPanel>
<SuperuserOverviewPanel
:title="$t('superuser_dashboard.department_navigation.modules')"
:subtitle="$t('superuser_dashboard.department_pages.modules.subtitle')"
>
<template #default>
<ConfigurationSwitch
title="Bookingsystem (ikke tidsbaseret)"
description="Aktiver bookingsystem"
v-bind:value="getBooleanValue('bookingsystem_enabled')"
:on-switch="(value) => {
SessionUser.objects.department_variables.add(
departmentId,
'bookingsystem_enabled',
value ? 'true' : 'false'
).then(() => {
getDepartmentVariables();
});
}"
></ConfigurationSwitch>
<ConfigurationSwitch
title="Bookingsystem (tidsbaseret)"
description="Aktiver bookingsystem"
v-bind:value="getBooleanValue('bookingsystem_time_based_enabled')"
:on-switch="(value) => {
SessionUser.objects.department_variables.add(
departmentId,
'bookingsystem_time_based_enabled',
value ? 'true' : 'false'
).then(() => {
getDepartmentVariables();
});
}"
></ConfigurationSwitch>
<ConfigurationInput
title="Booking system (tidsbaseret) kodeord"
description="Kodeord til booking system (tidsbaseret)"
v-bind:value="getVariableValue('bookingsystem_time_based_password')"
:on-save="(value) => {
SessionUser.objects.department_variables.add(
departmentId,
'bookingsystem_time_based_password',
value
).then(() => {
getDepartmentVariables();
});
}"
></ConfigurationInput>
</template>
</SuperuserOverviewPanel>
<SuperuserOverviewPanel
title="Invoicing"
:subtitle="$t('superuser_dashboard.department_pages.modules.variables.exclude_from_invoicing')"
>
<template #default>
<ConfigurationSwitch
title="Udeluk fra fakturering"
description="If enabled, this department is excluded from invoicing. Changes can take up to 5 minutes."
v-bind:value="getBooleanValue('exclude_from_invoicing')"
:on-switch="(value) => {
SessionUser.objects.department_variables.add(
departmentId,
'exclude_from_invoicing',
value ? 'true' : 'false'
).then(() => {
getDepartmentVariables();
});
}"
></ConfigurationSwitch>
</template>
</SuperuserOverviewPanel>
<SuperuserOverviewPanel
title="Self-serve"
:subtitle="$t('superuser_dashboard.department_pages.modules.subtitle')"
>
<template #default>
<div :title="selfServeAutoDeactivationText">
<ConfigurationSwitch
title="Selvvask"
:description="selfServeAutoDeactivationText"
:value="selfServeEnabled"
:disabled="isLoadingSelfServeEnabled || isSavingSelfServeEnabled"
:on-switch="updateSelfServeEnabled"
></ConfigurationSwitch>
</div>
<p class="help is-danger" v-if="selfServeEnabledError">
{{ selfServeEnabledError }}
</p>
</template>
</SuperuserOverviewPanel>
<SuperuserOverviewPanel
title="Workfeed"
:subtitle="$t('superuser_dashboard.department_pages.modules.variables.workfeed_department_id')"
>
<template #default>
<ConfigurationSelect
label="Workfeed department"
description="Select the Workfeed department that this local department should map to."
:options="workfeedDepartmentOptions"
:on-select="setWorkfeedDepartmentId"
:value="getVariableStringValue('workfeed_department_id')"
:disabled="isLoadingWorkfeedDepartments"
:icon="'fas fa-building'"
></ConfigurationSelect>
<p class="help is-danger" v-if="workfeedDepartmentOptionsError">
{{ workfeedDepartmentOptionsError }}
</p>
</template>
</SuperuserOverviewPanel>
</div>
</DepartmentSubPageWrapper>
</RestrictedPageWrapper>
</template>
<style scoped>
.department-modules {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>