| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- <template>
- <div v-if="props.data.length > 0" class="q-mx-md q-mb-md">
- <q-card
- v-for="item in props.data"
- :key="item.id"
- class="today-card card-border shadow-card bg-surface q-mb-sm"
- :flat="false"
- >
- <q-card-section class="q-pa-sm">
- <div class="row no-wrap items-center q-mb-sm">
- <q-avatar size="40px" class="q-mr-sm">
- <img :src="item.customer_photo || 'https://cdn.quasar.dev/img/avatar.png'" />
- </q-avatar>
- <div class="col column">
- <span class="text-body2 text-text">
- {{ $t('provider.dashboard.today_services.start_label') }}
- <span class="text-weight-bold">{{ item.client_name }}</span>
- </span>
- <div class="row items-center q-gutter-x-xs q-mt-xs">
- <q-icon name="mdi-clock-outline" color="grey-5" size="14px" />
- <span class="text-caption text-grey-6">
- {{ $t('common.from') }}
- <strong class="text-text">{{ item.start_time?.slice(0, 5) }}</strong>
- {{ $t('common.to') }}
- <strong class="text-text">{{ item.end_time?.slice(0, 5) }}</strong>
- </span>
- </div>
- </div>
- <div class="col-auto text-caption text-grey-5 text-right q-pl-xs hint-text">
- {{ $t('provider.dashboard.today_services.code_hint') }}
- </div>
- </div>
- <div
- class="code-container row justify-center q-gutter-x-sm q-mb-sm"
- :class="{ 'code-disabled': item.code_verified || !canEnterCode(item) }"
- @click="focusInput(item.id)"
- >
- <div
- v-for="i in 4"
- :key="i"
- class="code-box"
- :class="{
- 'code-box--filled': (codes[item.id] || '').length >= i,
- 'code-box--verified': item.code_verified
- }"
- >
- <template v-if="item.code_verified">
- <q-icon v-if="i === 2" name="mdi-check-circle" color="positive" size="18px" />
- <span v-else></span>
- </template>
- <span v-else>{{ (codes[item.id] || '')[i - 1] || '' }}</span>
- </div>
- <input
- :id="`code-input-${item.id}`"
- v-model="codes[item.id]"
- type="tel"
- inputmode="numeric"
- maxlength="4"
- class="code-real-input"
- :disabled="item.code_verified || !canEnterCode(item)"
- @input="onCodeInput(item)"
- />
- </div>
- <q-linear-progress
- :value="progressValue(item.status)"
- color="secondary"
- track-color="grey-3"
- rounded
- size="5px"
- class="q-mb-sm"
- />
- <div class="row items-center">
- <q-btn
- flat
- no-caps
- color="primary"
- size="sm"
- class="q-px-none btn-help"
- :label="$t('provider.dashboard.today_services.help')"
- @click="openHelp"
- />
- <q-space />
- <div class="row items-center no-wrap q-gutter-x-xs">
- <q-icon name="mdi-map-marker-outline" color="grey-5" size="14px" />
- <span class="text-caption text-grey-7 ellipsis address-text">
- {{ formatAddressShort(item.address) }}
- </span>
- <q-btn
- flat
- round
- dense
- icon="mdi-content-copy"
- color="primary"
- size="xs"
- @click.stop="copyAddress(item.address)"
- />
- </div>
- </div>
- </q-card-section>
- </q-card>
- </div>
- </template>
- <script setup>
- import { ref, nextTick } from 'vue'
- import { useI18n } from 'vue-i18n'
- import { useQuasar } from 'quasar'
- import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
- import { verifyScheduleCode } from 'src/api/schedule'
- const props = defineProps({
- data: {
- type: Array,
- default: () => []
- }
- })
- const emit = defineEmits(['refresh'])
- const { t } = useI18n()
- const $q = useQuasar()
- const codes = ref({})
- const loadingCode = ref({})
- const progressValue = (status) => {
- const map = { accepted: 0.4, paid: 0.6, started: 0.8, finished: 1.0 }
- return map[status] ?? 0.4
- }
- const canEnterCode = (item) => ['paid', 'started'].includes(item.status)
- const focusInput = (id) => {
- nextTick(() => document.getElementById(`code-input-${id}`)?.focus())
- }
- const onCodeInput = async (item) => {
- const val = codes.value[item.id] || ''
- if (val.length < 4 || item.code_verified || !canEnterCode(item)) return
- loadingCode.value[item.id] = true
- try {
- const response = await verifyScheduleCode(item.id, val)
- if (response?.data?.success || response?.success) {
- $q.notify({ type: 'positive', message: t('provider.dashboard.today_services.code_success'), position: 'top' })
- emit('refresh')
- } else {
- $q.notify({ type: 'negative', message: t('provider.dashboard.today_services.code_error'), position: 'top' })
- codes.value[item.id] = ''
- }
- } catch {
- $q.notify({ type: 'negative', message: t('provider.dashboard.today_services.code_error'), position: 'top' })
- codes.value[item.id] = ''
- } finally {
- loadingCode.value[item.id] = false
- }
- }
- const formatAddressShort = (address) => {
- if (!address) return ''
- return [address.address, address.number, address.district].filter(Boolean).join(', ')
- }
- const copyAddress = (address) => {
- const text = formatAddressShort(address)
- if (text) navigator.clipboard.writeText(text)
- $q.notify({ message: t('provider.dashboard.next_schedules.address_copied'), color: 'positive', position: 'top' })
- }
- const openHelp = () => {
- $q.dialog({ component: ProfileHelpDialog })
- }
- </script>
- <style scoped lang="scss">
- .today-card {
- border-radius: 12px;
- }
- .hint-text {
- max-width: 100px;
- line-height: 1.3;
- font-size: 11px;
- }
- /* OTP input */
- .code-container {
- position: relative;
- cursor: text;
- user-select: none;
- }
- .code-real-input {
- position: absolute;
- width: 1px;
- height: 1px;
- opacity: 0;
- pointer-events: none;
- top: 0;
- left: 0;
- }
- .code-box {
- width: 52px;
- height: 44px;
- background: #efefef;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 22px;
- font-weight: 700;
- color: #3a3a4a;
- transition: background 0.15s;
- }
- .code-box--filled {
- background: #e0d8f8;
- color: var(--q-secondary);
- }
- .code-box--verified {
- background: #e8f5e9;
- }
- .code-disabled .code-box {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .address-text {
- max-width: 150px;
- }
- .btn-help {
- font-weight: 700;
- font-size: 13px;
- }
- </style>
|