Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c11b164188 | ||
|
|
882bc64fa6 |
@@ -60,6 +60,16 @@ const menu_items = computed(() => [
|
|||||||
'user'
|
'user'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: t('user_menu.my_permissions'),
|
||||||
|
route: '/user/permissions',
|
||||||
|
icon: 'fas fa-user-shield',
|
||||||
|
children: [],
|
||||||
|
permissions: [
|
||||||
|
'user',
|
||||||
|
'SUBUSERS_LIST'
|
||||||
|
]
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -53,6 +53,30 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"statistics": "@:{'phrases.compat.global.statistics'}"
|
"statistics": "@:{'phrases.compat.global.statistics'}"
|
||||||
},
|
},
|
||||||
|
"permissions": {
|
||||||
|
"card_chauffor_label": "Chauffør (standard)",
|
||||||
|
"card_chauffor_description": "Kan bruge døgnvask, oprette bookinger, se køretøjer og ordrer.",
|
||||||
|
"card_booking_label": "Bookingkoodinator",
|
||||||
|
"card_booking_description": "Kan oprette og redigere bookinger samt se køretøjer og ordrer.",
|
||||||
|
"card_intro": "Vælg den rolle, som nye chauffører som standard skal have, når de oprettes via QR-kode.",
|
||||||
|
"groups": {
|
||||||
|
"vehicles": "Køretøjer",
|
||||||
|
"selfserve": "Døgnvask",
|
||||||
|
"bookings": "Bookinger",
|
||||||
|
"orders": "Ordrer",
|
||||||
|
"driver_management": "Chaufførstyring"
|
||||||
|
},
|
||||||
|
"loading": "Henter tilladelser...",
|
||||||
|
"save": "Gem standardrolle",
|
||||||
|
"saved": "Standardrollen er gemt.",
|
||||||
|
"save_error": "Standardrollen kunne ikke gemmes.",
|
||||||
|
"selected_template": "Valgt rolle: {label}",
|
||||||
|
"subtitle": "Vælg standardtilladelser for nye chauffører.",
|
||||||
|
"toggle_booking_coordinator": "Bookingkoodinator",
|
||||||
|
"toggle_chauffor": "Chauffør",
|
||||||
|
"toggle_description": "De 5 kategorier nedenfor viser, hvad rollen giver adgang til.",
|
||||||
|
"title": "Mine tilladelser"
|
||||||
|
},
|
||||||
"orders": {
|
"orders": {
|
||||||
"single_subtitle": "@.capitalize:{'terms.glossary.se'} @:{'terms.glossary.detaljer'} @:{'terms.glossary.om'} transaktionen",
|
"single_subtitle": "@.capitalize:{'terms.glossary.se'} @:{'terms.glossary.detaljer'} @:{'terms.glossary.om'} transaktionen",
|
||||||
"subtitle": "@.capitalize:{'terms.glossary.se'} @:{'terms.glossary.dine'} @:{'terms.glossary.vaske'}",
|
"subtitle": "@.capitalize:{'terms.glossary.se'} @:{'terms.glossary.dine'} @:{'terms.glossary.vaske'}",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"user_menu": {
|
"user_menu": {
|
||||||
"my_bookings": "@:{'terms.glossary.mine'} @:{'terms.glossary.bookinger'}",
|
"my_bookings": "@:{'terms.glossary.mine'} @:{'terms.glossary.bookinger'}",
|
||||||
"my_invoices": "@:{'terms.glossary.mine'} @:{'terms.glossary.fakturaer'}",
|
"my_invoices": "@:{'terms.glossary.mine'} @:{'terms.glossary.fakturaer'}",
|
||||||
|
"my_permissions": "@:{'terms.glossary.mine'} @:{'terms.glossary.tilladelser'}",
|
||||||
"my_vehicles": "@:{'terms.glossary.mine'} @:{'terms.glossary.køretøjer'}",
|
"my_vehicles": "@:{'terms.glossary.mine'} @:{'terms.glossary.køretøjer'}",
|
||||||
"my_washes": "@:{'terms.glossary.mine'} @:{'terms.glossary.vaske'}"
|
"my_washes": "@:{'terms.glossary.mine'} @:{'terms.glossary.vaske'}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ const DepartmentTimeBookingsTypes = lazyView('@/views/dashboards/departmentDashb
|
|||||||
const DepartmentModulesSetup = lazyView('@/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue');
|
const DepartmentModulesSetup = lazyView('@/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue');
|
||||||
const Vehicle = lazyView('@/views/dashboards/superUserDashboard/vehicle/Vehicle.vue');
|
const Vehicle = lazyView('@/views/dashboards/superUserDashboard/vehicle/Vehicle.vue');
|
||||||
const MyInvoices = lazyView('@/views/dashboards/userDashboard/invoices/MyInvoices.vue');
|
const MyInvoices = lazyView('@/views/dashboards/userDashboard/invoices/MyInvoices.vue');
|
||||||
|
const MyPermissions = lazyView('@/views/dashboards/userDashboard/permissions/MyPermissions.vue');
|
||||||
const MicrosoftCallbackToken = lazyView('@/views/callback/microsoft/MicrosoftCallbackToken.vue');
|
const MicrosoftCallbackToken = lazyView('@/views/callback/microsoft/MicrosoftCallbackToken.vue');
|
||||||
const ConfigurationEntra = lazyView('@/views/dashboards/superUserDashboard/configuration/ConfigurationEntra.vue');
|
const ConfigurationEntra = lazyView('@/views/dashboards/superUserDashboard/configuration/ConfigurationEntra.vue');
|
||||||
const DepartmentTimeBookingsNew = lazyView('@/views/dashboards/departmentDashboard/modules/time-bookings/book/DepartmentTimeBookingsNew.vue');
|
const DepartmentTimeBookingsNew = lazyView('@/views/dashboards/departmentDashboard/modules/time-bookings/book/DepartmentTimeBookingsNew.vue');
|
||||||
@@ -369,6 +370,12 @@ export const router = createRouter({
|
|||||||
component: MyInvoices,
|
component: MyInvoices,
|
||||||
meta: { middleware: authMiddleware }
|
meta: { middleware: authMiddleware }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'mypermissions',
|
||||||
|
path: '/user/permissions',
|
||||||
|
component: MyPermissions,
|
||||||
|
meta: { middleware: authMiddleware }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'myorder',
|
name: 'myorder',
|
||||||
path: '/user/orders/:orderId',
|
path: '/user/orders/:orderId',
|
||||||
|
|||||||
+57
-12
@@ -1264,14 +1264,19 @@ const createPathEditorQuestion = async () => {
|
|||||||
await loadPathOutcomes({ force: true });
|
await loadPathOutcomes({ force: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const pathEditorProgramButtonOptions = computed(() => [
|
const pathEditorProgramButtonOptions = computed(() => {
|
||||||
{ id: 0, value: "0", label: "0 = OFF" },
|
if (!pathEditorMachineIsAvailable.value) {
|
||||||
...Array.from({ length: 12 }, (_, index) => ({
|
return [{ id: 0, value: "0", label: "0 = OFF" }];
|
||||||
id: index + 1,
|
}
|
||||||
value: String(index + 1),
|
return [
|
||||||
label: `Wheel position #${index + 1}`,
|
{ id: 0, value: "0", label: "0 = OFF" },
|
||||||
})),
|
...Array.from({ length: 12 }, (_, index) => ({
|
||||||
]);
|
id: index + 1,
|
||||||
|
value: String(index + 1),
|
||||||
|
label: `Wheel position #${index + 1}`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1392,16 +1397,56 @@ const pathVerificationSelectedCaseHasRun = computed(() =>
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const pathEditorMachineButtonOptions = computed(() =>
|
// TRU-91: Filter path editor machine options to only show enabled/available
|
||||||
Array.from({ length: 12 }, (_, index) => {
|
// machine programs for the current scope. A machine is considered "available"
|
||||||
|
// when the current lane has a machine_type_id that resolves to a non-deleted
|
||||||
|
// selfserve_machine_types row in the lookups.
|
||||||
|
const pathEditorCurrentLaneRow = computed(() => {
|
||||||
|
const laneId = parseIntOrZero(selectedSimulatorLaneId.value);
|
||||||
|
if (laneId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return laneRowById(laneId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathEditorCurrentMachineType = computed(() => {
|
||||||
|
const lane = pathEditorCurrentLaneRow.value;
|
||||||
|
if (!lane) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const machineTypeId = parseNullableInt(lane.machine_type_id);
|
||||||
|
if (!machineTypeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const machineTypeRow = lookupRowById("machine_types", machineTypeId);
|
||||||
|
if (!machineTypeRow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
machineTypeRow.deleted_at !== null &&
|
||||||
|
machineTypeRow.deleted_at !== undefined &&
|
||||||
|
machineTypeRow.deleted_at !== ""
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return machineTypeRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathEditorMachineIsAvailable = computed(() => Boolean(pathEditorCurrentMachineType.value));
|
||||||
|
|
||||||
|
const pathEditorMachineButtonOptions = computed(() => {
|
||||||
|
if (!pathEditorMachineIsAvailable.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from({ length: 12 }, (_, index) => {
|
||||||
const option = taskButtonOptions.find((entry) => entry.id === index);
|
const option = taskButtonOptions.find((entry) => entry.id === index);
|
||||||
return {
|
return {
|
||||||
id: index,
|
id: index,
|
||||||
label: option?.description || `Machine button ${index}`,
|
label: option?.description || `Machine button ${index}`,
|
||||||
description: option?.name || `Program ${index + 1}`,
|
description: option?.name || `Program ${index + 1}`,
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
|
|
||||||
const pathEditorTaskTitleKeySlug = (key) => String(key || "").replace(/[^a-zA-Z0-9_-]/g, "-");
|
const pathEditorTaskTitleKeySlug = (key) => String(key || "").replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, computed } from "vue";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import UserDashboardPageWrapper from "@/views/dashboards/userDashboard/UserDashboardPageWrapper.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission groups (5 toggles) that mirror the existing 5 capability groups
|
||||||
|
* in modules/subusers/classes/subuser_permission_templates_service.php.
|
||||||
|
* They cover the 5 main areas a customer wants to grant their drivers:
|
||||||
|
* - vehicles (Køretøjer)
|
||||||
|
* - selfserve (Døgnvask)
|
||||||
|
* - bookings (Bookinger)
|
||||||
|
* - orders (Ordrer)
|
||||||
|
* - driver_management (Chaufførstyring)
|
||||||
|
*/
|
||||||
|
const PERMISSION_GROUPS = [
|
||||||
|
{ key: "vehicles", capabilities: ["VEHICLES_LIST", "VEHICLES_EDIT", "VEHICLES_DELETE", "VEHICLES_ADD"] },
|
||||||
|
{ key: "selfserve", capabilities: ["SELFSERVE_LIST", "SELFSERVE_EDIT", "SELFSERVE_DELETE", "SELFSERVE_ADD"] },
|
||||||
|
{ key: "bookings", capabilities: ["BOOKINGS_LIST", "BOOKINGS_EDIT", "BOOKINGS_DELETE", "BOOKINGS_ADD"] },
|
||||||
|
{ key: "orders", capabilities: ["ORDERS_LIST", "ORDERS_EDIT"] },
|
||||||
|
{ key: "driver_management", capabilities: ["SUBUSERS_LIST", "SUBUSERS_EDIT", "SUBUSERS_DELETE", "SUBUSERS_ADD"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two primary roles (templates) shown to the customer in the kunde portal
|
||||||
|
* "tilladelser" tab. The customer picks one as the default for new drivers
|
||||||
|
* created through the QR flow (see TRU-88 / TRU-90).
|
||||||
|
*
|
||||||
|
* - chauffeur (driver) : standard self-serve + bookings
|
||||||
|
* - booking_coordinator : extended bookings/orders (no self-serve)
|
||||||
|
*/
|
||||||
|
const PRIMARY_ROLE_KEYS = ["driver", "booking_coordinator"];
|
||||||
|
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const accessModel = ref({ templates: [], groups: [] });
|
||||||
|
const selectedTemplateKey = ref(null);
|
||||||
|
|
||||||
|
const selectedTemplate = computed(() => {
|
||||||
|
if (!selectedTemplateKey.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return accessModel.value.templates.find((template) => template.key === selectedTemplateKey.value) || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isGroupActive = (groupKey) => {
|
||||||
|
if (!selectedTemplate.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const groups = selectedTemplate.value.permission_groups || [];
|
||||||
|
return groups.some((group) => group.key === groupKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGroupFullyActive = (groupKey) => {
|
||||||
|
if (!selectedTemplate.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const groups = selectedTemplate.value.permission_groups || [];
|
||||||
|
const group = groups.find((g) => g.key === groupKey);
|
||||||
|
if (!group) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// "Fully active" means all 4 capabilities in the group are granted
|
||||||
|
const groupDefinition = PERMISSION_GROUPS.find((g) => g.key === groupKey);
|
||||||
|
if (!groupDefinition) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return groupDefinition.capabilities.every((capability) => group.capabilities.includes(capability));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGroupPartiallyActive = (groupKey) => {
|
||||||
|
if (!selectedTemplate.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isGroupActive(groupKey) && !isGroupFullyActive(groupKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPrimaryRole = (key) => PRIMARY_ROLE_KEYS.includes(key);
|
||||||
|
|
||||||
|
const loadAccessModel = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const model = await SessionUser.objects.subuser_grants.functions.getPermissionTemplates();
|
||||||
|
const payload = model?.data?.data || model?.data || model;
|
||||||
|
accessModel.value = {
|
||||||
|
templates: Array.isArray(payload?.templates) ? payload.templates : [],
|
||||||
|
groups: Array.isArray(payload?.groups) ? payload.groups : [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the customer's stored default template via SessionUser / API
|
||||||
|
const stored = await SessionUser.request("/customer/default-driver-template", "GET")
|
||||||
|
.then((response) => response?.data?.data?.template_key || response?.data?.template_key || null)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (stored && accessModel.value.templates.some((template) => template.key === stored)) {
|
||||||
|
selectedTemplateKey.value = stored;
|
||||||
|
} else {
|
||||||
|
const chauffeur = accessModel.value.templates.find((template) => template.key === "driver");
|
||||||
|
selectedTemplateKey.value = chauffeur?.key || accessModel.value.templates[0]?.key || null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load permission templates:", error);
|
||||||
|
accessModel.value = { templates: [], groups: [] };
|
||||||
|
await Swal.fire({
|
||||||
|
title: t("common.error"),
|
||||||
|
text: error?.message || t("user_dashboard.permissions.save_error"),
|
||||||
|
icon: "error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectTemplate = (key) => {
|
||||||
|
if (!accessModel.value.templates.some((template) => template.key === key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedTemplateKey.value = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSaveDefault = async () => {
|
||||||
|
if (!selectedTemplateKey.value || isSaving.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
await SessionUser.request("/customer/default-driver-template", "PUT", {
|
||||||
|
template_key: selectedTemplateKey.value,
|
||||||
|
});
|
||||||
|
await Swal.fire({
|
||||||
|
title: t("user_dashboard.permissions.saved"),
|
||||||
|
icon: "success",
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await Swal.fire({
|
||||||
|
title: t("common.error"),
|
||||||
|
text: SessionUser.functions.parseErrorMessage(error) || t("user_dashboard.permissions.save_error"),
|
||||||
|
icon: "error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadAccessModel);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UserDashboardPageWrapper
|
||||||
|
:title="$t('user_dashboard.permissions.title')"
|
||||||
|
:subtitle="$t('user_dashboard.permissions.subtitle')"
|
||||||
|
>
|
||||||
|
<div v-if="isLoading" class="notification is-light">
|
||||||
|
{{ $t("user_dashboard.permissions.loading") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="permissions-tab" data-testid="user-permissions-tab">
|
||||||
|
<p class="block has-text-grey">
|
||||||
|
{{ $t("user_dashboard.permissions.card_intro") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="columns is-multiline" data-testid="user-permissions-roles">
|
||||||
|
<div
|
||||||
|
v-for="template in accessModel.templates.filter((tpl) => isPrimaryRole(tpl.key))"
|
||||||
|
:key="template.key"
|
||||||
|
class="column is-half"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="permission-role-card"
|
||||||
|
:class="{ 'permission-role-card--selected': selectedTemplateKey === template.key }"
|
||||||
|
:data-testid="`user-permission-role-${template.key}`"
|
||||||
|
@click="onSelectTemplate(template.key)"
|
||||||
|
>
|
||||||
|
<span class="permission-role-card__title">
|
||||||
|
{{
|
||||||
|
template.key === "driver"
|
||||||
|
? $t("user_dashboard.permissions.card_chauffor_label")
|
||||||
|
: $t("user_dashboard.permissions.card_booking_label")
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span class="permission-role-card__description">
|
||||||
|
{{
|
||||||
|
template.key === "driver"
|
||||||
|
? $t("user_dashboard.permissions.card_chauffor_description")
|
||||||
|
: $t("user_dashboard.permissions.card_booking_description")
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="selectedTemplateKey === template.key"
|
||||||
|
class="tag is-success is-light mt-2"
|
||||||
|
:data-testid="`user-permission-role-selected-${template.key}`"
|
||||||
|
>
|
||||||
|
{{ $t("user_dashboard.permissions.selected_template", { label: template.label }) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="permissions-tab__toggles" data-testid="user-permissions-toggles">
|
||||||
|
<h3 class="title is-6 mb-2">
|
||||||
|
<span class="icon"><i class="fas fa-sliders-h"></i></span>
|
||||||
|
<span>{{ $t("user_dashboard.permissions.toggle_description") }}</span>
|
||||||
|
</h3>
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div
|
||||||
|
v-for="group in PERMISSION_GROUPS"
|
||||||
|
:key="group.key"
|
||||||
|
class="column is-half"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="permission-toggle box"
|
||||||
|
:class="{
|
||||||
|
'permission-toggle--active': isGroupFullyActive(group.key),
|
||||||
|
'permission-toggle--partial': isGroupPartiallyActive(group.key),
|
||||||
|
}"
|
||||||
|
:data-testid="`user-permission-toggle-${group.key}`"
|
||||||
|
>
|
||||||
|
<div class="level is-mobile mb-0">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<div>
|
||||||
|
<p class="permission-toggle__label">
|
||||||
|
{{ $t(`user_dashboard.permissions.groups.${group.key}`) }}
|
||||||
|
</p>
|
||||||
|
<p class="help has-text-grey">
|
||||||
|
{{ group.capabilities.length }} rettigheder
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item">
|
||||||
|
<b-switch
|
||||||
|
:model-value="isGroupFullyActive(group.key) || isGroupPartiallyActive(group.key)"
|
||||||
|
disabled
|
||||||
|
size="is-small"
|
||||||
|
type="is-success"
|
||||||
|
:data-testid="`user-permission-toggle-switch-${group.key}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="permissions-tab__actions mt-4">
|
||||||
|
<button
|
||||||
|
class="button is-link"
|
||||||
|
:class="{ 'is-loading': isSaving }"
|
||||||
|
:disabled="!selectedTemplateKey || isSaving"
|
||||||
|
data-testid="user-permissions-save"
|
||||||
|
@click="onSaveDefault"
|
||||||
|
>
|
||||||
|
<span class="icon"><i class="fas fa-save"></i></span>
|
||||||
|
<span>{{ $t("user_dashboard.permissions.save") }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UserDashboardPageWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.permissions-tab .permission-role-card {
|
||||||
|
align-items: flex-start;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dbdbdb;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-tab .permission-role-card:hover,
|
||||||
|
.permissions-tab .permission-role-card--selected {
|
||||||
|
border-color: #3273dc;
|
||||||
|
box-shadow: 0 0 0 1px #3273dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-tab .permission-role-card__title {
|
||||||
|
color: #1f2933;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-tab .permission-role-card__description {
|
||||||
|
color: #4a4a4a;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-tab .permission-toggle {
|
||||||
|
border: 1px solid #e6ebf1;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-tab .permission-toggle--active {
|
||||||
|
background: #f1faf3;
|
||||||
|
border-color: #48c78e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-tab .permission-toggle--partial {
|
||||||
|
background: #fff8e1;
|
||||||
|
border-color: #ffe08a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-tab .permission-toggle__label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2933;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
|
||||||
|
|
||||||
|
describe("self-serve studio path editor machine filter (TRU-91)", () => {
|
||||||
|
const studioSource = readSource(
|
||||||
|
"src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
|
||||||
|
);
|
||||||
|
|
||||||
|
it("defines a path editor machine-availability check tied to the current lane and machine type", () => {
|
||||||
|
expect(studioSource).toContain("pathEditorCurrentLaneRow");
|
||||||
|
expect(studioSource).toContain("pathEditorCurrentMachineType");
|
||||||
|
expect(studioSource).toContain("pathEditorMachineIsAvailable");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty machine button list when the machine is not available", () => {
|
||||||
|
expect(studioSource).toMatch(
|
||||||
|
/pathEditorMachineButtonOptions[\s\S]{0,400}if\s*\(\s*!pathEditorMachineIsAvailable\.value\s*\)\s*\{\s*return\s*\[\s*\]\s*;\s*\}/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("limits program wheel options to the OFF position when the machine is not available", () => {
|
||||||
|
expect(studioSource).toMatch(
|
||||||
|
/pathEditorProgramButtonOptions[\s\S]{0,600}if\s*\(\s*!pathEditorMachineIsAvailable\.value\s*\)[\s\S]{0,200}return\s*\[\s*\{\s*id:\s*0,\s*value:\s*"0",\s*label:\s*"0 = OFF"\s*\}\s*\]/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("considers a machine type deleted when its deleted_at is set", () => {
|
||||||
|
expect(studioSource).toMatch(/machineTypeRow\.deleted_at[\s\S]{0,80}return\s+null/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user