Explorar o código

feat: :sparkles: feat (agendamentos) criacao da tela de 'ver detalhes' proximos servicos

foi criada a tela para ver detalhes do agendamento no bloco dos proximos servicos, com possibilidade de cancelar e redirecionar ao chatbot

fase:dev | origin:escopo
Gustavo Zanatta hai 1 semana
pai
achega
45f9619318

+ 5 - 0
src/api/dashboard.js

@@ -5,6 +5,11 @@ export const dadosDashboard = async () => {
   return data.payload;
 }
 
+export const getScheduleClienteDetails = async (id) => {
+  const { data } = await api.get(`/dados-dashboard-cliente/schedule/${id}/detalhes`);
+  return data.payload;
+}
+
 export const buscaPrestadores = async ({ name = '', date = '' } = {}) => {
   const params = {};
   if (name) params.name = name;

+ 2 - 0
src/components/dashboard/DashboardNextSchedules.vue

@@ -66,6 +66,7 @@
                   size="sm"
                   class="full-width"
                   :label="$t('dashboard_client.next_schedules.details')"
+                  @click="emit('view-details', item)"
                 />
               </div>
             </div>
@@ -81,6 +82,7 @@ import { useI18n } from 'vue-i18n';
 import { formatCurrency } from 'src/helpers/utils';
 
 defineProps({ data: { type: Array, default: () => [] } });
+const emit = defineEmits(['view-details']);
 
 const { t } = useI18n();
 

+ 271 - 0
src/components/dashboard/NextSchedulesDetailsDialog.vue

@@ -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>

+ 3 - 2
src/components/dashboard/ScheduleCancelDialog.vue

@@ -8,7 +8,7 @@
 
       <q-card-section class="q-pt-none q-pb-sm q-px-lg text-center">
         <div class="cancel-title text-secondary text-weight-bold">
-          {{ $t('provider.dashboard.cancel_schedule.title') }} {{ schedule.id }}
+          {{ $t('provider.dashboard.cancel_schedule.title') }}
         </div>
       </q-card-section>
 
@@ -24,6 +24,7 @@
           :placeholder="$t('provider.dashboard.cancel_schedule.reason_placeholder')"
           rows="4"
           color="secondary"
+          input-class="text-black"
           :rules="[val => (val && val.trim().length >= 5) || ' ']"
           hide-bottom-space
         />
@@ -54,7 +55,7 @@
             rounded
             no-caps
             class="btn-action bg-grey-3 text-grey-8"
-            :label="$t('provider.dashboard.cancel_schedule.btn_cancel')"
+            :label="$t('provider.dashboard.cancel_schedule.btn_back')"
             @click="onDialogCancel"
           />
           <q-btn

+ 7 - 2
src/i18n/locales/en.json

@@ -410,7 +410,11 @@
       "place_home": "home",
       "place_apartment": "apartment",
       "place_unknown": "N/A",
-      "details": "view details"
+      "details": "view details",
+      "provider_age_unit": "years old",
+      "default_service": "Standard cleaning",
+      "btn_close": "close",
+      "btn_help": "Help"
     },
     "last_schedules": {
       "title": "Last services",
@@ -734,7 +738,8 @@
         "warning_free": "Free cancellations up to 12h before the scheduled time.",
         "warning_fee": "Cancellations after this period generate a 50% refund as a compensation fee to the professional.",
         "btn_cancel": "cancel",
-        "btn_keep": "keep the request"
+        "btn_back": "go back",
+        "btn_keep": "Cancel the service"
       }
     }
   }

+ 2 - 1
src/i18n/locales/es.json

@@ -734,7 +734,8 @@
         "warning_free": "Cancelaciones gratuitas hasta 12h antes del horario programado.",
         "warning_fee": "Las cancelaciones después de este período generan un reembolso del 50% del valor como compensación al profesional.",
         "btn_cancel": "cancelar",
-        "btn_keep": "mantener el pedido"
+        "btn_back": "volver",
+        "btn_keep": "Cancelar el servicio"
       }
     }
   }

+ 7 - 2
src/i18n/locales/pt.json

@@ -415,7 +415,11 @@
       "place_home": "casa",
       "place_apartment": "apartamento",
       "place_unknown": "N/A",
-      "details": "ver detalhes"
+      "details": "ver detalhes",
+      "provider_age_unit": "anos",
+      "default_service": "Faxina padrão",
+      "btn_close": "fechar",
+      "btn_help": "Ajuda"
     },
     "last_schedules": {
       "title": "Últimos serviços",
@@ -739,7 +743,8 @@
         "warning_free": "Cancelamentos gratuitos até 12h antes do horário agendado.",
         "warning_fee": "Cancelamentos após este período geram reembolso de 50% do valor, como taxa de compensação ao profissional.",
         "btn_cancel": "cancelar",
-        "btn_keep": "manter o pedido"
+        "btn_back": "voltar",
+        "btn_keep": "Cancelar o serviço"
       }
     }
   }

+ 11 - 1
src/pages/dashboard/DashboardPage.vue

@@ -16,7 +16,7 @@
       />
       <DashboardScrollAreaSchedules />
       <DashboardPendingCustomSchedules />
-      <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" />
+      <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" @view-details="openNextScheduleDialog" />
       <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
       <DashboardFavoriteProviders v-if="favoriteProviders.length > 0" :data="favoriteProviders" />
       <DashboardProvidersClose v-if="providersClose.length > 0" :data="providersClose" />
@@ -31,6 +31,7 @@ import DashboardPendingSchedules from 'src/components/dashboard/DashboardPending
 import ScheduleAcceptedDialog from 'src/components/dashboard/ScheduleAcceptedDialog.vue';
 import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
 import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
+import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
 import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardProvidersClose from 'src/components/dashboard/DashboardProvidersClose.vue';
@@ -90,6 +91,15 @@ const reloadDashboard = async () => {
   loading.value = false;
 };
 
+const openNextScheduleDialog = (schedule) => {
+  $q.dialog({
+    component: NextSchedulesDetailsDialog,
+    componentProps: { schedule }
+  }).onOk(() => {
+    reloadDashboard();
+  });
+};
+
 const cancelSchedule = (schedule) => {
   console.log(schedule)
   $q.dialog({