|
|
@@ -1,18 +1,21 @@
|
|
|
<template>
|
|
|
- <div v-if="props.data.length > 0" class="q-mx-md q-mb-md">
|
|
|
+ <div
|
|
|
+ v-if="visibleItems.length > 0"
|
|
|
+ class="q-mx-md q-mb-md"
|
|
|
+ >
|
|
|
<div class="scroll-wrapper">
|
|
|
<div
|
|
|
class="scroll-track"
|
|
|
- :class="{ 'scroll-track--single': props.data.length === 1 }"
|
|
|
+ :class="{ 'scroll-track--single': visibleItems.length === 1 }"
|
|
|
>
|
|
|
<q-card
|
|
|
- v-for="item in props.data"
|
|
|
+ v-for="item in visibleItems"
|
|
|
:key="item.id"
|
|
|
- :class="{ 'today-card--single': props.data.length === 1 }"
|
|
|
+ :class="{ 'today-card--single': visibleItems.length === 1 }"
|
|
|
:flat="false"
|
|
|
class="today-card card-border shadow-card bg-surface"
|
|
|
>
|
|
|
- <q-card-section class="column justify-between q-pa-md full-height">
|
|
|
+ <q-card-section class="column justify-between q-pa-sm full-height">
|
|
|
<div class="row no-wrap items-center q-mb-xs">
|
|
|
<div class="col-8 row">
|
|
|
<q-avatar size="40px" class="flex-shrink-0 q-mr-sm">
|
|
|
@@ -143,7 +146,11 @@
|
|
|
@click.stop="emit('rate', item)"
|
|
|
>
|
|
|
<div class="column items-center">
|
|
|
- <q-icon name="mdi-star-outline" size="14px" class="q-mr-xs" />
|
|
|
+ <q-icon
|
|
|
+ class="q-mr-xs"
|
|
|
+ name="mdi-star-outline"
|
|
|
+ size="14px"
|
|
|
+ />
|
|
|
|
|
|
{{ $t('provider.dashboard.today_services.rate_btn') }}
|
|
|
</div>
|
|
|
@@ -153,7 +160,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="progress-track q-mb-sm">
|
|
|
+ <div class="progress-track q-mb-xs">
|
|
|
<div
|
|
|
class="progress-fill"
|
|
|
:class="cardState(item) === 'finished' ? 'progress-fill--finished' : ''"
|
|
|
@@ -161,55 +168,57 @@
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- <div class="row items-center no-wrap">
|
|
|
- <q-btn
|
|
|
- class="flex-shrink-0"
|
|
|
- color="primary"
|
|
|
- flat no-caps dense
|
|
|
- size="sm"
|
|
|
- :label="$t('provider.dashboard.today_services.help')"
|
|
|
- @click.stop="openHelp"
|
|
|
- />
|
|
|
-
|
|
|
- <q-space />
|
|
|
-
|
|
|
- <template v-if="cardState(item) === 'in_progress'">
|
|
|
+ <template v-if="cardState(item) === 'in_progress'">
|
|
|
+ <div class="row items-center no-wrap">
|
|
|
<q-icon
|
|
|
class="q-mr-xs flex-shrink-0 gradient-diarista"
|
|
|
name="mdi-clock-outline"
|
|
|
size="13px"
|
|
|
/>
|
|
|
-
|
|
|
- <span class="text-grey-6 text-right text-no-wrap font11">
|
|
|
+
|
|
|
+ <span class="text-grey-6 text-right text-no-wrap font9 fontmedium">
|
|
|
{{ $t('provider.dashboard.today_services.end_time_label') }}
|
|
|
-
|
|
|
+
|
|
|
<span class="gradient-diarista">
|
|
|
{{ calculateTimeUntilEnd(item.end_time) }}
|
|
|
</span>
|
|
|
</span>
|
|
|
-
|
|
|
- </template>
|
|
|
- <template v-else-if="cardState(item) === 'awaiting_code'">
|
|
|
- <q-icon
|
|
|
- class="q-mr-xs flex-shrink-0"
|
|
|
- color="grey-6"
|
|
|
- name="mdi-map-marker-outline"
|
|
|
- size="13px"
|
|
|
- />
|
|
|
-
|
|
|
- <span class="text-grey-6 address-text ellipsis text-right font9 fontmedium">
|
|
|
- {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
|
|
|
- </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
|
|
|
- <q-icon
|
|
|
- class="q-ml-xs"
|
|
|
- color="primary"
|
|
|
- name="mdi-content-copy"
|
|
|
- size="16px"
|
|
|
- @click="copyAddress(item.address)"
|
|
|
- />
|
|
|
- </template>
|
|
|
+ <template v-else-if="cardState(item) === 'awaiting_code'">
|
|
|
+ <div class="address-row">
|
|
|
+ <q-icon
|
|
|
+ class="flex-shrink-0"
|
|
|
+ color="grey-6"
|
|
|
+ name="mdi-map-marker-outline"
|
|
|
+ size="13px"
|
|
|
+ />
|
|
|
+
|
|
|
+ <span class="text-grey-6 address-text text-right font9 fontmedium">
|
|
|
+ {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
|
|
|
+
|
|
|
+ </span>
|
|
|
+ <q-icon
|
|
|
+ class="flex-shrink-0 q-ml-sm"
|
|
|
+ color="primary"
|
|
|
+ name="mdi-content-copy"
|
|
|
+ size="16px"
|
|
|
+ @click="copyAddress(item.address)"
|
|
|
+ />
|
|
|
</div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="row items-center no-wrap">
|
|
|
+ <q-btn
|
|
|
+ class="flex-shrink-0"
|
|
|
+ color="primary"
|
|
|
+ flat no-caps dense
|
|
|
+ size="sm"
|
|
|
+ :label="$t('provider.dashboard.today_services.help')"
|
|
|
+ @click.stop="openHelp"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</q-card-section>
|
|
|
</q-card>
|
|
|
</div>
|
|
|
@@ -219,7 +228,7 @@
|
|
|
|
|
|
<script setup>
|
|
|
import { avatarColors, formatAddress, getFirstName } from 'src/helpers/utils';
|
|
|
-import { reactive } from 'vue'
|
|
|
+import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
import { useQuasar } from 'quasar'
|
|
|
import { verifyScheduleCode } from 'src/api/schedule'
|
|
|
@@ -232,6 +241,17 @@ const emit = defineEmits(['rate', 'refresh'])
|
|
|
const { t } = useI18n()
|
|
|
const $q = useQuasar()
|
|
|
|
|
|
+const now = ref(Date.now())
|
|
|
+let timer = null
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ timer = setInterval(() => { now.value = Date.now() }, 1000)
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (timer) clearInterval(timer)
|
|
|
+})
|
|
|
+
|
|
|
const codeInputs = reactive({})
|
|
|
const codeRefs = reactive({})
|
|
|
const codeError = reactive({})
|
|
|
@@ -324,21 +344,21 @@ const submitCode = async (item) => {
|
|
|
const calculateTimeUntilEnd = (endTimeStr) => {
|
|
|
const [h, m] = (endTimeStr || '23:59').slice(0, 5).split(':').map(Number)
|
|
|
|
|
|
- const now = new Date()
|
|
|
-
|
|
|
const endTime = new Date()
|
|
|
|
|
|
endTime.setHours(h, m, 0, 0)
|
|
|
|
|
|
- const diffMs = endTime - now
|
|
|
+ const diffMs = endTime.getTime() - now.value
|
|
|
|
|
|
- if (diffMs <= 0) return '00:00'
|
|
|
+ if (diffMs <= 0) return '00:00:00'
|
|
|
|
|
|
const diffH = Math.floor(diffMs / (1000 * 60 * 60))
|
|
|
|
|
|
const diffM = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
|
|
|
|
|
|
- return `${String(diffH).padStart(2, '0')}h:${String(diffM).padStart(2, '0')}`
|
|
|
+ const diffS = Math.floor((diffMs % (1000 * 60)) / 1000)
|
|
|
+
|
|
|
+ return `${String(diffH).padStart(2, '0')}:${String(diffM).padStart(2, '0')}:${String(diffS).padStart(2, '0')}`
|
|
|
}
|
|
|
|
|
|
const cardState = (item) => {
|
|
|
@@ -352,9 +372,13 @@ const cardState = (item) => {
|
|
|
|
|
|
endTime.setHours(h, m, 0, 0)
|
|
|
|
|
|
- return new Date() >= endTime ? 'finished' : 'in_progress'
|
|
|
+ return now.value >= endTime.getTime() ? 'finished' : 'in_progress'
|
|
|
}
|
|
|
|
|
|
+const visibleItems = computed(() =>
|
|
|
+ props.data.filter((item) => cardState(item) !== 'finished')
|
|
|
+)
|
|
|
+
|
|
|
const copyAddress = (address) => {
|
|
|
const formatted = formatAddress(address);
|
|
|
|
|
|
@@ -370,10 +394,23 @@ const copyAddress = (address) => {
|
|
|
const progressByState = (item) => {
|
|
|
const state = cardState(item)
|
|
|
|
|
|
- if (state === 'awaiting_code') return 60
|
|
|
- if (state === 'in_progress') return 80
|
|
|
+ if (state === 'awaiting_code') return 50
|
|
|
+ if (state === 'finished') return 100
|
|
|
+
|
|
|
+ const [sh, sm] = (item.start_time || '00:00').slice(0, 5).split(':').map(Number)
|
|
|
+ const [eh, em] = (item.end_time || '23:59').slice(0, 5).split(':').map(Number)
|
|
|
+
|
|
|
+ const startTime = new Date()
|
|
|
+ startTime.setHours(sh, sm, 0, 0)
|
|
|
+
|
|
|
+ const endTime = new Date()
|
|
|
+ endTime.setHours(eh, em, 0, 0)
|
|
|
+
|
|
|
+ const total = endTime.getTime() - startTime.getTime()
|
|
|
+ if (total <= 0) return 80
|
|
|
|
|
|
- return 100
|
|
|
+ const elapsed = now.value - startTime.getTime()
|
|
|
+ return Math.min(Math.max((elapsed / total) * 100, 10), 100)
|
|
|
}
|
|
|
|
|
|
//
|
|
|
@@ -448,8 +485,17 @@ const openHelp = () => {
|
|
|
line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
+.address-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: auto 1fr auto;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
.address-text {
|
|
|
- max-width: 110px;
|
|
|
+ overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
@@ -478,15 +524,17 @@ const openHelp = () => {
|
|
|
|
|
|
.progress-track {
|
|
|
width: 100%;
|
|
|
- height: 5px;
|
|
|
+ height: 8px;
|
|
|
background: #E2E8F0;
|
|
|
- border-radius: 3px;
|
|
|
+ border-radius: 4px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.progress-fill {
|
|
|
height: 100%;
|
|
|
- border-radius: 3px;
|
|
|
+ border-radius: 4px;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
background: linear-gradient(90deg, #8B5CF6, #EC4899);
|
|
|
transition: width 0.4s ease;
|
|
|
|
|
|
@@ -495,4 +543,29 @@ const openHelp = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+.progress-fill::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ left: -40%;
|
|
|
+ width: 40%;
|
|
|
+ background: linear-gradient(
|
|
|
+ 90deg,
|
|
|
+ transparent,
|
|
|
+ rgba(255,255,255,.35),
|
|
|
+ transparent
|
|
|
+ );
|
|
|
+ transform: skewX(-20deg);
|
|
|
+ animation: shine 2s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-fill--finished::after {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes shine {
|
|
|
+ from { left: -40%; }
|
|
|
+ to { left: 140%; }
|
|
|
+}
|
|
|
+
|
|
|
</style>
|