diff --git a/src/components/menus/superuser/SuperUserLeftMenu.vue b/src/components/menus/superuser/SuperUserLeftMenu.vue index 16e949ff..eabfa08b 100644 --- a/src/components/menus/superuser/SuperUserLeftMenu.vue +++ b/src/components/menus/superuser/SuperUserLeftMenu.vue @@ -155,6 +155,13 @@ const menu_items = ref([ children: [], 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 + }, ], onSelect: async (value) => { SessionUser.functions.redirectTo.superUser(value); diff --git a/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue b/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue index 62e2eb5c..ef45446c 100644 --- a/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue +++ b/src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue @@ -44,6 +44,7 @@ const items = computed(() => [ { label: firstToUpperCase(SessionUser.superUser.modules.stripe.meta.labels.multiple), to: '/superuser/configuration/stripe'}, { label: firstToUpperCase(SessionUser.superUser.modules.fxratesapi.meta.labels.multiple), to: '/superuser/configuration/fxratesapi'}, { label: firstToUpperCase(SessionUser.superUser.modules.weatherapi.meta.labels.multiple), to: '/superuser/configuration/weatherapi'}, + { label: firstToUpperCase(SessionUser.superUser.modules.workfeed.meta.labels.multiple), to: '/superuser/configuration/workfeed'}, { label: firstToUpperCase(SessionUser.superUser.modules.gatewayapi.meta.labels.multiple), to: '/superuser/configuration/gatewayapi'}, { label: firstToUpperCase(SessionUser.superUser.modules.entra.meta.labels.multiple), to: '/superuser/configuration/entra'}, { label: firstToUpperCase(SessionUser.superUser.modules.limble.meta.labels.multiple), to: '/superuser/configuration/limble'}, diff --git a/src/components/session/token/superUser/modules/workfeed/Config.vue b/src/components/session/token/superUser/modules/workfeed/Config.vue new file mode 100644 index 00000000..5e86596d --- /dev/null +++ b/src/components/session/token/superUser/modules/workfeed/Config.vue @@ -0,0 +1,57 @@ + diff --git a/src/components/session/token/superUser/modules/workfeed/Workfeed.vue b/src/components/session/token/superUser/modules/workfeed/Workfeed.vue new file mode 100644 index 00000000..8e7ebf4c --- /dev/null +++ b/src/components/session/token/superUser/modules/workfeed/Workfeed.vue @@ -0,0 +1,99 @@ + diff --git a/src/components/session/token/superUserObject.vue b/src/components/session/token/superUserObject.vue index 640d5a6f..77268571 100644 --- a/src/components/session/token/superUserObject.vue +++ b/src/components/session/token/superUserObject.vue @@ -11,6 +11,7 @@ import { MotorAPI } from "@/components/session/token/superUser/modules/motorapi/ import { Stripe } from "@/components/session/token/superUser/modules/stripe/Stripe.vue"; import { FXRatesAPI } from "@/components/session/token/superUser/modules/fxratesapi/FXRatesAPI.vue"; import { WeatherAPI } from "@/components/session/token/superUser/modules/weatherapi/WeatherAPI.vue"; +import { Workfeed } from "@/components/session/token/superUser/modules/workfeed/Workfeed.vue"; import { GatewayAPI } from "@/components/session/token/superUser/modules/gatewayapi/GatewayAPI.vue"; import { XLVask } from "@/components/session/token/superUser/modules/xlvask/XLVask.vue"; import { Entra } from "@/components/session/token/superUser/modules/entra/Entra.vue"; @@ -41,6 +42,7 @@ export const SuperUserObject = { get stripe() { return Stripe; }, get fxratesapi() { return FXRatesAPI; }, get weatherapi() { return WeatherAPI; }, + get workfeed() { return Workfeed; }, get gatewayapi() { return GatewayAPI; }, get xlvask() { return XLVask; }, get entra() { return Entra; }, diff --git a/src/i18n/locales/da.json b/src/i18n/locales/da.json index 48e64e72..fc316a63 100644 --- a/src/i18n/locales/da.json +++ b/src/i18n/locales/da.json @@ -1149,6 +1149,46 @@ "title": "XLVask konfiguration", "username": "Brugernavn", "username_desc": "Brugernavnet bruges til at autentificere API forbindelsen." + }, + "workfeed": { + "title": "Workfeed configuration", + "subtitle": "Configuration of the Workfeed integration", + "general_settings": "General settings", + "general_settings_desc": "General settings for the Workfeed integration.", + "enable": "Enable Workfeed", + "enable_desc": "Enable or disable the Workfeed integration.", + "api_settings": "API connection settings", + "api_settings_desc": "The API connection settings used for Workfeed requests.", + "api_url": "API URL", + "api_url_desc": "The base URL for the Workfeed API.", + "api_key": "API key", + "api_key_desc": "The API key used to authenticate requests to Workfeed.", + "close": "Close", + "lookup": "Lookup", + "validation_required": "This field is required.", + "request_failed": "Request failed: {message}", + "diagnostics": { + "title": "Diagnostics", + "description": "Run test lookups against the Workfeed integration endpoints.", + "list_departments": "List departments", + "list_employees": "List employees", + "list_shifts": "List shifts", + "get_employee": "Get employee by ID", + "get_shift": "Get shift by ID", + "prompt_search_optional": "Optional search text", + "prompt_status_optional": "Optional shift status", + "prompt_employee_id": "Employee ID", + "prompt_shift_id": "Shift ID", + "placeholder_search": "e.g. north facility", + "placeholder_status": "e.g. published", + "placeholder_employee_id": "e.g. emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + "placeholder_shift_id": "e.g. shf_01J5P3X9D35R9C6BDZ1S0R4V6Q", + "result_departments": "Department lookup result", + "result_employees": "Employee lookup result", + "result_shifts": "Shift lookup result", + "result_employee": "Employee result", + "result_shift": "Shift result" + } } }, "customer_creation": { @@ -4279,4 +4319,3 @@ } } } - diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index cd364da3..c512dfa0 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -1149,6 +1149,46 @@ "title": "XLVask-Konfiguration", "username": "Benutzername", "username_desc": "Der Benutzername wird zur Authentifizierung der API-Verbindung verwendet." + }, + "workfeed": { + "title": "Workfeed configuration", + "subtitle": "Configuration of the Workfeed integration", + "general_settings": "General settings", + "general_settings_desc": "General settings for the Workfeed integration.", + "enable": "Enable Workfeed", + "enable_desc": "Enable or disable the Workfeed integration.", + "api_settings": "API connection settings", + "api_settings_desc": "The API connection settings used for Workfeed requests.", + "api_url": "API URL", + "api_url_desc": "The base URL for the Workfeed API.", + "api_key": "API key", + "api_key_desc": "The API key used to authenticate requests to Workfeed.", + "close": "Close", + "lookup": "Lookup", + "validation_required": "This field is required.", + "request_failed": "Request failed: {message}", + "diagnostics": { + "title": "Diagnostics", + "description": "Run test lookups against the Workfeed integration endpoints.", + "list_departments": "List departments", + "list_employees": "List employees", + "list_shifts": "List shifts", + "get_employee": "Get employee by ID", + "get_shift": "Get shift by ID", + "prompt_search_optional": "Optional search text", + "prompt_status_optional": "Optional shift status", + "prompt_employee_id": "Employee ID", + "prompt_shift_id": "Shift ID", + "placeholder_search": "e.g. north facility", + "placeholder_status": "e.g. published", + "placeholder_employee_id": "e.g. emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + "placeholder_shift_id": "e.g. shf_01J5P3X9D35R9C6BDZ1S0R4V6Q", + "result_departments": "Department lookup result", + "result_employees": "Employee lookup result", + "result_shifts": "Shift lookup result", + "result_employee": "Employee result", + "result_shift": "Shift result" + } } }, "customer_creation": { @@ -4277,4 +4317,3 @@ } } } - diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 08d51c8b..35da69e5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1149,6 +1149,46 @@ "title": "XLVask configuration", "username": "Username", "username_desc": "The username is used to authenticate the API connection." + }, + "workfeed": { + "title": "Workfeed configuration", + "subtitle": "Configuration of the Workfeed integration", + "general_settings": "General settings", + "general_settings_desc": "General settings for the Workfeed integration.", + "enable": "Enable Workfeed", + "enable_desc": "Enable or disable the Workfeed integration.", + "api_settings": "API connection settings", + "api_settings_desc": "The API connection settings used for Workfeed requests.", + "api_url": "API URL", + "api_url_desc": "The base URL for the Workfeed API.", + "api_key": "API key", + "api_key_desc": "The API key used to authenticate requests to Workfeed.", + "close": "Close", + "lookup": "Lookup", + "validation_required": "This field is required.", + "request_failed": "Request failed: {message}", + "diagnostics": { + "title": "Diagnostics", + "description": "Run test lookups against the Workfeed integration endpoints.", + "list_departments": "List departments", + "list_employees": "List employees", + "list_shifts": "List shifts", + "get_employee": "Get employee by ID", + "get_shift": "Get shift by ID", + "prompt_search_optional": "Optional search text", + "prompt_status_optional": "Optional shift status", + "prompt_employee_id": "Employee ID", + "prompt_shift_id": "Shift ID", + "placeholder_search": "e.g. north facility", + "placeholder_status": "e.g. published", + "placeholder_employee_id": "e.g. emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + "placeholder_shift_id": "e.g. shf_01J5P3X9D35R9C6BDZ1S0R4V6Q", + "result_departments": "Department lookup result", + "result_employees": "Employee lookup result", + "result_shifts": "Shift lookup result", + "result_employee": "Employee result", + "result_shift": "Shift result" + } } }, "customer_creation": { diff --git a/src/i18n/locales/no.json b/src/i18n/locales/no.json index 7577266e..f4399dc7 100644 --- a/src/i18n/locales/no.json +++ b/src/i18n/locales/no.json @@ -1144,6 +1144,46 @@ "title": "XLVask-konfigurasjon", "username": "Brukernavn", "username_desc": "Brukernavnet brukes til å autentisere API-tilkoblingen." + }, + "workfeed": { + "title": "Workfeed configuration", + "subtitle": "Configuration of the Workfeed integration", + "general_settings": "General settings", + "general_settings_desc": "General settings for the Workfeed integration.", + "enable": "Enable Workfeed", + "enable_desc": "Enable or disable the Workfeed integration.", + "api_settings": "API connection settings", + "api_settings_desc": "The API connection settings used for Workfeed requests.", + "api_url": "API URL", + "api_url_desc": "The base URL for the Workfeed API.", + "api_key": "API key", + "api_key_desc": "The API key used to authenticate requests to Workfeed.", + "close": "Close", + "lookup": "Lookup", + "validation_required": "This field is required.", + "request_failed": "Request failed: {message}", + "diagnostics": { + "title": "Diagnostics", + "description": "Run test lookups against the Workfeed integration endpoints.", + "list_departments": "List departments", + "list_employees": "List employees", + "list_shifts": "List shifts", + "get_employee": "Get employee by ID", + "get_shift": "Get shift by ID", + "prompt_search_optional": "Optional search text", + "prompt_status_optional": "Optional shift status", + "prompt_employee_id": "Employee ID", + "prompt_shift_id": "Shift ID", + "placeholder_search": "e.g. north facility", + "placeholder_status": "e.g. published", + "placeholder_employee_id": "e.g. emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + "placeholder_shift_id": "e.g. shf_01J5P3X9D35R9C6BDZ1S0R4V6Q", + "result_departments": "Department lookup result", + "result_employees": "Employee lookup result", + "result_shifts": "Shift lookup result", + "result_employee": "Employee result", + "result_shift": "Shift result" + } } }, "customer_creation": { @@ -4227,4 +4267,3 @@ } } } - diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index 539adc21..940db41f 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -1144,6 +1144,46 @@ "title": "XLVask konfiguration", "username": "Användarnamn", "username_desc": "Användarnamnet används för att autentisera API-anslutningen." + }, + "workfeed": { + "title": "Workfeed configuration", + "subtitle": "Configuration of the Workfeed integration", + "general_settings": "General settings", + "general_settings_desc": "General settings for the Workfeed integration.", + "enable": "Enable Workfeed", + "enable_desc": "Enable or disable the Workfeed integration.", + "api_settings": "API connection settings", + "api_settings_desc": "The API connection settings used for Workfeed requests.", + "api_url": "API URL", + "api_url_desc": "The base URL for the Workfeed API.", + "api_key": "API key", + "api_key_desc": "The API key used to authenticate requests to Workfeed.", + "close": "Close", + "lookup": "Lookup", + "validation_required": "This field is required.", + "request_failed": "Request failed: {message}", + "diagnostics": { + "title": "Diagnostics", + "description": "Run test lookups against the Workfeed integration endpoints.", + "list_departments": "List departments", + "list_employees": "List employees", + "list_shifts": "List shifts", + "get_employee": "Get employee by ID", + "get_shift": "Get shift by ID", + "prompt_search_optional": "Optional search text", + "prompt_status_optional": "Optional shift status", + "prompt_employee_id": "Employee ID", + "prompt_shift_id": "Shift ID", + "placeholder_search": "e.g. north facility", + "placeholder_status": "e.g. published", + "placeholder_employee_id": "e.g. emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + "placeholder_shift_id": "e.g. shf_01J5P3X9D35R9C6BDZ1S0R4V6Q", + "result_departments": "Department lookup result", + "result_employees": "Employee lookup result", + "result_shifts": "Shift lookup result", + "result_employee": "Employee result", + "result_shift": "Shift result" + } } }, "customer_creation": { @@ -4227,4 +4267,3 @@ } } } - diff --git a/src/router.js b/src/router.js index 5899f0d3..e7a65eb2 100644 --- a/src/router.js +++ b/src/router.js @@ -95,6 +95,7 @@ import GuestBookExteriorWash from "@/views/guest/book/GuestBookExteriorWash.vue" import DepartmentProfile from "@/views/dashboards/superUserDashboard/department/DepartmentProfile.vue"; import ConfigurationFXRatesAPI from "@/views/dashboards/superUserDashboard/configuration/ConfigurationFXRatesAPI.vue"; import ConfigurationWeatherAPI from "@/views/dashboards/superUserDashboard/configuration/ConfigurationWeatherAPI.vue"; +import ConfigurationWorkfeed from "@/views/dashboards/superUserDashboard/configuration/ConfigurationWorkfeed.vue"; import ConfigurationGatewayAPI from "@/views/dashboards/superUserDashboard/configuration/ConfigurationGatewayAPI.vue"; import UserWashSubscriptions from "@/views/dashboards/superUserDashboard/user/UserWashSubscriptions.vue"; import UserProfile from "@/views/dashboards/userDashboard/profile/UserProfile.vue"; @@ -816,6 +817,12 @@ export const router = createRouter({ component: ConfigurationWeatherAPI, meta: { middleware: superUserMiddleware } }, + { + name: 'configurationWorkfeed', + path: '/superuser/configuration/workfeed', + component: ConfigurationWorkfeed, + meta: { middleware: superUserMiddleware } + }, { name: 'configurationGatewayAPI', path: '/superuser/configuration/gatewayapi', diff --git a/src/views/dashboards/superUserDashboard/configuration/ConfigurationWorkfeed.vue b/src/views/dashboards/superUserDashboard/configuration/ConfigurationWorkfeed.vue new file mode 100644 index 00000000..8caef1b9 --- /dev/null +++ b/src/views/dashboards/superUserDashboard/configuration/ConfigurationWorkfeed.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/src/views/dashboards/superUserDashboard/configuration/SuperUserDashboardConfigurationNavigation.vue b/src/views/dashboards/superUserDashboard/configuration/SuperUserDashboardConfigurationNavigation.vue index 77960df1..75933051 100644 --- a/src/views/dashboards/superUserDashboard/configuration/SuperUserDashboardConfigurationNavigation.vue +++ b/src/views/dashboards/superUserDashboard/configuration/SuperUserDashboardConfigurationNavigation.vue @@ -16,6 +16,7 @@ const tabs = [ { name: 'Stripe', path: '/superuser/configuration/stripe' }, { name: 'FXRatesAPI', path: '/superuser/configuration/fxratesapi' }, { name: 'WeatherAPI', path: '/superuser/configuration/weatherapi' }, + { name: 'Workfeed', path: '/superuser/configuration/workfeed' }, { name: 'GatewayAPI', path: '/superuser/configuration/gatewayapi' }, { name: 'XLVask', path: '/superuser/configuration/xlvask' }, { name: 'Entra', path: '/superuser/configuration/entra'}, diff --git a/tests/e2e/support/network.js b/tests/e2e/support/network.js index a16fb2be..203a9c6c 100644 --- a/tests/e2e/support/network.js +++ b/tests/e2e/support/network.js @@ -353,6 +353,150 @@ export async function mockApi(page, options = {}) { return; } + if (pathname.endsWith("/workfeed/config")) { + const workfeedConfig = [ + { module: "workfeed", variable: "enabled", type: "bool", value: true }, + { module: "workfeed", variable: "api_url", type: "string", value: "https://api.workfeed.test" }, + { module: "workfeed", variable: "api_key", type: "string", value: "test-api-key" }, + ]; + + if (method === "GET") { + const variable = parsedUrl.searchParams.get("variable"); + if (variable) { + const match = workfeedConfig.find((entry) => entry.variable === variable); + await route.fulfill(json({ data: match ? match.value : null })); + return; + } + await route.fulfill(json({ data: workfeedConfig })); + return; + } + + if (method === "POST") { + await route.fulfill(json({ data: { updated: true } })); + return; + } + } + + if (pathname.endsWith("/modules/workfeed/departments") && method === "GET") { + const search = (parsedUrl.searchParams.get("search") || "").toLowerCase(); + const departments = [ + { + id: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3", + name: "North Facility", + timezone: "Europe/Copenhagen", + active: true, + }, + { + id: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N4", + name: "South Facility", + timezone: "Europe/Copenhagen", + active: true, + }, + ]; + const filtered = search + ? departments.filter((entry) => entry.name.toLowerCase().includes(search)) + : departments; + + await route.fulfill(json({ + data: { + items: filtered, + pagination: { cursor: null, nextCursor: null, limit: 20, total: filtered.length }, + }, + })); + return; + } + + if (pathname.endsWith("/modules/workfeed/employees") && method === "GET") { + const search = (parsedUrl.searchParams.get("search") || "").toLowerCase(); + const employees = [ + { + id: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + fullName: "Anne Nielsen", + email: "anne.nielsen@example.com", + departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3", + active: true, + }, + { + id: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5Q", + fullName: "Mads Jensen", + email: "mads.jensen@example.com", + departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N4", + active: true, + }, + ]; + const filtered = search + ? employees.filter((entry) => entry.fullName.toLowerCase().includes(search)) + : employees; + + await route.fulfill(json({ + data: { + items: filtered, + pagination: { cursor: null, nextCursor: null, limit: 20, total: filtered.length }, + }, + })); + return; + } + + if (/\/modules\/workfeed\/employees\/[^/]+$/i.test(pathname) && method === "GET") { + const id = pathname.split("/").pop(); + await route.fulfill(json({ + data: { + id, + fullName: "Anne Nielsen", + email: "anne.nielsen@example.com", + departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3", + active: true, + }, + })); + return; + } + + if (pathname.endsWith("/modules/workfeed/shifts") && method === "GET") { + const status = (parsedUrl.searchParams.get("status") || "").toLowerCase(); + const shifts = [ + { + id: "shf_01J5P3X9D35R9C6BDZ1S0R4V6Q", + title: "Morning Shift", + status: "published", + employeeId: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + startAt: "2026-03-24T06:00:00Z", + endAt: "2026-03-24T14:00:00Z", + }, + { + id: "shf_01J5P3X9D35R9C6BDZ1S0R4V6R", + title: "Evening Shift", + status: "draft", + employeeId: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5Q", + startAt: "2026-03-24T14:00:00Z", + endAt: "2026-03-24T22:00:00Z", + }, + ]; + const filtered = status ? shifts.filter((entry) => entry.status.toLowerCase() === status) : shifts; + + await route.fulfill(json({ + data: { + items: filtered, + pagination: { cursor: null, nextCursor: null, limit: 20, total: filtered.length }, + }, + })); + return; + } + + if (/\/modules\/workfeed\/shifts\/[^/]+$/i.test(pathname) && method === "GET") { + const id = pathname.split("/").pop(); + await route.fulfill(json({ + data: { + id, + title: "Morning Shift", + status: "published", + employeeId: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P", + startAt: "2026-03-24T06:00:00Z", + endAt: "2026-03-24T14:00:00Z", + }, + })); + return; + } + if (selfServe) { if (pathname.endsWith("/guest/departments") && method === "GET") { await route.fulfill(json({ data: selfServe.departments })); diff --git a/tests/e2e/workfeed-config.smoke.spec.js b/tests/e2e/workfeed-config.smoke.spec.js new file mode 100644 index 00000000..1ce9701d --- /dev/null +++ b/tests/e2e/workfeed-config.smoke.spec.js @@ -0,0 +1,82 @@ +import { expect, test } from "@playwright/test"; +import { mockApi, seedAuthenticatedState } from "./support/network.js"; + +async function primeSuperuserSession(page) { + const token = "superuser-workfeed-token"; + await seedAuthenticatedState(page, token); + await page.goto("/login"); + await page.evaluate(async (sessionToken) => { + const sessionModule = await import("/src/components/session/token/SessionUser.vue"); + window.localStorage.setItem("token", sessionToken); + sessionModule.SessionUser.token.value = sessionToken; + sessionModule.SessionUser.authenticated.value = true; + sessionModule.SessionUser.permissions.value = ["superuser", "user"]; + sessionModule.SessionUser.initiated.value = true; + }, token); +} + +async function runDiagnostic(page, testId, inputValue = "") { + await page.locator(`[data-testid="${testId}"]:visible`).first().click(); + await expect(page.locator(".swal2-popup")).toBeVisible(); + const input = page.locator(".swal2-input"); + if (await input.isVisible()) { + await input.fill(inputValue); + } + await page.locator(".swal2-confirm").click(); + await expect(page.locator(".swal2-popup pre")).toBeVisible(); +} + +async function closeResultModal(page) { + await page.locator(".swal2-confirm").click(); + await expect(page.locator(".swal2-popup")).toBeHidden(); +} + +async function expandCategory(page, title) { + await page.locator(".card-header.is-clickable:visible").filter({ hasText: title }).first().click(); +} + +test.describe("Workfeed configuration smoke", () => { + test.beforeEach(async ({ page }) => { + await mockApi(page, { + authenticated: true, + permissions: ["superuser", "user"], + }); + await primeSuperuserSession(page); + }); + + test("@smoke renders workfeed config page and runs diagnostics", async ({ page }) => { + await page.goto("/superuser/configuration/workfeed"); + + await expect(page).toHaveURL(/\/superuser\/configuration\/workfeed$/); + await expect(page.locator("body")).toContainText("Workfeed configuration"); + await expandCategory(page, "General settings"); + await expect(page.locator("body")).toContainText("Enable Workfeed"); + + await expandCategory(page, "Diagnostics"); + await expect(page.locator('[data-testid="workfeed-diagnostics-list-departments"]:visible').first()).toBeVisible(); + await expect(page.locator('[data-testid="workfeed-diagnostics-list-employees"]:visible').first()).toBeVisible(); + await expect(page.locator('[data-testid="workfeed-diagnostics-list-shifts"]:visible').first()).toBeVisible(); + await expect(page.locator('[data-testid="workfeed-diagnostics-get-employee"]:visible').first()).toBeVisible(); + await expect(page.locator('[data-testid="workfeed-diagnostics-get-shift"]:visible').first()).toBeVisible(); + + await runDiagnostic(page, "workfeed-diagnostics-list-departments"); + await expect(page.locator(".swal2-popup pre")).toContainText("North Facility"); + await closeResultModal(page); + + await runDiagnostic(page, "workfeed-diagnostics-list-employees", "anne"); + await expect(page.locator(".swal2-popup pre")).toContainText("Anne Nielsen"); + await closeResultModal(page); + + await runDiagnostic(page, "workfeed-diagnostics-list-shifts", "published"); + await expect(page.locator(".swal2-popup pre")).toContainText("Morning Shift"); + await closeResultModal(page); + + await runDiagnostic(page, "workfeed-diagnostics-get-employee", "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P"); + await expect(page.locator(".swal2-popup pre")).toContainText("Anne Nielsen"); + await closeResultModal(page); + + await runDiagnostic(page, "workfeed-diagnostics-get-shift", "shf_01J5P3X9D35R9C6BDZ1S0R4V6Q"); + await expect(page.locator(".swal2-popup pre")).toContainText("Morning Shift"); + await closeResultModal(page); + }); +}); diff --git a/tests/unit/workfeed-i18n.spec.js b/tests/unit/workfeed-i18n.spec.js new file mode 100644 index 00000000..65d46d34 --- /dev/null +++ b/tests/unit/workfeed-i18n.spec.js @@ -0,0 +1,80 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const root = process.cwd(); + +const locales = { + da: JSON.parse(readFileSync(join(root, "src/i18n/locales/da.json"), "utf8")), + en: JSON.parse(readFileSync(join(root, "src/i18n/locales/en.json"), "utf8")), + sv: JSON.parse(readFileSync(join(root, "src/i18n/locales/sv.json"), "utf8")), + de: JSON.parse(readFileSync(join(root, "src/i18n/locales/de.json"), "utf8")), + no: JSON.parse(readFileSync(join(root, "src/i18n/locales/no.json"), "utf8")), +}; + +const flattenKeys = (value, prefix = "") => { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return [prefix]; + } + + const keys = []; + for (const [key, nestedValue] of Object.entries(value)) { + const nestedPrefix = prefix ? `${prefix}.${key}` : key; + keys.push(...flattenKeys(nestedValue, nestedPrefix)); + } + return keys; +}; + +describe("workfeed i18n coverage", () => { + it("defines configuration.workfeed namespace in every active locale", () => { + for (const [locale, data] of Object.entries(locales)) { + expect(data.configuration?.workfeed, `missing configuration.workfeed in ${locale}`).toBeTruthy(); + } + }); + + it("keeps the same workfeed key set across locales", () => { + const baselineKeys = flattenKeys(locales.en.configuration.workfeed).sort(); + + for (const [locale, data] of Object.entries(locales)) { + const localeKeys = flattenKeys(data.configuration.workfeed).sort(); + expect(localeKeys, `mismatched key set in ${locale}`).toEqual(baselineKeys); + } + }); + + it("contains required keys used by configuration view", () => { + const requiredKeys = [ + "title", + "subtitle", + "general_settings", + "enable", + "api_settings", + "api_url", + "api_key", + "close", + "lookup", + "validation_required", + "request_failed", + "diagnostics.title", + "diagnostics.description", + "diagnostics.list_departments", + "diagnostics.list_employees", + "diagnostics.list_shifts", + "diagnostics.get_employee", + "diagnostics.get_shift", + "diagnostics.prompt_search_optional", + "diagnostics.prompt_status_optional", + "diagnostics.prompt_employee_id", + "diagnostics.prompt_shift_id", + "diagnostics.result_departments", + "diagnostics.result_employees", + "diagnostics.result_shifts", + "diagnostics.result_employee", + "diagnostics.result_shift", + ]; + + const availableKeys = new Set(flattenKeys(locales.en.configuration.workfeed)); + requiredKeys.forEach((key) => { + expect(availableKeys.has(key)).toBe(true); + }); + }); +}); diff --git a/tests/unit/workfeed-module.spec.js b/tests/unit/workfeed-module.spec.js new file mode 100644 index 00000000..8ceb139a --- /dev/null +++ b/tests/unit/workfeed-module.spec.js @@ -0,0 +1,94 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const root = process.cwd(); +const workfeedModuleSource = readFileSync( + join(root, "src/components/session/token/superUser/modules/workfeed/Workfeed.vue"), + "utf8" +); +const workfeedConfigSource = readFileSync( + join(root, "src/components/session/token/superUser/modules/workfeed/Config.vue"), + "utf8" +); +const workfeedPageSource = readFileSync( + join(root, "src/views/dashboards/superUserDashboard/configuration/ConfigurationWorkfeed.vue"), + "utf8" +); + +describe("workfeed module contract", () => { + it("defines module metadata and config endpoint", () => { + expect(workfeedModuleSource).toContain('title: "Workfeed"'); + expect(workfeedModuleSource).toContain('endpoint: "/modules/workfeed"'); + expect(workfeedModuleSource).toContain('config_endpoint: "/configuration/workfeed"'); + }); + + it("defines list and get endpoint wrappers for employees, shifts, and departments", () => { + expect(workfeedModuleSource).toContain('"/modules/workfeed/employees"'); + expect(workfeedModuleSource).toContain('"/modules/workfeed/employees/" + encodeURIComponent(id)'); + expect(workfeedModuleSource).toContain('"/modules/workfeed/shifts"'); + expect(workfeedModuleSource).toContain('"/modules/workfeed/shifts/" + encodeURIComponent(id)'); + expect(workfeedModuleSource).toContain('"/modules/workfeed/departments"'); + }); + + it("supports all openapi filter parameters for list endpoints", () => { + expect(workfeedModuleSource).toContain("search"); + expect(workfeedModuleSource).toContain("departmentId"); + expect(workfeedModuleSource).toContain("includeInactive"); + expect(workfeedModuleSource).toContain("employeeId"); + expect(workfeedModuleSource).toContain("status"); + expect(workfeedModuleSource).toContain("from"); + expect(workfeedModuleSource).toContain("to"); + expect(workfeedModuleSource).toContain("limit"); + expect(workfeedModuleSource).toContain("cursor"); + expect(workfeedModuleSource).toContain("sanitizeQuery"); + }); +}); + +describe("workfeed config contract", () => { + it("uses workfeed config endpoints", () => { + expect(workfeedConfigSource).toContain('"/workfeed/config?variable=" + variable'); + expect(workfeedConfigSource).toContain('"/workfeed/config"'); + }); + + it("supports openapi config variables", () => { + expect(workfeedConfigSource).toContain('Config.get("enabled")'); + expect(workfeedConfigSource).toContain('Config.get("api_url")'); + expect(workfeedConfigSource).toContain('Config.get("api_key")'); + expect(workfeedConfigSource).toContain('Config.set("enabled", enabled)'); + expect(workfeedConfigSource).toContain('Config.set("api_url", value)'); + expect(workfeedConfigSource).toContain('Config.set("api_key", value)'); + }); +}); + +describe("workfeed config page contract", () => { + it("uses i18n keys and module bindings for controls", () => { + expect(workfeedPageSource).toContain("$t('configuration.workfeed.title')"); + expect(workfeedPageSource).toContain("$t('configuration.workfeed.subtitle')"); + expect(workfeedPageSource).toContain("$t('configuration.workfeed.enable')"); + expect(workfeedPageSource).toContain("$t('configuration.workfeed.api_url')"); + expect(workfeedPageSource).toContain("$t('configuration.workfeed.api_key')"); + expect(workfeedPageSource).toContain( + ":on-switch=\"SessionUser.superUser.modules.workfeed.config.enabled.set\"" + ); + expect(workfeedPageSource).toContain( + ":on-save=\"SessionUser.superUser.modules.workfeed.config.keys.api_url.set\"" + ); + expect(workfeedPageSource).toContain( + ":on-save=\"SessionUser.superUser.modules.workfeed.config.keys.api_key.set\"" + ); + }); + + it("exposes modal diagnostic actions for each workfeed endpoint family", () => { + expect(workfeedPageSource).toContain("onClickListDepartments"); + expect(workfeedPageSource).toContain("onClickListEmployees"); + expect(workfeedPageSource).toContain("onClickListShifts"); + expect(workfeedPageSource).toContain("onClickGetEmployee"); + expect(workfeedPageSource).toContain("onClickGetShift"); + expect(workfeedPageSource).toContain("workfeed-diagnostics-list-departments"); + expect(workfeedPageSource).toContain("workfeed-diagnostics-list-employees"); + expect(workfeedPageSource).toContain("workfeed-diagnostics-list-shifts"); + expect(workfeedPageSource).toContain("workfeed-diagnostics-get-employee"); + expect(workfeedPageSource).toContain("workfeed-diagnostics-get-shift"); + }); +}); diff --git a/tests/unit/workfeed-wiring.spec.js b/tests/unit/workfeed-wiring.spec.js new file mode 100644 index 00000000..0e194feb --- /dev/null +++ b/tests/unit/workfeed-wiring.spec.js @@ -0,0 +1,57 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const root = process.cwd(); +const superUserObjectSource = readFileSync( + join(root, "src/components/session/token/superUserObject.vue"), + "utf8" +); +const routerSource = readFileSync(join(root, "src/router.js"), "utf8"); +const navItemsSource = readFileSync( + join(root, "src/components/models/navigation/items/NavigationMenuItemsSuperUser.vue"), + "utf8" +); +const leftMenuSource = readFileSync( + join(root, "src/components/menus/superuser/SuperUserLeftMenu.vue"), + "utf8" +); +const configTabsSource = readFileSync( + join( + root, + "src/views/dashboards/superUserDashboard/configuration/SuperUserDashboardConfigurationNavigation.vue" + ), + "utf8" +); + +describe("workfeed superuser wiring", () => { + it("is registered in SessionUser superuser modules", () => { + expect(superUserObjectSource).toContain( + 'import { Workfeed } from "@/components/session/token/superUser/modules/workfeed/Workfeed.vue";' + ); + expect(superUserObjectSource).toContain("get workfeed() { return Workfeed; },"); + }); + + it("has a dedicated superuser configuration route", () => { + expect(routerSource).toContain( + 'import ConfigurationWorkfeed from "@/views/dashboards/superUserDashboard/configuration/ConfigurationWorkfeed.vue";' + ); + expect(routerSource).toContain("name: 'configurationWorkfeed'"); + expect(routerSource).toContain("path: '/superuser/configuration/workfeed'"); + expect(routerSource).toContain("component: ConfigurationWorkfeed"); + }); + + it("appears in superuser configuration navigation menus", () => { + expect(navItemsSource).toContain( + "SessionUser.superUser.modules.workfeed.meta.labels.multiple" + ); + expect(navItemsSource).toContain("to: '/superuser/configuration/workfeed'"); + + expect(leftMenuSource).toContain("SessionUser.superUser.modules.workfeed.meta.title"); + expect(leftMenuSource).toContain( + "SessionUser.superUser.modules.workfeed.meta.config_endpoint" + ); + + expect(configTabsSource).toContain("{ name: 'Workfeed', path: '/superuser/configuration/workfeed' }"); + }); +});