|
@@ -0,0 +1,271 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
|
|
+ <q-card class="next-schedule-dialog-card bg-surface" :flat="false">
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-section class="column items-center q-pt-lg q-pb-sm">
|
|
|
|
|
+ <q-avatar size="80px" :style="avatarStyle" class="text-weight-bold text-h5 q-mb-sm">
|
|
|
|
|
+ <img v-if="details?.provider_photo" :src="details.provider_photo" />
|
|
|
|
|
+ <span v-else>{{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}</span>
|
|
|
|
|
+ </q-avatar>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="provider-name text-weight-bold">
|
|
|
|
|
+ {{ schedule.provider_name }}
|
|
|
|
|
+ <span v-if="providerAge !== null" class="text-caption text-grey-6 text-weight-regular">
|
|
|
|
|
+ {{ '(' + providerAge + ' ' + $t('dashboard_client.next_schedules.provider_age_unit') + ')' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="schedule.address" class="text-caption text-grey-6 q-mt-xs">
|
|
|
|
|
+ <q-icon name="mdi-map-marker" color="text" size="14px" class="q-mr-xs" />
|
|
|
|
|
+ {{ formatAddress(schedule.address) }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ <q-separator class="q-mx-lg" />
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-section class="q-py-sm">
|
|
|
|
|
+ <template v-if="loadingDetails">
|
|
|
|
|
+ <div class="row justify-center q-py-sm">
|
|
|
|
|
+ <q-spinner-dots color="primary" size="24px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="sp in details?.specialities"
|
|
|
|
|
+ :key="sp.id"
|
|
|
|
|
+ class="row col-12 items-center q-gutter-x-sm q-mb-xs text-center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="full-width">
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ :name="sp.has_speciality ? 'mdi-check' : 'mdi-close'"
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ size="16px"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="text-body2 text-grey-8 q-pl-sm">{{ sp.description }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!details?.specialities?.length" class="row items-center q-gutter-x-sm">
|
|
|
|
|
+ <q-icon name="mdi-check" color="secondary" size="16px" />
|
|
|
|
|
+ <span class="text-body2 text-grey-8">{{ $t('dashboard_client.next_schedules.default_service') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ <q-separator class="q-mx-lg divisoria-tracejada" />
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-section class="q-py-md q-px-lg">
|
|
|
|
|
+ <div class="detail-row">
|
|
|
|
|
+ <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_date') }}</span>
|
|
|
|
|
+ <span class="detail-value">{{ fullDateLabel }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="detail-row">
|
|
|
|
|
+ <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_time') }}</span>
|
|
|
|
|
+ <span class="detail-value text-weight-bold">
|
|
|
|
|
+ {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="detail-row">
|
|
|
|
|
+ <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_value') }}</span>
|
|
|
|
|
+ <span class="detail-value">{{ formatCurrency(schedule.total_amount) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="detail-row">
|
|
|
|
|
+ <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_service_fee') }}</span>
|
|
|
|
|
+ <span class="detail-value">{{ formatCurrency(serviceFee) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="detail-row-total">
|
|
|
|
|
+ <span class="detail-label text-weight-bold text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
|
|
|
|
|
+ <span class="total-value">{{ formatCurrency(total) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <q-separator class="q-my-sm divisoria-tracejada" />
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-section class="q-pt-none q-pb-sm q-px-lg">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ rounded
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ class="close-btn full-width"
|
|
|
|
|
+ :label="$t('dashboard_client.next_schedules.btn_close')"
|
|
|
|
|
+ @click="onDialogCancel"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-section class="q-pt-xs q-pb-md text-center">
|
|
|
|
|
+ <div class="row justify-center q-gutter-x-lg">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ flat
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ color="grey-7"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ :label="$t('dashboard_client.pending_schedules.btn_cancel')"
|
|
|
|
|
+ @click="openCancelDialog"
|
|
|
|
|
+ />
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ flat
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ color="grey-7"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ :label="$t('dashboard_client.next_schedules.btn_help')"
|
|
|
|
|
+ @click="openHelp"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </q-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { computed, onMounted, ref } from 'vue'
|
|
|
|
|
+import { useDialogPluginComponent, useQuasar } from 'quasar'
|
|
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
|
|
+import { formatCurrency } from 'src/helpers/utils'
|
|
|
|
|
+import { getScheduleClienteDetails } from 'src/api/dashboard'
|
|
|
|
|
+import ScheduleCancelDialog from './ScheduleCancelDialog.vue'
|
|
|
|
|
+import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
|
|
|
|
|
+import { formatAddress } from 'src/helpers/utils';
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps({
|
|
|
|
|
+ schedule: {
|
|
|
|
|
+ type: Object,
|
|
|
|
|
+ required: true
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+defineEmits([...useDialogPluginComponent.emits])
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n()
|
|
|
|
|
+const $q = useQuasar()
|
|
|
|
|
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
|
|
|
|
|
+
|
|
|
|
|
+const details = ref(null)
|
|
|
|
|
+const loadingDetails = ref(true)
|
|
|
|
|
+
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ details.value = await getScheduleClienteDetails(props.schedule.id)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ message: t('http.errors.failed'), color: 'negative' })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loadingDetails.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const SERVICE_FEE_RATE = 0.10
|
|
|
|
|
+
|
|
|
|
|
+const serviceFee = computed(() => {
|
|
|
|
|
+ const base = parseFloat(props.schedule.total_amount) || 0
|
|
|
|
|
+ return parseFloat((base * SERVICE_FEE_RATE).toFixed(2))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const total = computed(() => {
|
|
|
|
|
+ const base = parseFloat(props.schedule.total_amount) || 0
|
|
|
|
|
+ return parseFloat((base + serviceFee.value).toFixed(2))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const providerAge = computed(() => {
|
|
|
|
|
+ if (!details.value?.provider_birth_date) return null
|
|
|
|
|
+ const birth = new Date(details.value.provider_birth_date)
|
|
|
|
|
+ const today = new Date()
|
|
|
|
|
+ let age = today.getFullYear() - birth.getFullYear()
|
|
|
|
|
+ const m = today.getMonth() - birth.getMonth()
|
|
|
|
|
+ if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--
|
|
|
|
|
+ return age
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const parseLocalDate = (dateStr) => {
|
|
|
|
|
+ if (!dateStr) return null
|
|
|
|
|
+ const s = String(dateStr)
|
|
|
|
|
+ const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
|
|
|
|
+ if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3])
|
|
|
|
|
+ const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/)
|
|
|
|
|
+ if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1])
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const fullDateLabel = computed(() => {
|
|
|
|
|
+ if (props.schedule.formatted_date) return props.schedule.formatted_date
|
|
|
|
|
+ const d = parseLocalDate(props.schedule.date)
|
|
|
|
|
+ if (!d) return props.schedule.date ?? ''
|
|
|
|
|
+ return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const avatarColors = [
|
|
|
|
|
+ { background: '#ffd5df', color: '#932e57' },
|
|
|
|
|
+ { background: '#d7e8ff', color: '#2158a8' },
|
|
|
|
|
+ { background: '#dfd', color: '#2a7a3b' },
|
|
|
|
|
+ { background: '#ffe5cc', color: '#8a4500' },
|
|
|
|
|
+]
|
|
|
|
|
+const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors.length])
|
|
|
|
|
+
|
|
|
|
|
+const openCancelDialog = () => {
|
|
|
|
|
+ $q.dialog({
|
|
|
|
|
+ component: ScheduleCancelDialog,
|
|
|
|
|
+ componentProps: { schedule: props.schedule }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openHelp = () => {
|
|
|
|
|
+ $q.dialog({ component: ProfileHelpDialog })
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.next-schedule-dialog-card {
|
|
|
|
|
+ width: 320px;
|
|
|
|
|
+ max-width: 92vw;
|
|
|
|
|
+ border-radius: 20px !important;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.provider-name {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #8B5CF6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: left;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-row-total {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 4px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-label {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ color: #8a8a9a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-value {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #3a3a4a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.total-value {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #3a3a4a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.close-btn {
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ height: 48px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.q-mt-xxs {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.divisoria-tracejada {
|
|
|
|
|
+ border-top: 1px dashed #cfcfcf;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|