Merge pull request #115 from copenhagentruckwash/update-userdashboard-to-inspect-lane-readiness

Guard wash entry self-serve CTA
This commit is contained in:
Jeppe B
2026-06-02 13:47:39 +02:00
committed by GitHub
2 changed files with 251 additions and 3 deletions
@@ -1,6 +1,6 @@
<script setup>
import { computed, onMounted } from "vue";
import { BLoading } from "buefy";
import { BLoading, BMessage } from "buefy";
import UserDashboardPageWrapper from "@/views/dashboards/userDashboard/UserDashboardPageWrapper.vue";
import PosDepartmentStepMobile1Location from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Location.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
@@ -14,12 +14,58 @@ const {
fetchDepartments,
evaluateLocationDepartments,
orderDepartmentsByDistance,
} = useWashDepartments();
selectDepartment,
} = useWashDepartments({ includeLanes: true });
const orderedDepartments = computed(() => orderDepartmentsByDistance(guestDepartments.value));
const hasLocationCoordinates = computed(() => !!locations.location.value?.coords);
const shouldShowChooseDepartmentMessage = computed(() => !hasLocationCoordinates.value && !nearestDepartment.value);
const isDepartmentSelfServeEnabled = (department) => department?.self_serve_enabled === true;
const isLaneSelfServeEnabled = (lane) =>
!(
lane?.selfserve_enabled === false ||
lane?.selfserve_enabled === 0 ||
lane?.selfserve_enabled === "0" ||
["false", "off", "no"].includes(
String(lane?.selfserve_enabled ?? "")
.trim()
.toLowerCase()
)
);
const isLaneReadyForSelfServe = (lane) => lane?.status === "AVAILABLE" && isLaneSelfServeEnabled(lane);
const canDepartmentStartSelfServe = (department) =>
isDepartmentSelfServeEnabled(department) && (department?.lanes || []).some(isLaneReadyForSelfServe);
const nearestDepartmentCanStartSelfServe = computed(() => canDepartmentStartSelfServe(nearestDepartment.value));
const eligibleDepartments = computed(() => guestDepartments.value.filter(canDepartmentStartSelfServe));
const eligibleAlternativeDepartments = computed(() =>
eligibleDepartments.value.filter((department) => department.id !== nearestDepartment.value?.id)
);
const hasEligibleAlternativeDepartment = computed(() => eligibleAlternativeDepartments.value.length > 0);
const selfServeUnavailableMessage = computed(() => {
if (!nearestDepartment.value) {
return "Vi kan ikke finde din nærmeste afdeling uden en placering. Vælg en selvvask-klar afdeling nedenfor.";
}
if (!isDepartmentSelfServeEnabled(nearestDepartment.value)) {
return `${nearestDepartment.value.name} er ikke aktiveret til selvvask lige nu.`;
}
if (!(nearestDepartment.value.lanes || []).some(isLaneReadyForSelfServe)) {
return `${nearestDepartment.value.name} har ingen ledige selvvask-baner lige nu.`;
}
return null;
});
const getDepartmentDistance = (department) => {
if (!locations.location.value?.coords) {
return null;
@@ -83,13 +129,22 @@ onMounted(async () => {
<template #footer>
<div class="card-footer-item">
<router-link
v-if="nearestDepartment"
v-if="nearestDepartment && nearestDepartmentCanStartSelfServe"
to="/user/wash/start"
class="button is-link is-fullwidth"
data-testid="self-serve-home-start"
>
{{ $t("user_dashboard.wash.start_wash") }}
</router-link>
<button
v-else
type="button"
class="button is-link is-light is-fullwidth"
data-testid="self-serve-home-start-disabled"
disabled
>
Selvvask er ikke tilgængelig her
</button>
</div>
</template>
</WhiteBoxCard>
@@ -117,7 +172,17 @@ onMounted(async () => {
</template>
<template #footer>
<div class="card-footer-item">
<button
v-if="canDepartmentStartSelfServe(department) && !nearestDepartmentCanStartSelfServe"
type="button"
class="button is-link is-fullwidth"
:data-testid="`self-serve-home-select-department-${department.id}`"
@click="selectDepartment(department.id)"
>
Vælg denne afdeling
</button>
<router-link
v-else
:to="{ name: 'pos', params: { departmentId: department.id } }"
class="button is-light is-fullwidth"
>
+183
View File
@@ -0,0 +1,183 @@
// @vitest-environment jsdom
import { flushPromises } from "@vue/test-utils";
import { nextTick } from "vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mountWithApp } from "./helpers/mountWithApp.js";
const mocks = vi.hoisted(() => {
const asMockRef = (value) => ({ value, __v_isRef: true });
return {
guestDepartments: asMockRef([]),
nearestDepartment: asMockRef(null),
fetchDepartments: vi.fn(async () => []),
evaluateLocationDepartments: vi.fn(),
orderDepartmentsByDistance: vi.fn((departments) => departments),
selectDepartment: vi.fn(),
useWashDepartments: vi.fn(),
locationRef: asMockRef({
coords: {
latitude: 55.5,
longitude: 12.4,
},
}),
getDistance: vi.fn(() => 3),
};
});
vi.mock("@/composables/useWashDepartments", () => ({
useWashDepartments: mocks.useWashDepartments,
}));
vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", () => ({
locations: {
location: mocks.locationRef,
getDistance: mocks.getDistance,
},
}));
import MyWash from "@/views/dashboards/userDashboard/wash/MyWash.vue";
const eligibleLane = { id: 10, name: "10", status: "AVAILABLE", selfserve_enabled: true };
const makeDepartment = (overrides = {}) => ({
id: 1,
name: "Glostrup",
address: "Fabriksparken 18",
latitude: 55.5,
longitude: 12.4,
distance: 1,
self_serve_enabled: true,
lanes: [eligibleLane],
...overrides,
});
const mountMyWash = async () => {
const wrapper = mountWithApp(MyWash, {
global: {
stubs: {
PosDepartmentStepMobile1Location: {
emits: ["location-updated"],
template: "<button data-testid='location-stub' @click=\"$emit('location-updated', {})\">location</button>",
},
BLoading: {
props: ["modelValue"],
template: "<div v-if='modelValue' data-testid='loading-stub' />",
},
BMessage: {
template: "<div class='b-message-stub' v-bind='$attrs'><slot /></div>",
},
},
},
});
await flushPromises();
await nextTick();
return wrapper;
};
describe("MyWash entry", () => {
beforeEach(() => {
mocks.guestDepartments.value = [];
mocks.nearestDepartment.value = null;
mocks.locationRef.value = {
coords: {
latitude: 55.5,
longitude: 12.4,
},
};
mocks.fetchDepartments.mockReset();
mocks.fetchDepartments.mockImplementation(async () => mocks.guestDepartments.value);
mocks.evaluateLocationDepartments.mockReset();
mocks.orderDepartmentsByDistance.mockReset();
mocks.orderDepartmentsByDistance.mockImplementation((departments) => departments);
mocks.selectDepartment.mockReset();
mocks.selectDepartment.mockImplementation((departmentId) => {
mocks.nearestDepartment.value =
mocks.guestDepartments.value.find((department) => department.id === departmentId) || null;
});
mocks.useWashDepartments.mockReset();
mocks.useWashDepartments.mockImplementation(() => ({
guestDepartments: mocks.guestDepartments,
nearestDepartment: mocks.nearestDepartment,
fetchDepartments: mocks.fetchDepartments,
evaluateLocationDepartments: mocks.evaluateLocationDepartments,
orderDepartmentsByDistance: mocks.orderDepartmentsByDistance,
selectDepartment: mocks.selectDepartment,
}));
mocks.getDistance.mockReset();
mocks.getDistance.mockReturnValue(3);
});
it("enables the start CTA when the nearest department is self-serve enabled with an available lane", async () => {
const nearest = makeDepartment();
mocks.guestDepartments.value = [nearest];
mocks.nearestDepartment.value = nearest;
const wrapper = await mountMyWash();
expect(mocks.useWashDepartments).toHaveBeenCalledWith({ includeLanes: true });
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="self-serve-home-start"]').attributes("href")).toBe("/user/wash/start");
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').exists()).toBe(false);
});
it("replaces the start CTA when the nearest department is disabled and lets users select another eligible department", async () => {
const nearest = makeDepartment({ self_serve_enabled: false });
const alternative = makeDepartment({ id: 2, name: "Roskilde", distance: 5 });
mocks.guestDepartments.value = [nearest, alternative];
mocks.nearestDepartment.value = nearest;
const wrapper = await mountMyWash();
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').text()).toContain(
"Glostrup er ikke aktiveret til selvvask lige nu."
);
await wrapper.find('[data-testid="self-serve-home-select-department-2"]').trigger("click");
await nextTick();
expect(mocks.selectDepartment).toHaveBeenCalledWith(2);
});
it("replaces the start CTA when the nearest department has no enabled and available lanes", async () => {
const nearest = makeDepartment({
lanes: [
{ id: 10, name: "10", status: "MAINTENANCE", selfserve_enabled: true },
{ id: 11, name: "11", status: "AVAILABLE", selfserve_enabled: false },
],
});
const alternative = makeDepartment({ id: 3, name: "Køge", distance: 7 });
mocks.guestDepartments.value = [nearest, alternative];
mocks.nearestDepartment.value = nearest;
const wrapper = await mountMyWash();
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').text()).toContain(
"Glostrup har ingen ledige selvvask-baner lige nu."
);
expect(wrapper.find('[data-testid="self-serve-home-select-department-3"]').exists()).toBe(true);
});
it("handles missing geolocation by disabling start and offering eligible departments", async () => {
const eligible = makeDepartment({ id: 4, name: "Odense" });
mocks.locationRef.value = null;
mocks.guestDepartments.value = [eligible];
mocks.nearestDepartment.value = null;
const wrapper = await mountMyWash();
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').text()).toContain(
"Vi kan ikke finde din nærmeste afdeling uden en placering."
);
expect(wrapper.find('[data-testid="self-serve-home-select-department-4"]').exists()).toBe(true);
});
});