| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- <template>
- <div class="q-mx-md q-mb-md">
- <div class="scroll-wrapper">
- <div
- ref="trackRef"
- class="scroll-track"
- @scroll="onTrackScroll"
- >
- <q-card
- v-for="item in data"
- :key="item.id"
- class="pending-card card-border shadow-card bg-surface"
- :flat="false"
- @click="seeDetails(item)"
- >
- <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"
- size="40px"
- :style="avatarColors[item.id % avatarColors.length]"
- >
- <img
- v-if="providerPhoto(item)"
- style="object-fit: cover; border-radius: 50%"
- :src="providerPhoto(item)"
- />
- <span v-else>
- {{
- providerName(item).slice(0, 2).toUpperCase() || "??"
- }}
- </span>
- </q-avatar>
- <div class="col column no-wrap overflow-hidden">
- <span class="font12 fontmedium text-text">
- <span v-if="type === 'servicePackage'">
- {{
- $t("dashboard_client.pending_schedules.pay_to_provider")
- }}
- </span>
- <span v-else-if="item.status == 'pending'">
- {{
- $t("dashboard_client.pending_schedules.requesting_with")
- }}
- </span>
- <span v-else-if="item.status == 'accepted'">
- {{
- $t("dashboard_client.pending_schedules.pay_to_provider")
- }}
- </span>
- <span class="font12 fontbold">
- {{ " " + (getFirstName(providerName(item)) || "—") }}</span
- >
- </span>
- <div class="row items-center q-mt-xs">
- <q-icon
- class="q-mr-xs"
- color="grey-5"
- name="mdi-clock-outline"
- size="13px"
- />
- <span class="font9 fontmedium text-grey-5 meta-text ellipsis">
- {{ requestStatusText(item) }}
- </span>
- </div>
- </div>
- <div
- class="clock-badge-col q-ml-sm flex-shrink-0 column items-center"
- >
- <div class="clock-badge">
- <q-icon
- color="white"
- name="mdi-clock-outline"
- size="14px"
- />
- </div>
- <span
- class="font10 fontmedium text-primary q-mt-xs badge-status-text"
- >
- {{
- type === 'servicePackage'
- ? $t("dashboard_client.pending_schedules.status.accepted")
- : $t(
- `dashboard_client.pending_schedules.status.${item.status ?? "pending"}`,
- )
- }}
- </span>
- </div>
- </div>
- <div class="progress-track q-mb-md">
- <div
- class="progress-fill"
- :class="progressClass"
- />
- </div>
- <div class="schedule-info">
- <div class="schedule-info-item">
- <q-icon
- name="mdi-calendar-outline"
- size="14px"
- />
- <span>{{ formatNumericDate(scheduleFor(item)?.date) || "—" }}</span>
- </div>
- <div class="schedule-info-item">
- <q-icon
- name="mdi-clock-outline"
- size="14px"
- />
- <span>
- {{
- formatTimeRange(
- scheduleFor(item)?.start_time,
- scheduleFor(item)?.end_time,
- ) || "—"
- }}
- </span>
- </div>
- </div>
- <div class="card-footer row items-center no-wrap">
- <q-btn
- v-if="type !== 'servicePackage'"
- class="q-mr-sm flex-shrink-0"
- color="primary"
- dense
- flat
- no-caps
- size="xs"
- :label="$t('dashboard_client.pending_schedules.cancel_btn')"
- @click.stop="$emit('cancel', item)"
- />
- <q-space />
- <span
- class="font9 fontregular text-grey-6 col ellipsis text-right"
- >
- <q-icon
- class="q-mr-xs flex-shrink-0"
- color="grey-6"
- name="mdi-map-marker-outline"
- size="13px"
- />
- {{
- formatAddress(addressFor(item)) || "—"
- }}
- </span>
- </div>
- </q-card-section>
- </q-card>
- </div>
- </div>
- <DefaultCarouselIndicator
- :active="activeIndex"
- :count="pageCount"
- />
- </div>
- </template>
- <script setup>
- import { avatarColors } from "src/helpers/avatarColors";
- import { computed } from "vue";
- import { formatAddress, getFirstName } from "src/helpers/utils";
- import { formatNumericDate, formatTimeRange } from "src/helpers/scheduleDate";
- import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
- import { useI18n } from "vue-i18n";
- import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
- const emit = defineEmits(["view-details", "cancel"]);
- const props = defineProps({
- data: { type: Array, default: () => [] },
- type: { type: String, default: 'schedule' },
- progressClass: { type: String, default: '' },
- });
- const { t } = useI18n();
- const { activeIndex, onTrackScroll, pageCount, trackRef } =
- useCarouselIndicator(() => props.data);
- const isServicePackage = computed(() => props.type === 'servicePackage');
- const addressFor = (item) => {
- const address = isServicePackage.value
- ? item.schedules?.[0]?.address
- : item.address;
- if (!address) return null;
- return {
- address: address.address,
- district: address.district,
- number: address.number,
- };
- };
- const providerName = (item) => {
- if (isServicePackage.value) {
- return item.provider?.user?.name ?? item.provider_name ?? '';
- }
- return item.provider_name ?? '';
- };
- const providerPhoto = (item) => {
- if (isServicePackage.value) {
- return item.provider?.profile_media?.url ?? item.provider_photo ?? null;
- }
- return item.provider_photo;
- };
- const scheduleCountLabel = (count) =>
- t("dashboard_client.pending_schedules.schedule_count", { count });
- const requestStatusText = (item) => {
- if (isServicePackage.value) {
- return item.schedules?.length ? scheduleCountLabel(item.schedules.length) : "—";
- }
- if (!item.time_since_request) return "—";
- 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 scheduleFor = (item) =>
- isServicePackage.value ? item.schedules?.[0] : item;
- const seeDetails = (item) => {
- emit("view-details", item);
- };
- </script>
- <style scoped lang="scss">
- .scroll-wrapper {
- overflow: hidden;
- }
- .scroll-track {
- display: flex;
- flex-direction: row;
- gap: 12px;
- overflow-x: auto;
- overscroll-behavior-x: contain;
- scroll-snap-type: x mandatory;
- padding-bottom: 8px;
- &::-webkit-scrollbar {
- display: none;
- }
- }
- .pending-card {
- 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 {
- 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;
- border-radius: 50%;
- background: linear-gradient(135deg, #8b5cf6, #ec4899);
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .badge-status-text {
- white-space: nowrap;
- }
- .progress-track {
- width: 100%;
- height: 6px;
- margin-top: auto;
- background: #e2e8f0;
- border-radius: 999px;
- overflow: hidden;
- }
- .schedule-info {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 4px;
- width: 100%;
- margin: 5px 0 4px;
- padding: 0 2px;
- color: #475569;
- font-size: 10px;
- font-weight: 500;
- }
- .schedule-info-item {
- display: flex;
- align-items: center;
- gap: 5px;
- white-space: nowrap;
- .q-icon {
- color: #64748b;
- }
- }
- .progress-fill {
- position: relative;
- height: 100%;
- width: 0;
- border-radius: inherit;
- background: linear-gradient(
- 90deg,
- #3b5cff,
- #7a4dff,
- #d94cff
- );
- overflow: hidden;
- }
- .progress-fill::after {
- content: "";
- position: absolute;
- top: 0;
- bottom: 0;
- left: -35%;
- width: 35%;
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(255,255,255,.45),
- transparent
- );
- transform: skewX(-20deg);
- animation: progressShine 2s linear infinite;
- }
- @keyframes progressShine {
- from {
- left: -35%;
- }
- to {
- 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>
|