Explorar o código

feat: :sparkles: fix (agendamento) corrigir exibição de informações e filtro de status

foram corrigidas algumas informacoes na exibição do agendamento e foi alterado o filtro de status para o pendingSchedules receber agendamentos pendentes e aceitos, para que seja possivel o fluxo de pagamento

fase:dev | origin:escopo
Gustavo Zanatta hai 2 semanas
pai
achega
7005fae3eb

+ 5 - 0
src/api/schedule.js

@@ -4,3 +4,8 @@ export const createSchedule = async (scheduleData) => {
   const { data } = await api.post('/schedule', scheduleData);
   return data.payload;
 };
+
+export const updateScheduleStatus = async (id, status) => {
+  const { data } = await api.patch(`/schedule/${id}/status`, { status });
+  return data.payload;
+};

+ 3 - 4
src/components/dashboard/DashboardPendingSchedules.vue

@@ -1,9 +1,5 @@
 <template>
   <div class="q-mx-md q-mb-md">
-    <div class="dashboard-section-title gradient-diarista q-mb-sm">
-      {{ $t('dashboard_client.pending_schedules.title') }}
-    </div>
-
     <div class="scroll-wrapper">
       <div class="scroll-track">
         <q-card
@@ -11,6 +7,8 @@
           :key="item.id"
           class="pending-card card-border shadow-card bg-surface"
           :flat="false"
+          :class="{ 'cursor-pointer': item.status === 'accepted' }"
+          @click="item.status === 'accepted' && emit('view-details', item)"
         >
           <q-card-section class="q-pa-md">
 
@@ -84,6 +82,7 @@ const statusProgressMap = {
 const progressPercent = (status) => statusProgressMap[status] ?? 20;
 
 defineProps({ data: { type: Array, default: () => [] } });
+const emit = defineEmits(['view-details']);
 </script>
 
 <style scoped lang="scss">

+ 176 - 0
src/components/dashboard/ScheduleAcceptedDialog.vue

@@ -0,0 +1,176 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="accepted-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">
+          {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+        </q-avatar>
+
+        <div class="text-h6 text-weight-bold provider-name">{{ schedule.provider_name }}</div>
+        <div class="text-caption text-text">{{ schedule.provider_district || '' }}</div>
+      </q-card-section>
+
+      <q-card-section class="text-center q-pt-xs q-pb-md">
+        <div class="accepted-title text-weight-bold">
+          {{ $t('dashboard_client.pending_schedules.accepted_title') }}
+        </div>
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-section class="q-py-md q-px-lg">
+        <div class="detail-row">
+          <span class="detail-label">{{ $t('dashboard_client.pending_schedules.detail_date') }}</span>
+          <span class="detail-value">{{ formattedDate }}</span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">{{ $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">{{ $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">{{ $t('dashboard_client.pending_schedules.detail_service_fee') }}</span>
+          <span class="detail-value">{{ formatCurrency(serviceFee) }}</span>
+        </div>
+
+        <q-separator class="q-my-sm" />
+
+        <div class="detail-row">
+          <span class="detail-label text-weight-bold text-text">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
+          <span class="total-value">{{ formatCurrency(total) }}</span>
+        </div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none q-pb-lg q-px-lg column q-gutter-y-sm">
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          class="payment-btn full-width"
+          :label="$t('dashboard_client.pending_schedules.btn_payment')"
+          @click="onGoToPayment"
+        />
+        <q-btn
+          flat
+          no-caps
+          color="grey-6"
+          class="full-width"
+          :label="$t('dashboard_client.pending_schedules.btn_cancel')"
+          @click="onDialogHide"
+        />
+      </q-card-section>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { formatCurrency } from 'src/helpers/utils'
+import SchedulePaymentDialog from './SchedulePaymentDialog.vue'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true
+  }
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
+
+const $q = useQuasar()
+
+const SERVICE_FEE_RATE = 0.10
+
+const serviceFee = computed(() => Number(props.schedule.total_amount) * SERVICE_FEE_RATE)
+const total = computed(() => Number(props.schedule.total_amount) + serviceFee.value)
+
+const onGoToPayment = () => {
+  onDialogHide()
+  $q.dialog({
+    component: SchedulePaymentDialog,
+    componentProps: {
+      schedule: props.schedule,
+      total: total.value,
+    },
+  }).onOk(() => {
+    onDialogOK()
+  })
+}
+
+const formattedDate = computed(() => {
+  if (props.schedule.formatted_date) return props.schedule.formatted_date
+  const raw = String(props.schedule.date || '')
+  const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/)
+  if (!m) return raw
+  const d = new Date(+m[1], +m[2] - 1, +m[3])
+  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])
+</script>
+
+<style scoped lang="scss">
+.accepted-dialog-card {
+  width: 320px;
+  max-width: 92vw;
+  border-radius: 20px !important;
+  overflow: hidden;
+}
+
+.accepted-title {
+  font-size: 22px;
+  line-height: 1.3;
+  color: #8B5CF6;
+}
+
+.provider-name {
+  color: #8B5CF6;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 4px 0;
+}
+
+.detail-label {
+  font-size: 13px;
+  color: #8a8a9a;
+}
+
+.detail-value {
+  font-size: 13px;
+  color: #3a3a4a;
+}
+
+.total-value {
+  font-size: 18px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.payment-btn {
+  background: linear-gradient(90deg, #8B5CF6, #EC4899);
+  color: white;
+  font-weight: 700;
+  font-size: 15px;
+  height: 48px;
+}
+</style>

+ 304 - 0
src/components/dashboard/SchedulePaymentDialog.vue

@@ -0,0 +1,304 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+    <div class="bg-page full-height column">
+
+      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
+        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-space />
+        <span class="text-subtitle1 text-weight-bold text-primary">
+          {{ $t('payment.title') }}
+        </span>
+        <q-space />
+        <div style="width: 32px" />
+      </div>
+
+      <div class="col overflow-auto q-px-md q-pt-lg q-pb-xl">
+
+        <div class="section-label q-mb-sm">{{ $t('payment.schedule_address') }}</div>
+        <div class="address-box row items-center no-wrap q-mb-lg">
+          <div class="col">
+            <div class="address-type-label">{{ addressTypeLabel }}</div>
+            <div class="address-full-text text-grey-7">{{ addressFullText }}</div>
+          </div>
+          <q-icon name="mdi-chevron-down" color="grey-5" size="22px" />
+        </div>
+
+        <div class="section-label q-mb-sm">{{ $t('payment.pay_with') }}</div>
+        <div class="row q-gutter-sm q-mb-sm">
+
+          <div
+            class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
+            :class="{ 'payment-option-selected': selectedMethod === 'pix' }"
+            @click="selectedMethod = 'pix'"
+          >
+            <span class="payment-option-title">{{ $t('payment.pix') }}</span>
+            <q-icon name="mdi-cash-fast" size="32px" color="teal" class="q-mt-xs" />
+          </div>
+
+          <div
+            class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
+            :class="{ 'payment-option-selected': selectedMethod === 'new_card' }"
+            @click="openAddCard"
+          >
+            <span class="payment-option-title">{{ $t('payment.add_card') }}</span>
+            <q-icon name="mdi-plus-circle-outline" size="22px" color="grey-5" class="q-mt-xs" />
+            <span class="payment-option-sub">{{ $t('payment.credit_debit') }}</span>
+          </div>
+
+        </div>
+
+        <div v-if="loadingCards" class="flex flex-center q-py-md">
+          <q-spinner color="primary" size="2em" />
+        </div>
+
+        <div v-else-if="paymentMethods.length > 0" class="column q-gutter-y-sm q-mb-lg">
+          <div
+            v-for="card in paymentMethods"
+            :key="card.id"
+            class="saved-card-box row items-center no-wrap q-pa-md cursor-pointer"
+            :class="{ 'payment-option-selected': selectedMethod === `card_${card.id}` }"
+            @click="selectedMethod = `card_${card.id}`"
+          >
+            <div class="col column">
+              <span class="card-titular-label">{{ $t('payment.card_holder') }}</span>
+              <span class="card-holder-name">{{ card.holder_name }}</span>
+            </div>
+            <div class="column items-end">
+              <span class="card-brand-text">{{ brandDisplay(card.brand) }}</span>
+              <span class="card-last-four">{{ '**** **** **** ' + card.last_four_digits }}</span>
+              <span class="card-expiry-text">{{ card.expiration }}</span>
+            </div>
+          </div>
+        </div>
+
+        <q-separator class="q-my-lg" />
+
+        <div class="row items-center q-mb-lg">
+          <q-checkbox v-model="agreedToTerms" color="primary" keep-color />
+          <span class="terms-text">
+            {{ $t('payment.agree_prefix') }}
+            <span class="text-primary text-weight-bold cursor-pointer text-underline">{{ $t('payment.terms_link') }}</span>
+          </span>
+        </div>
+
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="primary"
+          class="full-width confirm-btn"
+          :label="$t('payment.confirm_btn')"
+          :disable="!canConfirm"
+          @click="onConfirm"
+        />
+
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { userStore } from 'src/stores/user'
+import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
+import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
+import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
+import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true,
+  },
+  total: {
+    type: Number,
+    required: true,
+  },
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const $q = useQuasar()
+const { t } = useI18n()
+const store = userStore()
+
+const selectedMethod = ref(null)
+const agreedToTerms = ref(false)
+const paymentMethods = ref([])
+const loadingCards = ref(false)
+
+const addressTypeLabel = computed(() => {
+  const type = props.schedule.address?.address_type
+  if (!type) return ''
+  return t(`profile.address.type.${type}`, type)
+})
+
+const addressFullText = computed(() => {
+  const a = props.schedule.address
+  if (!a) return ''
+  const parts = [a.address, a.number, a.district].filter(Boolean)
+  return parts.join(', ')
+})
+
+const canConfirm = computed(() => selectedMethod.value !== null && agreedToTerms.value)
+
+const brandDisplay = (brand) => {
+  if (!brand) return ''
+  const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }
+  return map[brand] ?? brand.toUpperCase()
+}
+
+const loadCards = async () => {
+  loadingCards.value = true
+  try {
+    paymentMethods.value = await getClientPaymentMethods(store.user?.client_id)
+  } catch (e) {
+    console.error(e)
+  } finally {
+    loadingCards.value = false
+  }
+}
+
+const openAddCard = () => {
+  $q.dialog({
+    component: ProfilePaymentAddDialog,
+    componentProps: { clientId: store.user?.client_id },
+  }).onOk(() => {
+    loadCards()
+  })
+}
+
+const onConfirm = () => {
+  if (selectedMethod.value === 'pix') {
+    $q.dialog({
+      component: SchedulePaymentPixDialog,
+      componentProps: { schedule: props.schedule, total: props.total },
+    }).onOk(() => {
+      onDialogOK()
+    })
+  } else {
+    $q.dialog({
+      component: SchedulePaymentProcessingDialog,
+      componentProps: { schedule: props.schedule },
+    }).onOk(() => {
+      onDialogOK()
+    })
+  }
+}
+
+onMounted(() => {
+  loadCards()
+})
+</script>
+
+<style scoped lang="scss">
+.shadow-header {
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
+}
+
+.section-label {
+  font-size: 15px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.address-box {
+  border: 1px solid #e0e0e0;
+  border-radius: 10px;
+  padding: 12px 16px;
+  background: #fff;
+}
+
+.address-type-label {
+  font-size: 14px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.address-full-text {
+  font-size: 12px;
+  line-height: 1.4;
+  margin-top: 2px;
+}
+
+.payment-option-card {
+  border: 1.5px solid #e0e0e0;
+  border-radius: 12px;
+  background: #fff;
+  min-height: 90px;
+  text-align: center;
+  transition: border-color 0.2s;
+}
+
+.payment-option-selected {
+  border-color: #22c55e !important;
+  box-shadow: 0 0 0 1px #22c55e;
+}
+
+.payment-option-title {
+  font-size: 15px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.payment-option-sub {
+  font-size: 11px;
+  color: #9a9aaa;
+  margin-top: 2px;
+}
+
+.saved-card-box {
+  border: 1.5px solid #e0e0e0;
+  border-radius: 12px;
+  background: #fff;
+  transition: border-color 0.2s;
+}
+
+.card-titular-label {
+  font-size: 11px;
+  color: #9a9aaa;
+}
+
+.card-holder-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #3a3a4a;
+}
+
+.card-brand-text {
+  font-size: 14px;
+  font-weight: 700;
+  color: #3a3a4a;
+  text-align: right;
+}
+
+.card-last-four {
+  font-size: 13px;
+  color: #6a6a7a;
+  letter-spacing: 1px;
+}
+
+.card-expiry-text {
+  font-size: 12px;
+  color: #9a9aaa;
+}
+
+.terms-text {
+  font-size: 13px;
+  color: #5a5a6a;
+  line-height: 1.4;
+}
+
+.text-underline {
+  text-decoration: underline;
+}
+
+.confirm-btn {
+  font-size: 16px;
+  font-weight: 700;
+  height: 52px;
+}
+</style>

+ 213 - 0
src/components/dashboard/SchedulePaymentPixDialog.vue

@@ -0,0 +1,213 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+    <div class="bg-page full-height column">
+
+      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
+        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-space />
+        <span class="text-subtitle1 text-weight-bold text-primary">
+          {{ $t('payment.pix_title') }}
+        </span>
+        <q-space />
+        <div style="width: 32px" />
+      </div>
+
+      <div class="col scroll q-px-lg q-pt-lg q-pb-xl column">
+
+        <div class="row items-center justify-between q-mb-sm">
+          <span class="pix-label">{{ $t('payment.pix_total') }}</span>
+          <span class="pix-value text-primary">{{ formatCurrency(total) }}</span>
+        </div>
+        <q-separator />
+
+        <div class="row items-center justify-between q-mt-sm q-mb-lg">
+          <span class="pix-label">{{ $t('payment.pix_expires') }}</span>
+          <span class="pix-value text-primary">{{ countdown }}</span>
+        </div>
+
+        <div class="flex flex-center q-mb-md">
+          <q-icon name="mdi-cash-fast" size="48px" color="teal" />
+        </div>
+
+        <div class="flex flex-center q-mb-md">
+          <div class="qrcode-wrapper">
+            <svg viewBox="0 0 200 200" width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+              <rect x="10" y="10" width="60" height="60" rx="4" fill="none" stroke="#000" stroke-width="6"/>
+              <rect x="22" y="22" width="36" height="36" rx="2" fill="#000"/>
+              <rect x="130" y="10" width="60" height="60" rx="4" fill="none" stroke="#000" stroke-width="6"/>
+              <rect x="142" y="22" width="36" height="36" rx="2" fill="#000"/>
+              <rect x="10" y="130" width="60" height="60" rx="4" fill="none" stroke="#000" stroke-width="6"/>
+              <rect x="22" y="142" width="36" height="36" rx="2" fill="#000"/>
+              <g fill="#000">
+                <rect x="85" y="10" width="8" height="8"/>
+                <rect x="100" y="10" width="8" height="8"/>
+                <rect x="85" y="24" width="8" height="8"/>
+                <rect x="108" y="24" width="8" height="8"/>
+                <rect x="85" y="38" width="16" height="8"/>
+                <rect x="85" y="52" width="8" height="8"/>
+                <rect x="100" y="52" width="16" height="8"/>
+                <rect x="10" y="85" width="8" height="8"/>
+                <rect x="24" y="85" width="16" height="8"/>
+                <rect x="48" y="85" width="8" height="8"/>
+                <rect x="62" y="85" width="8" height="8"/>
+                <rect x="10" y="99" width="16" height="8"/>
+                <rect x="34" y="99" width="8" height="8"/>
+                <rect x="54" y="99" width="16" height="8"/>
+                <rect x="10" y="113" width="8" height="8"/>
+                <rect x="26" y="113" width="16" height="8"/>
+                <rect x="56" y="113" width="8" height="8"/>
+                <rect x="85" y="85" width="8" height="8"/>
+                <rect x="100" y="85" width="16" height="8"/>
+                <rect x="124" y="85" width="8" height="8"/>
+                <rect x="140" y="85" width="16" height="8"/>
+                <rect x="166" y="85" width="8" height="8"/>
+                <rect x="85" y="100" width="16" height="8"/>
+                <rect x="108" y="100" width="8" height="8"/>
+                <rect x="130" y="100" width="16" height="8"/>
+                <rect x="154" y="100" width="8" height="8"/>
+                <rect x="170" y="100" width="8" height="8"/>
+                <rect x="85" y="115" width="8" height="8"/>
+                <rect x="100" y="115" width="8" height="8"/>
+                <rect x="116" y="115" width="16" height="8"/>
+                <rect x="140" y="115" width="8" height="8"/>
+                <rect x="156" y="115" width="16" height="8"/>
+                <rect x="85" y="130" width="16" height="8"/>
+                <rect x="108" y="130" width="8" height="8"/>
+                <rect x="124" y="130" width="16" height="8"/>
+                <rect x="148" y="130" width="8" height="8"/>
+                <rect x="164" y="130" width="8" height="8"/>
+                <rect x="85" y="144" width="8" height="8"/>
+                <rect x="100" y="144" width="16" height="8"/>
+                <rect x="124" y="144" width="8" height="8"/>
+                <rect x="140" y="144" width="16" height="8"/>
+                <rect x="166" y="144" width="8" height="8"/>
+                <rect x="85" y="158" width="16" height="8"/>
+                <rect x="110" y="158" width="8" height="8"/>
+                <rect x="126" y="158" width="8" height="8"/>
+                <rect x="144" y="158" width="8" height="8"/>
+                <rect x="160" y="158" width="16" height="8"/>
+                <rect x="85" y="172" width="8" height="8"/>
+                <rect x="100" y="172" width="8" height="8"/>
+                <rect x="116" y="172" width="16" height="8"/>
+                <rect x="142" y="172" width="8" height="8"/>
+                <rect x="158" y="172" width="8" height="8"/>
+                <rect x="174" y="172" width="8" height="8"/>
+              </g>
+            </svg>
+          </div>
+        </div>
+
+        <div class="pix-code-text q-mb-md">{{ pixCode }}</div>
+
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="primary"
+          class="full-width copy-btn q-mb-lg"
+          :label="$t('payment.pix_copy_btn')"
+          @click="copyCode"
+        />
+
+        <p class="pix-instructions-text q-mb-sm">
+          <strong>{{ $t('payment.pix_instructions') }}</strong>
+        </p>
+        <p class="pix-instructions-text text-grey-6">
+          {{ $t('payment.pix_email_note') }}
+        </p>
+
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
+import { formatCurrency } from 'src/helpers/utils'
+
+const props = defineProps({
+  schedule: { type: Object, required: true },
+  total: { type: Number, required: true },
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogCancel } = useDialogPluginComponent()
+const $q = useQuasar()
+
+const pixCode = `00020126580014br.gov.bcb.pix0136sfp-diaria-${props.schedule.id}-${props.schedule.provider_id}5204000053039865406${(props.total * 100).toFixed(0).padStart(8, '0')}5802BR5913Diaria App6009SAO PAULO62070503***6304ABCD`
+
+const copyCode = () => {
+  copyToClipboard(pixCode)
+    .then(() => $q.notify({ type: 'positive', message: 'Código copiado!' }))
+    .catch(() => $q.notify({ type: 'negative', message: 'Erro ao copiar.' }))
+}
+
+const totalSeconds = ref(20 * 60)
+const countdown = ref('')
+let timer = null
+
+const updateCountdown = () => {
+  const m = Math.floor(totalSeconds.value / 60)
+  const s = totalSeconds.value % 60
+  countdown.value = `${m} min, ${String(s).padStart(2, '0')} seg`
+  if (totalSeconds.value > 0) totalSeconds.value--
+}
+
+onMounted(() => {
+  updateCountdown()
+  timer = setInterval(updateCountdown, 1000)
+})
+
+onUnmounted(() => {
+  clearInterval(timer)
+})
+</script>
+
+<style scoped lang="scss">
+.shadow-header {
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
+}
+
+.pix-label {
+  font-size: 15px;
+  color: #3a3a4a;
+  font-weight: 500;
+}
+
+.pix-value {
+  font-size: 15px;
+  font-weight: 700;
+}
+
+.qrcode-wrapper {
+  background: #fff;
+  border: 1px solid #e0e0e0;
+  border-radius: 12px;
+  padding: 12px;
+}
+
+.pix-code-text {
+  font-size: 11px;
+  color: #5a5a6a;
+  text-align: center;
+  word-break: break-all;
+  line-height: 1.5;
+  background: #f5f5f8;
+  border-radius: 8px;
+  padding: 10px;
+}
+
+.copy-btn {
+  font-size: 16px;
+  font-weight: 700;
+  height: 52px;
+}
+
+.pix-instructions-text {
+  font-size: 13px;
+  line-height: 1.5;
+  color: #3a3a4a;
+}
+</style>

+ 105 - 0
src/components/dashboard/SchedulePaymentProcessingDialog.vue

@@ -0,0 +1,105 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="fade" transition-hide="fade">
+    <div class="bg-surface full-height column items-center justify-center q-px-xl">
+
+      <template v-if="!success">
+        <q-spinner-oval color="primary" size="72px" class="q-mb-lg" />
+        <div class="processing-title text-primary text-weight-bold text-center q-mb-sm">
+          {{ $t('payment.processing_title') }}
+        </div>
+        <div class="processing-message text-grey-6 text-center">
+          {{ $t('payment.processing_message') }}
+        </div>
+      </template>
+
+      <template v-else>
+        <q-btn
+          flat
+          round
+          icon="mdi-close"
+          color="grey-5"
+          class="self-end q-mb-md"
+          @click="onDialogHide"
+        />
+        <img
+          src="/logo_diaria_branco.svg"
+          alt="mascote"
+          class="success-mascot q-mb-lg"
+          style="display:none"
+        />
+        <div class="success-icon-wrapper q-mb-lg">
+          <q-icon name="mdi-check-circle" size="100px" color="primary" />
+        </div>
+        <div class="success-title text-primary text-weight-bold text-center q-mb-sm">
+          {{ $t('payment.success_title') }}
+        </div>
+        <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center">
+          <template #nextServices>
+            <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
+          </template>
+          <template #agenda>
+            <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
+          </template>
+        </i18n-t>
+      </template>
+
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useDialogPluginComponent } from 'quasar'
+import { updateScheduleStatus } from 'src/api/schedule'
+
+const props = defineProps({
+  schedule: { type: Object, required: true },
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogHide } = useDialogPluginComponent()
+
+const success = ref(false)
+
+onMounted(async () => {
+  await new Promise(resolve => setTimeout(resolve, 4000))
+  try {
+    await updateScheduleStatus(props.schedule.id, 'paid')
+  } catch (e) {
+    console.error('Erro ao atualizar status:', e)
+  }
+  success.value = true
+})
+</script>
+
+<style scoped lang="scss">
+.processing-title {
+  font-size: 22px;
+  line-height: 1.3;
+}
+
+.processing-message {
+  font-size: 15px;
+  line-height: 1.5;
+  max-width: 280px;
+}
+
+.success-icon-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.success-title {
+  font-size: 24px;
+  line-height: 1.3;
+  max-width: 280px;
+}
+
+.success-message {
+  font-size: 14px;
+  line-height: 1.6;
+  max-width: 280px;
+}
+</style>

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

@@ -407,17 +407,25 @@
       "no_price": "to arrange"
     },
     "pending_schedules": {
-      "title": "Awaiting confirmation",
+      "title": "Awaiting",
       "requesting_with": "Requesting booking with",
       "no_provider": "Provider not defined",
       "cancel_btn": "cancel",
       "status": {
-        "pending": "Awaiting confirmation",
+        "pending": "Awaiting",
         "accepted": "Accepted",
         "paid": "Paid",
         "started": "In progress",
         "finished": "Completed"
-      }
+      },
+      "accepted_title": "Accepted to do your daily!",
+      "detail_date": "Date:",
+      "detail_time": "Time:",
+      "detail_value": "Amount:",
+      "detail_service_fee": "Service fee:",
+      "detail_total": "Total:",
+      "btn_payment": "go to payment",
+      "btn_cancel": "Cancel request"
     }
   },
   "profile": {
@@ -608,10 +616,26 @@
       "slot_required": "Select a time slot to continue."
     },
     "service_types": {
-      "integral":      { "label": "Full day",     "hours": "up to 8h of service", "description": "Ideal for cleaning with higher demands and larger spaces." },
-      "padrao":        { "label": "Standard",     "hours": "up to 6h of service", "description": "Ideal for residential and commercial cleaning seeking a traditional cleaning routine." },
-      "meio_periodo":  { "label": "Half day",     "hours": "up to 4h of service", "description": "Ideal for smaller spaces, studios or offices." },
-      "diaria_rapida": { "label": "Quick Clean",  "hours": "up to 2h of service", "description": "Ideal for hotel rooms, small spaces or specific services." }
+      "integral": {
+        "label": "Full day",
+        "hours": "up to 8h of service",
+        "description": "Ideal for cleaning with higher demands and larger spaces."
+      },
+      "padrao": {
+        "label": "Standard",
+        "hours": "up to 6h of service",
+        "description": "Ideal for residential and commercial cleaning seeking a traditional cleaning routine."
+      },
+      "meio_periodo": {
+        "label": "Half day",
+        "hours": "up to 4h of service",
+        "description": "Ideal for smaller spaces, studios or offices."
+      },
+      "diaria_rapida": {
+        "label": "Quick Clean",
+        "hours": "up to 2h of service",
+        "description": "Ideal for hotel rooms, small spaces or specific services."
+      }
     },
     "order_summary": {
       "title": "Order summary",
@@ -636,5 +660,29 @@
     "6": "Standard (up to 6h)",
     "8": "Full day (up to 8h)",
     "unknown": "No information"
+  },
+  "payment": {
+    "title": "Payment",
+    "schedule_address": "Schedule to address",
+    "pay_with": "Pay with",
+    "add_card": "Add card",
+    "credit_debit": "Credit or debit",
+    "card_holder": "Holder",
+    "agree_prefix": "I agree with the",
+    "terms_link": "terms and conditions",
+    "confirm_btn": "confirm payment",
+    "pix_title": "PIX Payment",
+    "pix_total": "Total payment",
+    "pix_expires": "Pay within",
+    "pix_copy_btn": "copy pix code",
+    "pix_instructions": "Copy the Pix code above, open your bank app, choose to pay with Pix, paste the code and complete the payment. Your payment will be approved in seconds.",
+    "pix_email_note": "You will also receive instructions by email. Then just wait for confirmation from the chosen housekeeper.",
+    "processing_title": "Awaiting payment",
+    "processing_message": "We are processing your payment, please wait a moment...",
+    "success_title": "Service scheduled successfully!",
+    "success_message": "You can view your bookings in {nextServices} and in your {agenda}.",
+    "success_next_services": "upcoming services",
+    "success_agenda": "calendar",
+    "pix": "PIX"
   }
 }

+ 53 - 5
src/i18n/locales/es.json

@@ -417,7 +417,15 @@
         "paid": "Pagado",
         "started": "En curso",
         "finished": "Completado"
-      }
+      },
+      "accepted_title": "¡Aceptó realizar su jornada!",
+      "detail_date": "Fecha:",
+      "detail_time": "Horario:",
+      "detail_value": "Valor:",
+      "detail_service_fee": "Tasa de servicio:",
+      "detail_total": "Total:",
+      "btn_payment": "ir al pago",
+      "btn_cancel": "Cancelar pedido"
     }
   },
   "profile": {
@@ -608,10 +616,26 @@
       "slot_required": "Seleccione un horario para continuar."
     },
     "service_types": {
-      "integral":      { "label": "Integral",        "hours": "hasta 8h de servicio", "description": "Ideal para limpieza con mayores demandas y espacios más amplios." },
-      "padrao":        { "label": "Estándar",        "hours": "hasta 6h de servicio", "description": "Ideal para limpiezas residenciales y comerciales con rutina de limpieza tradicional." },
-      "meio_periodo":  { "label": "Medio tiempo",    "hours": "hasta 4h de servicio", "description": "Ideal para espacios más pequeños, estudios u oficinas." },
-      "diaria_rapida": { "label": "Limpieza Rápida", "hours": "hasta 2h de servicio", "description": "Ideal para habitaciones de hotel, pequeños ambientes o servicios específicos." }
+      "integral": {
+        "label": "Integral",
+        "hours": "hasta 8h de servicio",
+        "description": "Ideal para limpieza con mayores demandas y espacios más amplios."
+      },
+      "padrao": {
+        "label": "Estándar",
+        "hours": "hasta 6h de servicio",
+        "description": "Ideal para limpiezas residenciales y comerciales con rutina de limpieza tradicional."
+      },
+      "meio_periodo": {
+        "label": "Medio tiempo",
+        "hours": "hasta 4h de servicio",
+        "description": "Ideal para espacios más pequeños, estudios u oficinas."
+      },
+      "diaria_rapida": {
+        "label": "Limpieza Rápida",
+        "hours": "hasta 2h de servicio",
+        "description": "Ideal para habitaciones de hotel, pequeños ambientes o servicios específicos."
+      }
     },
     "order_summary": {
       "title": "Resumen del pedido",
@@ -636,5 +660,29 @@
     "6": "Estándar (hasta 6h)",
     "8": "Día completo (hasta 8h)",
     "unknown": "Sin información"
+  },
+  "payment": {
+    "title": "Pago",
+    "schedule_address": "Agendar para la dirección",
+    "pay_with": "Pagar con",
+    "add_card": "Agregar tarjeta",
+    "credit_debit": "Crédito o débito",
+    "card_holder": "Titular",
+    "agree_prefix": "Acepto los",
+    "terms_link": "términos y condiciones",
+    "confirm_btn": "confirmar pago",
+    "pix_title": "Pago con PIX",
+    "pix_total": "Pago total",
+    "pix_expires": "Pagar en hasta",
+    "pix_copy_btn": "copiar código pix",
+    "pix_instructions": "Copie el código Pix de arriba, abra la aplicación de su banco, elija pagar con Pix, pegue el código y finalice el pago. Su pago será aprobado en segundos.",
+    "pix_email_note": "También recibirá instrucciones por correo electrónico. Luego solo espere la confirmación de la empleada doméstica elegida.",
+    "processing_title": "Esperando pago",
+    "processing_message": "Estamos procesando su pago, por favor espere un momento...",
+    "success_title": "¡Servicio programado con éxito!",
+    "success_message": "Puede ver sus reservas en {nextServices} y en su {agenda}.",
+    "success_next_services": "próximos servicios",
+    "success_agenda": "agenda",
+    "pix": "PIX"
   }
 }

+ 53 - 5
src/i18n/locales/pt.json

@@ -417,7 +417,15 @@
         "paid": "Pago",
         "started": "Em andamento",
         "finished": "Concluído"
-      }
+      },
+      "accepted_title": "Aceitou realizar sua diária!",
+      "detail_date": "Data:",
+      "detail_time": "Horário:",
+      "detail_value": "Valor:",
+      "detail_service_fee": "Taxa de serviço:",
+      "detail_total": "Total:",
+      "btn_payment": "ir para o pagamento",
+      "btn_cancel": "Cancelar pedido"
     }
   },
   "profile": {
@@ -608,10 +616,26 @@
       "slot_required": "Selecione um horário para continuar."
     },
     "service_types": {
-      "integral":      { "label": "Integral",      "hours": "até 8h de serviço", "description": "Ideal para limpeza com demandas maiores e espaços mais amplos." },
-      "padrao":        { "label": "Padrão",         "hours": "até 6h de serviço", "description": "Ideal para limpezas residenciais e comerciais que buscam uma rotina de limpeza tradicional." },
-      "meio_periodo":  { "label": "Meio período",   "hours": "até 4h de serviço", "description": "Ideal para limpezas de espaços menores, estúdios ou escritórios." },
-      "diaria_rapida": { "label": "Diária Rápida",  "hours": "até 2h de serviço", "description": "Ideal para limpezas de quartos de hotéis, pequenos ambientes ou serviços específicos." }
+      "integral": {
+        "label": "Integral",
+        "hours": "até 8h de serviço",
+        "description": "Ideal para limpeza com demandas maiores e espaços mais amplos."
+      },
+      "padrao": {
+        "label": "Padrão",
+        "hours": "até 6h de serviço",
+        "description": "Ideal para limpezas residenciais e comerciais que buscam uma rotina de limpeza tradicional."
+      },
+      "meio_periodo": {
+        "label": "Meio período",
+        "hours": "até 4h de serviço",
+        "description": "Ideal para limpezas de espaços menores, estúdios ou escritórios."
+      },
+      "diaria_rapida": {
+        "label": "Diária Rápida",
+        "hours": "até 2h de serviço",
+        "description": "Ideal para limpezas de quartos de hotéis, pequenos ambientes ou serviços específicos."
+      }
     },
     "order_summary": {
       "title": "Resumo do pedido",
@@ -636,5 +660,29 @@
     "6": "Padrão (até 6h)",
     "8": "Dia completo (até 8h)",
     "unknown": "Sem informação"
+  },
+  "payment": {
+    "title": "Pagamento",
+    "schedule_address": "Agendar para o endereço",
+    "pay_with": "Pagar com",
+    "add_card": "Adicionar cartão",
+    "credit_debit": "Crédito ou débito",
+    "card_holder": "Titular",
+    "agree_prefix": "Concordo com os",
+    "terms_link": "termos e condições",
+    "confirm_btn": "confirmar pagamento",
+    "pix_title": "Pagamento com PIX",
+    "pix_total": "Pagamento total",
+    "pix_expires": "Pagar em até",
+    "pix_copy_btn": "copiar código pix",
+    "pix_instructions": "Copie o código Pix acima, acesse o app do seu banco, escolha pagar com Pix, cole o código e finalize o pagamento. Seu pagamento será aprovado em alguns segundos.",
+    "pix_email_note": "Você também receberá as instruções no seu email. Depois, é só aguardar a confirmação do diarista escolhido.",
+    "processing_title": "Aguardando pagamento",
+    "processing_message": "Estamos processando seu pagamento, aguarde um momento...",
+    "success_title": "Diária agendada com sucesso!",
+    "success_message": "Você pode visualizar seus agendamentos em {nextServices} e em sua {agenda}.",
+    "success_next_services": "próximos serviços",
+    "success_agenda": "agenda",
+    "pix": "PIX"
   }
 }

+ 25 - 3
src/pages/dashboard/DashboardPage.vue

@@ -8,7 +8,11 @@
     <template v-else>
       <DashboardHeaderBar :data="headerBar" />
       <DashboardSummaryInfos :data="summaryInfos" />
-      <DashboardPendingSchedules v-if="pendingSchedules.length > 0" :data="pendingSchedules" />
+      <DashboardPendingSchedules
+        v-if="pendingSchedules.length > 0"
+        :data="pendingSchedules"
+        @view-details="openAcceptedDialog"
+      />
       <DashboardScrollAreaSchedules />
       <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" />
       <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
@@ -22,12 +26,14 @@
 import DashboardHeaderBar from 'src/components/dashboard/DashboardHeaderBar.vue';
 import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
 import DashboardPendingSchedules from 'src/components/dashboard/DashboardPendingSchedules.vue';
+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 DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
 import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardProvidersClose from 'src/components/dashboard/DashboardProvidersClose.vue';
 import { onMounted, ref } from 'vue';
+import { useQuasar } from 'quasar';
 import { dadosDashboard } from 'src/api/dashboard';
 
 const headerBar = ref({});
@@ -37,10 +43,22 @@ const nextSchedules = ref([]);
 const lastDoneSchedules = ref([]);
 const favoriteProviders = ref([]);
 const providersClose = ref([]);
+const $q = useQuasar();
 const loading = ref(true);
-onMounted( async () => {
+
+const openAcceptedDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleAcceptedDialog,
+    componentProps: { schedule }
+  }).onOk(() => {
+    reloadDashboard();
+  });
+};
+
+const reloadDashboard = async () => {
+  loading.value = true;
   const response = await dadosDashboard();
-  if(response) {
+  if (response) {
     headerBar.value = response.headerBar;
     summaryInfos.value = response.summaryInfos;
     pendingSchedules.value = response.pendingSchedules ?? [];
@@ -50,6 +68,10 @@ onMounted( async () => {
     providersClose.value = response.providersClose ?? [];
   }
   loading.value = false;
+};
+
+onMounted(async () => {
+  await reloadDashboard();
 });
 </script>