diff --git a/scripts/run-playwright-full-slice.mjs b/scripts/run-playwright-full-slice.mjs
index 023ef7e0..8531b22d 100644
--- a/scripts/run-playwright-full-slice.mjs
+++ b/scripts/run-playwright-full-slice.mjs
@@ -101,6 +101,7 @@ export const ownedFilesByRole = {
"superuser-customer-complaints.spec.ts",
"superuser-customers-mass-import.spec.ts",
"superuser-department-branding.spec.js",
+ "superuser-department-overview.spec.js",
"superuser-department-gates.spec.ts",
"superuser-department-lanes.spec.ts",
"superuser-departments-archive.spec.ts",
diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json
index c9bb93c9..91e14c73 100644
--- a/src/i18n/generated/global-v2.json
+++ b/src/i18n/generated/global-v2.json
@@ -4559,6 +4559,82 @@
"subtitle": "@:{'templates.generated.compat.superuser_dashboard.departments.subtitle'}",
"title": "@:common.departments"
},
+ "department_navigation": {
+ "overview": "Overview",
+ "modules": "Modules",
+ "branding": "Profile & Branding",
+ "gateways": "Gateways",
+ "stripe": "Stripe",
+ "pricing": "Pricing",
+ "categories": "Categories"
+ },
+ "department_overview": {
+ "title": "Department overview",
+ "subtitle": "Operational overview for the selected department",
+ "loading": "Loading department overview",
+ "date_from": "From",
+ "date_to": "To",
+ "range_label": "{from} to {to}",
+ "empty_value": "-",
+ "out_of": "of {total}",
+ "presets": {
+ "today": "Today",
+ "last_seven_days": "Last 7 days"
+ },
+ "metrics": {
+ "bookings": "Bookings",
+ "complaints": "Complaints",
+ "night_washes": "Night washes",
+ "overtime": "Overtime",
+ "products_sold": "Products sold",
+ "revenue": "Revenue",
+ "transactions": "Transactions",
+ "washes": "Washes",
+ "water_usage": "Water"
+ },
+ "units": {
+ "hours": "h",
+ "liters": "L"
+ },
+ "products": {
+ "title": "Product mix",
+ "subtitle": "Tracked wash products in the selected period",
+ "empty": "No product activity for the selected period"
+ },
+ "profile": {
+ "title": "Department profile",
+ "no_description": "No department description",
+ "department_id": "Department ID",
+ "economic_department_id": "Economic department",
+ "branding": "Branding",
+ "created_at": "Created",
+ "updated_at": "Updated"
+ },
+ "hardware": {
+ "title": "Hardware readiness",
+ "subtitle": "Gateway-backed lane and relay state",
+ "gateways": "Gateways online",
+ "lanes": "Lanes",
+ "gates": "Gates",
+ "relays": "Relays",
+ "scanners": "Scanners",
+ "issues": "Issues"
+ },
+ "quick_links": {
+ "title": "Department tools",
+ "subtitle": "Open the focused setup areas for this department",
+ "modules": "Modules",
+ "branding": "Branding",
+ "gateways": "Gateways",
+ "stripe": "Stripe",
+ "pricing": "Pricing",
+ "categories": "Categories"
+ },
+ "errors": {
+ "invalid_department": "A valid department is required",
+ "load": "Unable to load the department overview"
+ }
+ },
"employees": {
"edit_employee_title": "@:employees.edit_employee",
"new_employee_subtitle": "@:{'templates.generated.compat.superuser_dashboard.employees.new_employee_subtitle'}",
diff --git a/src/i18n/source/global/shared/superuser_dashboard/index.json b/src/i18n/source/global/shared/superuser_dashboard/index.json
index 8c33a175..a2059492 100644
--- a/src/i18n/source/global/shared/superuser_dashboard/index.json
+++ b/src/i18n/source/global/shared/superuser_dashboard/index.json
@@ -40,6 +40,82 @@
"subtitle": "@:{'phrases.compat.superuser_dashboard.departments.subtitle'}",
"title": "@:common.departments"
},
+ "department_navigation": {
+ "overview": "Overview",
+ "modules": "Modules",
+ "branding": "Profile & Branding",
+ "gateways": "Gateways",
+ "stripe": "Stripe",
+ "pricing": "Pricing",
+ "categories": "Categories"
+ },
+ "department_overview": {
+ "title": "Department overview",
+ "subtitle": "Operational overview for the selected department",
+ "loading": "Loading department overview",
+ "date_from": "From",
+ "date_to": "To",
+ "range_label": "{from} to {to}",
+ "empty_value": "-",
+ "out_of": "of {total}",
+ "presets": {
+ "today": "Today",
+ "last_seven_days": "Last 7 days"
+ },
+ "metrics": {
+ "bookings": "Bookings",
+ "complaints": "Complaints",
+ "night_washes": "Night washes",
+ "overtime": "Overtime",
+ "products_sold": "Products sold",
+ "revenue": "Revenue",
+ "transactions": "Transactions",
+ "washes": "Washes",
+ "water_usage": "Water"
+ },
+ "units": {
+ "hours": "h",
+ "liters": "L"
+ },
+ "products": {
+ "title": "Product mix",
+ "subtitle": "Tracked wash products in the selected period",
+ "empty": "No product activity for the selected period"
+ },
+ "profile": {
+ "title": "Department profile",
+ "no_description": "No department description",
+ "department_id": "Department ID",
+ "economic_department_id": "Economic department",
+ "branding": "Branding",
+ "created_at": "Created",
+ "updated_at": "Updated"
+ },
+ "hardware": {
+ "title": "Hardware readiness",
+ "subtitle": "Gateway-backed lane and relay state",
+ "gateways": "Gateways online",
+ "lanes": "Lanes",
+ "gates": "Gates",
+ "relays": "Relays",
+ "scanners": "Scanners",
+ "issues": "Issues"
+ },
+ "quick_links": {
+ "title": "Department tools",
+ "subtitle": "Open the focused setup areas for this department",
+ "modules": "Modules",
+ "branding": "Branding",
+ "gateways": "Gateways",
+ "stripe": "Stripe",
+ "pricing": "Pricing",
+ "categories": "Categories"
+ },
+ "errors": {
+ "invalid_department": "A valid department is required",
+ "load": "Unable to load the department overview"
+ }
+ },
"employees": {
"edit_employee_title": "@:employees.edit_employee",
"new_employee_subtitle": "@:{'phrases.compat.superuser_dashboard.employees.new_employee_subtitle'}",
diff --git a/src/services/superuserDepartmentOverview.js b/src/services/superuserDepartmentOverview.js
new file mode 100644
index 00000000..b48c8d58
--- /dev/null
+++ b/src/services/superuserDepartmentOverview.js
@@ -0,0 +1,23 @@
+import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
+
+export const getSuperuserDepartmentOverview = (departmentId, { date, dateTo } = {}) => {
+ const normalizedDepartmentId = Number.parseInt(String(departmentId ?? ""), 10);
+
+ if (!Number.isInteger(normalizedDepartmentId) || normalizedDepartmentId <= 0) {
+ return Promise.reject(new Error("A valid department id is required."));
+ }
+
+ const params = {
+ date,
+ };
+
+ if (dateTo) {
+ params.date_to = dateTo;
+ }
+
+ return authenticatedRequest(
+ `/superuser/departments/${encodeURIComponent(String(normalizedDepartmentId))}/overview`,
+ "GET",
+ params
+ );
+};
diff --git a/src/views/dashboards/superUserDashboard/department/Department.vue b/src/views/dashboards/superUserDashboard/department/Department.vue
index a4c6536a..eff54017 100644
--- a/src/views/dashboards/superUserDashboard/department/Department.vue
+++ b/src/views/dashboards/superUserDashboard/department/Department.vue
@@ -2,34 +2,871 @@
import PageTitle from "@/components/global/PageTitle.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
+import { getEdgeGatewayDepartmentWorkspace } from "@/services/edgeGateways.js";
+import { getSuperuserDepartmentOverview } from "@/services/superuserDepartmentOverview.js";
import DepartmentSubPageWrapper from "@/views/dashboards/superUserDashboard/department/DepartmentSubPageWrapper.vue";
-import { setDepartment, department, departmentId } from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
-import { useRouter } from 'vue-router'
+import { computed, ref, watch } from "vue";
+import { useI18n } from "vue-i18n";
+import { useRoute, useRouter } from "vue-router";
-// Get the department from the route
-const router = useRouter()
-setDepartment(router.currentRoute.value.params.departmentId);
+const route = useRoute();
+const router = useRouter();
+const { t } = useI18n();
+
+const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
+
+const formatDateInput = (value = new Date()) => {
+ const date = value instanceof Date ? value : new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return formatDateInput(new Date());
+ }
+
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+};
+
+const addDays = (value, days) => {
+ const date = new Date(`${value}T00:00:00`);
+ date.setDate(date.getDate() + days);
+ return formatDateInput(date);
+};
+
+const today = () => formatDateInput(new Date());
+const isDateValue = (value) => DATE_PATTERN.test(String(value || ""));
+const normalizeDate = (value, fallback = today()) => (isDateValue(value) ? String(value) : fallback);
+
+const dateFrom = ref(normalizeDate(route.query.date));
+const dateTo = ref(normalizeDate(route.query.date_to, dateFrom.value));
+const department = ref(null);
+const overview = ref(null);
+const hardwareWorkspace = ref(null);
+const loading = ref(false);
+const hardwareLoading = ref(false);
+const errorMessage = ref("");
+
+const departmentId = computed(() => Number.parseInt(String(route.params.departmentId || ""), 10));
+const hasDepartmentId = computed(() => Number.isInteger(departmentId.value) && departmentId.value > 0);
+const pageTitle = computed(() => department.value?.name || t("superuser_dashboard.department_overview.title"));
+const pageSubtitle = computed(() => t("superuser_dashboard.department_overview.subtitle"));
+const overviewMetrics = computed(() => overview.value?.metrics || {});
+const productTiles = computed(() => (Array.isArray(overview.value?.products) ? overview.value.products : []));
+const periodLabel = computed(() =>
+ dateFrom.value === dateTo.value
+ ? dateFrom.value
+ : t("superuser_dashboard.department_overview.range_label", {
+ from: dateFrom.value,
+ to: dateTo.value,
+ })
+);
+
+const metricDefinitions = computed(() => [
+ {
+ key: "revenue",
+ icon: "fa-sack-dollar",
+ label: t("superuser_dashboard.department_overview.metrics.revenue"),
+ formatter: formatCurrency,
+ },
+ {
+ key: "washes",
+ icon: "fa-truck",
+ label: t("superuser_dashboard.department_overview.metrics.washes"),
+ formatter: formatNumber,
+ },
+ {
+ key: "transactions",
+ icon: "fa-receipt",
+ label: t("superuser_dashboard.department_overview.metrics.transactions"),
+ formatter: formatNumber,
+ },
+ {
+ key: "bookings",
+ icon: "fa-calendar-check",
+ label: t("superuser_dashboard.department_overview.metrics.bookings"),
+ formatter: formatNumber,
+ showOutOf: true,
+ },
+ {
+ key: "products_sold",
+ icon: "fa-boxes-stacked",
+ label: t("superuser_dashboard.department_overview.metrics.products_sold"),
+ formatter: formatNumber,
+ },
+ {
+ key: "water_usage",
+ icon: "fa-droplet",
+ label: t("superuser_dashboard.department_overview.metrics.water_usage"),
+ formatter: formatNumber,
+ suffix: t("superuser_dashboard.department_overview.units.liters"),
+ },
+ {
+ key: "complaints",
+ icon: "fa-triangle-exclamation",
+ label: t("superuser_dashboard.department_overview.metrics.complaints"),
+ formatter: formatNumber,
+ },
+ {
+ key: "night_washes",
+ icon: "fa-moon",
+ label: t("superuser_dashboard.department_overview.metrics.night_washes"),
+ formatter: formatNumber,
+ },
+ {
+ key: "overtime",
+ icon: "fa-clock",
+ label: t("superuser_dashboard.department_overview.metrics.overtime"),
+ formatter: formatDecimal,
+ suffix: t("superuser_dashboard.department_overview.units.hours"),
+ },
+]);
+
+const quickLinks = computed(() => [
+ {
+ label: t("superuser_dashboard.department_overview.quick_links.modules"),
+ icon: "fa-toggle-on",
+ path: `/superuser/departments/${departmentId.value}/modules`,
+ },
+ {
+ label: t("superuser_dashboard.department_overview.quick_links.branding"),
+ icon: "fa-palette",
+ path: `/superuser/departments/${departmentId.value}/branding`,
+ },
+ {
+ label: t("superuser_dashboard.department_overview.quick_links.gateways"),
+ icon: "fa-network-wired",
+ path: `/superuser/departments/${departmentId.value}/gateways`,
+ },
+ {
+ label: t("superuser_dashboard.department_overview.quick_links.stripe"),
+ icon: "fa-credit-card",
+ path: `/superuser/departments/${departmentId.value}/stripe/terminals/readers`,
+ },
+ {
+ label: t("superuser_dashboard.department_overview.quick_links.pricing"),
+ icon: "fa-tags",
+ path: `/superuser/departments/${departmentId.value}/pricing`,
+ },
+ {
+ label: t("superuser_dashboard.department_overview.quick_links.categories"),
+ icon: "fa-layer-group",
+ path: `/superuser/departments/${departmentId.value}/categories`,
+ },
+]);
+
+function toNumber(value) {
+ const parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : 0;
+}
+
+function formatNumber(value) {
+ return new Intl.NumberFormat("da-DK").format(toNumber(value));
+}
+
+function formatDecimal(value) {
+ return new Intl.NumberFormat("da-DK", {
+ maximumFractionDigits: 2,
+ }).format(toNumber(value));
+}
+
+function formatCurrency(value) {
+ const amount = toNumber(value);
+ if (SessionUser.functions?.currency?.toLocal) {
+ return SessionUser.functions.currency.toLocal(amount);
+ }
+
+ return new Intl.NumberFormat("da-DK", {
+ style: "currency",
+ currency: "DKK",
+ maximumFractionDigits: 0,
+ }).format(amount);
+}
+
+function formatDateTime(value) {
+ if (!value) {
+ return t("superuser_dashboard.department_overview.empty_value");
+ }
+
+ if (SessionUser.functions?.date?.toLocal) {
+ return SessionUser.functions.date.toLocal(value);
+ }
+
+ const date = new Date(String(value).replace(" ", "T"));
+ return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat("da-DK").format(date);
+}
+
+function metricPayload(key) {
+ return overviewMetrics.value?.[key] || {
+ state: "unavailable",
+ value: null,
+ };
+}
+
+function metricValue(definition) {
+ const metric = metricPayload(definition.key);
+ if (metric.state && metric.state !== "ready") {
+ return t("superuser_dashboard.department_overview.empty_value");
+ }
+
+ const formatted = definition.formatter(metric.value);
+ return definition.suffix ? `${formatted} ${definition.suffix}` : formatted;
+}
+
+function metricSecondary(definition) {
+ const metric = metricPayload(definition.key);
+ if (!definition.showOutOf || metric.out_of === undefined || metric.out_of === null) {
+ return "";
+ }
+
+ return t("superuser_dashboard.department_overview.out_of", {
+ total: formatNumber(metric.out_of),
+ });
+}
+
+const asArray = (value) => {
+ if (Array.isArray(value)) {
+ return value;
+ }
+
+ if (value && typeof value === "object") {
+ return Object.values(value);
+ }
+
+ return [];
+};
+
+const hardwareSummary = computed(() => {
+ const workspace = hardwareWorkspace.value || {};
+ const gateways = asArray(workspace.gateways);
+ const lanes = asArray(workspace.lanes);
+ const gates = asArray(workspace.gates);
+ const relays = asArray(workspace.relays);
+ const scanners = asArray(workspace.scanners || workspace.number_plate_scanners);
+ const issues = asArray(workspace.issues || workspace.diagnostics || workspace.hardware?.issues);
+ const onlineGateways = gateways.filter((gateway) => {
+ const status = String(gateway?.status || gateway?.health || "").toLowerCase();
+ return ["online", "ready", "ok", "healthy"].includes(status);
+ }).length;
+
+ return {
+ gateways: gateways.length,
+ onlineGateways,
+ lanes: lanes.length,
+ gates: gates.length,
+ relays: relays.length,
+ scanners: scanners.length,
+ issues: issues.length,
+ };
+});
+
+const hasHardwareSummary = computed(() => hardwareLoading.value || Boolean(hardwareWorkspace.value));
+const departmentProfileRows = computed(() => [
+ {
+ label: t("superuser_dashboard.department_overview.profile.department_id"),
+ value: department.value?.id ?? t("superuser_dashboard.department_overview.empty_value"),
+ },
+ {
+ label: t("superuser_dashboard.department_overview.profile.economic_department_id"),
+ value: department.value?.economic_department_id ?? t("superuser_dashboard.department_overview.empty_value"),
+ },
+ {
+ label: t("superuser_dashboard.department_overview.profile.branding"),
+ value: department.value?.branding ?? t("superuser_dashboard.department_overview.empty_value"),
+ },
+ {
+ label: t("superuser_dashboard.department_overview.profile.created_at"),
+ value: formatDateTime(department.value?.created_at),
+ },
+ {
+ label: t("superuser_dashboard.department_overview.profile.updated_at"),
+ value: formatDateTime(department.value?.updated_at),
+ },
+]);
+
+function normalizeRange(from, to) {
+ const normalizedFrom = normalizeDate(from);
+ let normalizedTo = normalizeDate(to, normalizedFrom);
+
+ if (normalizedTo < normalizedFrom) {
+ normalizedTo = normalizedFrom;
+ }
+
+ return {
+ from: normalizedFrom,
+ to: normalizedTo,
+ };
+}
+
+async function replaceRange(from, to) {
+ const normalized = normalizeRange(from, to);
+ dateFrom.value = normalized.from;
+ dateTo.value = normalized.to;
+
+ await router.replace({
+ query: {
+ ...route.query,
+ date: normalized.from,
+ date_to: normalized.to === normalized.from ? undefined : normalized.to,
+ },
+ });
+}
+
+const applyDateInputs = () => replaceRange(dateFrom.value, dateTo.value);
+const setToday = () => replaceRange(today(), today());
+const setLastSevenDays = () => replaceRange(addDays(today(), -6), today());
+
+async function loadOverview() {
+ if (!hasDepartmentId.value) {
+ errorMessage.value = t("superuser_dashboard.department_overview.errors.invalid_department");
+ return;
+ }
+
+ loading.value = true;
+ errorMessage.value = "";
+
+ try {
+ const response = await getSuperuserDepartmentOverview(departmentId.value, {
+ date: dateFrom.value,
+ dateTo: dateTo.value,
+ });
+ const payload = response?.data?.data || {};
+ department.value = payload.department || null;
+ overview.value = payload.overview || null;
+ } catch (error) {
+ department.value = null;
+ overview.value = null;
+ errorMessage.value =
+ error?.response?.data?.message || error?.message || t("superuser_dashboard.department_overview.errors.load");
+ } finally {
+ loading.value = false;
+ }
+}
+
+async function loadHardware() {
+ if (!hasDepartmentId.value) {
+ return;
+ }
+
+ hardwareLoading.value = true;
+ hardwareWorkspace.value = null;
+
+ try {
+ const response = await getEdgeGatewayDepartmentWorkspace(departmentId.value);
+ hardwareWorkspace.value = response?.data?.data || null;
+ } catch (_error) {
+ hardwareWorkspace.value = null;
+ } finally {
+ hardwareLoading.value = false;
+ }
+}
+
+function openQuickLink(path) {
+ router.push(path);
+}
+
+watch(
+ () => [route.params.departmentId, route.query.date, route.query.date_to],
+ () => {
+ const normalized = normalizeRange(route.query.date, route.query.date_to || route.query.date);
+ dateFrom.value = normalized.from;
+ dateTo.value = normalized.to;
+ loadOverview();
+ loadHardware();
+ },
+ { immediate: true }
+);
{{ t("superuser_dashboard.department_overview.products.subtitle") }} {{ department?.description || t("superuser_dashboard.department_overview.profile.no_description") }} {{ t("superuser_dashboard.department_overview.hardware.subtitle") }} {{ t("superuser_dashboard.department_overview.quick_links.subtitle") }}
- Department stuff
-
+
+ {{ departmentId }}
- {{ department.id }}
- {{ department.name }}
- {{ department.description }}
- {{ department.created_at }}
- {{ department.updated_at }}
-
+ {{ errorMessage }}
+
+
+
+
+ {{ t("superuser_dashboard.department_overview.loading") }}
+
+
+
+
+
+
+
+ {{ metric.label }}
+ {{ metricValue(metric) }}
+ {{ metricSecondary(metric) }}
+
+
+
+
+
+
+
+
+ {{ productTiles.length }}
+ {{ t("superuser_dashboard.department_overview.products.title") }}
+
+
+
+
+
+ {{ product.title }}
+ {{ product.slug }}
+
+
+ {{ formatNumber(product.value) }}
+
+ {{
+ t("superuser_dashboard.department_overview.out_of", {
+ total: formatNumber(product.out_of),
+ })
+ }}
+
+
+
+ {{ t("superuser_dashboard.department_overview.products.empty") }}
+
+
+
+
+
+ {{ t("superuser_dashboard.department_overview.profile.title") }}
+
+
+
+
+
+
+
+
+
+ {{ t("superuser_dashboard.department_overview.hardware.title") }}
+
+
+
+ {{ t("superuser_dashboard.department_overview.hardware.gateways") }}
+ {{ formatNumber(hardwareSummary.onlineGateways) }} / {{ formatNumber(hardwareSummary.gateways) }}
+
+
+ {{ t("superuser_dashboard.department_overview.hardware.lanes") }}
+ {{ formatNumber(hardwareSummary.lanes) }}
+
+
+ {{ t("superuser_dashboard.department_overview.hardware.gates") }}
+ {{ formatNumber(hardwareSummary.gates) }}
+
+
+ {{ t("superuser_dashboard.department_overview.hardware.relays") }}
+ {{ formatNumber(hardwareSummary.relays) }}
+
+
+ {{ t("superuser_dashboard.department_overview.hardware.scanners") }}
+ {{ formatNumber(hardwareSummary.scanners) }}
+
+
+ {{ t("superuser_dashboard.department_overview.hardware.issues") }}
+ {{ formatNumber(hardwareSummary.issues) }}
+
+
+
+
+
+ {{ t("superuser_dashboard.department_overview.quick_links.title") }}
+
+
+
+
{{ subtitle }}