Gustavo Zanatta пре 1 дан
родитељ
комит
c12358434e

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

+ 36 - 3
src/api/opportunities.js

@@ -1,7 +1,40 @@
 import api from 'src/api'
 
 export const getProviderOpportunities = async (providerId) => {
-  const { data } = await api.get(`/custom-schedule-available?provider_id=${providerId}`)
+  try {
+    const { data } = await api.get(
+      `/custom-schedule-available`,
+      {
+        params: {
+          provider_id: providerId
+        }
+      }
+    )
 
-  return data.payload
-}
+    return data?.payload || []
+  } catch (error) {
+    console.error('[API] getProviderOpportunities error:', error)
+    return []
+  }
+}
+
+export const getOpportunityById = async (id) => {
+  try {
+    const { data } = await api.get(`/custom-schedule/${id}`)
+    return data?.payload || null
+  } catch (error) {
+    console.error('[API] getOpportunityById error:', error)
+    return null
+  }
+}
+
+
+export const proposalOpportunity = async (scheduleId, providerId) => {
+  try {
+    const { data } = await api.post(`/custom-schedule/${scheduleId}/propose`, { provider_id: providerId })
+    return data.payload || null
+  } catch (error) {
+    console.error('[API] proposalOpportunity error:', error)
+    return null
+  }
+} 

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

Разлика између датотеке није приказан због своје велике величине
+ 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>

+ 37 - 1
src/helpers/utils.js

@@ -225,6 +225,40 @@ const formatAddress = (address) => {
   return parts.join(', ');
 };
 
+const calculateDailyPrices = (dailyPrice8h) => {
+  if (!dailyPrice8h || dailyPrice8h <= 0) {
+    return {
+      daily_price_8h: null,
+      daily_price_6h: null,
+      daily_price_4h: null,
+      daily_price_2h: null,
+    };
+  }
+
+  return {
+    daily_price_8h: dailyPrice8h, 
+    daily_price_6h: dailyPrice8h * 0.85,
+    daily_price_4h: dailyPrice8h * 0.55,
+    daily_price_2h: dailyPrice8h * 0.30,
+  };
+};
+
+const chooseprice = (periodType, daily_price_8h) => {
+  let alldaily_prices = calculateDailyPrices(daily_price_8h);
+  switch (periodType) {
+    case "8":
+      return daily_price_8h
+    case "6":
+      return alldaily_prices.daily_price_6h
+    case "4":
+      return alldaily_prices.daily_price_4h
+    case "2":
+      return alldaily_prices.daily_price_2h
+    default:
+      return 0
+  }
+}
+
 export {
   formatDateDMYtoYMD,
   formatDateYMDtoDMY,
@@ -238,5 +272,7 @@ export {
   validaDataHora,
   formatQuantity,
   formatCurrency,
-  formatAddress
+  formatAddress,
+  calculateDailyPrices,
+  chooseprice,
 };

+ 30 - 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": {
@@ -275,6 +281,23 @@
         "address_not_found": "Address not available",
         "currency": "$ {value}"
       },
+      "opportunity_details": {
+        "title": "Service details",
+        "client_default": "Client",
+        "price_label": "Full day (up to 8h)",
+        "distance_text": "It is {distance} away from your registered address.",
+        "distance_default": "0 km",
+        "sob_medida": "Request made",
+        "sob_medida_highlight": "custom",
+        "para": "for",
+        "info_title": "Service information",
+        "description_not_found": "No description provided",
+        "address_not_found": "Address not provided",
+        "hour_not_found": "Time not provided",
+        "button_accept": "accept job",
+        "alert_text": "If your request is accepted by the client, you will receive a notification confirming the booking.",
+        "offers_meal": "On-site meal"
+      },
       "favorites": {
         "title": "Your favorites",
         "view_schedule": "View schedule"
@@ -295,7 +318,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 +334,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",

+ 28 - 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": {
@@ -275,6 +279,23 @@
         "address_not_found": "Dirección no informada",
         "currency": "R$ {value}"
       },
+      "opportunity_details": {
+        "title": "Detalles del servicio",
+        "client_default": "Cliente",
+        "price_label": "Jornada completa (hasta 8h)",
+        "distance_text": "Está a {distance} de distancia de su dirección registrada.",
+        "distance_default": "0 km",
+        "sob_medida": "Solicitud hecha",
+        "sob_medida_highlight": "a medida",
+        "para": "para",
+        "info_title": "Información del servicio",
+        "description_not_found": "Sin descripción proporcionada",
+        "address_not_found": "Dirección no informada",
+        "hour_not_found": "Horario no informado",
+        "button_accept": "quiero atender",
+        "alert_text": "Si el cliente acepta tu solicitud, recibirás una notificación confirmando el servicio.",
+        "offers_meal": "Comida en el lugar"
+      },
       "favorites": {
         "title": "Tus favoritos",
         "view_schedule": "Ver agenda"
@@ -295,7 +316,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 +332,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",

+ 51 - 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",
@@ -275,6 +293,23 @@
         "address_not_found": "Endereço não informado",
         "currency": "R$ {value}"
       },
+      "opportunity_details": {
+        "title": "Detalhes do serviço",
+        "client_default": "Cliente",
+        "price_label": "Integral (até 8h)",
+        "distance_text": "Há {distance} de distância do seu endereço cadastrado.",
+        "distance_default": "0 km",
+        "sob_medida": "Pedido feito",
+        "sob_medida_highlight": "sob medida",
+        "para": "para",
+        "info_title": "Informações do serviço",
+        "description_not_found": "Sem descrição informada",
+        "address_not_found": "Endereço não informado",
+        "hour_not_found": "Horário não informado",
+        "button_accept": "quero atender",
+        "alert_text": "Se seu pedido for aceito pelo cliente você receberá um aviso confirmando o agendamento.",
+        "offers_meal": "Refeição no local"
+      },
       "favorites": {
         "title": "Seus favoritos",
         "view_schedule": "Ver agenda"
@@ -295,7 +330,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 +346,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 +518,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;

+ 48 - 71
src/pages/opportunities/OpportunitiesPage.vue

@@ -1,14 +1,7 @@
 <template>
   <q-page class="opportunities-page">
     <div class="page-header">
-      <q-btn
-        flat
-        round
-        dense
-        icon="chevron_left"
-        class="back-btn"
-        @click="router.back()"
-      />
+      <q-btn flat round dense icon="chevron_left" class="back-btn" @click="router.back()" />
       <div class="page-title">
         {{ $t('provider.dashboard.opportunities.title') }}
       </div>
@@ -25,68 +18,60 @@
       <q-spinner-dots color="secondary" size="32px" />
     </div>
 
-    <div
-      v-else-if="!opportunities.length"
-      class="text-center q-pa-md text-grey"
-    >
+    <div v-else-if="!opportunities.length" class="text-center q-pa-md text-grey">
       {{ $t('provider.dashboard.opportunities.empty') }}
     </div>
 
     <div v-else class="opportunity-list">
-      <q-card
-        v-for="item in opportunities"
-        :key="item.id"
-        flat
-        class="opportunity-card"
-      >
+      <q-card v-for="item in opportunities" :key="item.id" flat class="opportunity-card">
         <div class="avatar-column">
           <img :src="item.avatar" class="client-avatar" />
           <div class="service-type">
-            {{ item.serviceType }}
+            {{ item.custom_schedule?.service_type.description }}
           </div>
         </div>
 
         <div class="center-content">
           <div class="client-name-row">
-            <span class="client-name">{{ item.clientName }}</span>
+            <span class="client-name">{{ item.client?.user.name }}</span>
 
+            <!-- campo de avaliação -->
             <span class="rating">
               <q-icon name="star" size="11px" />
-              {{ item.rating }}
+              {{ item.client?.average_rating }}
             </span>
           </div>
 
           <div class="service-date">
-            {{ item.date }}
+            {{ formatDate(item.date) }}
           </div>
 
           <div class="service-hour">
-            {{ item.hour }}
+           {{ `Das ${formatHour(item.start_time)} às ${formatHour(item.end_time)}` }}
           </div>
         </div>
 
         <div class="right-content">
           <div class="price">
-            {{ $t('provider.dashboard.opportunities.currency', { value: item.price }) }}
+            {{ $t('provider.dashboard.opportunities.currency', { value: chooseprice(item.period_type, user.user.provider.daily_price_8h) }) }}
           </div>
 
           <div class="service-address">
-            {{ item.address }}
+            {{ item.custom_schedule?.address_type }}
           </div>
 
+          <div class="district">
+             {{ item.address?.district }}
+          </div>
+
+
           <div class="distance">
             {{ $t('provider.dashboard.opportunities.distance_km', { distance: item.distance }) }}
           </div>
 
-          <q-btn
-            unelevated
-            rounded
-            no-caps
-            color="secondary"
-            :label="$t('provider.dashboard.opportunities.details')"
-            class="details-btn"
-            @click="goToOpportunityDetails(item)"
-          />
+          
+
+          <q-btn unelevated rounded no-caps color="secondary" :label="$t('provider.dashboard.opportunities.details')" class="details-btn" @click="goToOpportunityDetails(item)" />
         </div>
       </q-card>
     </div>
@@ -95,61 +80,47 @@
 <script setup>
 import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
-import { useI18n } from 'vue-i18n'
-
+import { chooseprice } from 'src/helpers/utils'
 import { getProviderOpportunities } from 'src/api/opportunities'
 import { userStore } from 'src/stores/user'
 
 const router = useRouter()
-const { t } = useI18n()
 const user = userStore()
 
 const opportunities = ref([])
 const loading = ref(false)
 
-const formatHour = (time) =>
-  time ? time.slice(0, 5).replace(':', 'h') : ''
-
-
-const normalizeOpportunity = (item) => ({
-  id: item.id,
 
-  avatar: item.client?.user?.photo || '/icons/avatar.svg',
-
-  clientName:
-    item.client?.user?.name ||
-    t('provider.dashboard.opportunities.client_default'),
-
-  rating: item.client?.average_rating || 5.0,
-
-  date: new Date(
-    item.custom_schedule?.created_at || item.created_at
-  ).toLocaleDateString(),
+const goToOpportunityDetails = (item) => {
 
-  hour: `${t('common.from')} ${formatHour(item.start_time)} ${t('common.to')} ${formatHour(item.end_time)}`,
+  const id = item.custom_schedule?.id || item.id
 
-  address:
-    item.address?.address ||
-    t('provider.dashboard.opportunities.address_not_found'),
+  router.push({
+    name: 'OpportunityDetailsPage',
+    params: { id },
+    state: { opportunity: item }
+  })
+}
 
-  serviceType:
-    item.custom_schedule?.service_type?.descritpion ||
-    t('provider.dashboard.opportunities.client_default'),
 
-  price: Number(
-    item.custom_schedule?.max_price || 0
-  ).toFixed(2),
 
-  distance: 0
-})
+// formatando a data
+const formatDate = (date) => {
+  if (!date) return ''
 
-const goToOpportunityDetails = (item) => {
-  router.push({
-    name: 'OpportunityDetailsPage',
-    params: { id: item.id }
+  return new Date(date).toLocaleDateString('pt-BR', {
+    weekday: 'long',
+    day: '2-digit',
+    month: '2-digit'
   })
 }
 
+// formatando hora para exibir só HH:mm
+const formatHour = (time) => {
+  if (!time) return ''
+  return time.slice(0, 5) 
+}
+
 const loadOpportunities = async () => {
   loading.value = true
 
@@ -158,7 +129,7 @@ const loadOpportunities = async () => {
       user.user.provider.id
     )
 
-    opportunities.value = (response || []).map(normalizeOpportunity)
+    opportunities.value = (response || [])
   } catch (error) {
     console.error('Erro ao buscar oportunidades:', error)
     opportunities.value = []
@@ -267,6 +238,12 @@ onMounted(loadOpportunities)
   color: #2d2d2d;
 }
 
+.district {
+  margin-top: 4px;
+  font-size: 11px;
+  color: #666;
+}
+
 .rating {
   display: flex;
   align-items: center;

+ 211 - 153
src/pages/opportunities/components/OpportunityDetailsPage.vue

@@ -1,169 +1,183 @@
 <template>
-  <q-page class="details-page">
+  <q-page v-if="details" class="details-page">
+
     <!-- HEADER -->
     <div class="page-header">
-      <q-btn
-        flat
-        round
-        dense
-        icon="chevron_left"
-        class="back-btn"
-        @click="router.back()"
-      />
-      <div class="page-title">{{ details.title }}</div>
+      <q-btn flat round dense icon="chevron_left" class="back-btn" @click="router.back()" />
+      <div class="page-title">
+        {{ $t('provider.dashboard.opportunity_details.title') }}
+      </div>
     </div>
 
     <!-- CLIENTE -->
     <div class="client-section">
-      <img :src="AvatarMock" class="client-avatar" />
-      <div class="client-name">{{ details.clientName }}</div>
-      <div class="client-price">{{ details.price }}</div>
+      <img :src="details.avatar" class="client-avatar" />
+
+      <div class="client-name">
+        {{ details.schedule?.client_name }}
+        <span class="rating"> {{ details.schedule?.rating }}</span>
+      </div>
+
+      <div class="client-price">{{ $t('provider.dashboard.opportunities.currency', { value: chooseprice(details.schedule?.period_type) }) }}</div>
+
+      <div class="date">  {{ formatDate(details.schedule?.date) }}</div>
+      <div class="hour">{{ formatHour(details.schedule?.start_time ) }} {{ formatHour(details.schedule?.end_time ) }}</div>
     </div>
 
-    <!-- INFOS -->
-    <div class="details-info">
-      <div>{{ details.date }}</div>
-      <div>{{ details.hour }}</div>
-      <div>{{ details.address }}</div>
-      <div>{{ details.distance }}</div>
+    <!-- ENDEREÇO -->
+    <div class="address">
+      <q-icon name="place" size="16px" />
+      {{ details.schedule?.address?.district }}
     </div>
 
-    <!-- TAGS -->
-    <div class="tags-row">
-      <q-chip dense color="grey-3">
-        {{ details.tags[0] }}
-      </q-chip>
+    <div class="distance">
+      {{ $t('provider.dashboard.opportunity_details.distance_text', { distance: details.schedule?.distance }) }}
+    </div>
 
-      <q-chip dense color="grey-3">
-        {{ details.tags[1] }}
+    <!-- TAGS -->
+    <div v-if="details.tags?.length" class="tags-row">
+      <q-chip v-for="(tag, index) in details.tags" :key="index" outline class="chip">
+        {{ tag }}
       </q-chip>
     </div>
 
+    <!-- INFO -->
+    <div class="service-type">
+  {{ $t('provider.dashboard.opportunity_details.sob_medida') }}
+  <span>
+    {{ $t('provider.dashboard.opportunity_details.sob_medida_highlight') }}
+  </span>
+  <br />
+  {{ $t('provider.dashboard.opportunity_details.para') }}
+  <strong class="highlight-service">
+    {{ details.service_type_name }}
+  </strong>
+</div>
+    
+
+
+    <div class="address-type">
+  <span class="chip-type">
+    {{ (details.address_type) }}
+  </span> 
+  <span v-if="details.offers_meal" class="chip-type">
+    {{ $t('provider.dashboard.opportunity_details.offers_meal') }}
+  </span>    
+</div>
+    
+
+
+     <div v-if="details.description" class="service-type gradient-diarista">
+  {{ $t('provider.dashboard.opportunity_details.info_title') }}
+  </div>
     <!-- DESCRIÇÃO -->
     <div class="description-box">
       {{ details.description }}
     </div>
 
     <!-- BOTÃO -->
-    <q-btn
-      unelevated
-      rounded
-      no-caps
-      color="secondary"
-      :label="details.buttonLabel"
-      class="full-width q-mt-md"
-      @click="goToProposalFlow"
-    />
+    <q-btn unelevated rounded no-caps class="accept-btn" :label="$t('provider.dashboard.opportunity_details.button_accept')" @click="goToProposalFlow" />
 
     <!-- ALERTA -->
-    <q-card flat class="bottom-alert">
-      {{ details.alertText }}
-    </q-card>
+    <div class="alert-box">
+      <q-icon name="warning" size="18px" class="alert-icon" />
+      <span class="alert-text">
+        {{ $t('provider.dashboard.opportunity_details.alert_text') }}
+      </span>
+    </div>
+
   </q-page>
 </template>
 
 <script setup>
 import { ref, onMounted } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
+import { userStore } from 'src/stores/user'
+import { getOpportunityById, proposalOpportunity } from 'src/api/opportunities'
 
-import AvatarMock from 'src/assets/foto_diarista_login.svg'
-import { getOpportunityById } from 'src/api/opportunities'
 
-// router
 const router = useRouter()
 const route = useRoute()
 
-// state
+const user = userStore()
 const details = ref(null)
-const loading = ref(false)
-
-// params
-const opportunityId = route.params.id
-
-// helpers
-const formatHour = (time) =>
-  time ? time.slice(0, 5).replace(':', 'h') : ''
-
-// normalize (PADRÃO EMPRESA)
-const normalizeDetails = (item) => ({
-  title: 'Detalhes do serviço',
-
-  avatar: item.client?.user?.photo || AvatarMock,
-
-  clientName:
-    item.client?.user?.name || 'Cliente',
-
-  price: `R$${Number(
-    item.custom_schedule?.max_price || 0
-  ).toFixed(2)}`,
-
-  date: new Date(
-    item.custom_schedule?.created_at || item.created_at
-  ).toLocaleDateString('pt-BR'),
-
-  hour: `Das ${formatHour(item.start_time)} às ${formatHour(item.end_time)}`,
-
-  address:
-    item.address?.address || 'Endereço não informado',
+const loading = ref(true)
+
+const chooseprice = (periodType) => {
+  switch (periodType) {
+    case "8":
+      return user.user.provider_daily_price_8h
+    case "6":
+      return user.user.provider_daily_price_6h
+    case "4":
+      return user.user.provider_daily_price_4h
+    case "2":
+      return user.user.provider_daily_price_2h
+    default:
+      return 0
+  }
+}
 
-  distance: '0 km',
+// formatando a data
+const formatDate = (date) => {
+  if (!date) return ''
 
-  tags: [
-    item.custom_schedule?.service_type?.description,
-    item.custom_schedule?.offers_meal
-      ? 'Refeição no local'
-      : null
-  ].filter(Boolean),
+  const [day, month, year] = date.split('/')
 
-  description:
-    item.custom_schedule?.description || '',
+  const parsedDate = new Date(`${year}-${month}-${day}`)
 
-  buttonLabel: 'Quero atender',
+  const formatted = parsedDate.toLocaleDateString('pt-BR', {
+    weekday: 'long',
+    day: '2-digit',
+    month: '2-digit'
+  })
 
-  alertText:
-    'Se seu pedido for aceito pelo cliente você receberá um aviso confirmando o agendamento e aparecerá nos seus próximos serviços.'
-})
+  return formatted.charAt(0).toUpperCase() + formatted.slice(1)
+}
 
-// load
-const loadDetails = async () => {
-  loading.value = true
+// formatando hora para exibir só HH:mm
+const formatHour = (time) => {
+  if (!time) return ''
+  return time.slice(0, 5) 
+}
 
+onMounted(async () => {
   try {
-    const response = await getOpportunityById(opportunityId)
-
-    console.log('DETAILS RESPONSE:', response)
-
-    details.value = normalizeDetails(response)
+    const id = route.params.id
+    const response = await getOpportunityById(id)
+
+    if (response) {
+      details.value = response
+    } else {
+      console.warn('Nenhum dado retornado')
+    }
   } catch (error) {
     console.error('Erro ao carregar detalhes:', error)
-    details.value = null
   } finally {
     loading.value = false
   }
-}
-
-// actions
-const goToProposalFlow = () => {
-  console.log('Ir para proposta', details.value)
-}
+})
 
-// lifecycle
-onMounted(loadDetails)
+const goToProposalFlow = async () => {
 
+  await proposalOpportunity(details.value.schedule_id, user.user.provider.id)
+  router.push({ name: 'DashboardPage' })
+}
 </script>
 
 <style scoped lang="scss">
 .details-page {
   padding: 16px;
-  background: #f7f7fb;
+  background: #f4f5f7;
   min-height: 100vh;
 }
 
+/* HEADER */
 .page-header {
   display: flex;
   justify-content: center;
   position: relative;
-  margin-bottom: 24px;
+  margin-bottom: 16px;
 }
 
 .back-btn {
@@ -173,108 +187,152 @@ onMounted(loadDetails)
 }
 
 .page-title {
-  font-size: 16px;
-  font-weight: 700;
+  font-size: 15px;
+  font-weight: 600;
   color: #7c5cff;
 }
 
+/* CLIENTE */
 .client-section {
   text-align: center;
-  margin-top: 8px;
 }
 
 .client-avatar {
-  width: 84px;
-  height: 84px;
+  width: 88px;
+  height: 88px;
   border-radius: 50%;
   object-fit: cover;
 }
 
 .client-name {
-  margin-top: 8px;
-  font-size: 18px;
-  font-weight: 500;
+  margin-top: 6px;
+  font-size: 14px;
   color: #666;
 }
 
+.rating {
+  color: #ffb800;
+  font-size: 12px;
+  margin-left: 4px;
+}
+
 .client-price {
   margin-top: 8px;
-  color: #7c5cff;
-  font-size: 32px;
+  font-size: 24px;
   font-weight: 700;
+  color: #7c5cff;
 }
 
-.details-info {
-  margin-top: 12px;
-  text-align: center;
-  font-size: 13px;
-  line-height: 1.6;
-  color: #666;
+/* DATA */
+.date {
+  margin-top: 6px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #555;
 }
 
-.distance-info {
-  margin-top: 12px;
-  text-align: center;
+.hour {
   font-size: 12px;
-  color: #999;
+  color: #777;
 }
 
-.service-highlight {
-  margin-top: 16px;
-  text-align: center;
-  font-size: 13px;
-  color: #666;
+.highlight-service{
+  color: #7c5cff;
+  font-weight: 600;
 }
 
-.highlight-text {
+/* ENDEREÇO */
+.address {
+  margin-top: 12px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 4px;
   color: #7c5cff;
-  font-weight: 700;
+  font-size: 13px;
+  font-weight: 600;
+}
+
+.distance {
+  text-align: center;
+  font-size: 11px;
+  color: #999;
+  margin-top: 4px;
 }
 
-.tags-row {
+/* ADDRESS TYPE (CHIPS) */
+.address-type {
   display: flex;
   justify-content: center;
-  gap: 8px;
-  margin: 18px 0;
+  gap: 10px;
+  margin-top: 10px;
 }
 
-.tags-row .q-chip {
-  border: 1px solid #7c5cff;
+.chip-type {
+  border: 1.5px solid #7c5cff;
   color: #7c5cff;
+  padding: 6px 14px;
+  border-radius: 999px;
+  font-size: 12px;
+  font-weight: 600;
   background: white;
+  text-transform: lowercase;
+}
+
+/* TEXTO SOB MEDIDA */
+.service-type {
+  text-align: center;
   font-size: 12px;
+  margin-top: 12px;
+  color: #666;
 }
 
+.service-type span {
+  color: #7c5cff;
+  font-weight: 600;
+}
+
+/* INFO */
 .info-title {
   text-align: center;
-  color: #7c5cff;
-  font-size: 18px;
   font-weight: 700;
-  margin-bottom: 12px;
+  color: #7c5cff;
+  margin-top: 14px;
+  font-size: 13px;
 }
 
+/* DESCRIÇÃO */
 .description-box {
   text-align: center;
-  font-size: 13px;
-  line-height: 1.6;
+  font-size: 12px;
   color: #666;
+  margin: 10px 0 20px;
+  line-height: 1.4;
 }
 
-.full-width {
-  margin-top: 20px;
+/* BOTÃO */
+.accept-btn {
+  width: 100%;
   height: 48px;
-  font-size: 16px;
-  background: #8f6dfc !important;
+  border-radius: 25px;
+  background: linear-gradient(90deg, #7c5cff, #9f7aea);
+  color: white;
+  font-weight: 600;
+  font-size: 14px;
 }
 
-.bottom-alert {
-  margin-top: 18px;
-  padding: 14px;
-  border-radius: 14px;
-  background: #dfeeff;
-  font-size: 12px;
-  line-height: 1.5;
-  color: #5c6b8a;
+/* ALERTA */
+.alert-box {
+  margin-top: 12px;
+  background: #e9f0ff;
+  padding: 10px;
+  border-radius: 12px;
+  font-size: 11px;
   text-align: center;
+  color: #5c6b8a;
+  display: flex;
+  gap: 6px;
+  align-items: center;
+  justify-content: center;
 }
 </style>

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

Неке датотеке нису приказане због велике количине промена