Jelajahi Sumber

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

zntt 1 hari lalu
induk
melakukan
f828e199eb

+ 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",
     },

+ 11 - 0
src/api/providerClientBlock.js

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

+ 11 - 0
src/api/review.js

@@ -0,0 +1,11 @@
+import api from 'src/api'
+
+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
+}

File diff ditekan karena terlalu besar
+ 6 - 0
src/assets/diarinho_perfil_cliente_favoritos.svg


+ 300 - 181
src/components/dashboard/DashboardTodayServices.vue

@@ -1,174 +1,247 @@
 <template>
   <div v-if="props.data.length > 0" class="q-mx-md q-mb-md">
-    <q-card
-      v-for="item in props.data"
-      :key="item.id"
-      class="today-card card-border shadow-card bg-surface q-mb-sm"
-      :flat="false"
-    >
-      <q-card-section class="q-pa-sm">
-
-        <div class="row no-wrap items-center q-mb-sm">
-          <q-avatar size="40px" class="q-mr-sm">
-            <img :src="item.customer_photo || 'https://cdn.quasar.dev/img/avatar.png'" />
-          </q-avatar>
-          <div class="col column">
-            <span class="text-body2 text-text">
-              {{ $t('provider.dashboard.today_services.start_label') }}
-              <span class="text-weight-bold">{{ item.client_name }}</span>
-            </span>
-            <div class="row items-center q-gutter-x-xs q-mt-xs">
-              <q-icon name="mdi-clock-outline" color="grey-5" size="14px" />
-              <span class="text-caption text-grey-6">
-                {{ $t('common.from') }}
-                <strong class="text-text">{{ item.start_time?.slice(0, 5) }}</strong>
-                {{ $t('common.to') }}
-                <strong class="text-text">{{ item.end_time?.slice(0, 5) }}</strong>
-              </span>
-            </div>
-          </div>
-          <div class="col-auto text-caption text-grey-5 text-right q-pl-xs hint-text">
-            {{ $t('provider.dashboard.today_services.code_hint') }}
-          </div>
-        </div>
-
-        <div
-          class="code-container row justify-center q-gutter-x-sm q-mb-sm"
-          :class="{ 'code-disabled': item.code_verified || !canEnterCode(item) }"
-          @click="focusInput(item.id)"
+    <div class="scroll-wrapper">
+      <div class="scroll-track">
+        <q-card
+          v-for="item in props.data"
+          :key="item.id"
+          class="today-card card-border shadow-card bg-surface"
+          :flat="false"
         >
-          <div
-            v-for="i in 4"
-            :key="i"
-            class="code-box"
-            :class="{
-              'code-box--filled': (codes[item.id] || '').length >= i,
-              'code-box--verified': item.code_verified
-            }"
-          >
-            <template v-if="item.code_verified">
-              <q-icon v-if="i === 2" name="mdi-check-circle" color="positive" size="18px" />
-              <span v-else></span>
-            </template>
-            <span v-else>{{ (codes[item.id] || '')[i - 1] || '' }}</span>
-          </div>
-          <input
-            :id="`code-input-${item.id}`"
-            v-model="codes[item.id]"
-            type="tel"
-            inputmode="numeric"
-            maxlength="4"
-            class="code-real-input"
-            :disabled="item.code_verified || !canEnterCode(item)"
-            @input="onCodeInput(item)"
-          />
-        </div>
-
-        <q-linear-progress
-          :value="progressValue(item.status)"
-          color="secondary"
-          track-color="grey-3"
-          rounded
-          size="5px"
-          class="q-mb-sm"
-        />
-
-        <div class="row items-center">
-          <q-btn
-            flat
-            no-caps
-            color="primary"
-            size="sm"
-            class="q-px-none btn-help"
-            :label="$t('provider.dashboard.today_services.help')"
-            @click="openHelp"
-          />
-          <q-space />
-          <div class="row items-center no-wrap q-gutter-x-xs">
-            <q-icon name="mdi-map-marker-outline" color="grey-5" size="14px" />
-            <span class="text-caption text-grey-7 ellipsis address-text">
-              {{ formatAddressShort(item.address) }}
-            </span>
-            <q-btn
-              flat
-              round
-              dense
-              icon="mdi-content-copy"
-              color="primary"
-              size="xs"
-              @click.stop="copyAddress(item.address)"
-            />
-          </div>
-        </div>
-
-      </q-card-section>
-    </q-card>
+          <q-card-section class="q-pa-md">
+            <div class="row no-wrap items-start q-mb-xs">
+              <div class="col-7 row">
+                <q-avatar size="40px" class="flex-shrink-0 q-mr-sm">
+                  <span
+                    :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.client_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) === 'finished'">
+                      {{ $t('provider.dashboard.today_services.finished_label') }}
+                      <span class="text-weight-bold"> {{ ' ' + item.client_name ?? '—' }}</span>
+                      {{ ' ' + $t('provider.dashboard.today_services.finished_suffix') }}
+                    </template>
+                    <template v-else>
+                      {{ $t('provider.dashboard.today_services.start_label') }}
+                      <span class="text-weight-bold"> {{ ' ' + item.client_name ?? '—' }}</span>
+                    </template>
+                  </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">
+                      {{ $t('common.from') }} {{  item.start_time?.slice(0, 5) }} {{ $t('common.to') }} {{ item.end_time?.slice(0, 5) }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+
+              <div class="flex-shrink-0 row items-center justify-center col-5">
+                <div v-if="cardState(item) === 'awaiting_code'" class="col-12 row items-center justify-center q-pb-sm q-px-sm">
+                  <div class="hint-text text-caption text-text text-weight-bold text-center q-mb-xs col-12">
+                    {{ $t('provider.dashboard.today_services.code_hint1') }}
+                  </div>
+                  <div class="hint-text text-caption text-text text-weight-bold text-center q-mb-xs col-12">
+                    {{ $t('provider.dashboard.today_services.code_hint2') }}
+                  </div>
+                  <div class="code-input-row col-12">
+                    <input
+                      v-for="(_, idx) in 4"
+                      :key="idx"
+                      :ref="el => setCodeRef(item.id, idx, el)"
+                      v-model="codeInputs[item.id][idx]"
+                      class="code-input-box"
+                      type="tel"
+                      maxlength="1"
+                      inputmode="numeric"
+                      @input="onCodeInput(item, idx)"
+                      @keydown.delete="onCodeDelete(item.id, idx)"
+                      @paste.prevent="onCodePaste(item, $event)"
+                    />
+                  </div>
+                  <div v-if="codeError[item.id]" class="text-negative code-error-text q-mt-xs text-center">
+                    {{ codeError[item.id] }}
+                  </div>
+                </div>
+
+                <div v-else-if="cardState(item) === 'in_progress'" class="col-12 text-center q-mb-xs">
+                  <div class="column items-center">
+                    <span class="badge-status-text text-text text-weight-bold q-my-xs">
+                      {{ $t('provider.dashboard.today_services.in_progress') }}
+                    </span>
+                    <div class="code-pill bg-positive row">
+                      <q-icon
+                        name="mdi-check" size="14px" class="q-mr-xs q-my-auto"
+                      />
+                      {{ item.code || localVerified[item.id] }}
+                    </div>
+                  </div>
+                </div>
+
+                <div v-else>
+                  <div v-if="item.provider_reviewed" class="rate-btn reviewed-badge">
+                    <q-icon name="mdi-star" size="14px" class="q-mr-xs" />
+                    {{ $t('provider.dashboard.today_services.reviewed_badge') }}
+                  </div>
+                  <q-btn
+                    v-else
+                    unelevated no-caps
+                    class="rate-btn"
+                    icon="mdi-star-outline"
+                    :label="$t('provider.dashboard.today_services.rate_btn')"
+                    @click.stop="emit('rate', item)"
+                  />
+                </div>
+              </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">
+              <q-btn
+                flat no-caps dense
+                :label="$t('provider.dashboard.today_services.help')"
+                color="primary"
+                size="sm"
+                class="flex-shrink-0"
+                @click.stop="openHelp"
+              />
+              <q-space />
+              <template v-if="cardState(item) === 'in_progress'">
+                <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('provider.dashboard.today_services.end_time_label') }} 
+                  <span class="gradient-diarista">{{ item.end_time?.slice(0, 5) }}</span>
+                </span>
+              </template>
+              <template v-else-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>
+            </div>
+          </q-card-section>
+        </q-card>
+      </div>
+    </div>
   </div>
 </template>
 
 <script setup>
-import { ref, nextTick } from 'vue'
-import { useI18n } from 'vue-i18n'
+import { reactive } from 'vue'
 import { useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
 import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
 import { verifyScheduleCode } from 'src/api/schedule'
 
-const props = defineProps({
-  data: {
-    type: Array,
-    default: () => []
-  }
-})
+const props = defineProps({ data: { type: Array, default: () => [] } })
+const emit = defineEmits(['rate', 'refresh'])
 
-const emit = defineEmits(['refresh'])
-
-const { t } = useI18n()
 const $q = useQuasar()
+const { t } = useI18n()
+
+const codeInputs    = reactive({})
+const codeRefs      = reactive({})
+const codeError     = reactive({})
+const localVerified = reactive({})
+
+const ensureCode = (id) => {
+  if (!codeInputs[id]) codeInputs[id] = ['', '', '', '']
+  if (!codeRefs[id])   codeRefs[id]   = [null, null, null, null]
+}
 
-const codes = ref({})
-const loadingCode = ref({})
+props.data.forEach(item => ensureCode(item.id))
 
-const progressValue = (status) => {
-  const map = { accepted: 0.4, paid: 0.6, started: 0.8, finished: 1.0 }
-  return map[status] ?? 0.4
+const setCodeRef = (id, idx, el) => {
+  ensureCode(id)
+  codeRefs[id][idx] = el
 }
 
-const canEnterCode = (item) => ['paid', 'started'].includes(item.status)
+const onCodeInput = async (item, idx) => {
+  const id  = item.id
+  const val = codeInputs[id][idx]
+  if (!/^\d$/.test(val)) { codeInputs[id][idx] = ''; return }
+  codeError[id] = null
+  if (idx < 3) {
+    codeRefs[id][idx + 1]?.focus()
+  } else {
+    await submitCode(item)
+  }
+}
+
+const onCodeDelete = (id, idx) => {
+  if (codeInputs[id][idx] === '' && idx > 0) {
+    codeInputs[id][idx - 1] = ''
+    codeRefs[id][idx - 1]?.focus()
+  }
+}
 
-const focusInput = (id) => {
-  nextTick(() => document.getElementById(`code-input-${id}`)?.focus())
+const onCodePaste = async (item, e) => {
+  const text = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 4)
+  if (text.length !== 4) return
+  const id = item.id
+  text.split('').forEach((ch, i) => { codeInputs[id][i] = ch })
+  await submitCode(item)
 }
 
-const onCodeInput = async (item) => {
-  const val = codes.value[item.id] || ''
-  if (val.length < 4 || item.code_verified || !canEnterCode(item)) return
-  loadingCode.value[item.id] = true
+const submitCode = async (item) => {
+  const id   = item.id
+  const code = codeInputs[id].join('')
+  if (code.length < 4) return
   try {
-    const response = await verifyScheduleCode(item.id, val)
-    if (response?.data?.success || response?.success) {
-      $q.notify({ type: 'positive', message: t('provider.dashboard.today_services.code_success'), position: 'top' })
+    const response = await verifyScheduleCode(id, code)
+    console.log(response)
+    if (response?.payload) {
+      localVerified[id] = code
       emit('refresh')
     } else {
-      $q.notify({ type: 'negative', message: t('provider.dashboard.today_services.code_error'), position: 'top' })
-      codes.value[item.id] = ''
+      const msg = response?.data?.message || response?.message || t('provider.dashboard.today_services.code_error')
+      codeError[id] = msg
+      codeInputs[id] = ['', '', '', '']
+      codeRefs[id][0]?.focus()
     }
-  } catch {
-    $q.notify({ type: 'negative', message: t('provider.dashboard.today_services.code_error'), position: 'top' })
-    codes.value[item.id] = ''
-  } finally {
-    loadingCode.value[item.id] = false
+  } catch (err) {
+    const msg = err?.response?.data?.message || t('provider.dashboard.today_services.code_error')
+    codeError[id] = msg
+    codeInputs[id] = ['', '', '', '']
+    codeRefs[id][0]?.focus()
   }
 }
 
-const formatAddressShort = (address) => {
-  if (!address) return ''
-  return [address.address, address.number, address.district].filter(Boolean).join(', ')
+const avatarColors = [
+  { background: '#ffd5df', color: '#932e57' },
+  { background: '#d7e8ff', color: '#2158a8' },
+  { background: '#dfd',    color: '#2a7a3b' },
+  { background: '#ffe5cc', color: '#8a4500' },
+]
+
+const cardState = (item) => {
+  const verified = item.code_verified || !!localVerified[item.id]
+  if (!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 copyAddress = (address) => {
-  const text = formatAddressShort(address)
-  if (text) navigator.clipboard.writeText(text)
-  $q.notify({ message: t('provider.dashboard.next_schedules.address_copied'), color: 'positive', position: 'top' })
+const progressByState = (item) => {
+  const state = cardState(item)
+  if (state === 'awaiting_code') return 60
+  if (state === 'in_progress')   return 80
+  return 100
 }
 
 const openHelp = () => {
@@ -177,67 +250,113 @@ const openHelp = () => {
 </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;
 }
 
 .hint-text {
   max-width: 100px;
   line-height: 1.3;
-  font-size: 11px;
+  font-size: 9px;
 }
 
-/* OTP input */
-.code-container {
-  position: relative;
-  cursor: text;
-  user-select: none;
+.code-input-row {
+  display: flex;
+  gap: 5px;
 }
 
-.code-real-input {
-  position: absolute;
-  width: 1px;
-  height: 1px;
-  opacity: 0;
-  pointer-events: none;
-  top: 0;
-  left: 0;
+.code-input-box {
+  width: 26px;
+  height: 32px;
+  background: #d1d5db;
+  border: none;
+  border-radius: 6px;
+  text-align: center;
+  font-size: 15px;
+  font-weight: 700;
+  color: #1a1a2e;
+  outline: none;
+  caret-color: transparent;
+  &:focus {
+    background: #bec3cc;
+    box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.4);
+  }
 }
 
-.code-box {
-  width: 52px;
-  height: 44px;
-  background: #efefef;
-  border-radius: 10px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 22px;
-  font-weight: 700;
-  color: #3a3a4a;
-  transition: background 0.15s;
+.code-error-text {
+  font-size: 9px;
+  max-width: 110px;
+  line-height: 1.2;
+}
+
+.code-pill {
+  color: white;
+  font-weight: 800;
+  font-size: 15px;
+  letter-spacing: 2px;
+  border-radius: 20px;
+  padding: 1px 18px;
 }
 
-.code-box--filled {
-  background: #e0d8f8;
-  color: var(--q-secondary);
+.badge-status-text {
+  font-size: 11px;
+  
 }
 
-.code-box--verified {
-  background: #e8f5e9;
+.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;
 }
 
-.code-disabled .code-box {
-  opacity: 0.5;
-  cursor: not-allowed;
+.reviewed-badge {
+  cursor: default;
+  opacity: 0.85;
+  font-size: 11px;
 }
 
-.address-text {
-  max-width: 150px;
+.progress-track {
+  width: 100%;
+  height: 5px;
+  background: #E2E8F0;
+  border-radius: 3px;
+  overflow: hidden;
 }
 
-.btn-help {
-  font-weight: 700;
-  font-size: 13px;
+.progress-fill {
+  height: 100%;
+  border-radius: 3px;
+  background: linear-gradient(90deg, #8B5CF6, #EC4899);
+  transition: width 0.4s ease;
+
+  &--finished {
+    background: #22c55e;
+  }
 }
 </style>

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

@@ -0,0 +1,210 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="next-schedule-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="column items-center q-pt-xs q-pb-sm">
+        <q-avatar size="72px" class="q-mb-sm">
+          <img :src="schedule.customer_photo || 'https://cdn.quasar.dev/img/avatar.png'" />
+        </q-avatar>
+        <div class="text-subtitle1 text-weight-bold text-text">
+          {{ schedule.customer_name ?? schedule.client_name }}
+        </div>
+        <div class="text-price q-mt-xs">{{ formatCurrency(schedule.total_amount) }}</div>
+        <div class="text-caption text-grey-6 q-mt-xxs">{{ periodLabel }}</div>
+      </q-card-section>
+
+      <q-separator class="q-mx-lg" />
+
+      <q-card-section class="column items-center q-py-sm text-text">
+        <div class="text-body2 text-center">
+          <span class="text-weight-bold">{{ weekdayLabel }}</span>
+          <span class="text-grey-7">{{', ' + dayMonthLabel }}</span>
+        </div>
+        <div class="text-body2 text-center q-mt-xxs">
+          {{ $t('common.from') }}
+          <span class="text-weight-bold">{{ schedule.start_time?.slice(0, 5) }}</span>
+          {{ $t('common.to') }}
+          <span class="text-weight-bold">{{ schedule.end_time?.slice(0, 5) }}</span>
+        </div>
+      </q-card-section>
+
+      <q-separator class="q-mx-lg" />
+
+      <q-card-section class="column items-center q-py-sm">
+        <div class="row items-center q-gutter-x-xs">
+          <q-icon
+            :name="schedule.offers_meal ? 'mdi-silverware' : 'mdi-close-circle-outline'"
+            :color="schedule.offers_meal ? 'secondary' : 'grey-5'"
+            size="18px"
+          />
+          <span class="text-body2" :class="schedule.offers_meal ? 'text-text' : 'text-grey-6'">
+            {{ schedule.offers_meal
+              ? $t('provider.dashboard.next_schedules.offers_meal')
+              : $t('provider.dashboard.next_schedules.no_meal') }}
+          </span>
+        </div>
+      </q-card-section>
+
+      <q-separator class="q-mx-lg" />
+
+      <q-card-section class="column items-center q-py-sm q-px-lg">
+        <div class="row items-start q-gutter-x-xs no-wrap">
+          <q-icon name="mdi-map-marker-outline" color="primary" size="20px" class="q-mt-xxs flex-shrink-0" />
+          <span class="text-body2 text-grey-8 text-center">{{ formattedAddress || 'N/A' }}</span>
+        </div>
+        <q-btn
+          flat
+          no-caps
+          dense
+          color="primary"
+          icon="mdi-content-copy"
+          :label="$t('provider.dashboard.next_schedules.copy_address')"
+          class="q-mt-xs btn-copy"
+          @click="copyAddress"
+        />
+      </q-card-section>
+
+      <q-card-section class="q-pt-xs q-pb-sm q-px-lg">
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="primary"
+          class="full-width btn-close"
+          :label="$t('provider.dashboard.cancel_schedule.close')"
+          @click="onDialogCancel"
+        />
+      </q-card-section>
+
+      <q-card-section class="q-pt-none 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('provider.dashboard.next_schedules.btn_cancel_service')"
+            @click="openCancelDialog"
+          />
+          <q-btn
+            flat
+            no-caps
+            color="grey-7"
+            size="sm"
+            :label="$t('provider.dashboard.next_schedules.btn_need_help')"
+            @click="openHelp"
+          />
+        </div>
+      </q-card-section>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { formatCurrency, formatAddress } from 'src/helpers/utils'
+import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js'
+import ScheduleCancelDialog from './ScheduleCancelDialog.vue'
+import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true
+  }
+})
+
+const { t } = useI18n()
+const $q = useQuasar()
+const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } = useDialogPluginComponent()
+
+const periodLabel = computed(() => {
+  const found = labelsPeriodTypes.find(l => l.value == props.schedule.period_type)
+  return found ? t(found.label) : ''
+})
+
+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 weekdayLabel = computed(() => {
+  const d = parseLocalDate(props.schedule.date)
+  if (!d) return ''
+  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' })
+  return w.charAt(0).toUpperCase() + w.slice(1)
+})
+
+const dayMonthLabel = computed(() => {
+  const d = parseLocalDate(props.schedule.date)
+  if (!d) return ''
+  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
+})
+
+const formattedAddress = computed(() => formatAddress(props.schedule.address))
+
+const copyAddress = () => {
+  const addr = formattedAddress.value
+  if (addr) navigator.clipboard.writeText(addr)
+  $q.notify({ message: t('provider.dashboard.next_schedules.address_copied'), color: 'positive' })
+}
+
+const openCancelDialog = () => {
+  $q.dialog({
+    component: ScheduleCancelDialog,
+    componentProps: { schedule: props.schedule }
+  }).onOk(() => {
+    onDialogOK({ action: 'cancelled', id: props.schedule.id })
+  })
+}
+
+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;
+}
+
+.text-price {
+  font-size: 26px;
+  font-weight: 700;
+  color: var(--q-primary);
+}
+
+.btn-close {
+  font-weight: 700;
+  font-size: 16px;
+  padding: 10px 0;
+}
+
+.btn-copy {
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+
+.q-mt-xxs {
+  margin-top: 2px;
+}
+</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('provider.dashboard.schedule_rating.title') }}
+          <span class="text-primary"> {{ schedule.client_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('provider.dashboard.schedule_rating.negative_label') : $t('provider.dashboard.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('provider.dashboard.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 — apenas para avaliação negativa -->
+      <q-card-section v-if="isNegative" class="q-pt-xs q-pb-xs q-px-lg">
+        <q-checkbox
+          v-model="blockClient"
+          :label="$t('provider.dashboard.schedule_rating.block_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('provider.dashboard.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('provider.dashboard.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 blockClient = 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.client_id % avatarColors.length]
+  return { background: c.background, color: c.color }
+})
+
+const initials = computed(() =>
+  props.schedule.client_name?.slice(0, 2).toUpperCase() ?? '??'
+)
+
+const onStarsChange = () => {
+  selectedTagIds.value = []
+  blockClient.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: 'provider',
+      origin_id: store.user.provider.id,
+      stars: stars.value,
+      comment: comment.value || null,
+      improvements_ids: selectedTagIds.value,
+      block_provider: false,
+      block_client: isNegative.value && blockClient.value,
+      favorite_provider: false,
+    })
+
+    onDialogOK(true)
+  } catch (error) {
+    const status = error?.response?.status
+    if (status === 422) {
+      $q.notify({ message: t('provider.dashboard.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('provider')
+    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>

+ 2 - 1
src/components/dashboard/SolicitationDetailsDialog.vue

@@ -328,7 +328,8 @@ const fullDateLabel = computed(() => {
   return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })
 })
 
-const SERVICE_FEE_RATE = 0.06
+const SERVICE_FEE_RATE = 0.10
+
 const serviceFee = computed(() => {
   const total = parseFloat(props.solicitation.total_amount) || 0
   return parseFloat((total * SERVICE_FEE_RATE).toFixed(2))

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

@@ -0,0 +1,155 @@
+<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.client_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.client_name }}
+              </span>
+              <div v-if="block.client_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.client_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 { getProviderClientBlocks, deleteProviderClientBlock } from 'src/api/providerClientBlock'
+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?.client_id ?? 0) % avatarColors.length
+  return avatarColors[idx]
+}
+
+const unblock = async (block) => {
+  unblockingId.value = block.id
+  try {
+    await deleteProviderClientBlock(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 getProviderClientBlocks(store.user.provider.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>

+ 13 - 4
src/i18n/locales/en.json

@@ -254,7 +254,10 @@
       },
       "today_services": {
         "start_label": "Service start for",
-        "code_hint": "Ask the client for the code to begin",
+        "finished_label": "Service by",
+        "finished_suffix": "completed",
+        "code_hint1": "Ask the client for ",
+        "code_hint2": "the code to begin",
         "code_placeholder": "0000",
         "code_success": "Code verified successfully!",
         "code_error": "Invalid code. Please try again.",
@@ -262,6 +265,9 @@
         "step_paid": "Paid",
         "step_started": "Started",
         "step_finished": "Finished",
+        "in_progress": "Service in progress!",
+        "end_time_label": "Service ends at",
+        "rate_btn": "Rate",
         "help": "help"
       },
       "opportunities": {
@@ -295,7 +301,10 @@
         "place_home": "Residential",
         "place_apartment": "Apartment",
         "place_unknown": "Address",
-        "address_copied": "Address copied to clipboard"
+        "address_copied": "Address copied to clipboard",
+        "copy_address": "copy address",
+        "btn_cancel_service": "Cancel service",
+        "btn_need_help": "I need help"
       },
       "last_schedules": {
         "title": "Last ones performed",
@@ -308,8 +317,8 @@
         "warning_title": "Attention!",
         "warning_free": "Free cancellations up to 12 hours before the scheduled time.",
         "warning_fee": "Cancellations after this period result in a 50% refund as compensation for the professional.",
-        "btn_cancel": "cancel",
-        "btn_keep": "keep the order",
+        "btn_cancel": "Go back",
+        "btn_keep": "Cancel the service",
         "btn_cancel_order": "Cancel order",
         "help": "Help",
         "schedule_details_title": "Details",

+ 11 - 4
src/i18n/locales/es.json

@@ -254,7 +254,8 @@
       },
       "today_services": {
         "start_label": "Inicio del servicio para",
-        "code_hint": "Solicite al cliente el código para comenzar",
+        "code_hint1": "Solicite al cliente el ",
+        "code_hint2": "código para comenzar",
         "code_placeholder": "0000",
         "code_success": "¡Código verificado con éxito!",
         "code_error": "Código inválido. Inténtalo de nuevo.",
@@ -262,6 +263,9 @@
         "step_paid": "Pagado",
         "step_started": "Iniciado",
         "step_finished": "Concluido",
+        "in_progress": "Servicio en progreso!",
+        "end_time_label": "Término del servicio en",
+        "rate_btn": "Evaluar",
         "help": "ayuda"
       },
       "opportunities": {
@@ -295,7 +299,10 @@
         "place_home": "Residencial",
         "place_apartment": "Apartamento",
         "place_unknown": "Dirección",
-        "address_copied": "Dirección copiada al portapapeles"
+        "address_copied": "Dirección copiada al portapapeles",
+        "copy_address": "copiar dirección",
+        "btn_cancel_service": "Cancelar servicio",
+        "btn_need_help": "Preciso de ayuda"
       },
       "last_schedules": {
         "title": "Últimas realizadas",
@@ -308,8 +315,8 @@
         "warning_title": "¡Atención!",
         "warning_free": "Cancelaciones gratuitas hasta 12 horas antes del horario programado.",
         "warning_fee": "Las cancelaciones después de este período generan un reembolso del 50% como compensación al profesional.",
-        "btn_cancel": "cancelar",
-        "btn_keep": "mantener el pedido",
+        "btn_cancel": "Volver",
+        "btn_keep": "Cancelar el servicio",
         "btn_cancel_order": "Cancelar pedido",
         "help": "Ayuda",
         "schedule_details_title": "Detalles",

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

@@ -254,7 +254,10 @@
       },
       "today_services": {
         "start_label": "Início do serviço para",
-        "code_hint": "Solicite ao cliente o código para começar",
+        "finished_label": "Serviço de",
+        "finished_suffix": "concluído",
+        "code_hint1": "Solicite ao cliente o ",
+        "code_hint2": "código para começar",
         "code_placeholder": "0000",
         "code_success": "Código verificado com sucesso!",
         "code_error": "Código inválido. Tente novamente.",
@@ -262,7 +265,22 @@
         "step_paid": "Pago",
         "step_started": "Iniciado",
         "step_finished": "Concluído",
-        "help": "ajuda"
+        "in_progress": "Serviço em andamento!",
+        "end_time_label": "Término do serviço em",
+        "rate_btn": "Avaliar",
+        "help": "ajuda",
+        "reviewed_badge": "avaliado!"
+      },
+      "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?",
+        "block_label": "Não receber mais pedidos deste cliente",
+        "submit_btn": "enviar avaliação",
+        "help_link": "Ajuda",
+        "already_reviewed": "Você já avaliou este serviço.",
+        "reviewed_badge": "avaliado!"
       },
       "opportunities": {
         "title": "Oportunidades",
@@ -295,7 +313,10 @@
         "place_home": "Residencial",
         "place_apartment": "Apartamento",
         "place_unknown": "Endereço",
-        "address_copied": "Endereço copiado!"
+        "address_copied": "Endereço copiado!",
+        "copy_address": "copiar endereço",
+        "btn_cancel_service": "Cancelar serviço",
+        "btn_need_help": "Preciso de ajuda"
       },
       "last_schedules": {
         "title": "Últimas realizadas",
@@ -308,8 +329,8 @@
         "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_keep": "manter o pedido",
+        "btn_cancel": "Voltar",
+        "btn_keep": "Cancelar o serviço",
         "btn_cancel_order": "Cancelar pedido",
         "help": "Ajuda",
         "schedule_details_title": "Detalhes",
@@ -480,6 +501,14 @@
       "footer_disclaimer": "⚡ Respostas automáticas por IA • Atendimento humano disponível",
       "coming_soon": "Em breve disponível"
     },
+    "privacy": {
+      "title": "Privacidade",
+      "description": "Contas bloqueadas",
+      "blocked_title": "Contas bloqueadas",
+      "empty_message": "Você não possui clientes bloqueados",
+      "empty_sub": "Você pode bloquear clientes que não deseja receber novos pedidos.",
+      "unblock_btn": "Desbloquear"
+    },
     "logout": {
       "title": "Sair",
       "description": "Desconectar da sua conta"

+ 17 - 6
src/pages/dashboard/DashboardPage.vue

@@ -9,7 +9,7 @@
       <DashboardHeaderBar :data="headerBar" />
       <DashboardSummaryInfos :data="summaryInfos" />
       <DashboardPriceSuggest :data="priceSuggestion"/>
-      <DashboardTodayServices v-if="todayServices?.length > 0" :data="todayServices" @refresh="loadDashboard" />
+      <DashboardTodayServices v-if="todayServices?.length > 0" :data="todayServices" @refresh="loadDashboard" @rate="openRatingDialog" />
       <DashboardScrollAreaSchedules />
       <DashboardSolicitations
         v-if="solicitations?.length > 0"
@@ -34,6 +34,8 @@ import DashboardTodayServices from 'src/components/dashboard/DashboardTodayServi
 import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardOpportunities from 'src/components/dashboard/DashboardOpportunities.vue';
 import SolicitationDetailsDialog from 'src/components/dashboard/SolicitationDetailsDialog.vue';
+import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
+import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
 import { onMounted, ref } from 'vue';
 import { useQuasar } from 'quasar';
 import { dadosDashboard } from 'src/api/dashboard';
@@ -57,10 +59,10 @@ const loadDashboard = async () => {
     headerBar.value = response.headerBar;
     summaryInfos.value = response.summaryInfos;
     priceSuggestion.value = response.priceSuggested;
-    solicitations.value = response.solicitations;
+    solicitations.value = response.solicitations ?? [];
     todayServices.value = response.todayServices ?? [];
-    nextSchedules.value = response.nextSchedules;
-    opportunities.value = response.opportunities;
+    nextSchedules.value = response.nextSchedules ?? [];
+    opportunities.value = response.opportunities ?? [];
   }
 };
 
@@ -84,8 +86,8 @@ const openDetailsDialog = (solicitation, initialView = 'details') => {
 
 const openNextScheduleDialog = (schedule) => {
   $q.dialog({
-    component: SolicitationDetailsDialog,
-    componentProps: { solicitation: schedule }
+    component: NextSchedulesDetailsDialog,
+    componentProps: { schedule }
   }).onOk(async ({ action }) => {
     if (action === 'cancelled') {
       await loadDashboard();
@@ -93,6 +95,15 @@ const openNextScheduleDialog = (schedule) => {
   });
 };
 
+const openRatingDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleRatingDialog,
+    componentProps: { schedule }
+  }).onOk(() => {
+    loadDashboard()
+  })
+};
+
 onMounted(async () => {
   await loadDashboard();
   loading.value = false;

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

@@ -82,6 +82,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="logOutPrestador">
@@ -107,6 +116,7 @@ import ProfileBankDataDialog from 'src/components/profile/ProfileBankDataDialog.
 import ProfileAvailabilityDialog from 'src/components/profile/ProfileAvailabilityDialog.vue';
 import ProfileServiceDataDialog from 'src/components/profile/ProfileServiceDataDialog.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();
@@ -160,6 +170,12 @@ const openHelpDialog = () => {
   });
 };
 
+const openPrivacyDialog = () => {
+  $q.dialog({
+    component: ProfilePrivacyDialog
+  });
+};
+
 const logOutPrestador = async () => {
   await logout();
   router.push('/login');

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini