Parcourir la source

feat: :sparkles: feat (agendamentos) inclusao do novo bloco todayServices na dashboard do prestador

foi incluido o novo bloco de todayServices que exibe os servicos do dia atual na dashboard do prestador, com as devidas informacoes

fase:dev | origin:escopo
Gustavo Zanatta il y a 3 semaines
Parent
commit
a90e90b88a

+ 5 - 0
src/api/schedule.js

@@ -4,3 +4,8 @@ export const updateScheduleStatus = async (id, status) => {
   const { data } = await api.patch(`/schedule/${id}/status`, { status })
   return data.payload
 }
+
+export const verifyScheduleCode = async (scheduleId, code) => {
+  const response = await api.post(`/custom-schedule-verify-code/${scheduleId}`, { code })
+  return response.data
+}

+ 243 - 0
src/components/dashboard/DashboardTodayServices.vue

@@ -0,0 +1,243 @@
+<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>

+ 12 - 0
src/i18n/locales/en.json

@@ -252,6 +252,18 @@
         "tip_label": "Tip:",
         "reject_tip": "If you are not available, access your profile and block the days you do not want to receive requests."
       },
+      "today_services": {
+        "start_label": "Service start for",
+        "code_hint": "Ask the client for the code to begin",
+        "code_placeholder": "0000",
+        "code_success": "Code verified successfully!",
+        "code_error": "Invalid code. Please try again.",
+        "step_accepted": "Accepted",
+        "step_paid": "Paid",
+        "step_started": "Started",
+        "step_finished": "Finished",
+        "help": "help"
+      },
       "opportunities": {
         "title": "Opportunities"
       },

+ 12 - 0
src/i18n/locales/es.json

@@ -252,6 +252,18 @@
         "tip_label": "Consejo:",
         "reject_tip": "Si no tienes disponibilidad, accede a tu perfil y cierra los días en que no deseas recibir solicitudes."
       },
+      "today_services": {
+        "start_label": "Inicio del servicio para",
+        "code_hint": "Solicite al cliente el código para comenzar",
+        "code_placeholder": "0000",
+        "code_success": "¡Código verificado con éxito!",
+        "code_error": "Código inválido. Inténtalo de nuevo.",
+        "step_accepted": "Aceptado",
+        "step_paid": "Pagado",
+        "step_started": "Iniciado",
+        "step_finished": "Concluido",
+        "help": "ayuda"
+      },
       "opportunities": {
         "title": "Oportunidades"
       },

+ 12 - 0
src/i18n/locales/pt.json

@@ -252,6 +252,18 @@
         "tip_label": "Dica:",
         "reject_tip": "Se você não tiver disponibilidade, acesse o seu perfil e feche os dias que não deseja receber solicitações."
       },
+      "today_services": {
+        "start_label": "Início do serviço para",
+        "code_hint": "Solicite ao cliente o código para começar",
+        "code_placeholder": "0000",
+        "code_success": "Código verificado com sucesso!",
+        "code_error": "Código inválido. Tente novamente.",
+        "step_accepted": "Aceito",
+        "step_paid": "Pago",
+        "step_started": "Iniciado",
+        "step_finished": "Concluído",
+        "help": "ajuda"
+      },
       "opportunities": {
         "title": "Oportunidades"
       },

+ 4 - 0
src/pages/dashboard/DashboardPage.vue

@@ -9,6 +9,7 @@
       <DashboardHeaderBar :data="headerBar" />
       <DashboardSummaryInfos :data="summaryInfos" />
       <DashboardPriceSuggest :data="priceSuggestion"/>
+      <DashboardTodayServices :data="todayServices" @refresh="loadDashboard" />
       <DashboardScrollAreaSchedules />
       <DashboardSolicitations
         :data="solicitations"
@@ -28,6 +29,7 @@ import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfo
 import DashboardPriceSuggest from 'src/components/dashboard/DashboardPriceSuggest.vue';
 import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
 import DashboardSolicitations from 'src/components/dashboard/DashboardSolicitations.vue';
+import DashboardTodayServices from 'src/components/dashboard/DashboardTodayServices.vue';
 import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardOpportunities from 'src/components/dashboard/DashboardOpportunities.vue';
 import SolicitationDetailsDialog from 'src/components/dashboard/SolicitationDetailsDialog.vue';
@@ -40,6 +42,7 @@ const headerBar = ref({});
 const summaryInfos = ref({});
 const priceSuggestion = ref({});
 const solicitations = ref([]);
+const todayServices = ref([]);
 const nextSchedules = ref([]);
 const opportunities = ref([]);
 
@@ -54,6 +57,7 @@ const loadDashboard = async () => {
     summaryInfos.value = response.summaryInfos;
     priceSuggestion.value = response.priceSuggested;
     solicitations.value = response.solicitations;
+    todayServices.value = response.todayServices ?? [];
     nextSchedules.value = response.nextSchedules;
     opportunities.value = response.opportunities;
   }