Refactor Self-Serve task handling and enhance UI components:

- Update `SelfServeTaskList.vue` to handle image and non-image task attachments separately and improve task card styling.
- Add utility methods for filtering task services, attachments, and images.
- Modify `MyWashStart.vue` and `SelfServeTasksStep.vue` headers for better readability, styling, and alignment.
- Improve layout responsiveness with scoped styles for task cards and progress headers.
This commit is contained in:
Jeppe Bundgaard
2026-03-18 13:56:39 +01:00
parent 65b173094f
commit e785275c24
3 changed files with 197 additions and 14 deletions
@@ -29,6 +29,32 @@ const hasTaskDescription = (task: any) => {
const trimmedDescription = description.trim();
return trimmedDescription.length > 0 && trimmedDescription !== "-";
};
const getImageAttachments = (task: any) => {
if (!Array.isArray(task?.attachments)) {
return [];
}
return task.attachments.filter((attachment: any) => isImageAttachment(attachment));
};
const getPrimaryImageAttachment = (task: any) => getImageAttachments(task)[0] || null;
const getNonImageAttachments = (task: any) => {
if (!Array.isArray(task?.attachments)) {
return [];
}
return task.attachments.filter((attachment: any) => !isImageAttachment(attachment));
};
const getVisibleServices = (task: any) => {
if (!Array.isArray(task?.services)) {
return [];
}
return task.services.filter((service: any) => String(service || "").trim().toUpperCase() !== "MACHINE");
};
</script>
<template>
@@ -36,11 +62,62 @@ const hasTaskDescription = (task: any) => {
<div
v-for="task in tasks"
:key="task.id || task.task_id || task.task"
class="notification is-warning is-light mb-4"
:class="{ 'is-success': completedTasks[task.id] }"
class="notification is-warning is-light mb-4 self-serve-task-card"
:class="{
'is-success': completedTasks[task.id],
'self-serve-task-card--image': !!getPrimaryImageAttachment(task),
}"
:data-testid="`self-serve-task-${task.id}`"
>
<div class="columns is-mobile is-vcentered">
<template v-if="getPrimaryImageAttachment(task)">
<div class="self-serve-task-image-wrapper">
<img
:src="getPrimaryImageAttachment(task)?.download_link"
:alt="getPrimaryImageAttachment(task)?.content?.other || task.task"
class="self-serve-task-image is-clickable"
:data-testid="`self-serve-task-${task.id}-attachment-${getPrimaryImageAttachment(task)?.id}`"
@click="emit('download-attachment', task.id, getPrimaryImageAttachment(task)?.id)"
/>
<div class="self-serve-task-image-overlay">
<div class="self-serve-task-image-overlay-content">
<div v-if="props.showCheckboxes" class="self-serve-task-image-overlay-checkbox">
<b-field>
<b-checkbox
size="is-large"
:model-value="completedTasks[task.id] === true"
type="is-success"
:data-testid="`self-serve-task-${task.id}-toggle`"
@update:model-value="emit('toggle-task', task.id, $event)"
@input="emit('toggle-task', task.id, $event)"
/>
</b-field>
</div>
<div class="self-serve-task-image-overlay-text">
<p class="self-serve-task-title"><strong>{{ task.task }}</strong></p>
<p v-if="hasTaskDescription(task)" class="self-serve-task-description">{{ task.description }}</p>
</div>
</div>
</div>
</div>
<div v-if="getVisibleServices(task).length > 0" class="tags p-3 pb-0">
<span v-for="service in getVisibleServices(task)" :key="service" class="tag is-success">{{ service }}</span>
</div>
<div v-if="getNonImageAttachments(task).length > 0" class="p-3 pt-2">
<div v-for="attachment in getNonImageAttachments(task)" :key="attachment.id" class="mb-2">
<div class="is-flex is-align-items-center">
<b-icon pack="fas" icon="paperclip" size="is-small" class="mr-1" />
<a
class="is-size-7"
:data-testid="`self-serve-task-${task.id}-attachment-${attachment.id}`"
@click="emit('download-attachment', task.id, attachment.id)"
>
{{ attachment.content?.other || $t("self_wash.attached_file") }}
</a>
</div>
</div>
</div>
</template>
<div v-else class="columns is-mobile is-vcentered">
<div v-if="props.showCheckboxes" class="column is-narrow">
<b-field>
<b-checkbox
@@ -56,8 +133,8 @@ const hasTaskDescription = (task: any) => {
<div class="column">
<p><strong>{{ task.task }}</strong></p>
<p v-if="hasTaskDescription(task)" class="is-size-7">{{ task.description }}</p>
<div v-if="task.services?.length" class="tags mt-2">
<span v-for="service in task.services" :key="service" class="tag is-success">{{ service }}</span>
<div v-if="getVisibleServices(task).length > 0" class="tags mt-2">
<span v-for="service in getVisibleServices(task)" :key="service" class="tag is-success">{{ service }}</span>
</div>
<div v-if="task.attachments && task.attachments.length > 0" class="mt-2">
<div v-for="attachment in task.attachments" :key="attachment.id" class="mb-2">
@@ -88,3 +165,55 @@ const hasTaskDescription = (task: any) => {
</div>
</div>
</template>
<style scoped>
.self-serve-task-card--image {
padding: 0.6rem;
}
.self-serve-task-image-wrapper {
position: relative;
width: 100%;
}
.self-serve-task-image {
width: 100%;
height: auto;
display: block;
border-radius: 6px;
}
.self-serve-task-image-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.08) 35%, rgba(0, 0, 0, 0.55) 100%);
pointer-events: none;
}
.self-serve-task-image-overlay-content {
margin: 0.75rem;
width: calc(100% - 1.5rem);
background: rgba(255, 255, 255, 0.92);
border-radius: 8px;
padding: 0.65rem 0.75rem;
display: flex;
align-items: flex-start;
gap: 0.6rem;
pointer-events: auto;
}
.self-serve-task-image-overlay-checkbox :deep(.field) {
margin-bottom: 0;
}
.self-serve-task-title {
margin: 0;
}
.self-serve-task-description {
margin: 0.25rem 0 0;
font-size: 0.85rem;
}
</style>
@@ -33,12 +33,12 @@ const emitDownloadAttachment = (taskId: number, attachmentId: number) => {
<b-icon pack="fas" icon="spinner" custom-class="fa-pulse" size="is-large" />
<p>{{ $t("self_wash.loading_data") }}</p>
</div>
<div v-else class="mt-6">
<div style="position: sticky; top: 0; background-color: white; z-index: 10; padding-top: 1rem; padding-bottom: 1rem; border-bottom: 1px solid #ddd; margin-bottom: 1rem;">
<h1 class="title has-text-centered">{{ $t("self_wash.wash_in_progress") }}</h1>
<h2 class="subtitle has-text-centered">
<div v-else class="self-serve-tasks-content">
<div class="self-serve-progress-header">
<h1 class="title has-text-centered self-serve-progress-title">{{ $t("self_wash.wash_in_progress") }}</h1>
<p class="self-serve-progress-timer">
<strong data-testid="self-serve-elapsed">{{ formattedElapsed }}</strong>
</h2>
</p>
</div>
<div v-if="dynamicImageUrl" class="mb-4">
<img
@@ -62,3 +62,32 @@ const emitDownloadAttachment = (taskId: number, attachmentId: number) => {
</div>
</div>
</template>
<style scoped>
.self-serve-tasks-content {
margin-top: 1rem;
}
.self-serve-progress-header {
position: sticky;
top: 0;
z-index: 10;
background-color: #fff;
border-bottom: 1px solid #ddd;
padding: 0.75rem 0 0.9rem;
margin-bottom: 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.self-serve-progress-title {
margin: 0;
}
.self-serve-progress-timer {
margin: 0;
line-height: 1;
}
</style>
@@ -783,11 +783,11 @@ watch(() => forceNearestDepartmentEvaluationId.value, () => {
:clickable="clickableSteps[steps.WASH_IN_PROGRESS]()"
:visible="currentStep === steps.WASH_IN_PROGRESS"
>
<div style="position: sticky; top: 0; background-color: white; z-index: 10; padding-top: 1rem; padding-bottom: 1rem; border-bottom: 1px solid #ddd; margin-bottom: 1rem;">
<h1 class="title has-text-centered">{{ $t("self_wash.wash_in_progress") }}</h1>
<h2 class="subtitle has-text-centered">
<div class="wash-live-progress-header">
<h1 class="title has-text-centered wash-live-progress-title">{{ $t("self_wash.wash_in_progress") }}</h1>
<p class="wash-live-progress-timer">
<strong data-testid="self-serve-live-elapsed">{{ formattedElapsed }}</strong>
</h2>
</p>
</div>
<SelfServeGuidedInstructions
@@ -890,3 +890,28 @@ watch(() => forceNearestDepartmentEvaluationId.value, () => {
</div>
</UserDashboardPageWrapper>
</template>
<style scoped>
.wash-live-progress-header {
position: sticky;
top: 0;
z-index: 10;
background-color: #fff;
border-bottom: 1px solid #ddd;
padding: 0.75rem 0 0.9rem;
margin-bottom: 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.wash-live-progress-title {
margin: 0;
}
.wash-live-progress-timer {
margin: 0;
line-height: 1;
}
</style>