From 6c1c1def784938b8a5272e6160b35eb652b5d6ec Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Mon, 6 Jul 2026 17:32:58 +0200 Subject: [PATCH] Add limited backoffice employee QR login links --- src/i18n/generated/da-v2.json | 22 ++- src/i18n/generated/de-v2.json | 22 ++- src/i18n/generated/en-v2.json | 22 ++- src/i18n/generated/no-v2.json | 22 ++- src/i18n/generated/sv-v2.json | 22 ++- src/i18n/source/da/phrases/index.json | 22 ++- src/i18n/source/de/phrases/index.json | 22 ++- src/i18n/source/en/phrases/index.json | 22 ++- src/i18n/source/no/phrases/index.json | 22 ++- src/i18n/source/sv/phrases/index.json | 22 ++- src/services/limitedBackoffice.js | 3 + .../backoffice/LimitedBackofficeEmployees.vue | 185 +++++++++++++++++- tests/e2e/limited-backoffice.spec.ts | 66 +++++++ 13 files changed, 433 insertions(+), 41 deletions(-) diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json index 7fe9d27d..9c7a39c9 100644 --- a/src/i18n/generated/da-v2.json +++ b/src/i18n/generated/da-v2.json @@ -1220,7 +1220,13 @@ "active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}", "inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -1250,7 +1256,8 @@ "save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}", @@ -6083,7 +6090,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@:{'words.generated.rediger'}", - "deactivate": "Deaktiver" + "deactivate": "Deaktiver", + "login_link": "QR-login", + "login_link_title": "Medarbejderloginlink", + "login_link_qr_alt": "QR-kode-loginlink", + "login_link_url": "Loginlink", + "copy_login_link": "Kopiér link", + "login_link_copied": "Linket er kopieret." }, "roles": { "viewer": { @@ -6113,7 +6126,8 @@ "save_prices": "@.capitalize:{'words.generated.afdelingspriser'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.gemmes'}.", "load_employees": "@.capitalize:{'words.generated.medarbejdere'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.indlæses'}.", "save_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.gemmes'}.", - "deactivate_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktiveres." + "deactivate_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktiveres.", + "generate_employee_login_link": "Medarbejderens loginlink kunne ikke oprettes." }, "role_permissions": { "title": "Rolletilladelser", diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json index 33fca3e6..8c10d809 100644 --- a/src/i18n/generated/de-v2.json +++ b/src/i18n/generated/de-v2.json @@ -1331,7 +1331,13 @@ "active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}", "inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -1361,7 +1367,8 @@ "save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}", @@ -6172,7 +6179,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@.capitalize:{'words.generated.bearbeiten'}", - "deactivate": "Deaktivieren" + "deactivate": "Deaktivieren", + "login_link": "QR-Login", + "login_link_title": "Mitarbeiter-Loginlink", + "login_link_qr_alt": "QR-Code-Loginlink", + "login_link_url": "Loginlink", + "copy_login_link": "Link kopieren", + "login_link_copied": "Link kopiert." }, "roles": { "viewer": { @@ -6202,7 +6215,8 @@ "save_prices": "@.capitalize:{'words.generated.abteilungspreise'} @:{'words.generated.konnten'} @:{'words.generated.nicht'} @:{'words.generated.gespeichert'} @:{'words.generated.werden'}.", "load_employees": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnten'} @:{'words.generated.nicht'} @:{'words.generated.geladen'} @:{'words.generated.werden'}.", "save_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.gespeichert'} @:{'words.generated.werden'}.", - "deactivate_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.deaktiviert'} @:{'words.generated.werden'}." + "deactivate_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.deaktiviert'} @:{'words.generated.werden'}.", + "generate_employee_login_link": "Mitarbeiter-Loginlink konnte nicht erstellt werden." }, "role_permissions": { "title": "Rollenberechtigungen", diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json index b331d8a2..1fc7f8ed 100644 --- a/src/i18n/generated/en-v2.json +++ b/src/i18n/generated/en-v2.json @@ -1052,7 +1052,13 @@ "active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}", "inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -1082,7 +1088,8 @@ "save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}", @@ -5915,7 +5922,13 @@ "active": "Active", "inactive": "@.capitalize:{'words.generated.inactive'}", "edit": "@.capitalize:{'words.generated.edit'}", - "deactivate": "@.capitalize:{'words.generated.deactivate'}" + "deactivate": "@.capitalize:{'words.generated.deactivate'}", + "login_link": "QR login", + "login_link_title": "Employee login link", + "login_link_qr_alt": "QR code login link", + "login_link_url": "Login link", + "copy_login_link": "Copy link", + "login_link_copied": "Link copied." }, "roles": { "viewer": { @@ -5945,7 +5958,8 @@ "save_prices": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.save'} @:{'words.generated.department'} @:{'words.generated.prices'}.", "load_employees": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.load'} @:{'words.generated.employees'}.", "save_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.save'} @:{'words.generated.employee'}.", - "deactivate_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.deactivate'} @:{'words.generated.employee'}." + "deactivate_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.deactivate'} @:{'words.generated.employee'}.", + "generate_employee_login_link": "Could not create employee login link." }, "role_permissions": { "title": "Role permissions", diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json index 36fa81eb..11200e87 100644 --- a/src/i18n/generated/no-v2.json +++ b/src/i18n/generated/no-v2.json @@ -1334,7 +1334,13 @@ "active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}", "inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -1364,7 +1370,8 @@ "save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}", @@ -6175,7 +6182,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@:{'words.generated.rediger'}", - "deactivate": "Deaktiver" + "deactivate": "Deaktiver", + "login_link": "QR-innlogging", + "login_link_title": "Innloggingslenke for medarbeider", + "login_link_qr_alt": "QR-kode for innloggingslenke", + "login_link_url": "Innloggingslenke", + "copy_login_link": "Kopier lenke", + "login_link_copied": "Lenken er kopiert." }, "roles": { "viewer": { @@ -6205,7 +6218,8 @@ "save_prices": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.lagre'} @:{'words.generated.avdelingspriser'}.", "load_employees": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.laste'} @:{'words.generated.medarbeidere'}.", "save_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.lagre'} @:{'words.generated.medarbeideren'}.", - "deactivate_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktivere @:{'words.generated.medarbeideren'}." + "deactivate_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktivere @:{'words.generated.medarbeideren'}.", + "generate_employee_login_link": "Kunne ikke opprette innloggingslenke for medarbeideren." }, "role_permissions": { "title": "Rolletillatelser", diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json index 8b7d6534..761a576e 100644 --- a/src/i18n/generated/sv-v2.json +++ b/src/i18n/generated/sv-v2.json @@ -1384,7 +1384,13 @@ "active": "@:{'templates.generated.compat.limited_backoffice.employees.active'}", "inactive": "@:{'templates.generated.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'templates.generated.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'templates.generated.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'templates.generated.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'templates.generated.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'templates.generated.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -1414,7 +1420,8 @@ "save_prices": "@:{'templates.generated.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'templates.generated.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}", @@ -6225,7 +6232,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@:{'words.generated.redigera'}", - "deactivate": "@.capitalize:{'words.generated.inaktivera'}" + "deactivate": "@.capitalize:{'words.generated.inaktivera'}", + "login_link": "QR-inloggning", + "login_link_title": "Inloggningslänk för medarbetare", + "login_link_qr_alt": "QR-kod för inloggningslänk", + "login_link_url": "Inloggningslänk", + "copy_login_link": "Kopiera länk", + "login_link_copied": "Länken har kopierats." }, "roles": { "viewer": { @@ -6255,7 +6268,8 @@ "save_prices": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.spara'} @:{'words.generated.avdelningspriser'}.", "load_employees": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.ladda'} @:{'words.generated.medarbetare'}.", "save_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.spara'} @:{'words.generated.medarbetaren'}.", - "deactivate_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.inaktivera'} @:{'words.generated.medarbetaren'}." + "deactivate_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.inaktivera'} @:{'words.generated.medarbetaren'}.", + "generate_employee_login_link": "Kunde inte skapa inloggningslänk för medarbetaren." }, "role_permissions": { "title": "Rollbehörigheter", diff --git a/src/i18n/source/da/phrases/index.json b/src/i18n/source/da/phrases/index.json index cce91d0a..3f7d7a17 100644 --- a/src/i18n/source/da/phrases/index.json +++ b/src/i18n/source/da/phrases/index.json @@ -76,7 +76,13 @@ "active": "@:{'phrases.compat.limited_backoffice.employees.active'}", "inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -106,7 +112,8 @@ "save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}", @@ -321,7 +328,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@:{'terms.glossary.rediger'}", - "deactivate": "Deaktiver" + "deactivate": "Deaktiver", + "login_link": "QR-login", + "login_link_title": "Medarbejderloginlink", + "login_link_qr_alt": "QR-kode-loginlink", + "login_link_url": "Loginlink", + "copy_login_link": "Kopiér link", + "login_link_copied": "Linket er kopieret." }, "roles": { "viewer": { @@ -351,7 +364,8 @@ "save_prices": "@.capitalize:{'terms.glossary.afdelingspriser'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.gemmes'}.", "load_employees": "@.capitalize:{'terms.glossary.medarbejdere'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.indlæses'}.", "save_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.gemmes'}.", - "deactivate_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktiveres." + "deactivate_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktiveres.", + "generate_employee_login_link": "Medarbejderens loginlink kunne ikke oprettes." }, "role_permissions": { "title": "Rolletilladelser", diff --git a/src/i18n/source/de/phrases/index.json b/src/i18n/source/de/phrases/index.json index bff5f515..3d76479a 100644 --- a/src/i18n/source/de/phrases/index.json +++ b/src/i18n/source/de/phrases/index.json @@ -76,7 +76,13 @@ "active": "@:{'phrases.compat.limited_backoffice.employees.active'}", "inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -106,7 +112,8 @@ "save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}", @@ -321,7 +328,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@.capitalize:{'terms.glossary.bearbeiten'}", - "deactivate": "Deaktivieren" + "deactivate": "Deaktivieren", + "login_link": "QR-Login", + "login_link_title": "Mitarbeiter-Loginlink", + "login_link_qr_alt": "QR-Code-Loginlink", + "login_link_url": "Loginlink", + "copy_login_link": "Link kopieren", + "login_link_copied": "Link kopiert." }, "roles": { "viewer": { @@ -351,7 +364,8 @@ "save_prices": "@.capitalize:{'terms.glossary.abteilungspreise'} @:{'terms.glossary.konnten'} @:{'terms.glossary.nicht'} @:{'terms.glossary.gespeichert'} @:{'terms.glossary.werden'}.", "load_employees": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnten'} @:{'terms.glossary.nicht'} @:{'terms.glossary.geladen'} @:{'terms.glossary.werden'}.", "save_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.gespeichert'} @:{'terms.glossary.werden'}.", - "deactivate_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.deaktiviert'} @:{'terms.glossary.werden'}." + "deactivate_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.deaktiviert'} @:{'terms.glossary.werden'}.", + "generate_employee_login_link": "Mitarbeiter-Loginlink konnte nicht erstellt werden." }, "role_permissions": { "title": "Rollenberechtigungen", diff --git a/src/i18n/source/en/phrases/index.json b/src/i18n/source/en/phrases/index.json index 433d6edd..a69618bf 100644 --- a/src/i18n/source/en/phrases/index.json +++ b/src/i18n/source/en/phrases/index.json @@ -76,7 +76,13 @@ "active": "@:{'phrases.compat.limited_backoffice.employees.active'}", "inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -106,7 +112,8 @@ "save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}", @@ -321,7 +328,13 @@ "active": "Active", "inactive": "@.capitalize:{'terms.glossary.inactive'}", "edit": "@.capitalize:{'terms.glossary.edit'}", - "deactivate": "@.capitalize:{'terms.glossary.deactivate'}" + "deactivate": "@.capitalize:{'terms.glossary.deactivate'}", + "login_link": "QR login", + "login_link_title": "Employee login link", + "login_link_qr_alt": "QR code login link", + "login_link_url": "Login link", + "copy_login_link": "Copy link", + "login_link_copied": "Link copied." }, "roles": { "viewer": { @@ -351,7 +364,8 @@ "save_prices": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.save'} @:{'terms.glossary.department'} @:{'terms.glossary.prices'}.", "load_employees": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.load'} @:{'terms.glossary.employees'}.", "save_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.save'} @:{'terms.glossary.employee'}.", - "deactivate_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.deactivate'} @:{'terms.glossary.employee'}." + "deactivate_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.deactivate'} @:{'terms.glossary.employee'}.", + "generate_employee_login_link": "Could not create employee login link." }, "role_permissions": { "title": "Role permissions", diff --git a/src/i18n/source/no/phrases/index.json b/src/i18n/source/no/phrases/index.json index cacdeeb7..728812f0 100644 --- a/src/i18n/source/no/phrases/index.json +++ b/src/i18n/source/no/phrases/index.json @@ -76,7 +76,13 @@ "active": "@:{'phrases.compat.limited_backoffice.employees.active'}", "inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -106,7 +112,8 @@ "save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}", @@ -321,7 +328,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@:{'terms.glossary.rediger'}", - "deactivate": "Deaktiver" + "deactivate": "Deaktiver", + "login_link": "QR-innlogging", + "login_link_title": "Innloggingslenke for medarbeider", + "login_link_qr_alt": "QR-kode for innloggingslenke", + "login_link_url": "Innloggingslenke", + "copy_login_link": "Kopier lenke", + "login_link_copied": "Lenken er kopiert." }, "roles": { "viewer": { @@ -351,7 +364,8 @@ "save_prices": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.lagre'} @:{'terms.glossary.avdelingspriser'}.", "load_employees": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.laste'} @:{'terms.glossary.medarbeidere'}.", "save_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.lagre'} @:{'terms.glossary.medarbeideren'}.", - "deactivate_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktivere @:{'terms.glossary.medarbeideren'}." + "deactivate_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktivere @:{'terms.glossary.medarbeideren'}.", + "generate_employee_login_link": "Kunne ikke opprette innloggingslenke for medarbeideren." }, "role_permissions": { "title": "Rolletillatelser", diff --git a/src/i18n/source/sv/phrases/index.json b/src/i18n/source/sv/phrases/index.json index 87c61ad2..9bd94a57 100644 --- a/src/i18n/source/sv/phrases/index.json +++ b/src/i18n/source/sv/phrases/index.json @@ -76,7 +76,13 @@ "active": "@:{'phrases.compat.limited_backoffice.employees.active'}", "inactive": "@:{'phrases.compat.limited_backoffice.employees.inactive'}", "edit": "@:{'phrases.compat.limited_backoffice.employees.edit'}", - "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}" + "deactivate": "@:{'phrases.compat.limited_backoffice.employees.deactivate'}", + "login_link": "@:{'phrases.compat.limited_backoffice.employees.login_link'}", + "login_link_title": "@:{'phrases.compat.limited_backoffice.employees.login_link_title'}", + "login_link_qr_alt": "@:{'phrases.compat.limited_backoffice.employees.login_link_qr_alt'}", + "login_link_url": "@:{'phrases.compat.limited_backoffice.employees.login_link_url'}", + "copy_login_link": "@:{'phrases.compat.limited_backoffice.employees.copy_login_link'}", + "login_link_copied": "@:{'phrases.compat.limited_backoffice.employees.login_link_copied'}" }, "roles": { "viewer": { @@ -106,7 +112,8 @@ "save_prices": "@:{'phrases.compat.limited_backoffice.errors.save_prices'}", "load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}", "save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}", - "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}" + "deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}", + "generate_employee_login_link": "@:{'phrases.compat.limited_backoffice.errors.generate_employee_login_link'}" }, "role_permissions": { "title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}", @@ -321,7 +328,13 @@ "active": "Aktiv", "inactive": "Inaktiv", "edit": "@:{'terms.glossary.redigera'}", - "deactivate": "@.capitalize:{'terms.glossary.inaktivera'}" + "deactivate": "@.capitalize:{'terms.glossary.inaktivera'}", + "login_link": "QR-inloggning", + "login_link_title": "Inloggningslänk för medarbetare", + "login_link_qr_alt": "QR-kod för inloggningslänk", + "login_link_url": "Inloggningslänk", + "copy_login_link": "Kopiera länk", + "login_link_copied": "Länken har kopierats." }, "roles": { "viewer": { @@ -351,7 +364,8 @@ "save_prices": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.spara'} @:{'terms.glossary.avdelningspriser'}.", "load_employees": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.ladda'} @:{'terms.glossary.medarbetare'}.", "save_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.spara'} @:{'terms.glossary.medarbetaren'}.", - "deactivate_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.inaktivera'} @:{'terms.glossary.medarbetaren'}." + "deactivate_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.inaktivera'} @:{'terms.glossary.medarbetaren'}.", + "generate_employee_login_link": "Kunde inte skapa inloggningslänk för medarbetaren." }, "role_permissions": { "title": "Rollbehörigheter", diff --git a/src/services/limitedBackoffice.js b/src/services/limitedBackoffice.js index 9750d30c..f99ffcb7 100644 --- a/src/services/limitedBackoffice.js +++ b/src/services/limitedBackoffice.js @@ -24,6 +24,9 @@ export const createLimitedBackofficeEmployee = (payload) => export const updateLimitedBackofficeEmployee = (employeeId, payload) => limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}`, "PUT", payload); +export const generateLimitedBackofficeEmployeeLoginLink = (employeeId) => + limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}/login-link`, "POST"); + export const deactivateLimitedBackofficeEmployee = (employeeId) => limitedBackofficeRequest(`/employees/${encodeURIComponent(String(employeeId))}`, "DELETE"); diff --git a/src/views/backoffice/LimitedBackofficeEmployees.vue b/src/views/backoffice/LimitedBackofficeEmployees.vue index 681091f5..728a891c 100644 --- a/src/views/backoffice/LimitedBackofficeEmployees.vue +++ b/src/views/backoffice/LimitedBackofficeEmployees.vue @@ -7,6 +7,7 @@ import LimitedBackofficeLayout from "@/views/backoffice/components/LimitedBackof import { createLimitedBackofficeEmployee, deactivateLimitedBackofficeEmployee, + generateLimitedBackofficeEmployeeLoginLink, getLimitedBackofficeDepartments, getLimitedBackofficeEmployees, getLimitedBackofficeRoles, @@ -22,6 +23,14 @@ const PHONE_COUNTRY_CODES = [ { value: "47", flag: "🇳🇴", labelKey: "norway" }, { value: "358", flag: "🇫🇮", labelKey: "finland" }, ]; +let qrCodeModulePromise = null; + +const loadQRCodeModule = () => { + if (!qrCodeModulePromise) { + qrCodeModulePromise = import("qrcode"); + } + return qrCodeModulePromise; +}; const { t } = useI18n(); const departments = ref([]); @@ -35,6 +44,12 @@ const successMessage = ref(""); const editingEmployee = ref(null); const formAttempted = ref(false); const showRolePermissions = ref(false); +const loginLinkModalOpen = ref(false); +const loginLinkEmployee = ref(null); +const loginLinkUrl = ref(""); +const loginLinkQrSrc = ref(""); +const loginLinkGeneratingId = ref(null); +const loginLinkCopyMessage = ref(""); const form = ref({ display_name: "", email: "", @@ -255,6 +270,8 @@ const sanitizePhone = (event) => { form.value.phone = String(event?.target?.value || "").replace(/\D/g, ""); }; +const absoluteLoginLink = (loginPath) => new URL(String(loginPath || ""), window.location.origin).toString(); + const loadEmployees = async () => { const response = await getLimitedBackofficeEmployees({ includeInactive: includeInactive.value }); employees.value = unwrapLimitedBackofficeResponse(response) || []; @@ -396,6 +413,72 @@ const deactivateEmployee = async (employee) => { } }; +const showEmployeeLoginLink = async (employee) => { + successMessage.value = ""; + errorMessage.value = ""; + loginLinkCopyMessage.value = ""; + loginLinkGeneratingId.value = employee.id; + + try { + const response = await generateLimitedBackofficeEmployeeLoginLink(employee.id); + const payload = unwrapLimitedBackofficeResponse(response) || {}; + const loginPath = String(payload.login_path || ""); + if (!loginPath) { + throw new Error(t("templates.limited_backoffice.errors.generate_employee_login_link")); + } + + const loginUrl = absoluteLoginLink(loginPath); + const { toDataURL } = await loadQRCodeModule(); + + loginLinkEmployee.value = employee; + loginLinkUrl.value = loginUrl; + loginLinkQrSrc.value = await toDataURL(loginUrl, { margin: 2, width: 240 }); + loginLinkModalOpen.value = true; + } catch (error) { + errorMessage.value = limitedBackofficeErrorMessage( + error, + t("templates.limited_backoffice.errors.generate_employee_login_link") + ); + } finally { + loginLinkGeneratingId.value = null; + } +}; + +const copyText = async (text) => { + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text); + return; + } catch { + // Fall back for browser contexts where the Clipboard API is present but denied. + } + } + + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + textarea.remove(); +}; + +const copyEmployeeLoginLink = async () => { + if (!loginLinkUrl.value) { + return; + } + + await copyText(loginLinkUrl.value); + loginLinkCopyMessage.value = t("templates.limited_backoffice.employees.login_link_copied"); +}; + +const closeEmployeeLoginLink = () => { + loginLinkModalOpen.value = false; + loginLinkCopyMessage.value = ""; +}; + const reloadEmployees = async () => { loading.value = true; errorMessage.value = ""; @@ -697,6 +780,18 @@ onMounted(() => {
+
+ + + + @@ -882,7 +1048,24 @@ onMounted(() => { } .limited-employees-table__actions { - width: 190px; + width: 300px; +} + +.limited-login-link-modal { + width: 100%; + max-width: 420px; +} + +.limited-login-link-modal__qr { + display: flex; + justify-content: center; + margin-bottom: 1rem; +} + +.limited-login-link-modal__qr img { + width: 240px; + max-width: 100%; + height: auto; } .limited-role-permissions-modal { diff --git a/tests/e2e/limited-backoffice.spec.ts b/tests/e2e/limited-backoffice.spec.ts index 69c08cd7..3f90ab73 100644 --- a/tests/e2e/limited-backoffice.spec.ts +++ b/tests/e2e/limited-backoffice.spec.ts @@ -289,6 +289,7 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt const priceUpdateCalls: unknown[] = []; const employeeCreateCalls: unknown[] = []; const employeeUpdateCalls: unknown[] = []; + const loginLinkCalls: string[] = []; const currentPricePayload = { value: cloneJson(options.pricePayload ?? pricePayload), }; @@ -400,6 +401,27 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt return; } + const loginLinkMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)\/login-link$/); + if (loginLinkMatch && method === "POST") { + const employeeId = loginLinkMatch[1]; + const status = options.loginLinkStatus ?? 200; + loginLinkCalls.push(employeeId); + await route.fulfill( + json( + status === 200 + ? { + data: { + employee_id: Number(employeeId), + login_path: `/login/qr?token=${"a".repeat(64)}`, + }, + } + : { message: options.loginLinkMessage ?? "Could not create employee login link." }, + status + ) + ); + return; + } + const employeeMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)$/); if (employeeMatch && method === "PUT") { const body = request.postDataJSON?.() || {}; @@ -436,6 +458,7 @@ async function mockLimitedBackofficeApi(page, authSessionData = sessionData, opt priceUpdateCalls, employeeCreateCalls, employeeUpdateCalls, + loginLinkCalls, }; } @@ -710,6 +733,49 @@ test.describe("Limited backoffice", () => { expect(api.forbiddenCalls).toEqual([]); }); + test("generates an employee QR login link", async ({ page }, testInfo) => { + test.skip(!isDesktopProject(testInfo), "Desktop only"); + + await seedLimitedBackofficeSession(page); + const api = await mockLimitedBackofficeApi(page); + + await page.goto("/backoffice/employees"); + + await page.getByTestId("limited-employee-login-link-501").click(); + + const modal = page.getByTestId("limited-login-link-modal"); + await expect(modal).toBeVisible(); + await expect(modal).toContainText("Employee login link"); + await expect(modal).toContainText("Casey Clerk"); + await expect(page.getByTestId("limited-login-link-qr")).toHaveAttribute("src", /^data:image\/png;base64,/); + await expect(page.getByTestId("limited-login-link-url")).toHaveValue(/\/login\/qr\?token=a{64}$/); + + await page.getByTestId("limited-login-link-copy").click(); + + await expect(page.getByTestId("limited-login-link-copy-message")).toHaveText("Link copied."); + expect(api.loginLinkCalls).toEqual(["501"]); + expect(api.forbiddenCalls).toEqual([]); + }); + + test("shows an error when employee QR login link generation fails", async ({ page }, testInfo) => { + test.skip(!isDesktopProject(testInfo), "Desktop only"); + + await seedLimitedBackofficeSession(page); + const api = await mockLimitedBackofficeApi(page, sessionData, { + loginLinkMessage: "Missing employee management permission.", + loginLinkStatus: 403, + }); + + await page.goto("/backoffice/employees"); + + await page.getByTestId("limited-employee-login-link-501").click(); + + await expect(page.getByTestId("limited-employees-error")).toHaveText("Missing employee management permission."); + await expect(page.getByTestId("limited-login-link-modal")).toHaveCount(0); + expect(api.loginLinkCalls).toEqual(["501"]); + expect(api.forbiddenCalls).toEqual([]); + }); + test("shows grouped human-readable role permission help without raw permission keys", async ({ page }, testInfo) => { await seedLimitedBackofficeSession(page); await mockLimitedBackofficeApi(page);