diff --git a/src/components/displays/superuser/tables/rolesTable.vue b/src/components/displays/superuser/tables/rolesTable.vue
index 237a4394..53adc3e2 100644
--- a/src/components/displays/superuser/tables/rolesTable.vue
+++ b/src/components/displays/superuser/tables/rolesTable.vue
@@ -1,21 +1,136 @@
@@ -66,6 +181,46 @@ const redirect = (path) => {
icon="fas fa-clone"
:click-action="() => SessionUser.objects.roles.functions.cloneObject(object.id).then(() => loadList())"
>
+
+
@@ -85,4 +240,4 @@ const redirect = (path) => {
\ No newline at end of file
+
diff --git a/src/components/session/token/SessionUser/Objects/Roles.vue b/src/components/session/token/SessionUser/Objects/Roles.vue
index 6e42dedf..916421f2 100644
--- a/src/components/session/token/SessionUser/Objects/Roles.vue
+++ b/src/components/session/token/SessionUser/Objects/Roles.vue
@@ -1,5 +1,4 @@
\ No newline at end of file
+
diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json
index 93ba2d6d..8ee5383d 100644
--- a/src/i18n/generated/da-v2.json
+++ b/src/i18n/generated/da-v2.json
@@ -5048,6 +5048,22 @@
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
},
+ "templates": {
+ "apply": "Anvend {template}-skabelon",
+ "confirm_button": "Anvend skabelon",
+ "confirm_text": "Tilfoej {count} manglende tilladelser fra {template} til {role}?",
+ "confirm_title": "Anvend tilladelsesskabelon?",
+ "error_text": "En eller flere tilladelser fra {template} kunne ikke tilfoejes til {role}. Rollelisten blev genindlaest.",
+ "error_title": "Kunne ikke anvende skabelon",
+ "load_error": "Kunne ikke indlaese skabeloner. Proev igen",
+ "loading": "Indlaeser skabeloner...",
+ "section": "Begraenset backoffice-skabeloner",
+ "success_text": "Tilfoejede {count} tilladelser fra {template} til {role}.",
+ "success_title": "Skabelon anvendt",
+ "unavailable": "Ingen skabeloner er tilgaengelige.",
+ "up_to_date_text": "{role} har allerede alle tilladelser i {template}.",
+ "up_to_date_title": "Rollen er opdateret"
+ },
"subtitle": "@.capitalize:{'words.generated.brugerroller'}"
},
"self_wash": {
diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json
index 24561469..2c477fa9 100644
--- a/src/i18n/generated/de-v2.json
+++ b/src/i18n/generated/de-v2.json
@@ -5159,6 +5159,22 @@
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
},
+ "templates": {
+ "apply": "{template}-Vorlage anwenden",
+ "confirm_button": "Vorlage anwenden",
+ "confirm_text": "{count} fehlende Berechtigungen aus {template} zu {role} hinzufuegen?",
+ "confirm_title": "Berechtigungsvorlage anwenden?",
+ "error_text": "Eine oder mehrere Berechtigungen aus {template} konnten {role} nicht hinzugefuegt werden. Die Rollenliste wurde neu geladen.",
+ "error_title": "Vorlage konnte nicht angewendet werden",
+ "load_error": "Vorlagen konnten nicht geladen werden. Erneut versuchen",
+ "loading": "Vorlagen werden geladen...",
+ "section": "Limited-Backoffice-Vorlagen",
+ "success_text": "{count} Berechtigungen aus {template} zu {role} hinzugefuegt.",
+ "success_title": "Vorlage angewendet",
+ "unavailable": "Keine Vorlagen verfuegbar.",
+ "up_to_date_text": "{role} hat bereits alle Berechtigungen in {template}.",
+ "up_to_date_title": "Rolle ist aktuell"
+ },
"subtitle": "Benutzerrollen"
},
"self_wash": {
diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json
index 120928db..e0b6d072 100644
--- a/src/i18n/generated/en-v2.json
+++ b/src/i18n/generated/en-v2.json
@@ -4880,6 +4880,22 @@
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
},
+ "templates": {
+ "apply": "Apply {template} template",
+ "confirm_button": "Apply template",
+ "confirm_text": "Add {count} missing permissions from {template} to {role}?",
+ "confirm_title": "Apply permission template?",
+ "error_text": "One or more permissions from {template} could not be added to {role}. The role list was reloaded.",
+ "error_title": "Could not apply template",
+ "load_error": "Could not load templates. Try again",
+ "loading": "Loading templates...",
+ "section": "Limited backoffice templates",
+ "success_text": "Added {count} permissions from {template} to {role}.",
+ "success_title": "Template applied",
+ "unavailable": "No templates are available.",
+ "up_to_date_text": "{role} already has every permission in {template}.",
+ "up_to_date_title": "Role is up to date"
+ },
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json
index bec0cdf2..a4565474 100644
--- a/src/i18n/generated/global-v2.json
+++ b/src/i18n/generated/global-v2.json
@@ -4373,6 +4373,22 @@
"title": "@:{'templates.generated.compat.roles.permissions.title'}",
"title_with_name": "@:{'templates.generated.compat.roles.permissions.title_with_name'}"
},
+ "templates": {
+ "apply": "@:{'templates.generated.compat.roles.templates.apply'}",
+ "confirm_button": "@:{'templates.generated.compat.roles.templates.confirm_button'}",
+ "confirm_text": "@:{'templates.generated.compat.roles.templates.confirm_text'}",
+ "confirm_title": "@:{'templates.generated.compat.roles.templates.confirm_title'}",
+ "error_text": "@:{'templates.generated.compat.roles.templates.error_text'}",
+ "error_title": "@:{'templates.generated.compat.roles.templates.error_title'}",
+ "load_error": "@:{'templates.generated.compat.roles.templates.load_error'}",
+ "loading": "@:{'templates.generated.compat.roles.templates.loading'}",
+ "section": "@:{'templates.generated.compat.roles.templates.section'}",
+ "success_text": "@:{'templates.generated.compat.roles.templates.success_text'}",
+ "success_title": "@:{'templates.generated.compat.roles.templates.success_title'}",
+ "unavailable": "@:{'templates.generated.compat.roles.templates.unavailable'}",
+ "up_to_date_text": "@:{'templates.generated.compat.roles.templates.up_to_date_text'}",
+ "up_to_date_title": "@:{'templates.generated.compat.roles.templates.up_to_date_title'}"
+ },
"subtitle": "@:{'templates.generated.compat.roles.subtitle'}",
"title": "@:{'templates.generated.compat.common.roles'}"
},
diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json
index 0cf1e09a..18593fb8 100644
--- a/src/i18n/generated/no-v2.json
+++ b/src/i18n/generated/no-v2.json
@@ -5162,6 +5162,22 @@
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
},
+ "templates": {
+ "apply": "Bruk {template}-mal",
+ "confirm_button": "Bruk mal",
+ "confirm_text": "Legg til {count} manglende tillatelser fra {template} til {role}?",
+ "confirm_title": "Bruke tillatelsesmal?",
+ "error_text": "En eller flere tillatelser fra {template} kunne ikke legges til i {role}. Rollelisten ble lastet inn pa nytt.",
+ "error_title": "Kunne ikke bruke mal",
+ "load_error": "Kunne ikke laste maler. Prov igjen",
+ "loading": "Laster maler...",
+ "section": "Begrenset backoffice-maler",
+ "success_text": "La til {count} tillatelser fra {template} i {role}.",
+ "success_title": "Mal brukt",
+ "unavailable": "Ingen maler er tilgjengelige.",
+ "up_to_date_text": "{role} har allerede alle tillatelser i {template}.",
+ "up_to_date_title": "Rollen er oppdatert"
+ },
"subtitle": "@.capitalize:{'words.generated.user'} roles"
},
"self_wash": {
diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json
index f037f4a9..61435a34 100644
--- a/src/i18n/generated/sv-v2.json
+++ b/src/i18n/generated/sv-v2.json
@@ -5212,6 +5212,22 @@
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
},
+ "templates": {
+ "apply": "Anvand mallen {template}",
+ "confirm_button": "Anvand mall",
+ "confirm_text": "Lagg till {count} saknade behorigheter fran {template} till {role}?",
+ "confirm_title": "Anvanda behorighetsmall?",
+ "error_text": "En eller flera behorigheter fran {template} kunde inte laggas till i {role}. Rolllistan laddades om.",
+ "error_title": "Kunde inte anvanda mall",
+ "load_error": "Kunde inte ladda mallar. Forsok igen",
+ "loading": "Laddar mallar...",
+ "section": "Begransade backoffice-mallar",
+ "success_text": "Lade till {count} behorigheter fran {template} i {role}.",
+ "success_title": "Mall anvand",
+ "unavailable": "Inga mallar ar tillgangliga.",
+ "up_to_date_text": "{role} har redan alla behorigheter i {template}.",
+ "up_to_date_title": "Rollen ar uppdaterad"
+ },
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
diff --git a/src/i18n/source/da/phrases/compat/roles/permissions.json b/src/i18n/source/da/phrases/compat/roles/permissions.json
index 70bd9c03..b0edf540 100644
--- a/src/i18n/source/da/phrases/compat/roles/permissions.json
+++ b/src/i18n/source/da/phrases/compat/roles/permissions.json
@@ -91,6 +91,22 @@
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
+ },
+ "templates": {
+ "apply": "Anvend {template}-skabelon",
+ "confirm_button": "Anvend skabelon",
+ "confirm_text": "Tilfoej {count} manglende tilladelser fra {template} til {role}?",
+ "confirm_title": "Anvend tilladelsesskabelon?",
+ "error_text": "En eller flere tilladelser fra {template} kunne ikke tilfoejes til {role}. Rollelisten blev genindlaest.",
+ "error_title": "Kunne ikke anvende skabelon",
+ "load_error": "Kunne ikke indlaese skabeloner. Proev igen",
+ "loading": "Indlaeser skabeloner...",
+ "section": "Begraenset backoffice-skabeloner",
+ "success_text": "Tilfoejede {count} tilladelser fra {template} til {role}.",
+ "success_title": "Skabelon anvendt",
+ "unavailable": "Ingen skabeloner er tilgaengelige.",
+ "up_to_date_text": "{role} har allerede alle tilladelser i {template}.",
+ "up_to_date_title": "Rollen er opdateret"
}
}
}
diff --git a/src/i18n/source/de/phrases/compat/roles/permissions.json b/src/i18n/source/de/phrases/compat/roles/permissions.json
index ce6a00e6..7640ad81 100644
--- a/src/i18n/source/de/phrases/compat/roles/permissions.json
+++ b/src/i18n/source/de/phrases/compat/roles/permissions.json
@@ -91,6 +91,22 @@
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
+ },
+ "templates": {
+ "apply": "{template}-Vorlage anwenden",
+ "confirm_button": "Vorlage anwenden",
+ "confirm_text": "{count} fehlende Berechtigungen aus {template} zu {role} hinzufuegen?",
+ "confirm_title": "Berechtigungsvorlage anwenden?",
+ "error_text": "Eine oder mehrere Berechtigungen aus {template} konnten {role} nicht hinzugefuegt werden. Die Rollenliste wurde neu geladen.",
+ "error_title": "Vorlage konnte nicht angewendet werden",
+ "load_error": "Vorlagen konnten nicht geladen werden. Erneut versuchen",
+ "loading": "Vorlagen werden geladen...",
+ "section": "Limited-Backoffice-Vorlagen",
+ "success_text": "{count} Berechtigungen aus {template} zu {role} hinzugefuegt.",
+ "success_title": "Vorlage angewendet",
+ "unavailable": "Keine Vorlagen verfuegbar.",
+ "up_to_date_text": "{role} hat bereits alle Berechtigungen in {template}.",
+ "up_to_date_title": "Rolle ist aktuell"
}
}
}
diff --git a/src/i18n/source/en/phrases/compat/roles/permissions.json b/src/i18n/source/en/phrases/compat/roles/permissions.json
index 68707f08..bad7fa5a 100644
--- a/src/i18n/source/en/phrases/compat/roles/permissions.json
+++ b/src/i18n/source/en/phrases/compat/roles/permissions.json
@@ -91,6 +91,22 @@
"subtitle": "Role permissions",
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
+ },
+ "templates": {
+ "apply": "Apply {template} template",
+ "confirm_button": "Apply template",
+ "confirm_text": "Add {count} missing permissions from {template} to {role}?",
+ "confirm_title": "Apply permission template?",
+ "error_text": "One or more permissions from {template} could not be added to {role}. The role list was reloaded.",
+ "error_title": "Could not apply template",
+ "load_error": "Could not load templates. Try again",
+ "loading": "Loading templates...",
+ "section": "Limited backoffice templates",
+ "success_text": "Added {count} permissions from {template} to {role}.",
+ "success_title": "Template applied",
+ "unavailable": "No templates are available.",
+ "up_to_date_text": "{role} already has every permission in {template}.",
+ "up_to_date_title": "Role is up to date"
}
}
}
diff --git a/src/i18n/source/global/shared/roles/index.json b/src/i18n/source/global/shared/roles/index.json
index 2c0cf1bb..020bd5a6 100644
--- a/src/i18n/source/global/shared/roles/index.json
+++ b/src/i18n/source/global/shared/roles/index.json
@@ -91,6 +91,22 @@
"title": "@:{'phrases.compat.roles.permissions.title'}",
"title_with_name": "@:{'phrases.compat.roles.permissions.title_with_name'}"
},
+ "templates": {
+ "apply": "@:{'phrases.compat.roles.templates.apply'}",
+ "confirm_button": "@:{'phrases.compat.roles.templates.confirm_button'}",
+ "confirm_text": "@:{'phrases.compat.roles.templates.confirm_text'}",
+ "confirm_title": "@:{'phrases.compat.roles.templates.confirm_title'}",
+ "error_text": "@:{'phrases.compat.roles.templates.error_text'}",
+ "error_title": "@:{'phrases.compat.roles.templates.error_title'}",
+ "load_error": "@:{'phrases.compat.roles.templates.load_error'}",
+ "loading": "@:{'phrases.compat.roles.templates.loading'}",
+ "section": "@:{'phrases.compat.roles.templates.section'}",
+ "success_text": "@:{'phrases.compat.roles.templates.success_text'}",
+ "success_title": "@:{'phrases.compat.roles.templates.success_title'}",
+ "unavailable": "@:{'phrases.compat.roles.templates.unavailable'}",
+ "up_to_date_text": "@:{'phrases.compat.roles.templates.up_to_date_text'}",
+ "up_to_date_title": "@:{'phrases.compat.roles.templates.up_to_date_title'}"
+ },
"subtitle": "@:{'phrases.compat.roles.subtitle'}",
"title": "@:{'phrases.compat.common.roles'}"
}
diff --git a/src/i18n/source/no/phrases/compat/roles/permissions.json b/src/i18n/source/no/phrases/compat/roles/permissions.json
index e9dca406..9df5fed2 100644
--- a/src/i18n/source/no/phrases/compat/roles/permissions.json
+++ b/src/i18n/source/no/phrases/compat/roles/permissions.json
@@ -91,6 +91,22 @@
"subtitle": "Rolletillatelser",
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
+ },
+ "templates": {
+ "apply": "Bruk {template}-mal",
+ "confirm_button": "Bruk mal",
+ "confirm_text": "Legg til {count} manglende tillatelser fra {template} til {role}?",
+ "confirm_title": "Bruke tillatelsesmal?",
+ "error_text": "En eller flere tillatelser fra {template} kunne ikke legges til i {role}. Rollelisten ble lastet inn pa nytt.",
+ "error_title": "Kunne ikke bruke mal",
+ "load_error": "Kunne ikke laste maler. Prov igjen",
+ "loading": "Laster maler...",
+ "section": "Begrenset backoffice-maler",
+ "success_text": "La til {count} tillatelser fra {template} i {role}.",
+ "success_title": "Mal brukt",
+ "unavailable": "Ingen maler er tilgjengelige.",
+ "up_to_date_text": "{role} har allerede alle tillatelser i {template}.",
+ "up_to_date_title": "Rollen er oppdatert"
}
}
}
diff --git a/src/i18n/source/sv/phrases/compat/roles/permissions.json b/src/i18n/source/sv/phrases/compat/roles/permissions.json
index f8a9b0c2..4e99ce2d 100644
--- a/src/i18n/source/sv/phrases/compat/roles/permissions.json
+++ b/src/i18n/source/sv/phrases/compat/roles/permissions.json
@@ -91,6 +91,22 @@
"subtitle": "Rollbehorigheter",
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
+ },
+ "templates": {
+ "apply": "Anvand mallen {template}",
+ "confirm_button": "Anvand mall",
+ "confirm_text": "Lagg till {count} saknade behorigheter fran {template} till {role}?",
+ "confirm_title": "Anvanda behorighetsmall?",
+ "error_text": "En eller flera behorigheter fran {template} kunde inte laggas till i {role}. Rolllistan laddades om.",
+ "error_title": "Kunde inte anvanda mall",
+ "load_error": "Kunde inte ladda mallar. Forsok igen",
+ "loading": "Laddar mallar...",
+ "section": "Begransade backoffice-mallar",
+ "success_text": "Lade till {count} behorigheter fran {template} i {role}.",
+ "success_title": "Mall anvand",
+ "unavailable": "Inga mallar ar tillgangliga.",
+ "up_to_date_text": "{role} har redan alla behorigheter i {template}.",
+ "up_to_date_title": "Rollen ar uppdaterad"
}
}
}
diff --git a/tests/e2e/superuser-roles-permissions.spec.ts b/tests/e2e/superuser-roles-permissions.spec.ts
index ea327277..376d5b79 100644
--- a/tests/e2e/superuser-roles-permissions.spec.ts
+++ b/tests/e2e/superuser-roles-permissions.spec.ts
@@ -43,11 +43,46 @@ const departments = [
},
];
+const roleTemplatePermissions = [
+ "user",
+ "permissions_list_own",
+ "list_departments",
+ "list_orders",
+ "fetch_order",
+ "statistics_orders_new",
+];
+
+const limitedBackofficePermissionTemplates = [
+ {
+ key: "viewer",
+ label: "Deactivated",
+ description: "Keeps the employee registered without order, booking, or management permissions.",
+ permissions: ["user", "permissions_list_own"],
+ },
+ {
+ key: "cashier",
+ label: "Cashier",
+ description: "Can work with POS orders for assigned departments.",
+ permissions: roleTemplatePermissions,
+ },
+];
+
type PermissionCall = {
action: "add" | "remove";
permission: string;
};
+const roleListEnvelope = (rows: Array>) => ({
+ data: rows,
+ meta: {
+ pagination: {
+ page: 1,
+ per_page: 100,
+ total: rows.length,
+ },
+ },
+});
+
const openGroupTab = async (page: Page, label: RegExp) => {
await page.locator(".tabs").getByText(label).click();
};
@@ -147,6 +182,107 @@ const bootRolePermissionsPage = async (page: Page, routeOptions = {}) => {
};
test.describe("Superuser role permissions", () => {
+ test("applies limited backoffice templates from the role action wheel in add-only mode", async ({ page }) => {
+ const role = {
+ id: 2,
+ name: "Operations manager",
+ description: "Can manage operations.",
+ created_at: "2026-07-06T08:00:00.000Z",
+ permissions: ["user", "list_orders"],
+ };
+ const calls: PermissionCall[] = [];
+
+ await page.addInitScript(() => {
+ window.localStorage.setItem("locale", "en");
+ });
+ await seedAuthenticatedState(page, "superuser-roles-template-token");
+ await mockApi(page, {
+ authenticated: true,
+ permissions: ["superuser", "user", "list_roles", "add_role_permission"],
+ sessionData: {
+ group_id: 1,
+ },
+ });
+
+ await page.route(apiPathPattern("/roles/limited-backoffice-permission-templates"), async (route: Route) => {
+ await route.fulfill(json({ data: limitedBackofficePermissionTemplates }));
+ });
+
+ await page.route(apiPathPattern("/roles"), async (route: Route) => {
+ await route.fulfill(json(roleListEnvelope([role])));
+ });
+
+ await page.route(apiPathPattern("/roles/permissions"), async (route: Route) => {
+ const body = route.request().postDataJSON();
+ const permission = String(body.permission_id || "");
+ calls.push({ action: "add", permission });
+ if (!role.permissions.includes(permission)) {
+ role.permissions = [...role.permissions, permission];
+ }
+ await route.fulfill(json({ data: role }));
+ });
+
+ await page.goto("/superuser/roles", { waitUntil: "domcontentloaded" });
+ await expect(page.getByRole("row", { name: /Operations manager/ })).toBeVisible();
+
+ const row = page.getByRole("row", { name: /Operations manager/ });
+ await row.locator(".action-settings-wheel-trigger").click();
+ await expect(page.getByTestId("role-limited-backoffice-template-actions-2")).toBeVisible();
+ await page.getByTestId("role-template-2-cashier").click();
+
+ await expect(page.getByText("Apply permission template?")).toBeVisible();
+ await page.getByRole("button", { name: "Apply template" }).click();
+
+ await expect
+ .poll(() => calls.map((call) => call.permission))
+ .toEqual(["permissions_list_own", "list_departments", "fetch_order", "statistics_orders_new"]);
+ await expect(page.getByText("Template applied")).toBeVisible();
+ });
+
+ test("disables limited backoffice template actions when a role is already up to date", async ({ page }) => {
+ const role = {
+ id: 3,
+ name: "Cashier role",
+ description: "Current cashier permissions.",
+ created_at: "2026-07-06T08:00:00.000Z",
+ permissions: [...roleTemplatePermissions],
+ };
+ const calls: PermissionCall[] = [];
+
+ await page.addInitScript(() => {
+ window.localStorage.setItem("locale", "en");
+ });
+ await seedAuthenticatedState(page, "superuser-roles-template-up-to-date-token");
+ await mockApi(page, {
+ authenticated: true,
+ permissions: ["superuser", "user", "list_roles", "add_role_permission"],
+ sessionData: {
+ group_id: 1,
+ },
+ });
+
+ await page.route(apiPathPattern("/roles/limited-backoffice-permission-templates"), async (route: Route) => {
+ await route.fulfill(json({ data: limitedBackofficePermissionTemplates }));
+ });
+
+ await page.route(apiPathPattern("/roles"), async (route: Route) => {
+ await route.fulfill(json(roleListEnvelope([role])));
+ });
+
+ await page.route(apiPathPattern("/roles/permissions"), async (route: Route) => {
+ calls.push({ action: "add", permission: String(route.request().postDataJSON().permission_id || "") });
+ await route.fulfill(json({ data: role }));
+ });
+
+ await page.goto("/superuser/roles", { waitUntil: "domcontentloaded" });
+ const row = page.getByRole("row", { name: /Cashier role/ });
+ await expect(row).toBeVisible();
+ await row.locator(".action-settings-wheel-trigger").click();
+
+ await expect(page.getByTestId("role-template-3-cashier")).toBeDisabled();
+ expect(calls).toEqual([]);
+ });
+
test("@smoke @pr loads grouped permissions, searches, and toggles without a blank page", async ({ page }) => {
const { calls } = await bootRolePermissionsPage(page);