Kaynağa Gözat

Merge branch 'feature/diariaapp-gus-agendamentos-apps' of Softpar/sfp_front_vue_diarista_cliente into development

zntt 1 gün önce
ebeveyn
işleme
552c5604c9

+ 7 - 7
eslint.config.js

@@ -68,15 +68,15 @@ export default [
       "prefer-promise-reject-errors": "off",
       "vue/require-prop-types": "off",
       "vue/no-v-model-argument": "off",
-      "vue/no-unused-vars": "warn",
+      // "vue/no-unused-vars": "warn",
       "vue/no-unused-components": "warn",
       "@intlify/vue-i18n/no-dynamic-keys": "off",
-      "@intlify/vue-i18n/no-unused-keys": [
-        "error",
-        {
-          extensions: [".js", ".vue"],
-        },
-      ],
+      // "@intlify/vue-i18n/no-unused-keys": [
+      //   "error",
+      //   {
+      //     extensions: [".js", ".vue"],
+      //   },
+      // ],
       // allow debugger during development only
       "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
     },

+ 5 - 0
src/api/clientFavoriteProvider.js

@@ -5,6 +5,11 @@ export const getClientFavoriteProviders = async (clientId) => {
   return data.payload;
 }
 
+export const createClientFavoriteProvider = async (info) => {
+  const { data } = await api.post(`/client/favorite-provider`, info);
+  return data.payload;
+}
+
 export const deleteClientFavoriteProvider = async (id) => {
   const { data } = await api.delete(`/client/favorite-provider/${id}`);
   return data.payload;

+ 11 - 0
src/api/clientProviderBlock.js

@@ -0,0 +1,11 @@
+import api from 'src/api'
+
+export const getClientProviderBlocks = async (clientId) => {
+  const { data } = await api.get(`/client-provider-blocks/${clientId}`)
+  return data.payload
+}
+
+export const deleteClientProviderBlock = async (id) => {
+  const { data } = await api.delete(`/client-provider-blocks/${id}`)
+  return data.payload
+}

+ 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;

+ 10 - 0
src/api/review.js

@@ -4,3 +4,13 @@ export const getProviderReceivedReviews = async (providerId) => {
   const { data } = await api.get(`/reviews/provider/${providerId}/received`)
   return data.payload
 }
+
+export const createReview = async (reviewData) => {
+  const { data } = await api.post('/reviews', reviewData)
+  return data.payload
+}
+
+export const getImprovementTypes = async (origin = 'both') => {
+  const { data } = await api.get('/improvement-types', { params: { origin } })
+  return data.payload
+}

+ 5 - 0
src/api/schedule.js

@@ -20,3 +20,8 @@ export const getClientProviderBlocks = async (clientId, providerId) => {
 
   return data.payload;
 };
+
+export const cancelSchedule = async (id, cancelText) => {
+  const { data } = await api.patch(`/schedule/${id}/cancel`, { cancel_text: cancelText })
+  return data.payload
+}

+ 10 - 35
src/components/dashboard/DashboardFavoriteProviders.vue

@@ -23,7 +23,6 @@
                 <q-icon name="mdi-star" color="warning" size="sm" />
                 <span class="text-fav-name">{{ Number(item.average_rating).toFixed(1) }}</span>
               </div>
-              <!-- <span class="text-fav-price">{{ bestPrice(item) }}</span> -->
             </div>
             <div class="col-4 column q-mt-auto">
               <q-btn
@@ -31,6 +30,7 @@
                 padding="1px 5px"
                 size="sm"
                 :label="$t('dashboard_client.favorites.view_schedule')"
+                @click="goToScheduling(item)"
               />
             </div>
           </div>
@@ -41,18 +41,12 @@
 </template>
 
 <script setup>
-// import { formatCurrency } from 'src/helpers/utils';
-// import { useI18n } from 'vue-i18n';
-
-// import { formatCurrency } from 'src/helpers/utils';
-// import { useI18n } from 'vue-i18n';
+import { useQuasar } from 'quasar';
+import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
 
 defineProps({ data: { type: Array, default: () => [] } });
 
-// const { t } = useI18n();
-
-// const { t } = useI18n();
-
+const $q = useQuasar();
 const avatarColors = [
   { background: '#ffd5df', color: '#932e57' },
   { background: '#d7e8ff', color: '#2158a8' },
@@ -60,31 +54,12 @@ const avatarColors = [
   { background: '#ffe5cc', color: '#8a4500' },
 ];
 
-// const bestPrice = (item) => {
-//   const prices = [
-//     item.daily_price_2h,
-//     item.daily_price_4h,
-//     item.daily_price_6h,
-//     item.daily_price_8h,
-//   ].filter(p => p != null && Number(p) > 0);
-
-//   if (!prices.length) return t('dashboard_client.favorites.no_price');
-//   const min = Math.min(...prices.map(Number));
-//   return t('dashboard_client.favorites.from') + ' ' + formatCurrency(min);
-// };
-
-/*const bestPrice = (item) => {
-  const prices = [
-    item.daily_price_2h,
-    item.daily_price_4h,
-    item.daily_price_6h,
-    item.daily_price_8h,
-  ].filter(p => p != null && Number(p) > 0);
-
-  if (!prices.length) return t('dashboard_client.favorites.no_price');
-  const min = Math.min(...prices.map(Number));
-  return t('dashboard_client.favorites.from') + ' ' + formatCurrency(min);
-};*/
+const goToScheduling = (provider) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+};
 </script>
 
 <style scoped lang="scss">

+ 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();
 

+ 17 - 5
src/components/dashboard/DashboardPendingSchedules.vue

@@ -7,8 +7,7 @@
           :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)"
+          @click="seeDetails(item)"
         >
           <q-card-section class="q-pa-md">
 
@@ -19,8 +18,14 @@
 
               <div class="col column no-wrap overflow-hidden">
                 <span class="text-body2 text-text">
-                  {{ $t('dashboard_client.pending_schedules.requesting_with') }}
-                  <span class="text-weight-bold">{{ item.provider_name ?? '—' }}</span>
+                  <span v-if="item.status == 'pending'">
+                    {{ $t('dashboard_client.pending_schedules.requesting_with') }}
+                  </span>
+                  <span v-else-if="item.status == 'accepted'">
+                    {{ $t('dashboard_client.pending_schedules.pay_to_provider') }}
+                  </span>
+                  
+                  <span class="text-weight-bold">{{ ' ' +  item.provider_name ?? '—' }}</span>
                 </span>
                 <div class="row items-center q-mt-xs">
                   <q-icon name="mdi-clock-outline" size="13px" color="grey-5" class="q-mr-xs" />
@@ -49,6 +54,7 @@
                 color="primary"
                 size="sm"
                 class="q-mr-sm flex-shrink-0"
+                @click="$emit('cancel', item)"
               />
               <q-space />
               <q-icon name="mdi-map-marker-outline" size="13px" color="grey-6" class="q-mr-xs flex-shrink-0" />
@@ -82,7 +88,13 @@ const statusProgressMap = {
 const progressPercent = (status) => statusProgressMap[status] ?? 20;
 
 defineProps({ data: { type: Array, default: () => [] } });
-const emit = defineEmits(['view-details']);
+const emit = defineEmits(['view-details', 'cancel']);
+
+const seeDetails = (item) => {
+  if (item.status === 'accepted') {
+    emit('view-details', item);
+  }
+};
 </script>
 
 <style scoped lang="scss">

+ 12 - 1
src/components/dashboard/DashboardProvidersClose.vue

@@ -57,6 +57,7 @@
                     size="sm"
                     padding="3px 12px"
                     :label="$t('dashboard_client.providers_close.schedule')"
+                    @click="goToScheduling(p)"
                   />
                 </div>
               </div>
@@ -69,14 +70,16 @@
 </template>
 
 <script setup>
+import { useQuasar } from 'quasar';
 import { formatCurrency } from 'src/helpers/utils';
+import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
 import { ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 
 defineProps({ data: { type: Array, default: () => [] } });
 
+const $q = useQuasar();
 const { t } = useI18n();
-
 const currentPeriodType = ref(8);
 const periodTypeMap = ref({
   2: 'daily_price_2h',
@@ -136,6 +139,14 @@ const avatarColors = [
   { background: '#dfd',    color: '#2a7a3b' },
   { background: '#ffe5cc', color: '#8a4500' },
 ];
+
+
+const goToScheduling = (provider) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+};
 </script>
 
 <style scoped lang="scss">

+ 228 - 0
src/components/dashboard/DashboardTodaySchedules.vue

@@ -0,0 +1,228 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <div class="scroll-wrapper">
+      <div class="scroll-track">
+        <q-card
+          v-for="item in data"
+          :key="item.id"
+          class="today-card card-border shadow-card bg-surface"
+          :flat="false"
+        >
+          <q-card-section class="q-pa-md">
+            <div class="row no-wrap items-start q-mb-xs">
+              <q-avatar size="40px" class="flex-shrink-0 q-mr-sm">
+                <img v-if="item.provider_photo" :src="item.provider_photo" />
+                <span v-else :style="avatarColors[item.id % avatarColors.length]" class="text-weight-bold full-width full-height flex flex-center" style="font-size:14px; border-radius:50%;">
+                  {{ item.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+                </span>
+              </q-avatar>
+              <div class="col column no-wrap overflow-hidden justify-center">
+                <span class="text-body2 text-text leading-tight">
+                  <template v-if="cardState(item) === 'awaiting_code'">{{ $t('dashboard_client.today_schedules.start_with') }}</template>
+                  <template v-else-if="cardState(item) === 'in_progress'">{{ $t('dashboard_client.today_schedules.started_by') }}</template>
+                  <template v-else>{{ $t('dashboard_client.today_schedules.finished_by') }}</template>
+                  <span class="text-weight-bold"> {{ ' ' +  item.provider_name ?? '—' }}</span>
+                </span>
+                <div class="row items-center q-mt-xs">
+                  <q-icon name="mdi-clock-outline" size="13px" class="q-mr-xs gradient-diarista" />
+                  <span class="text-caption text-grey-5">
+                    <template v-if="cardState(item) !== 'finished'">{{ $t('dashboard_client.next_schedules.from') }} </template>
+                    {{ item.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ item.end_time?.slice(0, 5) }}
+                  </span>
+                </div>
+              </div>
+              <div class="flex-shrink-0 q-ml-sm column items-center justify-start">
+                <template v-if="cardState(item) === 'awaiting_code'">
+                  <div class="column items-center">
+                    <span class="text-caption text-primary q-mb-xs">{{ $t('dashboard_client.today_schedules.code_label') }}</span>
+                    <div class="code-pill bg-primary">{{ item.code }}</div>
+                  </div>
+                </template>
+
+                <template v-else-if="cardState(item) === 'in_progress'">
+                  <div class="column items-center">
+                    <div class="clock-badge">
+                      <q-icon name="mdi-clock-outline" size="18px" color="white" />
+                    </div>
+                    <span class="badge-status-text text-primary text-weight-bold q-mt-xs">
+                      {{ $t('dashboard_client.today_schedules.in_progress') }}
+                    </span>
+                  </div>
+                </template>
+
+                <template v-else>
+                  <div v-if="item.client_reviewed" class="rate-btn reviewed-badge">
+                    <q-icon name="mdi-star" size="14px" class="q-mr-xs" />
+                    {{ $t('dashboard_client.schedule_rating.reviewed_badge') }}
+                  </div>
+                  <q-btn
+                    v-else
+                    unelevated no-caps
+                    class="rate-btn"
+                    icon="mdi-star-outline"
+                    :label="$t('dashboard_client.today_schedules.rate_btn')"
+                    @click.stop="emit('rate', item)"
+                  />
+                </template>
+              </div>
+            </div>
+
+            <div class="progress-track q-mb-sm">
+              <div class="progress-fill" :class="cardState(item) === 'finished' ? 'progress-fill--finished' : ''" :style="{ width: progressByState(item) + '%' }" />
+            </div>
+
+            <div class="row items-center no-wrap">
+              <template v-if="cardState(item) !== 'finished'">
+                <q-btn
+                  flat no-caps dense
+                  :label="$t('dashboard_client.today_schedules.help_btn')"
+                  color="primary"
+                  size="sm"
+                  class="flex-shrink-0"
+                  @click.stop="openHelp"
+                />
+                <q-space />
+                <template v-if="cardState(item) === 'awaiting_code'">
+                  <q-icon name="mdi-map-marker-outline" size="13px" color="grey-6" class="q-mr-xs flex-shrink-0" />
+                  <span class="text-caption text-grey-6 col ellipsis text-right">
+                    {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
+                  </span>
+                </template>
+                <template v-else>
+                  <q-icon name="mdi-clock-outline" size="13px" class="q-mr-xs flex-shrink-0 gradient-diarista" />
+                  <span class="text-caption text-grey-6 text-right text-no-wrap">
+                    {{ $t('dashboard_client.today_schedules.end_time_label') }} 
+                    <span class="gradient-diarista">{{ item.end_time?.slice(0, 5) }}</span>
+                  </span>
+                </template>
+              </template>
+            </div>
+          </q-card-section>
+        </q-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useQuasar } from 'quasar'
+import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
+
+defineProps({ data: { type: Array, default: () => [] } })
+const emit = defineEmits(['rate'])
+
+const $q = useQuasar()
+
+const avatarColors = [
+  { background: '#ffd5df', color: '#932e57' },
+  { background: '#d7e8ff', color: '#2158a8' },
+  { background: '#dfd',    color: '#2a7a3b' },
+  { background: '#ffe5cc', color: '#8a4500' },
+]
+
+const cardState = (item) => {
+  if (!item.code_verified) return 'awaiting_code'
+  const [h, m] = (item.end_time || '23:59').slice(0, 5).split(':').map(Number)
+  const endTime = new Date()
+  endTime.setHours(h, m, 0, 0)
+  return new Date() >= endTime ? 'finished' : 'in_progress'
+}
+
+const progressByState = (item) => {
+  const state = cardState(item)
+  if (state === 'awaiting_code') return 60
+  if (state === 'in_progress')   return 80
+  return 100
+}
+
+const openHelp = () => {
+  $q.dialog({ component: ProfileHelpDialog })
+}
+</script>
+
+<style scoped lang="scss">
+.scroll-wrapper { overflow: hidden; }
+.scroll-track {
+  display: flex;
+  flex-direction: row;
+  gap: 12px;
+  overflow-x: auto;
+  overscroll-behavior-x: contain;
+  scroll-snap-type: x proximity;
+  padding-bottom: 8px;
+  &::-webkit-scrollbar { display: none; }
+  &::after { content: ''; flex: 0 0 1px; }
+}
+
+.today-card {
+  min-width: 80%;
+  scroll-snap-align: start;
+  border-radius: 12px;
+}
+
+.code-pill {
+  color: white;
+  font-weight: 700;
+  font-size: 15px;
+  letter-spacing: 2px;
+  border-radius: 20px;
+  padding: 4px 14px;
+}
+
+.clock-badge {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #8B5CF6, #EC4899);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.badge-status-text {
+  font-size: 10px;
+  white-space: nowrap;
+}
+
+.rate-btn {
+  background: #EC4899;
+  color: white;
+  font-weight: 700;
+  font-size: 13px;
+  white-space: nowrap;
+  border-radius: 10px !important;
+  min-width: 72px;
+  min-height: 56px;
+  padding: 6px 10px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+}
+
+.reviewed-badge {
+  cursor: default;
+  opacity: 0.85;
+  font-size: 11px;
+}
+
+.progress-track {
+  width: 100%;
+  height: 5px;
+  background: #E2E8F0;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  border-radius: 3px;
+  background: linear-gradient(90deg, #8B5CF6, #EC4899);
+  transition: width 0.4s ease;
+
+  &--finished {
+    background: #22c55e;
+  }
+}
+</style>

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

+ 139 - 0
src/components/dashboard/ScheduleCancelDialog.vue

@@ -0,0 +1,139 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="cancel-dialog-card bg-surface shadow-card" :flat="false">
+
+      <div class="row justify-end q-pt-sm q-pr-sm">
+        <q-btn flat round dense icon="close" color="grey-6" size="sm" @click="onDialogCancel" />
+      </div>
+
+      <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') }}
+        </div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none q-pb-sm q-px-lg">
+        <div class="text-body2 text-grey-8 text-weight-bold q-mb-xs">
+          {{ $t('provider.dashboard.cancel_schedule.reason_label') }}
+        </div>
+        <q-input
+          v-model="cancelText"
+          type="textarea"
+          outlined
+          dense
+          :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
+        />
+      </q-card-section>
+
+      <q-card-section class="q-pt-xs q-pb-md q-px-lg">
+        <div class="warning-box row no-wrap q-gutter-x-sm q-pa-sm">
+          <q-icon name="mdi-alert-outline" color="secondary" size="22px" class="q-mt-xs flex-shrink-0" />
+          <div>
+            <span class="text-caption text-weight-bold text-grey-9">
+              {{ $t('provider.dashboard.cancel_schedule.warning_title') }}
+            </span>
+            <span class="text-caption text-grey-8">
+              {{ ' ' + $t('provider.dashboard.cancel_schedule.warning_free') }}
+            </span>
+            <br />
+            <span class="text-caption text-grey-8">
+              {{ $t('provider.dashboard.cancel_schedule.warning_fee') }}
+            </span>
+          </div>
+        </div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none q-pb-lg q-px-lg">
+        <div class="row justify-center q-gutter-x-md">
+          <q-btn
+            unelevated
+            rounded
+            no-caps
+            class="btn-action bg-grey-3 text-grey-8"
+            :label="$t('provider.dashboard.cancel_schedule.btn_back')"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            unelevated
+            rounded
+            no-caps
+            color="secondary"
+            class="btn-action"
+            :loading="loading"
+            :disable="!cancelText || cancelText.trim().length < 5"
+            :label="$t('provider.dashboard.cancel_schedule.btn_keep')"
+            @click="confirmCancel"
+          />
+        </div>
+      </q-card-section>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { cancelSchedule } from 'src/api/schedule'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true
+  }
+})
+
+const { t } = useI18n()
+const $q = useQuasar()
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
+
+const cancelText = ref('')
+const loading = ref(false)
+
+const confirmCancel = async () => {
+  if (!cancelText.value || cancelText.value.trim().length < 5) return
+  loading.value = true
+  try {
+    await cancelSchedule(props.schedule.id, cancelText.value.trim())
+    dialogRef.value.hide()
+  } catch {
+    $q.notify({ message: t('http.errors.failed'), color: 'negative' })
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.cancel-dialog-card {
+  width: 340px;
+  max-width: 94vw;
+  border-radius: 20px !important;
+  overflow: hidden;
+}
+
+.cancel-title {
+  font-size: 18px;
+  line-height: 1.35;
+}
+
+.warning-box {
+  background: #e8f4fd;
+  border-radius: 10px;
+}
+
+.btn-action {
+  min-width: 110px;
+  font-weight: 700;
+}
+
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+</style>

+ 252 - 0
src/components/dashboard/ScheduleRatingDialog.vue

@@ -0,0 +1,252 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="rating-dialog-card bg-surface shadow-card" :flat="false">
+
+      <div class="row justify-end q-pt-sm q-pr-sm">
+        <q-btn flat round dense icon="close" color="grey-6" size="sm" @click="onDialogCancel" />
+      </div>
+
+      <!-- Avatar -->
+      <div class="column items-center q-pb-sm">
+        <q-avatar size="64px" class="q-mb-sm">
+          <span
+            :style="avatarStyle"
+            class="text-weight-bold full-width full-height flex flex-center"
+            style="font-size: 20px; border-radius: 50%;"
+          >
+            {{ initials }}
+          </span>
+        </q-avatar>
+        <div class="text-body1 text-text  text-weight-bold text-center q-px-lg" style="line-height:1.3">
+          {{ $t('dashboard_client.schedule_rating.title') }}
+          <span class="text-primary"> {{ schedule.provider_name + '?' }}</span>
+        </div>
+      </div>
+
+      <!-- Estrelas -->
+      <div class="column items-center q-pb-xs">
+        <q-rating
+          v-model="stars"
+          :max="5"
+          size="lg"
+          color="amber"
+          icon="mdi-star-outline"
+          icon-selected="mdi-star"
+          @update:model-value="onStarsChange"
+        />
+      </div>
+
+      <!-- Tags de melhoria/qualidade -->
+      <q-card-section v-if="stars > 0" class="q-pt-xs q-pb-xs">
+        <div class="text-caption text-grey-7 text-center q-mb-sm">
+          {{ isNegative ? $t('dashboard_client.schedule_rating.negative_label') : $t('dashboard_client.schedule_rating.positive_label') }}
+        </div>
+        <div v-if="loadingTags" class="row justify-center q-py-sm">
+          <q-spinner-dots color="primary" size="24px" />
+        </div>
+        <div v-else class="row justify-center q-gutter-xs">
+          <div
+            v-for="tag in tags"
+            :key="tag.id"
+            class="tag-pill"
+            :class="{ 'tag-pill--selected': selectedTagIds.includes(tag.id) }"
+            @click="toggleTag(tag.id)"
+          >
+            {{ tag.description }}
+          </div>
+        </div>
+      </q-card-section>
+
+      <!-- Comentário -->
+      <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
+        <div class="text-caption text-grey-7 q-mb-xs">
+          {{ $t('dashboard_client.schedule_rating.comment_placeholder') }}
+        </div>
+        <q-input
+          v-model="comment"
+          type="textarea"
+          outlined
+          dense
+          rows="3"
+          color="primary"
+          input-class="text-black"
+          hide-bottom-space
+        />
+      </q-card-section>
+
+      <!-- Checkbox condicional -->
+      <q-card-section v-if="stars > 0" class="q-pt-xs q-pb-xs q-px-lg">
+        <q-checkbox
+          v-model="checkboxValue"
+          :label="isNegative ? $t('dashboard_client.schedule_rating.block_label') : $t('dashboard_client.schedule_rating.favorite_label')"
+          color="primary"
+          keep-color
+          class="text-text"
+          checked-icon="mdi-check-circle"
+          unchecked-icon="mdi-checkbox-blank-circle-outline"
+        />
+      </q-card-section>
+
+      <!-- Botão enviar -->
+      <q-card-section class="q-pt-sm q-pb-xs q-px-lg row">
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          full-width
+          color="primary"
+          class="submit-btn col-12"
+          :label="$t('dashboard_client.schedule_rating.submit_btn')"
+          :loading="loading"
+          :disable="stars === 0"
+          @click="submit"
+        />
+      </q-card-section>
+
+      <!-- Ajuda -->
+      <q-card-section class="q-pt-xs q-pb-lg text-center">
+        <span class="text-caption text-grey-6 cursor-pointer" @click="openHelp">
+          {{ $t('dashboard_client.schedule_rating.help_link') }}
+        </span>
+      </q-card-section>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { createReview, getImprovementTypes } from 'src/api/review'
+import { userStore } from 'src/stores/user'
+import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true
+  }
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const { t } = useI18n()
+const $q = useQuasar()
+const store = userStore()
+
+const stars = ref(0)
+const selectedTagIds = ref([])
+const comment = ref(null)
+const checkboxValue = ref(false)
+const tags = ref([])
+const loadingTags = ref(false)
+const loading = ref(false)
+
+const isNegative = computed(() => stars.value > 0 && stars.value <= 2)
+const isPositive = computed(() => stars.value >= 3)
+
+const avatarColors = [
+  { background: '#ffd5df', color: '#932e57' },
+  { background: '#d7e8ff', color: '#2158a8' },
+  { background: '#dfd',    color: '#2a7a3b' },
+  { background: '#ffe5cc', color: '#8a4500' },
+]
+
+const avatarStyle = computed(() => {
+  const c = avatarColors[props.schedule.provider_id % avatarColors.length]
+  return { background: c.background, color: c.color }
+})
+
+const initials = computed(() =>
+  props.schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??'
+)
+
+const onStarsChange = () => {
+  selectedTagIds.value = []
+  checkboxValue.value = false
+}
+
+const toggleTag = (id) => {
+  const idx = selectedTagIds.value.indexOf(id)
+  if (idx === -1) selectedTagIds.value.push(id)
+  else selectedTagIds.value.splice(idx, 1)
+}
+
+const openHelp = () => {
+  $q.dialog({ component: ProfileHelpDialog })
+}
+
+const submit = async () => {
+  if (stars.value === 0) return
+  loading.value = true
+  try {
+    await createReview({
+      schedule_id: props.schedule.id,
+      origin: 'client',
+      origin_id: store.user.client.id,
+      stars: stars.value,
+      comment: comment.value || null,
+      improvements_ids: selectedTagIds.value,
+      block_provider: isNegative.value && checkboxValue.value,
+      block_client: false,
+      favorite_provider: isPositive.value && checkboxValue.value,
+    })
+
+    onDialogOK(true)
+  } catch (error) {
+    const status = error?.response?.status
+    if (status === 422) {
+      $q.notify({ message: t('dashboard_client.schedule_rating.already_reviewed'), color: 'negative', icon: 'mdi-alert-circle-outline' })
+    } else {
+      $q.notify({ message: t('http.errors.failed'), color: 'negative' })
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(async () => {
+  loadingTags.value = true
+  try {
+    const result = await getImprovementTypes('client')
+    tags.value = result ?? []
+  } catch {
+    tags.value = []
+  } finally {
+    loadingTags.value = false
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.rating-dialog-card {
+  width: 320px;
+  max-width: 96vw;
+  border-radius: 20px !important;
+  overflow: hidden;
+}
+
+.tag-pill {
+  border: 1.5px solid #d1d5db;
+  border-radius: 20px;
+  padding: 5px 14px;
+  font-size: 12px;
+  color: #6b7280;
+  cursor: pointer;
+  transition: border-color 0.15s, color 0.15s;
+  user-select: none;
+
+  &--selected {
+    border-color: #8B5CF6;
+    color: #8B5CF6;
+    font-weight: 600;
+  }
+}
+
+.submit-btn {
+  font-size: 15px;
+  font-weight: 700;
+  padding: 10px 0;
+}
+</style>

+ 158 - 0
src/components/profile/ProfilePrivacyDialog.vue

@@ -0,0 +1,158 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+    <div class="bg-page full-height column no-shadow">
+
+      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-space />
+        <span class="text-subtitle1 text-weight-bold text-primary">{{ $t('profile.privacy.title') }}</span>
+        <q-space />
+        <div style="width: 32px"></div>
+      </div>
+
+      <div v-if="loading" class="col flex flex-center">
+        <q-spinner color="primary" size="3em" />
+      </div>
+
+      <div v-else-if="blocks.length === 0" class="col column items-center justify-center q-px-xl q-pb-xl">
+        <q-img
+          :src="diarinho"
+          style="width: 220px; height: 220px;"
+          fit="contain"
+          class="q-mb-lg"
+        />
+        <p class="text-text text-center text-weight-bold q-mb-xs" style="font-size: 15px;">
+          {{ $t('profile.privacy.empty_message') }}
+        </p>
+        <p class="text-grey-6 text-center q-mb-xl" style="font-size: 13px;">
+          {{ $t('profile.privacy.empty_sub') }}
+        </p>
+      </div>
+
+      <div v-else class="col overflow-auto q-pb-xl">
+        <div class="q-px-md q-mt-md">
+          <p class="text-weight-bold text-primary q-mb-md blocked-title">
+            {{ $t('profile.privacy.blocked_title') }}
+          </p>
+
+          <div
+            v-for="block in blocks"
+            :key="block.id"
+            class="block-item row items-center no-wrap q-mb-md"
+          >
+            <q-avatar size="48px" class="flex-shrink-0 q-mr-sm">
+              <span
+                :style="avatarStyle(block)"
+                class="text-weight-bold full-width full-height flex flex-center"
+                style="font-size: 16px; border-radius: 50%;"
+              >
+                {{ block.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+              </span>
+            </q-avatar>
+
+            <div class="col column no-wrap overflow-hidden">
+              <span class="text-weight-bold text-text" style="font-size: 14px; line-height: 1.3;">
+                {{ block.provider_name }}
+              </span>
+              <span v-if="block.provider_district" class="text-grey-6" style="font-size: 12px;">
+                {{ block.provider_district }}
+              </span>
+              <div v-if="block.provider_rating" class="row items-center q-mt-xs">
+                <q-icon name="mdi-star" color="amber" size="13px" class="q-mr-xs" />
+                <span class="text-grey-7" style="font-size: 12px;">{{ Number(block.provider_rating).toFixed(1) }}</span>
+              </div>
+            </div>
+
+            <q-btn
+              outline
+              no-caps
+              rounded
+              color="primary"
+              size="sm"
+              class="flex-shrink-0 unblock-btn"
+              :loading="unblockingId === block.id"
+              :label="$t('profile.privacy.unblock_btn')"
+              @click="unblock(block)"
+            />
+          </div>
+        </div>
+      </div>
+
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useDialogPluginComponent } from 'quasar'
+import { userStore } from 'src/stores/user'
+import { getClientProviderBlocks, deleteClientProviderBlock } from 'src/api/clientProviderBlock'
+import diarinho from 'src/assets/diarinho_perfil_cliente_favoritos.svg'
+
+defineEmits([...useDialogPluginComponent.emits])
+const { dialogRef } = useDialogPluginComponent()
+const store = userStore()
+
+const blocks = ref([])
+const loading = ref(false)
+const unblockingId = ref(null)
+
+const avatarColors = [
+  { background: '#ffd5df', color: '#932e57' },
+  { background: '#d7e8ff', color: '#2158a8' },
+  { background: '#dfd',    color: '#2a7a3b' },
+  { background: '#ffe5cc', color: '#8a4500' },
+  { background: '#ede0ff', color: '#6200ea' },
+]
+
+const avatarStyle = (block) => {
+  const idx = (block?.provider_id ?? 0) % avatarColors.length
+  return avatarColors[idx]
+}
+
+const unblock = async (block) => {
+  unblockingId.value = block.id
+  try {
+    await deleteClientProviderBlock(block.id)
+    blocks.value = blocks.value.filter(b => b.id !== block.id)
+  } finally {
+    unblockingId.value = null
+  }
+}
+
+onMounted(async () => {
+  loading.value = true
+  try {
+    blocks.value = await getClientProviderBlocks(store.user.client.id) ?? []
+  } finally {
+    loading.value = false
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.shadow-profile {
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
+}
+
+.blocked-title {
+  font-size: 18px;
+}
+
+.block-item {
+  background: white;
+  border-radius: 12px;
+  padding: 12px;
+  box-shadow: 0 1px 6px rgba(0,0,0,0.07);
+}
+
+.unblock-btn {
+  font-size: 12px;
+  font-weight: 600;
+  padding: 4px 14px;
+}
+
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+</style>

+ 31 - 1
src/i18n/locales/en.json

@@ -397,6 +397,16 @@
       "welcome": "Welcome,",
       "my_schedules": "My schedules"
     },
+    "today_schedules": {
+      "start_with": "Service starting with",
+      "started_by": "Service started by",
+      "finished_by": "Service completed by",
+      "code_label": "Code",
+      "in_progress": "in progress",
+      "end_time_label": "Ends at",
+      "rate_btn": "Rate",
+      "help_btn": "help"
+    },
     "next_schedules": {
       "title": "Next services",
       "no_provider": "No provider assigned",
@@ -410,7 +420,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",
@@ -436,6 +450,7 @@
     "pending_schedules": {
       "title": "Awaiting",
       "requesting_with": "Requesting booking with",
+      "pay_to_provider": "Make payment to",
       "no_provider": "Provider not defined",
       "cancel_btn": "cancel",
       "status": {
@@ -722,5 +737,20 @@
     "search": "Search",
     "agenda": "Schedule",
     "profile": "Profile"
+  },
+  "provider": {
+    "dashboard": {
+      "cancel_schedule": {
+        "title": "Are you sure you want to cancel your request?",
+        "reason_label": "What is the reason for cancellation?",
+        "reason_placeholder": "Describe the reason...",
+        "warning_title": "Attention!",
+        "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_back": "go back",
+        "btn_keep": "Cancel the service"
+      }
+    }
   }
 }

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

@@ -397,6 +397,16 @@
       "welcome": "Bienvenido (a),",
       "my_schedules": "Mis jornadas"
     },
+    "today_schedules": {
+      "start_with": "Servicio iniciado con",
+      "started_by": "Servicio iniciado por",
+      "finished_by": "Servicio concluido por",
+      "code_label": "Código",
+      "in_progress": "en progreso",
+      "end_time_label": "Término a las",
+      "rate_btn": "Avaliar",
+      "help_btn": "ayuda"
+    },
     "next_schedules": {
       "title": "Próximos servicios",
       "no_provider": "Sin prestador asignado",
@@ -436,6 +446,7 @@
     "pending_schedules": {
       "title": "En espera de confirmación",
       "requesting_with": "Solicitando reserva con",
+      "pay_to_provider": "Realizar pago a",
       "no_provider": "Proveedor no definido",
       "cancel_btn": "cancelar",
       "status": {
@@ -722,5 +733,20 @@
     "search": "Buscar",
     "agenda": "Agenda",
     "profile": "Perfil"
+  },
+  "provider": {
+    "dashboard": {
+      "cancel_schedule": {
+        "title": "¿Estás seguro de que deseas cancelar tu pedido?",
+        "reason_label": "¿Cuál es el motivo de la cancelación?",
+        "reason_placeholder": "Describe el motivo...",
+        "warning_title": "¡Atención!",
+        "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_back": "volver",
+        "btn_keep": "Cancelar el servicio"
+      }
+    }
   }
-}
+}

+ 51 - 1
src/i18n/locales/pt.json

@@ -402,6 +402,16 @@
       "welcome": "Bem-vindo (a),",
       "my_schedules": "Minhas diárias"
     },
+    "today_schedules": {
+      "start_with": "Início do serviço com",
+      "started_by": "Serviço iniciado por",
+      "finished_by": "Serviço concluído por",
+      "code_label": "Código",
+      "in_progress": "em andamento",
+      "end_time_label": "Término às",
+      "rate_btn": "Avaliar",
+      "help_btn": "ajuda"
+    },
     "next_schedules": {
       "title": "Próximos serviços",
       "no_provider": "Sem prestador definido",
@@ -415,7 +425,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",
@@ -441,6 +455,7 @@
     "pending_schedules": {
       "title": "Aguardando confirmação",
       "requesting_with": "Solicitando agendamento com",
+      "pay_to_provider": "Realize o pagamento para",
       "no_provider": "Prestador não definido",
       "cancel_btn": "cancelar",
       "status": {
@@ -458,6 +473,18 @@
       "detail_total": "Total:",
       "btn_payment": "ir para o pagamento",
       "btn_cancel": "Cancelar pedido"
+    },
+    "schedule_rating": {
+      "title": "Como foi o serviço de",
+      "positive_label": "O que mais gostou?",
+      "negative_label": "O que poderia melhorar?",
+      "comment_placeholder": "Deseja deixar um comentário?",
+      "favorite_label": "Favoritar este diarista",
+      "block_label": "Não solicitar mais este diarista",
+      "submit_btn": "enviar avaliação",
+      "help_link": "Ajuda",
+      "already_reviewed": "Você já avaliou este serviço.",
+      "reviewed_badge": "avaliado!"
     }
   },
   "profile": {
@@ -579,6 +606,14 @@
       "suggestion_payment": "Como funciona o pagamento?",
       "suggestion_human": "Falar com um humano"
     },
+    "privacy": {
+      "title": "Privacidade",
+      "description": "Usuários bloqueados",
+      "blocked_title": "Contas bloqueadas",
+      "empty_message": "Você não possui diaristas bloqueados",
+      "empty_sub": "Você pode bloquear diaristas que não deseja visualizar mais nas buscas.",
+      "unblock_btn": "desbloquear"
+    },
     "logout": {
       "title": "Sair",
       "description": "Desconectar da sua conta"
@@ -727,5 +762,20 @@
     "search": "Busca",
     "agenda": "Agenda",
     "profile": "Perfil"
+  },
+  "provider": {
+    "dashboard": {
+      "cancel_schedule": {
+        "title": "Tem certeza que deseja cancelar seu pedido?",
+        "reason_label": "Qual é o motivo do cancelamento?",
+        "reason_placeholder": "Descreva o motivo...",
+        "warning_title": "Atenção!",
+        "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_back": "voltar",
+        "btn_keep": "Cancelar o serviço"
+      }
+    }
   }
 }

+ 40 - 2
src/pages/dashboard/DashboardPage.vue

@@ -12,10 +12,12 @@
         v-if="pendingSchedules.length > 0"
         :data="pendingSchedules"
         @view-details="openAcceptedDialog"
+        @cancel="cancelSchedule"
       />
+      <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
       <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" />
@@ -33,12 +35,16 @@ import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedu
 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 DashboardTodaySchedules from 'src/components/dashboard/DashboardTodaySchedules.vue';
 import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
 import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
 import { useRouter } from 'vue-router'
 import { onMounted, ref } from 'vue';
-import { useQuasar } from 'quasar';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { dadosDashboard } from 'src/api/dashboard';
+import ScheduleCancelDialog from 'src/components/dashboard/ScheduleCancelDialog.vue';
+import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
+import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
 
 const router = useRouter()
 const headerBar = ref({});
@@ -48,10 +54,12 @@ const nextSchedules = ref([]);
 const lastDoneSchedules = ref([]);
 const favoriteProviders = ref([]);
 const providersClose = ref([]);
+const todaySchedules = ref([]);
 const $q = useQuasar();
 const loading = ref(true);
 
 const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || false);
+const { onDialogOK } = useDialogPluginComponent();
 
 const openAcceptedDialog = (schedule) => {
   $q.dialog({
@@ -73,6 +81,7 @@ const reloadDashboard = async () => {
     lastDoneSchedules.value = response.lastDoneSchedules ?? [];
     favoriteProviders.value = response.favoriteProviders ?? [];
     providersClose.value = response.providersClose ?? [];
+    todaySchedules.value = response.todaySchedules ?? [];
   }
   if( showSuccessModal.value ) {
     $q.dialog({
@@ -80,12 +89,41 @@ const reloadDashboard = async () => {
        })
 
     showSuccessModal.value = false;
+    router.replace({ path: router.currentRoute.value.path, query: {} });
   }
 
 
   loading.value = false;
 };
 
+const openNextScheduleDialog = (schedule) => {
+  $q.dialog({
+    component: NextSchedulesDetailsDialog,
+    componentProps: { schedule }
+  }).onOk(() => {
+    reloadDashboard();
+  });
+};
+
+const cancelSchedule = (schedule) => {
+  console.log(schedule)
+  $q.dialog({
+    component: ScheduleCancelDialog,
+    componentProps: { schedule: schedule }
+  }).onDismiss(() => {
+    onDialogOK(reloadDashboard());
+  })
+}
+
+const openRatingDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleRatingDialog,
+    componentProps: { schedule }
+  }).onOk(() => {
+    reloadDashboard()
+  })
+}
+
 onMounted(async () => {
   await reloadDashboard();
 });

+ 16 - 0
src/pages/profile/ProfilePage.vue

@@ -73,6 +73,15 @@
         <q-icon name="mdi-chevron-right" color="primary" size="md" />
       </div>
 
+      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openPrivacyDialog">
+        <div class="column">
+          <span class="menu-title gradient-diarista text-weight-bold">{{ $t('profile.privacy.title') }}</span>
+          <span class="menu-description text-text">{{ $t('profile.privacy.description') }}</span>
+        </div>
+        <q-space/>
+        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+      </div>
+
       <q-separator class="q-my-sm bg-grey-3" inset />
 
       <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="handleLogout">
@@ -97,6 +106,7 @@ import ProfileAddressDialog from 'src/components/profile/ProfileAddressDialog.vu
 import ProfilePaymentsDialog from 'src/components/profile/ProfilePaymentsDialog.vue';
 import ProfileFavoritesDialog from 'src/components/profile/ProfileFavoritesDialog.vue';
 import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue';
+import ProfilePrivacyDialog from 'src/components/profile/ProfilePrivacyDialog.vue';
 import { useRouter } from 'vue-router';
 
 const $q = useQuasar();
@@ -144,6 +154,12 @@ const openHelpDialog = () => {
   });
 };
 
+const openPrivacyDialog = () => {
+  $q.dialog({
+    component: ProfilePrivacyDialog
+  });
+};
+
 const handleLogout = async () => {
   await logout();
   router.push('/login');

+ 6 - 4
src/pages/search/components/OrderSummaryDialog.vue

@@ -1,5 +1,5 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+  <q-dialog ref="dialogRef" maximized transition-show="slide-up" transition-hide="slide-down">
     <div class="dialog-root">
 
       <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
@@ -104,6 +104,7 @@ import { createSchedule, getClientProviderBlocks } from 'src/api/schedule';
 import { userStore } from 'src/stores/user';
 import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
 import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
+import { useRouter } from 'vue-router';
 
 const props = defineProps({
   provider:       { type: Object, required: true },
@@ -112,7 +113,7 @@ const props = defineProps({
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef } = useDialogPluginComponent();
+const { dialogRef, onDialogOK } = useDialogPluginComponent();
 const $q = useQuasar();
 const { t, locale } = useI18n();
 const store = userStore();
@@ -120,7 +121,7 @@ const store = userStore();
 const bookings = ref([props.initialBooking]);
 const submitting = ref(false);
 const primaryAddress = ref(null);
-
+const router = useRouter();
 const showCalendar = ref(false);
 const addDateValue = ref(null);
 const loadingAvailability = ref(false);
@@ -322,7 +323,7 @@ const submitOrder = async () => {
   try {
     await createSchedule(payload);
     $q.notify({ type: 'positive', message: t('scheduling_page.order_summary.submit_success') });
-    dialogRef.value.hide();
+    onDialogOK();
   } catch (err) {
     const msg = err?.response?.data?.message
       ?? err?.message
@@ -330,6 +331,7 @@ const submitOrder = async () => {
     $q.notify({ type: 'negative', message: msg });
   } finally {
     submitting.value = false;
+    router.push({ name: 'DashboardPage' });
   }
 };
 </script>

+ 6 - 8
src/pages/search/components/SchedulingDialog.vue

@@ -1,5 +1,5 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+  <q-dialog ref="dialogRef" maximized transition-show="slide-up" transition-hide="slide-down">
     <div class="dialog-root">
 
       <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
@@ -19,7 +19,7 @@
           <q-card class="card-border shadow-card bg-surface text-text" :flat="false">
             <q-card-section class="q-pa-md">
               <div class="row items-center no-wrap q-gutter-x-md">
-                <q-avatar :style="avatarStyle" size="52px" class="text-weight-bold text-body1">
+                <q-avatar size="52px" class="text-weight-bold text-body1">
                   {{ provider?.provider_name?.slice(0, 1).toUpperCase() ?? '—' }}
                 </q-avatar>
                 <div class="col min-width-0">
@@ -96,7 +96,7 @@
             >
               <q-card-section class="q-pa-sm">
                 <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
-                  <q-avatar size="32px" :style="clientAvatarStyle(review)" class="text-weight-bold text-caption">
+                  <q-avatar size="32px" class="text-weight-bold text-caption">
                     {{ review.schedule?.client?.name?.slice(0, 1).toUpperCase() ?? '?' }}
                   </q-avatar>
                   <div class="col text-weight-medium text-text text-caption ellipsis">
@@ -302,21 +302,19 @@ const onDateSelected = (val) => {
   }).onOk((booking) => {
 
     bookings.value.push(booking);
-
     $q.dialog({
       component: OrderSummaryDialog,
       componentProps: {
         provider: props.provider,
         initialBooking: booking
       }
-    });
-
+    }).onOk(() => {
+      dialogRef.value.hide();
+    })
   });
-
 });
 };
 
-
 const loadAvailability = async () => {
   loadingAvailability.value = true;