|
|
@@ -1,7 +1,11 @@
|
|
|
<template>
|
|
|
<div class="q-mx-md q-mb-md">
|
|
|
<div class="scroll-wrapper">
|
|
|
- <div class="scroll-track">
|
|
|
+ <div
|
|
|
+ ref="trackRef"
|
|
|
+ class="scroll-track"
|
|
|
+ @scroll="onTrackScroll"
|
|
|
+ >
|
|
|
<q-card
|
|
|
v-for="item in data"
|
|
|
:key="item.id"
|
|
|
@@ -9,7 +13,7 @@
|
|
|
class="pending-card card-border shadow-card bg-surface"
|
|
|
@click="seeDetails(item)"
|
|
|
>
|
|
|
- <q-card-section class="q-pa-md">
|
|
|
+ <q-card-section class="card-body q-pa-md">
|
|
|
<div class="row no-wrap items-start q-mb-sm">
|
|
|
<q-avatar
|
|
|
class="q-mr-sm flex-shrink-0"
|
|
|
@@ -61,7 +65,7 @@
|
|
|
size="13px"
|
|
|
/>
|
|
|
|
|
|
- <span class="font9 fontmedium text-grey-5">
|
|
|
+ <span class="font9 fontmedium text-grey-5 meta-text ellipsis">
|
|
|
{{ displayTime(item) }}
|
|
|
</span>
|
|
|
</div>
|
|
|
@@ -95,11 +99,11 @@
|
|
|
<div class="progress-track q-mb-md">
|
|
|
<div
|
|
|
class="progress-fill"
|
|
|
- :style="{ width: progressPercent(item.status) + '%' }"
|
|
|
+ :class="progressClass"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- <div class="row items-center no-wrap">
|
|
|
+ <div class="card-footer row items-center no-wrap">
|
|
|
<q-btn
|
|
|
v-if="type !== 'servicePackage'"
|
|
|
class="q-mr-sm flex-shrink-0"
|
|
|
@@ -109,7 +113,7 @@
|
|
|
no-caps
|
|
|
size="xs"
|
|
|
:label="$t('dashboard_client.pending_schedules.cancel_btn')"
|
|
|
- @click="$emit('cancel', item)"
|
|
|
+ @click.stop="$emit('cancel', item)"
|
|
|
/>
|
|
|
|
|
|
<q-space />
|
|
|
@@ -133,21 +137,49 @@
|
|
|
</q-card>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="data.length > 1"
|
|
|
+ class="row justify-center q-gutter-x-xs q-mt-sm"
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ v-for="(item, index) in data"
|
|
|
+ :key="`dot-${item.id}`"
|
|
|
+ class="carousel-dot"
|
|
|
+ :class="{ 'carousel-dot--active': index === activeIndex }"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed } from "vue";
|
|
|
+import { computed, ref } from "vue";
|
|
|
import { avatarColors } from "src/helpers/avatarColors";
|
|
|
import { getFirstName } from "src/helpers/utils";
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
|
|
|
const props = defineProps({
|
|
|
data: { type: Array, default: () => [] },
|
|
|
type: { type: String, default: 'schedule' },
|
|
|
+
|
|
|
+ progressClass: { type: String, default: '' },
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(["view-details", "cancel"]);
|
|
|
|
|
|
+const { t } = useI18n();
|
|
|
+
|
|
|
+const trackRef = ref(null);
|
|
|
+const activeIndex = ref(0);
|
|
|
+
|
|
|
+const onTrackScroll = () => {
|
|
|
+ const track = trackRef.value;
|
|
|
+
|
|
|
+ if (!track || !track.clientWidth) return;
|
|
|
+
|
|
|
+ activeIndex.value = Math.round(track.scrollLeft / track.clientWidth);
|
|
|
+};
|
|
|
+
|
|
|
const isServicePackage = computed(() => props.type === 'servicePackage');
|
|
|
|
|
|
const providerPhoto = (item) => {
|
|
|
@@ -180,24 +212,26 @@ const displayAddress = (item) => {
|
|
|
].filter(Boolean).join(', ') || '—';
|
|
|
};
|
|
|
|
|
|
+const scheduleCountLabel = (count) =>
|
|
|
+ t("dashboard_client.pending_schedules.schedule_count", { count });
|
|
|
+
|
|
|
const displayTime = (item) => {
|
|
|
if (isServicePackage.value) {
|
|
|
- return item.schedules?.length
|
|
|
- ? `${item.schedules.length} agendamentos`
|
|
|
- : '—';
|
|
|
+ return item.schedules?.length ? scheduleCountLabel(item.schedules.length) : "—";
|
|
|
}
|
|
|
- return item.time_since_request;
|
|
|
-};
|
|
|
|
|
|
-const statusProgressMap = {
|
|
|
- pending: 20,
|
|
|
- accepted: 40,
|
|
|
- paid: 60,
|
|
|
- started: 80,
|
|
|
- finished: 100,
|
|
|
-};
|
|
|
+ if (!item.time_since_request) return "—";
|
|
|
|
|
|
-const progressPercent = (status) => statusProgressMap[status] ?? 20;
|
|
|
+ const timeAgo = t("dashboard_client.pending_schedules.time_ago", {
|
|
|
+ time: item.time_since_request,
|
|
|
+ });
|
|
|
+
|
|
|
+ const packageCount = Number(item.service_package_items_count ?? 0);
|
|
|
+
|
|
|
+ return packageCount > 1
|
|
|
+ ? `${timeAgo} · ${scheduleCountLabel(packageCount)}`
|
|
|
+ : timeAgo;
|
|
|
+};
|
|
|
|
|
|
const seeDetails = (item) => {
|
|
|
if (isServicePackage.value || item.status === "accepted") {
|
|
|
@@ -216,26 +250,55 @@ const seeDetails = (item) => {
|
|
|
gap: 12px;
|
|
|
overflow-x: auto;
|
|
|
overscroll-behavior-x: contain;
|
|
|
- scroll-snap-type: x proximity;
|
|
|
+ scroll-snap-type: x mandatory;
|
|
|
padding-bottom: 8px;
|
|
|
&::-webkit-scrollbar {
|
|
|
display: none;
|
|
|
}
|
|
|
- &::after {
|
|
|
- content: "";
|
|
|
- flex: 0 0 1px;
|
|
|
- }
|
|
|
+}
|
|
|
+
|
|
|
+.carousel-dot {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #cbd5e1;
|
|
|
+ transition: background 0.3s ease, width 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.carousel-dot--active {
|
|
|
+ width: 16px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #8b5cf6;
|
|
|
}
|
|
|
.pending-card {
|
|
|
- min-width: 80%;
|
|
|
- scroll-snap-align: start;
|
|
|
+ flex: 0 0 100%;
|
|
|
+ width: 100%;
|
|
|
+ min-height: 146px;
|
|
|
+ display: flex;
|
|
|
+ scroll-snap-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.card-body {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
|
|
|
.clock-badge-col {
|
|
|
- min-width: 52px;
|
|
|
+ flex: 0 0 68px;
|
|
|
+ width: 68px;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
+.meta-text {
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.card-footer {
|
|
|
+ min-height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
.clock-badge {
|
|
|
width: 24px;
|
|
|
height: 24px;
|
|
|
@@ -253,6 +316,7 @@ const seeDetails = (item) => {
|
|
|
.progress-track {
|
|
|
width: 100%;
|
|
|
height: 6px;
|
|
|
+ margin-top: auto;
|
|
|
background: #e2e8f0;
|
|
|
border-radius: 999px;
|
|
|
overflow: hidden;
|
|
|
@@ -261,6 +325,7 @@ const seeDetails = (item) => {
|
|
|
.progress-fill {
|
|
|
position: relative;
|
|
|
height: 100%;
|
|
|
+ width: 0;
|
|
|
border-radius: inherit;
|
|
|
|
|
|
background: linear-gradient(
|
|
|
@@ -270,7 +335,6 @@ const seeDetails = (item) => {
|
|
|
#d94cff
|
|
|
);
|
|
|
|
|
|
- transition: width .4s ease;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
@@ -302,4 +366,56 @@ const seeDetails = (item) => {
|
|
|
left: 135%;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+.progress-fill--loop {
|
|
|
+ background: #ec4899;
|
|
|
+ animation: progressLoopFill 3s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-fill--loop::after {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes progressLoopFill {
|
|
|
+ 0% {
|
|
|
+ width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ 80% {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.progress-fill--urgent {
|
|
|
+ width: 100%;
|
|
|
+ animation: progressUrgentPulse 1.2s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes progressUrgentPulse {
|
|
|
+ 0%,
|
|
|
+ 100% {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ 50% {
|
|
|
+ opacity: 0.2;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (prefers-reduced-motion: reduce) {
|
|
|
+ .progress-fill,
|
|
|
+ .progress-fill::after {
|
|
|
+ animation: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .progress-fill--loop,
|
|
|
+ .progress-fill--urgent {
|
|
|
+ width: 100%;
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|