Ensure department-scoped self-serve settings load and save safely across route transitions, document the API contract, and cover stale in-flight state.
429 lines
16 KiB
Vue
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>
|