Add reusable ErrorBanner component and integrate into MyWashStart
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"setupCompletedAt": "2026-06-07T11:37:04.444Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<!--
|
||||||
|
ErrorBanner.vue – reusable self-serve error notification bar.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
message – error text to display (blank ⇒ hidden)
|
||||||
|
type – Bulma tint: is-danger | is-warning (default is-danger)
|
||||||
|
iconLeft – Font Awesome icon name
|
||||||
|
:loading – whether retry button is in loading state
|
||||||
|
:showRetry – whether to show the retry button
|
||||||
|
Emit:
|
||||||
|
retry – fired when retry button is clicked
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
message: string | null;
|
||||||
|
type?: "is-danger" | "is-warning";
|
||||||
|
iconLeft?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
showRetry?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
message: string | null;
|
||||||
|
type?: "is-danger" | "is-warning";
|
||||||
|
iconLeft?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
showRetry?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
type: "is-danger",
|
||||||
|
iconLeft: "sync-alt",
|
||||||
|
loading: false,
|
||||||
|
showRetry: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
retry: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<b-message
|
||||||
|
v-if="message"
|
||||||
|
:type="type"
|
||||||
|
has-icon
|
||||||
|
:closable="false"
|
||||||
|
class="self-serve-error-banner"
|
||||||
|
>
|
||||||
|
<div class="is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
|
||||||
|
<span class="mr-3">{{ message }}</span>
|
||||||
|
<b-button
|
||||||
|
v-if="showRetry"
|
||||||
|
size="is-small"
|
||||||
|
:type="`${type} is-light`"
|
||||||
|
icon-pack="fas"
|
||||||
|
:icon-left="iconLeft"
|
||||||
|
:loading="loading"
|
||||||
|
@click="emit('retry')"
|
||||||
|
>
|
||||||
|
{{ $t("common.try_again") }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-message>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.self-serve-error-banner {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<!--
|
||||||
|
LaneSelectionSection.vue – bane-valg radio buttons and lane status display.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
lanes – array of lane objects from the department
|
||||||
|
selectedLaneId – currently selected lane id
|
||||||
|
washType – "Manual" | "Machine"
|
||||||
|
departmentName – human-readable department name
|
||||||
|
isLaneAvailableFn – function(lane) => boolean
|
||||||
|
isMachineAvailableFn – function(laneId) => boolean
|
||||||
|
|
||||||
|
Emit:
|
||||||
|
update:selectedLaneId – new lane id selected
|
||||||
|
update:washType – "Manual" | "Machine"
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SelfServeLaneStep from "@/components/displays/selfServe/SelfServeLaneStep.vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
lanes: any[];
|
||||||
|
selectedLaneId: number | string | null;
|
||||||
|
washType: string;
|
||||||
|
departmentName: string | null;
|
||||||
|
isLaneAvailableFn: (lane: any) => boolean;
|
||||||
|
isMachineAvailableFn: (laneId: number | string | null) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:selectedLaneId": [id: number];
|
||||||
|
"update:washType": [type: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleLaneUpdate(id: number) {
|
||||||
|
emit("update:selectedLaneId", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWashTypeUpdate(type: string) {
|
||||||
|
emit("update:washType", type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelfServeLaneStep
|
||||||
|
:lanes="lanes"
|
||||||
|
:selected-lane-id="selectedLaneId"
|
||||||
|
:wash-type="washType"
|
||||||
|
:department-name="departmentName"
|
||||||
|
:is-lane-available="isLaneAvailableFn"
|
||||||
|
:is-machine-available="isMachineAvailableFn"
|
||||||
|
@update:selected-lane-id="handleLaneUpdate"
|
||||||
|
@update:wash-type="handleWashTypeUpdate"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Lane selection styling is inherited from SelfServeLaneStep */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<!--
|
||||||
|
VehicleInputSection.vue – customer number, license plate, and vehicle type inputs.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
showCustomerNumberInput – whether to render the customer number field
|
||||||
|
customerNumber – current customer number string
|
||||||
|
registrationNumber – current license plate string
|
||||||
|
registrationOptions – autocomplete options for existing vehicles
|
||||||
|
selectedVehicleTypeId – currently selected vehicle type id
|
||||||
|
selectedVehicleName – human-readable name of selected vehicle type
|
||||||
|
availableProductIds – allowed product ids for the department
|
||||||
|
vehicleTypes – list of vehicle type options
|
||||||
|
|
||||||
|
Emit:
|
||||||
|
update:customerNumber – new customer number string
|
||||||
|
update:registrationNumber – new license plate string
|
||||||
|
selectVehicleType – selected VehicleTypeTemplate object
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import SelfServeVehicleStep from "@/components/displays/selfServe/SelfServeVehicleStep.vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showCustomerNumberInput: boolean;
|
||||||
|
customerNumber: string | null;
|
||||||
|
registrationNumber: string | null;
|
||||||
|
registrationOptions: string[];
|
||||||
|
selectedVehicleTypeId: number | null;
|
||||||
|
selectedVehicleName: string | null;
|
||||||
|
availableProductIds: (number | string)[];
|
||||||
|
vehicleTypes: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:customerNumber": [value: string];
|
||||||
|
"update:registrationNumber": [value: string];
|
||||||
|
selectVehicleType: [type: { id: number }];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleCustomerNumberUpdate(value: string) {
|
||||||
|
emit("update:customerNumber", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRegistrationNumberUpdate(value: string) {
|
||||||
|
emit("update:registrationNumber", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectVehicleType(selection: any) {
|
||||||
|
emit("selectVehicleType", selection);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelfServeVehicleStep
|
||||||
|
:customer-number="customerNumber"
|
||||||
|
:show-customer-number-input="showCustomerNumberInput"
|
||||||
|
:registration-number="registrationNumber"
|
||||||
|
:registration-options="registrationOptions"
|
||||||
|
:is-customer-vehicles-loading="false"
|
||||||
|
:has-matching-vehicle="false"
|
||||||
|
:selected-vehicle-type-id="selectedVehicleTypeId"
|
||||||
|
:selected-vehicle-name="selectedVehicleName"
|
||||||
|
:selected-vehicle-description="null"
|
||||||
|
:available-product-ids="availableProductIds"
|
||||||
|
:vehicle-types="vehicleTypes"
|
||||||
|
:vehicle-step-error="null"
|
||||||
|
:vehicle-step-guidance="null"
|
||||||
|
@update:customer-number="handleCustomerNumberUpdate"
|
||||||
|
@update:customerNumber="handleCustomerNumberUpdate"
|
||||||
|
@update:registration-number="handleRegistrationNumberUpdate"
|
||||||
|
@update:registrationNumber="handleRegistrationNumberUpdate"
|
||||||
|
@select-vehicle-type="handleSelectVehicleType"
|
||||||
|
@selected="handleSelectVehicleType"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Vehicle input section styling is inherited from SelfServeVehicleStep */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<!--
|
||||||
|
WashProgressCard.vue – timer, guided instructions, and bottom actions during active wash.
|
||||||
|
|
||||||
|
Renders when a wash session is in progress (WASH_IN_PROGRESS step).
|
||||||
|
|
||||||
|
Props:
|
||||||
|
currentGuidedWashStep – index into guidedWashFlowSteps
|
||||||
|
guidedWashFlowSteps – array of instruction step objects
|
||||||
|
formattedElapsed – human-readable elapsed time string
|
||||||
|
isCompletingWash – whether the wash is finishing right now
|
||||||
|
openingPropertyAccessGate – loading state for access gate button
|
||||||
|
openingPropertyExitGate – loading state for exit gate button
|
||||||
|
Emit:
|
||||||
|
update:currentGuidedWashStep – new step index
|
||||||
|
goPreviousGuidedWashStep – navigate to previous instruction
|
||||||
|
goNextGuidedWashStep – navigate to next instruction
|
||||||
|
completeWash – finish the wash session
|
||||||
|
openPropertyAccessGate – open the property access gate (laneId)
|
||||||
|
openPropertyExitGate – open the property exit gate (laneId)
|
||||||
|
requestAssistance – call for assistance
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { BButton } from "buefy";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentGuidedWashStep: number;
|
||||||
|
guidedWashFlowSteps: any[];
|
||||||
|
formattedElapsed: string;
|
||||||
|
isCompletingWash: boolean;
|
||||||
|
openingPropertyAccessGate: boolean;
|
||||||
|
openingPropertyExitGate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:currentGuidedWashStep": [step: number];
|
||||||
|
goPreviousGuidedWashStep: [];
|
||||||
|
goNextGuidedWashStep: [];
|
||||||
|
completeWash: [];
|
||||||
|
openPropertyAccessGate: [laneId: number | null];
|
||||||
|
openPropertyExitGate: [laneId: number | null];
|
||||||
|
requestAssistance: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isLastGuidedWashStep = computed(
|
||||||
|
() =>
|
||||||
|
typeof guidedWashFlowSteps !== "undefined" &&
|
||||||
|
guidedWashFlowSteps.length > 0 &&
|
||||||
|
currentGuidedWashStep >= guidedWashFlowSteps.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleGoPrevious() {
|
||||||
|
emit("goPreviousGuidedWashStep");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGoNext() {
|
||||||
|
emit("goNextGuidedWashStep");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleComplete() {
|
||||||
|
emit("completeWash");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenAccessGate(laneId: number | null) {
|
||||||
|
emit("openPropertyAccessGate", laneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenExitGate(laneId: number | null) {
|
||||||
|
emit("openPropertyExitGate", laneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestAssistance() {
|
||||||
|
emit("requestAssistance");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-testid="self-serve-wash-progress">
|
||||||
|
<!-- Finishing screen -->
|
||||||
|
<div
|
||||||
|
v-if="isCompletingWash"
|
||||||
|
class="notification is-info is-light self-serve-finishing-screen"
|
||||||
|
data-testid="self-serve-finishing-wash"
|
||||||
|
>
|
||||||
|
<p class="title is-5 mb-0">{{ $t("self_wash.finishing_wash_exit_opening") }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Guided wash instructions (stepped walkthrough) -->
|
||||||
|
<template v-if="!isCompletingWash">
|
||||||
|
<!--
|
||||||
|
Accept a callback slot for the guided-instructions component so the
|
||||||
|
parent can plug in SelfServeGuidedInstructions or its own implementation.
|
||||||
|
-->
|
||||||
|
<slot
|
||||||
|
name="guided-instructions"
|
||||||
|
:current-step="currentGuidedWashStep"
|
||||||
|
:steps="guidedWashFlowSteps"
|
||||||
|
:is-last-step="isLastGuidedWashStep"
|
||||||
|
@update:current-step="emit('update:currentGuidedWashStep', $event)"
|
||||||
|
>
|
||||||
|
<!-- Default: render nothing (parent provides via slot) -->
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<!-- Bottom action bar while wash is in progress -->
|
||||||
|
<div
|
||||||
|
class="self-serve-bottom-actions"
|
||||||
|
data-testid="self-serve-bottom-actions"
|
||||||
|
>
|
||||||
|
<!-- Help / assistance -->
|
||||||
|
<div class="self-serve-bottom-actions__row" data-testid="self-serve-session-bottom-actions">
|
||||||
|
<b-button
|
||||||
|
class="self-serve-bottom-actions__button"
|
||||||
|
type="is-link is-light"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-right="info-circle"
|
||||||
|
data-testid="self-serve-nav-help"
|
||||||
|
@click.prevent="handleRequestAssistance"
|
||||||
|
>
|
||||||
|
{{ $t("self_wash.assistance") }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Property gate controls -->
|
||||||
|
<div class="self-serve-bottom-actions__row" data-testid="self-serve-property-gate-actions">
|
||||||
|
<b-button
|
||||||
|
class="self-serve-bottom-actions__button"
|
||||||
|
type="is-link is-light"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="sign-in-alt"
|
||||||
|
data-testid="self-serve-nav-open-property-access-gate"
|
||||||
|
:loading="openingPropertyAccessGate"
|
||||||
|
:disabled="openingPropertyAccessGate"
|
||||||
|
@click.prevent="$emit('openPropertyAccessGate', $props)"
|
||||||
|
>
|
||||||
|
{{ $t("self_wash.open_property_access_gate") }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
class="self-serve-bottom-actions__button"
|
||||||
|
type="is-link is-light"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="sign-out-alt"
|
||||||
|
data-testid="self-serve-nav-open-property-exit-gate"
|
||||||
|
:loading="openingPropertyExitGate"
|
||||||
|
:disabled="openingPropertyExitGate"
|
||||||
|
@click.prevent="$emit('openPropertyExitGate', $props)"
|
||||||
|
>
|
||||||
|
{{ $t("self_wash.open_property_exit_gate") }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Guided step navigation -->
|
||||||
|
<div class="self-serve-bottom-actions__row" data-testid="self-serve-guided-bottom-actions">
|
||||||
|
<b-button
|
||||||
|
class="self-serve-bottom-actions__button"
|
||||||
|
type="is-link is-light"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-left"
|
||||||
|
data-testid="self-serve-guided-prev"
|
||||||
|
:disabled="currentGuidedWashStep === 0"
|
||||||
|
@click.prevent="handleGoPrevious"
|
||||||
|
>
|
||||||
|
{{ $t("common.previous") }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
v-if="!isLastGuidedWashStep"
|
||||||
|
class="self-serve-bottom-actions__button"
|
||||||
|
type="is-link"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-right="arrow-right"
|
||||||
|
data-testid="self-serve-guided-next"
|
||||||
|
@click.prevent="handleGoNext"
|
||||||
|
>
|
||||||
|
{{ $t("common.next") }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
v-else
|
||||||
|
class="self-serve-bottom-actions__button"
|
||||||
|
type="is-link"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-right="check"
|
||||||
|
data-testid="self-serve-nav-complete"
|
||||||
|
:loading="isCompletingWash"
|
||||||
|
:disabled="isCompletingWash"
|
||||||
|
@click.prevent="handleComplete"
|
||||||
|
>
|
||||||
|
{{ $t("common.done") }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.self-serve-finishing-screen {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 32rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-bottom-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0.75rem auto 0;
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-bottom-actions__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-bottom-actions__button {
|
||||||
|
flex: 1 1 0;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
max-width: 14rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.self-serve-bottom-actions {
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #dfe5f0;
|
||||||
|
bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px));
|
||||||
|
box-shadow: 0 -0.25rem 0.75rem rgba(17, 47, 95, 0.08);
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-width: none;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
z-index: 41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-bottom-actions__button {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
max-width: none;
|
||||||
|
min-height: 2.65rem;
|
||||||
|
padding-left: 0.55rem;
|
||||||
|
padding-right: 0.55rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<!--
|
||||||
|
WashTypeSelector.vue – manual vs machine wash toggle.
|
||||||
|
|
||||||
|
Renders two radio-style cards: "Manual" (always available) and
|
||||||
|
"Machine" (available when the lane supports it).
|
||||||
|
|
||||||
|
Props:
|
||||||
|
modelValue – current wash type ("Manual" | "Machine")
|
||||||
|
isMachineAvailable – whether machine wash is available for this lane
|
||||||
|
|
||||||
|
Emit:
|
||||||
|
update:modelValue – new wash type string
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BRadioButton } from "buefy";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: string;
|
||||||
|
isMachineAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [type: string];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="self-serve-choice-group" data-testid="self-serve-wash-type-group">
|
||||||
|
<h2 class="self-serve-choice-label">{{ $t("self_wash.wash_type") }}</h2>
|
||||||
|
<div
|
||||||
|
class="self-serve-choice-grid self-serve-choice-grid--centered"
|
||||||
|
data-testid="self-serve-wash-type-options"
|
||||||
|
>
|
||||||
|
<!-- Manual option (always available) -->
|
||||||
|
<div class="self-serve-choice-grid__item">
|
||||||
|
<b-radio-button
|
||||||
|
class="self-serve-choice-card"
|
||||||
|
:model-value="modelValue"
|
||||||
|
native-value="Manual"
|
||||||
|
type="is-link"
|
||||||
|
data-testid="self-serve-wash-type-manual"
|
||||||
|
@update:model-value="emit('update:modelValue', 'Manual')"
|
||||||
|
@input="emit('update:modelValue', 'Manual')"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span>{{ $t("self_wash.manual") }}<br /></span>
|
||||||
|
<small>
|
||||||
|
<b-icon icon="check-circle" type="is-success" pack="fas" class="mr-1" />
|
||||||
|
{{ $t("self_wash.available") }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</b-radio-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Machine option (conditioned) -->
|
||||||
|
<div class="self-serve-choice-grid__item">
|
||||||
|
<b-radio-button
|
||||||
|
class="self-serve-choice-card"
|
||||||
|
:model-value="modelValue"
|
||||||
|
native-value="Machine"
|
||||||
|
type="is-link"
|
||||||
|
:disabled="!isMachineAvailable"
|
||||||
|
:title="!isMachineAvailable ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||||
|
:aria-label="!isMachineAvailable ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||||
|
data-testid="self-serve-wash-type-machine"
|
||||||
|
@update:model-value="emit('update:modelValue', 'Machine')"
|
||||||
|
@input="emit('update:modelValue', 'Machine')"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span>{{ $t("self_wash.machine") }}<br /></span>
|
||||||
|
<span v-if="!isMachineAvailable">
|
||||||
|
<small>
|
||||||
|
<b-icon icon="times-circle" type="is-danger" pack="fas" class="mr-1" />
|
||||||
|
{{ $t("self_wash.unavailable") }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<small>
|
||||||
|
<b-icon icon="check-circle" type="is-success" pack="fas" class="mr-1" />
|
||||||
|
{{ $t("self_wash.available") }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</b-radio-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-message
|
||||||
|
v-if="!isMachineAvailable"
|
||||||
|
type="is-warning"
|
||||||
|
aria-close-label="Luk besked"
|
||||||
|
data-testid="self-serve-machine-unavailable-guidance"
|
||||||
|
>
|
||||||
|
{{ $t("self_wash.machine_unavailable_for_lane") }}
|
||||||
|
</b-message>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.self-serve-choice-group {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-label {
|
||||||
|
color: #303440;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-grid--centered {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-grid__item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-card {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 5rem;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
white-space: normal;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-card :deep(.button) {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 5rem;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
white-space: normal;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-card :deep(.button),
|
||||||
|
.self-serve-choice-card span,
|
||||||
|
.self-serve-choice-card :deep(.button span) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-choice-card small,
|
||||||
|
.self-serve-choice-card :deep(.button small) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 769px) {
|
||||||
|
.self-serve-choice-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(11rem, 15rem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,6 +13,8 @@ import SelfServeLaneStep from "@/components/displays/selfServe/SelfServeLaneStep
|
|||||||
import SelfServeTasksStep from "@/components/displays/selfServe/SelfServeTasksStep.vue";
|
import SelfServeTasksStep from "@/components/displays/selfServe/SelfServeTasksStep.vue";
|
||||||
import SelfServeGuidedInstructions from "@/components/displays/selfServe/SelfServeGuidedInstructions.vue";
|
import SelfServeGuidedInstructions from "@/components/displays/selfServe/SelfServeGuidedInstructions.vue";
|
||||||
import SelfServeCompletedStep from "@/components/displays/selfServe/SelfServeCompletedStep.vue";
|
import SelfServeCompletedStep from "@/components/displays/selfServe/SelfServeCompletedStep.vue";
|
||||||
|
import ErrorBanner from "@/components/displays/selfServe/ErrorBanner.vue";
|
||||||
|
import WashProgressCard from "@/components/displays/selfServe/WashProgressCard.vue";
|
||||||
import { guidedWashFlowSteps } from "@/constants/guidedWashFlowSteps";
|
import { guidedWashFlowSteps } from "@/constants/guidedWashFlowSteps";
|
||||||
import { useSelfServeLogic } from "@/composables/useSelfServeLogic";
|
import { useSelfServeLogic } from "@/composables/useSelfServeLogic";
|
||||||
import { useWashDepartments } from "@/composables/useWashDepartments";
|
import { useWashDepartments } from "@/composables/useWashDepartments";
|
||||||
@@ -2022,73 +2024,32 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<b-message
|
<ErrorBanner
|
||||||
v-if="departmentFetchError"
|
v-if="departmentFetchError"
|
||||||
|
:message="departmentFetchError"
|
||||||
type="is-warning"
|
type="is-warning"
|
||||||
has-icon
|
icon-left="sync-alt"
|
||||||
:closable="false"
|
|
||||||
data-testid="self-serve-departments-error"
|
data-testid="self-serve-departments-error"
|
||||||
>
|
@retry="fetchDepartments"
|
||||||
<div class="is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
|
/>
|
||||||
<span class="mr-3">{{ departmentFetchError }}</span>
|
|
||||||
<b-button
|
|
||||||
size="is-small"
|
|
||||||
type="is-warning is-light"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="sync-alt"
|
|
||||||
data-testid="self-serve-departments-retry"
|
|
||||||
@click="fetchDepartments"
|
|
||||||
>
|
|
||||||
{{ $t("common.try_again") }}
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</b-message>
|
|
||||||
|
|
||||||
<b-message
|
<ErrorBanner
|
||||||
v-if="selfServeDataError"
|
v-if="selfServeDataError"
|
||||||
type="is-danger"
|
:message="selfServeDataError"
|
||||||
has-icon
|
|
||||||
:closable="false"
|
|
||||||
data-testid="self-serve-runtime-error"
|
|
||||||
>
|
|
||||||
<div class="is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
|
|
||||||
<span class="mr-3">{{ selfServeDataError }}</span>
|
|
||||||
<b-button
|
|
||||||
size="is-small"
|
|
||||||
type="is-danger is-light"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="sync-alt"
|
icon-left="sync-alt"
|
||||||
data-testid="self-serve-runtime-retry"
|
|
||||||
:loading="isSelfServeRetrying"
|
:loading="isSelfServeRetrying"
|
||||||
@click="retrySelfServeData"
|
data-testid="self-serve-runtime-error"
|
||||||
>
|
@retry="retrySelfServeData"
|
||||||
{{ $t("common.try_again") }}
|
/>
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</b-message>
|
|
||||||
|
|
||||||
<b-message
|
<ErrorBanner
|
||||||
v-if="washActionError"
|
v-if="washActionError"
|
||||||
type="is-danger"
|
:message="washActionError"
|
||||||
has-icon
|
|
||||||
:closable="false"
|
|
||||||
data-testid="self-serve-action-error"
|
|
||||||
>
|
|
||||||
<div class="is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
|
|
||||||
<span class="mr-3">{{ washActionError }}</span>
|
|
||||||
<b-button
|
|
||||||
size="is-small"
|
|
||||||
type="is-danger is-light"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="sync-alt"
|
icon-left="sync-alt"
|
||||||
data-testid="self-serve-action-retry"
|
|
||||||
:loading="isStartingWash"
|
:loading="isStartingWash"
|
||||||
@click="retryWashAction"
|
data-testid="self-serve-action-error"
|
||||||
>
|
@retry="retryWashAction"
|
||||||
{{ $t("common.try_again") }}
|
/>
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</b-message>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="isCompletedWashState"
|
v-show="isCompletedWashState"
|
||||||
@@ -2373,86 +2334,28 @@ watch(
|
|||||||
class="self-serve-bottom-actions"
|
class="self-serve-bottom-actions"
|
||||||
data-testid="self-serve-bottom-actions"
|
data-testid="self-serve-bottom-actions"
|
||||||
>
|
>
|
||||||
<div class="self-serve-bottom-actions__row" data-testid="self-serve-session-bottom-actions">
|
<WashProgressCard
|
||||||
<b-button
|
:current-guided-wash-step="currentGuidedWashStep"
|
||||||
class="self-serve-bottom-actions__button"
|
:guided-wash-flow-steps="guidedWashFlowSteps"
|
||||||
type="is-link is-light"
|
:formatted-elapsed="formattedElapsed"
|
||||||
icon-pack="fas"
|
:is-completing-wash="isCompletingWash"
|
||||||
icon-right="info-circle"
|
:opening-property-access-gate="openingPropertyAccessGate"
|
||||||
data-testid="self-serve-nav-help"
|
:opening-property-exit-gate="openingPropertyExitGate"
|
||||||
@click.prevent="SessionUser.functions.contact.onClickCallPhoneNumber()"
|
@update:currentGuidedWashStep="currentGuidedWashStep = $event"
|
||||||
|
@goPreviousGuidedWashStep="goPreviousGuidedWashStep()"
|
||||||
|
@goNextGuidedWashStep="goNextGuidedWashStep()"
|
||||||
|
@completeWash="completeGuidedWash()"
|
||||||
|
@requestAssistance="SessionUser.functions.contact.onClickCallPhoneNumber()"
|
||||||
>
|
>
|
||||||
{{ $t("self_wash.assistance") }}
|
<template #guided-instructions="{ currentStep: cs, steps: gs }">
|
||||||
</b-button>
|
<SelfServeGuidedInstructions
|
||||||
</div>
|
:steps="gs"
|
||||||
|
:current-step="cs"
|
||||||
<div class="self-serve-bottom-actions__row" data-testid="self-serve-property-gate-actions">
|
:show-actions="false"
|
||||||
<b-button
|
@update:current-step="currentGuidedWashStep = $event"
|
||||||
class="self-serve-bottom-actions__button"
|
/>
|
||||||
type="is-link is-light"
|
</template>
|
||||||
icon-pack="fas"
|
</WashProgressCard>
|
||||||
icon-left="sign-in-alt"
|
|
||||||
data-testid="self-serve-nav-open-property-access-gate"
|
|
||||||
:loading="openingPropertyAccessGate"
|
|
||||||
:disabled="openingPropertyAccessGate"
|
|
||||||
@click.prevent="openPropertyAccessGate(washLaneId)"
|
|
||||||
>
|
|
||||||
{{ $t("self_wash.open_property_access_gate") }}
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button
|
|
||||||
class="self-serve-bottom-actions__button"
|
|
||||||
type="is-link is-light"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="sign-out-alt"
|
|
||||||
data-testid="self-serve-nav-open-property-exit-gate"
|
|
||||||
:loading="openingPropertyExitGate"
|
|
||||||
:disabled="openingPropertyExitGate"
|
|
||||||
@click.prevent="openPropertyExitGate(washLaneId)"
|
|
||||||
>
|
|
||||||
{{ $t("self_wash.open_property_exit_gate") }}
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="self-serve-bottom-actions__row" data-testid="self-serve-guided-bottom-actions">
|
|
||||||
<b-button
|
|
||||||
class="self-serve-bottom-actions__button"
|
|
||||||
type="is-link is-light"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="arrow-left"
|
|
||||||
data-testid="self-serve-guided-prev"
|
|
||||||
:disabled="currentGuidedWashStep === 0"
|
|
||||||
@click.prevent="goPreviousGuidedWashStep"
|
|
||||||
>
|
|
||||||
{{ $t("common.previous") }}
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button
|
|
||||||
v-if="!isLastGuidedWashStep"
|
|
||||||
class="self-serve-bottom-actions__button"
|
|
||||||
type="is-link"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-right="arrow-right"
|
|
||||||
data-testid="self-serve-guided-next"
|
|
||||||
@click.prevent="goNextGuidedWashStep"
|
|
||||||
>
|
|
||||||
{{ $t("common.next") }}
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button
|
|
||||||
v-else
|
|
||||||
class="self-serve-bottom-actions__button"
|
|
||||||
type="is-link"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-right="check"
|
|
||||||
data-testid="self-serve-nav-complete"
|
|
||||||
:loading="isCompletingWash"
|
|
||||||
:disabled="isCompletingWash"
|
|
||||||
@click.prevent="completeGuidedWash"
|
|
||||||
>
|
|
||||||
{{ $t("common.done") }}
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -2543,21 +2446,6 @@ watch(
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.self-serve-bottom-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0.75rem auto 0;
|
|
||||||
max-width: 30rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.self-serve-bottom-actions__row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.self-serve-bottom-actions__row--prewash {
|
.self-serve-bottom-actions__row--prewash {
|
||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
@@ -2573,10 +2461,21 @@ watch(
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.self-serve-finishing-screen {
|
/* Wash progress card bottom actions (during active wash) */
|
||||||
margin: 0 auto;
|
.self-serve-bottom-actions {
|
||||||
max-width: 32rem;
|
display: flex;
|
||||||
text-align: left;
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0.75rem auto 0;
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-serve-bottom-actions__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user