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 SelfServeGuidedInstructions from "@/components/displays/selfServe/SelfServeGuidedInstructions.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 { useSelfServeLogic } from "@/composables/useSelfServeLogic";
|
||||
import { useWashDepartments } from "@/composables/useWashDepartments";
|
||||
@@ -2022,73 +2024,32 @@ watch(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<b-message
|
||||
<ErrorBanner
|
||||
v-if="departmentFetchError"
|
||||
:message="departmentFetchError"
|
||||
type="is-warning"
|
||||
has-icon
|
||||
:closable="false"
|
||||
icon-left="sync-alt"
|
||||
data-testid="self-serve-departments-error"
|
||||
>
|
||||
<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>
|
||||
@retry="fetchDepartments"
|
||||
/>
|
||||
|
||||
<b-message
|
||||
<ErrorBanner
|
||||
v-if="selfServeDataError"
|
||||
type="is-danger"
|
||||
has-icon
|
||||
:closable="false"
|
||||
:message="selfServeDataError"
|
||||
icon-left="sync-alt"
|
||||
:loading="isSelfServeRetrying"
|
||||
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"
|
||||
data-testid="self-serve-runtime-retry"
|
||||
:loading="isSelfServeRetrying"
|
||||
@click="retrySelfServeData"
|
||||
>
|
||||
{{ $t("common.try_again") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-message>
|
||||
@retry="retrySelfServeData"
|
||||
/>
|
||||
|
||||
<b-message
|
||||
<ErrorBanner
|
||||
v-if="washActionError"
|
||||
type="is-danger"
|
||||
has-icon
|
||||
:closable="false"
|
||||
:message="washActionError"
|
||||
icon-left="sync-alt"
|
||||
:loading="isStartingWash"
|
||||
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"
|
||||
data-testid="self-serve-action-retry"
|
||||
:loading="isStartingWash"
|
||||
@click="retryWashAction"
|
||||
>
|
||||
{{ $t("common.try_again") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-message>
|
||||
@retry="retryWashAction"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-show="isCompletedWashState"
|
||||
@@ -2373,86 +2334,28 @@ watch(
|
||||
class="self-serve-bottom-actions"
|
||||
data-testid="self-serve-bottom-actions"
|
||||
>
|
||||
<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="SessionUser.functions.contact.onClickCallPhoneNumber()"
|
||||
>
|
||||
{{ $t("self_wash.assistance") }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<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="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>
|
||||
<WashProgressCard
|
||||
:current-guided-wash-step="currentGuidedWashStep"
|
||||
:guided-wash-flow-steps="guidedWashFlowSteps"
|
||||
:formatted-elapsed="formattedElapsed"
|
||||
:is-completing-wash="isCompletingWash"
|
||||
:opening-property-access-gate="openingPropertyAccessGate"
|
||||
:opening-property-exit-gate="openingPropertyExitGate"
|
||||
@update:currentGuidedWashStep="currentGuidedWashStep = $event"
|
||||
@goPreviousGuidedWashStep="goPreviousGuidedWashStep()"
|
||||
@goNextGuidedWashStep="goNextGuidedWashStep()"
|
||||
@completeWash="completeGuidedWash()"
|
||||
@requestAssistance="SessionUser.functions.contact.onClickCallPhoneNumber()"
|
||||
>
|
||||
<template #guided-instructions="{ currentStep: cs, steps: gs }">
|
||||
<SelfServeGuidedInstructions
|
||||
:steps="gs"
|
||||
:current-step="cs"
|
||||
:show-actions="false"
|
||||
@update:current-step="currentGuidedWashStep = $event"
|
||||
/>
|
||||
</template>
|
||||
</WashProgressCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2543,21 +2446,6 @@ watch(
|
||||
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 {
|
||||
max-width: 30rem;
|
||||
@@ -2573,10 +2461,21 @@ watch(
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.self-serve-finishing-screen {
|
||||
margin: 0 auto;
|
||||
max-width: 32rem;
|
||||
text-align: left;
|
||||
/* Wash progress card bottom actions (during active wash) */
|
||||
.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%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
Reference in New Issue
Block a user