Enhance subuser management functionality and improve UI responsiveness
This commit is contained in:
@@ -134,7 +134,7 @@ onBeforeUnmount(() => {
|
||||
padding: 0 0.3rem;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: -0.35rem;
|
||||
right: -0.55rem;
|
||||
top: -0.25rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const props = defineProps({
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["mutated"]);
|
||||
|
||||
const paginatedList = usePaginatedList();
|
||||
provide(PaginatedListKey, paginatedList);
|
||||
@@ -52,6 +53,10 @@ if (props.hideSearch) {
|
||||
if (props.autoLoad) {
|
||||
loadList();
|
||||
}
|
||||
|
||||
const onTableMutated = () => {
|
||||
emit("mutated");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,6 +69,7 @@ if (props.autoLoad) {
|
||||
:objects="list"
|
||||
:show-customer="showCustomer"
|
||||
:user-scoped-user-id="userScopedUserId"
|
||||
@mutated="onTableMutated"
|
||||
/>
|
||||
</TableLabeledPagination>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import Swal from "sweetalert2";
|
||||
import { computed } from "vue";
|
||||
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
@@ -24,10 +25,12 @@ const props = defineProps({
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["mutated"]);
|
||||
|
||||
const { loadList } = usePaginatedListInstance();
|
||||
|
||||
const isUserScoped = () => Boolean(props.userScopedUserId);
|
||||
const shouldRenderGrantRows = () => props.showCustomer || isUserScoped();
|
||||
const canEditPermissions = () =>
|
||||
props.showCustomer ? SessionUser.canAccessSuperUser() : SessionUser.canManageSubusers("edit");
|
||||
const canDisableAccess = () =>
|
||||
@@ -74,10 +77,54 @@ const patchGrant = async (subuser, payload) => {
|
||||
|
||||
const refreshList = async () => {
|
||||
await loadList();
|
||||
emit("mutated");
|
||||
};
|
||||
|
||||
const rowTestKey = (subuser) =>
|
||||
props.showCustomer && subuser?.grant_id ? `${subuser.id}-${subuser.grant_id}` : `${subuser?.id}`;
|
||||
shouldRenderGrantRows() && subuser?.grant_id ? `${subuser.id}-${subuser.grant_id}` : `${subuser?.id}`;
|
||||
|
||||
const normalizeGrantRow = (subuser, grant) => {
|
||||
const customerNumber = grant?.customer_number ?? grant?.billing_customer_number ?? subuser?.customer_number ?? null;
|
||||
const assignedVehicle = grant?.assigned_vehicle ?? subuser?.assigned_vehicle ?? null;
|
||||
|
||||
return {
|
||||
...subuser,
|
||||
grants: [],
|
||||
grant_id: grant?.grant_id ?? grant?.id ?? subuser?.grant_id ?? null,
|
||||
grant_enabled: grant?.grant_enabled ?? grant?.enabled ?? subuser?.grant_enabled ?? true,
|
||||
grant_permissions: grant?.grant_permissions ?? grant?.permissions ?? subuser?.grant_permissions ?? [],
|
||||
permission_template_key: grant?.permission_template_key ?? subuser?.permission_template_key ?? null,
|
||||
permission_groups: grant?.permission_groups ?? subuser?.permission_groups ?? [],
|
||||
grant_note: grant?.grant_note ?? grant?.note ?? subuser?.grant_note ?? "",
|
||||
customer_number: customerNumber,
|
||||
billing_customer_number: customerNumber,
|
||||
customer_name: grant?.customer_name ?? subuser?.customer_name ?? null,
|
||||
assigned_vehicle_id: grant?.assigned_vehicle_id ?? assignedVehicle?.id ?? subuser?.assigned_vehicle_id ?? null,
|
||||
assigned_vehicle_reg:
|
||||
grant?.assigned_vehicle_reg ??
|
||||
grant?.vehicle_reg ??
|
||||
assignedVehicle?.reg ??
|
||||
subuser?.assigned_vehicle_reg ??
|
||||
null,
|
||||
assigned_vehicle: assignedVehicle,
|
||||
dognvask_enabled:
|
||||
typeof grant?.dognvask_enabled === "boolean" ? grant.dognvask_enabled : subuser?.dognvask_enabled,
|
||||
access_state: grant?.access_state ?? subuser?.access_state,
|
||||
can_resend_invite: grant?.can_resend_invite ?? subuser?.can_resend_invite,
|
||||
};
|
||||
};
|
||||
|
||||
const tableRows = computed(() =>
|
||||
props.objects.flatMap((subuser) => {
|
||||
const grants = Array.isArray(subuser?.grants) ? subuser.grants.filter(Boolean) : [];
|
||||
|
||||
if (!shouldRenderGrantRows() || grants.length === 0) {
|
||||
return [subuser];
|
||||
}
|
||||
|
||||
return grants.map((grant) => normalizeGrantRow(subuser, grant));
|
||||
})
|
||||
);
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) {
|
||||
@@ -481,48 +528,21 @@ const onResendInvite = async (subuser) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="props.objects.length === 0">
|
||||
<tr v-if="tableRows.length === 0">
|
||||
<td colspan="6" class="has-text-centered has-text-grey py-6">Ingen chauffører fundet.</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="subuser in props.objects" :key="`${subuser.id}-${subuser.grant_id || 'profile'}`">
|
||||
<tr v-for="subuser in tableRows" :key="`${subuser.id}-${subuser.grant_id || 'profile'}`">
|
||||
<td :data-testid="`subuser-name-${rowTestKey(subuser)}`">
|
||||
<button
|
||||
v-if="canEditDriverAccount()"
|
||||
type="button"
|
||||
class="subusers-table__cell-button has-text-weight-semibold"
|
||||
:data-testid="`subuser-name-edit-${rowTestKey(subuser)}`"
|
||||
@click="onEditName(subuser)"
|
||||
>
|
||||
{{ subuser.name || "-" }}
|
||||
</button>
|
||||
<div v-else class="has-text-weight-semibold">{{ subuser.name || "-" }}</div>
|
||||
<div class="has-text-weight-semibold">{{ subuser.name || "-" }}</div>
|
||||
</td>
|
||||
|
||||
<td :data-testid="`subuser-phone-${rowTestKey(subuser)}`">
|
||||
<button
|
||||
v-if="canEditDriverAccount()"
|
||||
type="button"
|
||||
class="subusers-table__cell-button"
|
||||
:data-testid="`subuser-phone-edit-${rowTestKey(subuser)}`"
|
||||
@click="onEditContact(subuser)"
|
||||
>
|
||||
{{ formatPhone(subuser) }}
|
||||
</button>
|
||||
<span v-else>{{ formatPhone(subuser) }}</span>
|
||||
<span>{{ formatPhone(subuser) }}</span>
|
||||
</td>
|
||||
|
||||
<td class="license-plate" :data-testid="`subuser-plate-${rowTestKey(subuser)}`">
|
||||
<button
|
||||
v-if="canEditVehicleAssignment(subuser)"
|
||||
type="button"
|
||||
class="subusers-table__cell-button license-plate"
|
||||
:data-testid="`subuser-plate-edit-${rowTestKey(subuser)}`"
|
||||
@click="onAssignVehicle(subuser)"
|
||||
>
|
||||
{{ formatPlate(subuser) }}
|
||||
</button>
|
||||
<span v-else>{{ formatPlate(subuser) }}</span>
|
||||
<span>{{ formatPlate(subuser) }}</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
@@ -713,25 +733,6 @@ const onResendInvite = async (subuser) => {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.subusers-table__cell-button {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.subusers-table__cell-button:hover,
|
||||
.subusers-table__cell-button:focus {
|
||||
color: #0f5aa7;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.18rem;
|
||||
}
|
||||
|
||||
.subusers-table__tooltip-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import MenuDefault from "@/components/menus/MenuDefault.vue";
|
||||
import {ref, watch} from "vue";
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
// Define the isLoaded function
|
||||
@@ -10,308 +10,292 @@ import i18n from "@/i18n";
|
||||
const isLoaded = ref(true);
|
||||
const doesRouteStartWith = (route) => {
|
||||
return window.location.pathname.startsWith(route);
|
||||
}
|
||||
};
|
||||
const t = (key, fallback = key) => {
|
||||
const value = i18n.global.t(key);
|
||||
return value === key ? fallback : value;
|
||||
}
|
||||
};
|
||||
|
||||
const menu_items = ref([
|
||||
{
|
||||
name: SessionUser.objects.departments.meta.title,
|
||||
route: '/superuser' +SessionUser.objects.departments.meta.endpoint,
|
||||
route: "/superuser" + SessionUser.objects.departments.meta.endpoint,
|
||||
icon: SessionUser.objects.departments.meta.icon,
|
||||
children: []
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: 'Nummerplade scanning',
|
||||
route: '/superuser/scanners',
|
||||
icon: 'fas fa-cogs',
|
||||
children: []
|
||||
name: "Nummerplade scanning",
|
||||
route: "/superuser/scanners",
|
||||
icon: "fas fa-cogs",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: 'Kassesystem',
|
||||
permissions: [
|
||||
'canAccessSuperUser'
|
||||
],
|
||||
icon: 'fas fa-cogs',
|
||||
name: "Kassesystem",
|
||||
permissions: ["canAccessSuperUser"],
|
||||
icon: "fas fa-cogs",
|
||||
options: [
|
||||
{
|
||||
label: SessionUser.objects.collectedOrderInvoices.meta.title,
|
||||
value: SessionUser.objects.collectedOrderInvoices.meta.endpoint,
|
||||
icon: SessionUser.objects.collectedOrderInvoices.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.products.meta.title,
|
||||
value: SessionUser.objects.products.meta.endpoint,
|
||||
icon: SessionUser.objects.products.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.orders.meta.title,
|
||||
value: SessionUser.objects.orders.meta.endpoint,
|
||||
icon: SessionUser.objects.orders.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.categories.meta.title,
|
||||
value: SessionUser.objects.categories.meta.endpoint,
|
||||
icon: SessionUser.objects.categories.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
onSelect: async (value) => {
|
||||
SessionUser.functions.redirectTo.superUser(value);
|
||||
},
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'Brugere',
|
||||
permissions: [
|
||||
'canAccessSuperUser'
|
||||
],
|
||||
icon: 'fas fa-cogs',
|
||||
name: "Brugere",
|
||||
permissions: ["canAccessSuperUser"],
|
||||
icon: "fas fa-cogs",
|
||||
options: [
|
||||
{
|
||||
label: 'Kunder',
|
||||
value: '/customers',
|
||||
icon: 'fas fa-users',
|
||||
label: "Kunder",
|
||||
value: "/customers",
|
||||
icon: "fas fa-users",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: 'Medarbejdere',
|
||||
value: '/users',
|
||||
icon: 'fas fa-users-cog',
|
||||
label: "Medarbejdere",
|
||||
value: "/users",
|
||||
icon: "fas fa-users-cog",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: 'Chauffører',
|
||||
value: '/subusers',
|
||||
icon: 'fas fa-id-card',
|
||||
label: "Chauffører",
|
||||
value: "/subusers",
|
||||
icon: "fas fa-id-card",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.roles.meta.title,
|
||||
value: SessionUser.objects.roles.meta.endpoint,
|
||||
icon: SessionUser.objects.roles.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
onSelect: async (value) => {
|
||||
SessionUser.functions.redirectTo.superUser(value);
|
||||
},
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'Konfiguration',
|
||||
permissions: [
|
||||
'canAccessSuperUser'
|
||||
],
|
||||
icon: 'fas fa-cogs',
|
||||
name: "Konfiguration",
|
||||
permissions: ["canAccessSuperUser"],
|
||||
icon: "fas fa-cogs",
|
||||
options: [
|
||||
{
|
||||
label: SessionUser.superUser.modules.economic.meta.title,
|
||||
value: SessionUser.superUser.modules.economic.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.economic.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.reCAPTCHA.meta.title,
|
||||
value: SessionUser.superUser.modules.reCAPTCHA.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.reCAPTCHA.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.email.meta.title,
|
||||
value: SessionUser.superUser.modules.email.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.email.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.backups.meta.title,
|
||||
value: SessionUser.superUser.modules.backups.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.backups.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.coolify.meta.title,
|
||||
value: SessionUser.superUser.modules.coolify.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.coolify.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.releasemanager.meta.title,
|
||||
value: SessionUser.superUser.modules.releasemanager.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.releasemanager.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.motorapi.meta.title,
|
||||
value: SessionUser.superUser.modules.motorapi.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.motorapi.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.stripe.meta.title,
|
||||
value: SessionUser.superUser.modules.stripe.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.stripe.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.weatherapi.meta.title,
|
||||
value: SessionUser.superUser.modules.weatherapi.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.weatherapi.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.superUser.modules.workfeed.meta.title,
|
||||
value: SessionUser.superUser.modules.workfeed.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.workfeed.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
onSelect: async (value) => {
|
||||
SessionUser.functions.redirectTo.superUser(value);
|
||||
},
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: t("cron.system_nav", "System"),
|
||||
permissions: [
|
||||
'canAccessSuperUser'
|
||||
],
|
||||
icon: 'fas fa-shield-alt',
|
||||
permissions: ["canAccessSuperUser"],
|
||||
icon: "fas fa-shield-alt",
|
||||
options: [
|
||||
{
|
||||
label: t("system_status.title", "System status"),
|
||||
value: null,
|
||||
icon: 'fas fa-heartbeat',
|
||||
icon: "fas fa-heartbeat",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: t("cron.nav", "Cron tasks"),
|
||||
value: '/system/cron',
|
||||
icon: 'fas fa-clock',
|
||||
value: "/system/cron",
|
||||
icon: "fas fa-clock",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: t("system_status.cards.database", "Database"),
|
||||
value: '/system/database',
|
||||
icon: 'fas fa-database',
|
||||
value: "/system/database",
|
||||
icon: "fas fa-database",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: t("system_status.cards.minio", "MinIO"),
|
||||
value: '/system/minio',
|
||||
icon: 'fas fa-archive',
|
||||
value: "/system/minio",
|
||||
icon: "fas fa-archive",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: t("system_status.cards.redis", "Redis"),
|
||||
value: '/system/redis',
|
||||
icon: 'fas fa-memory',
|
||||
value: "/system/redis",
|
||||
icon: "fas fa-memory",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: t("security.nav", "Security"),
|
||||
value: '/system/security/overview',
|
||||
icon: 'fas fa-shield-alt',
|
||||
value: "/system/security/overview",
|
||||
icon: "fas fa-shield-alt",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
onSelect: async (value) => {
|
||||
SessionUser.functions.redirectTo.superUser(value);
|
||||
},
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'XL Vask',
|
||||
permissions: [
|
||||
'canAccessSuperUser'
|
||||
],
|
||||
icon: 'fas fa-cogs',
|
||||
name: "XL Vask",
|
||||
permissions: ["canAccessSuperUser"],
|
||||
icon: "fas fa-cogs",
|
||||
options: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
value: '/xlvask',
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
label: "Dashboard",
|
||||
value: "/xlvask",
|
||||
icon: "fas fa-tachometer-alt",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.global.language.customers,
|
||||
value: '/xlvask/customers',
|
||||
icon: 'fas fa-users',
|
||||
value: "/xlvask/customers",
|
||||
icon: "fas fa-users",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.global.language.usage_log,
|
||||
value: '/xlvask/usagelogs',
|
||||
icon: 'fas fa-users',
|
||||
value: "/xlvask/usagelogs",
|
||||
icon: "fas fa-users",
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
label: SessionUser.objects.global.language.configuration,
|
||||
value: SessionUser.superUser.modules.xlvask.meta.config_endpoint,
|
||||
icon: SessionUser.superUser.modules.xlvask.meta.icon,
|
||||
children: [],
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
onSelect: async (value) => {
|
||||
SessionUser.functions.redirectTo.superUser(value);
|
||||
},
|
||||
hidden: false
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const selectDepartment = (department) => {
|
||||
// Get the current path (After /admin/:department_id)
|
||||
const currentPath = window.location.pathname.split('/').slice(3).join('/');
|
||||
console.log('Selected department: ' + department);
|
||||
const currentPath = window.location.pathname.split("/").slice(3).join("/");
|
||||
console.log("Selected department: " + department);
|
||||
menu_items.value[1].selected = department;
|
||||
// Hide the menu
|
||||
menu_items.value[1].isExpanded = false;
|
||||
// Redirect to the department
|
||||
SessionUser.functions.redirectTo.department(department, currentPath);
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenuDefault v-if="isLoaded"
|
||||
v-bind:menu_items="menu_items"
|
||||
:show_icons="false"
|
||||
/>
|
||||
<MenuDefault v-if="isLoaded" v-bind:menu_items="menu_items" :show_icons="false" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -4679,6 +4679,7 @@
|
||||
},
|
||||
"pagination": {
|
||||
"archive": "Arkiv",
|
||||
"clear_all": "Ryd alle",
|
||||
"ascending": "Stigende (Ældste @:{'words.generated.først'})",
|
||||
"booking_status": "Bogføringsstatus",
|
||||
"bookings_overview": "@:{'templates.generated.compat.objects.bookings.description'}",
|
||||
|
||||
@@ -4793,6 +4793,7 @@
|
||||
},
|
||||
"pagination": {
|
||||
"archive": "Arkiv",
|
||||
"clear_all": "Tøm alle",
|
||||
"ascending": "Stigende (eldst @:{'words.generated.først'})",
|
||||
"booking_status": "Bestillingsstatus",
|
||||
"bookings_overview": "Bestillingsoversikt",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compat": {
|
||||
"pagination": {
|
||||
"archive": "Arkiv",
|
||||
"clear_all": "Ryd alle",
|
||||
"ascending": "Stigende (Ældste @:{'terms.glossary.først'})",
|
||||
"booking_status": "Bogføringsstatus",
|
||||
"bookings_overview": "@:{'phrases.compat.objects.bookings.description'}",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compat": {
|
||||
"pagination": {
|
||||
"archive": "Arkiv",
|
||||
"clear_all": "Tøm alle",
|
||||
"ascending": "Stigende (eldst @:{'terms.glossary.først'})",
|
||||
"booking_status": "Bestillingsstatus",
|
||||
"bookings_overview": "Bestillingsoversikt",
|
||||
|
||||
@@ -171,6 +171,7 @@ watch(
|
||||
:endpoint="endpoint"
|
||||
:user-scoped-user-id="routeUserId"
|
||||
auto-load="true"
|
||||
@mutated="loadSummary"
|
||||
/>
|
||||
</SuperuserOverviewPanel>
|
||||
</template>
|
||||
|
||||
@@ -886,8 +886,10 @@ test("customer user with own-subuser permissions can invite and manage grant acc
|
||||
await page.getByTestId("subuser-dognvask-tooltip-3").hover();
|
||||
await expect(page.locator(".tooltip-content:visible")).toContainText("Kundeadgangen er deaktiveret");
|
||||
await page.mouse.move(0, 0);
|
||||
await page.getByTestId("subuser-remove-2").hover();
|
||||
await expect(page.locator(".tooltip-content", { hasText: "Deaktiverer chaufførens kundeadgang" })).toBeVisible();
|
||||
await page.locator(".b-tooltip", { has: page.getByTestId("subuser-remove-2") }).hover();
|
||||
await expect(
|
||||
page.locator(".tooltip-content:visible", { hasText: "Deaktiverer chaufførens kundeadgang" }).first()
|
||||
).toContainText("Deaktiverer chaufførens kundeadgang");
|
||||
|
||||
await page.getByRole("button", { name: /Invit.*chauff/i }).click();
|
||||
await page.fill("#subuser-form-name", "Invited Driver");
|
||||
@@ -901,16 +903,17 @@ test("customer user with own-subuser permissions can invite and manage grant acc
|
||||
await expect(page.getByTestId("subuser-phone-100")).toHaveText("+45 44444444");
|
||||
await expect(page.getByTestId("subuser-plate-100")).toHaveText("-");
|
||||
|
||||
await page.getByTestId("subuser-plate-edit-100").click();
|
||||
await page.selectOption("#subuser-assigned-vehicle", "104");
|
||||
await page.getByRole("button", { name: "Gem" }).click();
|
||||
await expect(page.getByTestId("subuser-plate-100")).toHaveText("NY44444");
|
||||
|
||||
let actions = await openSubuserActions(page, 100);
|
||||
await expect(actions.getByTestId("subuser-section-account-100")).toHaveCount(0);
|
||||
await expect(actions.getByTestId("subuser-section-vehicle-100")).toBeVisible();
|
||||
await expect(actions.getByTestId("subuser-section-grant-100")).toBeVisible();
|
||||
await expect(actions.getByTestId("subuser-section-details-100")).toBeVisible();
|
||||
await actions.getByTestId("subuser-assign-vehicle-100").click();
|
||||
await page.selectOption("#subuser-assigned-vehicle", "104");
|
||||
await page.getByRole("button", { name: "Gem" }).click();
|
||||
await expect(page.getByTestId("subuser-plate-100")).toHaveText("NY44444");
|
||||
|
||||
actions = await openSubuserActions(page, 100);
|
||||
await actions.getByTestId("subuser-permissions-100").click();
|
||||
await expect(page.getByText("Adgangsprofil for Invited Driver")).toBeVisible();
|
||||
await expect(page.getByTestId("permission-template-driver")).toBeVisible();
|
||||
@@ -985,14 +988,19 @@ test("superusers can list and invite chauffeurs across customers", async ({ page
|
||||
await expect(actions.getByTestId("subuser-section-details-51-510")).toBeVisible();
|
||||
await expect(actions).toContainText("12345678 - Nordic Transport");
|
||||
await expect(actions.getByTestId("subuser-resend-51-510")).toBeVisible();
|
||||
await expect(page.getByTestId("subuser-name-edit-51-510")).toHaveCount(0);
|
||||
await expect(page.getByTestId("subuser-phone-edit-51-510")).toHaveCount(0);
|
||||
await expect(page.getByTestId("subuser-plate-edit-51-510")).toHaveCount(0);
|
||||
await expect(page.getByTestId("subuser-dognvask-51-511")).toBeVisible();
|
||||
|
||||
await page.getByTestId("subuser-name-edit-51-510").click();
|
||||
await actions.getByTestId("subuser-edit-name-51-510").click();
|
||||
await page.fill(".swal2-input", "Renamed Super Driver");
|
||||
await page.getByRole("button", { name: "Gem" }).click();
|
||||
await expect(page.getByTestId("subuser-name-51-510")).toContainText("Renamed Super Driver");
|
||||
expect(profilePayloads).toContainEqual({ id: 51, payload: { name: "Renamed Super Driver" } });
|
||||
|
||||
await page.getByTestId("subuser-phone-edit-51-510").click();
|
||||
actions = await openSubuserActions(page, "51-510");
|
||||
await actions.getByTestId("subuser-edit-contact-51-510").click();
|
||||
await page.fill("#subuser-admin-email", "renamed@example.com");
|
||||
await page.fill("#subuser-admin-phone-country-code", "46");
|
||||
await page.fill("#subuser-admin-phone", "12312312");
|
||||
@@ -1007,7 +1015,8 @@ test("superusers can list and invite chauffeurs across customers", async ({ page
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByTestId("subuser-plate-edit-51-510").click();
|
||||
actions = await openSubuserActions(page, "51-510");
|
||||
await actions.getByTestId("subuser-assign-vehicle-51-510").click();
|
||||
await page.selectOption("#subuser-assigned-vehicle", "5102");
|
||||
await page.getByRole("button", { name: "Gem" }).click();
|
||||
await expect(page.getByTestId("subuser-plate-51-510")).toHaveText("NT24680");
|
||||
|
||||
@@ -88,6 +88,7 @@ test.describe("superuser order date filters", () => {
|
||||
await expect(otherFiltersButton).toBeVisible();
|
||||
await expect(otherFiltersButton).not.toHaveClass(/is-small/);
|
||||
const otherFiltersCount = page.getByTestId("invoice-orders-other-filters-count");
|
||||
const clearOtherFiltersButton = page.getByTestId("invoice-orders-other-filters-clear");
|
||||
await expect(otherFiltersCount).toHaveText("3");
|
||||
await expect(anytimeDateShortcut).toBeVisible();
|
||||
await expect(todayDateShortcut).toBeVisible();
|
||||
@@ -107,8 +108,8 @@ test.describe("superuser order date filters", () => {
|
||||
const shortcutCenterY = shortcutBox.y + shortcutBox.height / 2;
|
||||
const otherToAnytimeGap = shortcutBox.x - (buttonBox.x + buttonBox.width);
|
||||
const dateShortcutGap = todayShortcutBox.x - (shortcutBox.x + shortcutBox.width);
|
||||
const doesCountProtrudeFromTopRight =
|
||||
countBox.y < buttonBox.y && countBox.x + countBox.width > buttonBox.x + buttonBox.width;
|
||||
const countRightProtrusion = countBox.x + countBox.width - (buttonBox.x + buttonBox.width);
|
||||
const doesCountProtrudeFromTopRight = countBox.y < buttonBox.y && countRightProtrusion >= 8;
|
||||
const hasCountTopClearance = countBox.y >= shortcutActionsBox.y + 2;
|
||||
|
||||
return (
|
||||
@@ -120,35 +121,29 @@ test.describe("superuser order date filters", () => {
|
||||
);
|
||||
})
|
||||
.toBe(true);
|
||||
await expect(otherFiltersMenu).toBeHidden();
|
||||
|
||||
await otherFiltersButton.click();
|
||||
await expect(otherFiltersMenu).toBeVisible();
|
||||
for (const label of [
|
||||
"Rækkefølge",
|
||||
"Bogføringsstatus",
|
||||
"Betalingsprocessor",
|
||||
"Fuldført",
|
||||
"Faktura valg",
|
||||
"Specialaftale",
|
||||
"Vaskeabonnement",
|
||||
]) {
|
||||
await expect(otherFiltersLabel(label)).toBeVisible();
|
||||
const verifiedButtonBox = await otherFiltersButton.boundingBox();
|
||||
const verifiedCountBox = await otherFiltersCount.boundingBox();
|
||||
const verifiedShortcutBox = await anytimeDateShortcut.boundingBox();
|
||||
if (!verifiedButtonBox || !verifiedCountBox || !verifiedShortcutBox) {
|
||||
throw new Error("Unable to capture verified other filters badge screenshot");
|
||||
}
|
||||
|
||||
await page.getByTestId("invoice-orders-other-filter-processor").selectOption("2");
|
||||
await expect(otherFiltersCount).toHaveText("4");
|
||||
await expect.poll(() => orderRequests.some((request) => request.filters.includes("processor:2"))).toBeTruthy();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
const verifiedTop = Math.min(verifiedButtonBox.y, verifiedCountBox.y);
|
||||
const verifiedBottom = Math.max(
|
||||
verifiedButtonBox.y + verifiedButtonBox.height,
|
||||
verifiedCountBox.y + verifiedCountBox.height,
|
||||
verifiedShortcutBox.y + verifiedShortcutBox.height
|
||||
);
|
||||
await page.screenshot({
|
||||
path: "output/playwright/manual/invoice-orders-other-filters-badge-right-protrusion.png",
|
||||
clip: {
|
||||
x: Math.max(0, verifiedButtonBox.x - 28),
|
||||
y: Math.max(0, verifiedTop - 24),
|
||||
width: Math.ceil(verifiedShortcutBox.x + verifiedShortcutBox.width - verifiedButtonBox.x + 56),
|
||||
height: Math.ceil(verifiedBottom - verifiedTop + 48),
|
||||
},
|
||||
});
|
||||
await expect(otherFiltersMenu).toBeHidden();
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
orderRequests.some((request) => request.limit === 1000 && !request.filters.includes("created_at-date_"))
|
||||
)
|
||||
.toBeTruthy();
|
||||
|
||||
const fromField = page.getByTestId("invoice-orders-date-from");
|
||||
const fromInput = fromField.locator("input").first();
|
||||
const toInput = page.getByTestId("invoice-orders-date-to").locator("input").first();
|
||||
@@ -177,6 +172,80 @@ test.describe("superuser order date filters", () => {
|
||||
.poll(() => orderRequests.some((request) => request.filters.includes("created_at-date_from:2026-07-02")))
|
||||
.toBeTruthy();
|
||||
|
||||
await otherFiltersButton.click();
|
||||
await expect(otherFiltersMenu).toBeVisible();
|
||||
for (const label of [
|
||||
"Rækkefølge",
|
||||
"Bogføringsstatus",
|
||||
"Betalingsprocessor",
|
||||
"Fuldført",
|
||||
"Faktura valg",
|
||||
"Specialaftale",
|
||||
"Vaskeabonnement",
|
||||
]) {
|
||||
await expect(otherFiltersLabel(label)).toBeVisible();
|
||||
}
|
||||
await expect(clearOtherFiltersButton).toBeVisible();
|
||||
await expect(clearOtherFiltersButton).toHaveText("Ryd alle");
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const menuBox = await otherFiltersMenu.boundingBox();
|
||||
const clearBox = await clearOtherFiltersButton.boundingBox();
|
||||
const lastSelectBox = await page.getByTestId("invoice-orders-other-filter-wash-subscription").boundingBox();
|
||||
|
||||
if (!menuBox || !clearBox || !lastSelectBox) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
clearBox.y > lastSelectBox.y + lastSelectBox.height &&
|
||||
clearBox.y + clearBox.height <= menuBox.y + menuBox.height
|
||||
);
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
await page.getByTestId("invoice-orders-other-filter-processor").selectOption("2");
|
||||
await expect(otherFiltersCount).toHaveText("4");
|
||||
await expect.poll(() => orderRequests.some((request) => request.filters.includes("processor:2"))).toBeTruthy();
|
||||
|
||||
const requestCountBeforeClear = orderRequests.length;
|
||||
await clearOtherFiltersButton.click();
|
||||
|
||||
await expect.poll(() => orderRequests.length > requestCountBeforeClear).toBeTruthy();
|
||||
await expect(page.getByTestId("invoice-orders-other-filters-count")).toHaveCount(0);
|
||||
await expect(page.getByTestId("invoice-orders-other-filter-order-direction")).toHaveValue("desc");
|
||||
for (const filterTestId of [
|
||||
"invoice-orders-other-filter-booked-invoice-id",
|
||||
"invoice-orders-other-filter-processor",
|
||||
"invoice-orders-other-filter-completed-at",
|
||||
"invoice-orders-other-filter-invoice-selection",
|
||||
"invoice-orders-other-filter-special-agreement",
|
||||
"invoice-orders-other-filter-wash-subscription",
|
||||
]) {
|
||||
await expect(page.getByTestId(filterTestId)).toHaveValue("*");
|
||||
}
|
||||
await expect(fromInput).toHaveValue("02.07.2026");
|
||||
await expect(toInput).toHaveValue("");
|
||||
await expect
|
||||
.poll(() => {
|
||||
const latestTableRequest = [...orderRequests].reverse().find((request) => request.limit !== 1000);
|
||||
|
||||
return Boolean(
|
||||
latestTableRequest?.filters.includes("created_at-date_from:2026-07-02") &&
|
||||
!latestTableRequest.filters.includes("processor:2")
|
||||
);
|
||||
})
|
||||
.toBeTruthy();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(otherFiltersMenu).toBeHidden();
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
orderRequests.some((request) => request.limit === 1000 && !request.filters.includes("created_at-date_"))
|
||||
)
|
||||
.toBeTruthy();
|
||||
|
||||
await fromField.locator(".icon.is-right").click();
|
||||
|
||||
await expect(fromInput).toHaveValue("");
|
||||
|
||||
@@ -210,6 +210,9 @@ test.describe("Superuser system security", () => {
|
||||
|
||||
await page.goto("/superuser/system/security/overview");
|
||||
await expect(page.getByTestId("superuser-security-page")).toBeVisible();
|
||||
const desktopNavigation = page.getByTestId("desktop-buefy-navigation");
|
||||
await expect(desktopNavigation.getByText("System", { exact: true })).toBeVisible();
|
||||
await expect(desktopNavigation.getByText("Security", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Security overview")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Open incidents" })).toBeVisible();
|
||||
await expect(page.getByText("Block firewall rule matched: ip 203.0.113.9")).toBeVisible();
|
||||
|
||||
@@ -433,6 +433,7 @@ describe("DatePeriodSelector month warning", () => {
|
||||
showUpdateButton: false,
|
||||
showStartDate: false,
|
||||
showEndDate: false,
|
||||
showToLabel: false,
|
||||
showSelectionValidity: false,
|
||||
showMonthSelector: false,
|
||||
showYearSelector: false,
|
||||
@@ -450,6 +451,8 @@ describe("DatePeriodSelector month warning", () => {
|
||||
|
||||
expect(anytimeButton.classes()).toContain("is-info");
|
||||
expect(todayButton.classes()).not.toContain("is-info");
|
||||
expect(wrapper.find(".level").exists()).toBe(false);
|
||||
expect(wrapper.get("[data-testid='date-period-shortcut-anytime']").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("hides empty range controls unless the selector is optional", () => {
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("NavigationMenuItemsAdmin contract", () => {
|
||||
expect(superuserSource).not.toContain('to: "/superuser/system/replication"');
|
||||
|
||||
expect(superuserLeftMenuSource).toContain('t("security.nav", "Security")');
|
||||
expect(superuserLeftMenuSource).toContain("value: '/system/security/overview'");
|
||||
expect(superuserLeftMenuSource).toContain('value: "/system/security/overview"');
|
||||
expect(superuserLeftMenuSource).toContain('t("cron.system_nav", "System")');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user