Prechádzať zdrojové kódy

cadastro + login prestador, dashboard funcional

Gustavo Zanatta 1 mesiac pred
rodič
commit
6e140e9a14

+ 1 - 1
quasar.config.js

@@ -102,7 +102,7 @@ export default defineConfig((ctx) => {
     // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
     devServer: {
       // https: true
-      open: true, // opens browser window automatically
+      open: false, // opens browser window automatically
     },
 
     // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

+ 6 - 0
src/api/dashboard.js

@@ -0,0 +1,6 @@
+import api from "src/api";
+
+export const dadosDashboard = async () => {
+  const { data } = await api.get("/dados-dashboard-prestador");
+  return data.payload;
+}

+ 6 - 0
src/api/serviceType.js

@@ -0,0 +1,6 @@
+import api from 'src/api';
+
+export const getPublicServiceTypes = async () => {
+  const { data } = await api.get('/public-service-types');
+  return data.payload;
+};

+ 2 - 2
src/api/user.js

@@ -35,8 +35,8 @@ export const sendCode = async (email, phone, type = 'PROVIDER') => {
   return data;
 }
 
-export const validateCode = async (email, phone, code) => {
-  const data = await api.post("/user-validate-code", { email, phone, code });
+export const validateCode = async (email, phone, code, isLogin = false) => {
+  const data = await api.post("/user-validate-code", { email, phone, code, isLogin });
   return data;
 }
 

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 6 - 0
src/assets/banner_1.svg


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 6 - 0
src/assets/banner_2.svg


+ 56 - 0
src/components/dashboard/DashboardHeaderBar.vue

@@ -0,0 +1,56 @@
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+<template>
+  <div class="dashboard-header shadow-card bg-white row items-center no-wrap q-px-md q-pb-sm">
+    <div class="col column q-gutter-y-xs">
+      <div class="row items-center q-gutter-x-xs">
+        <q-icon name="mdi-star" color="warning" size="14px" />
+        <span class="dashboard-metric-value">{{ data?.rating != null ? Number(data.rating).toFixed(1).replace('.', ',') : '-' }}</span>
+        <span class="dashboard-metric-meta">({{ data?.total_ratings ?? 0 }})</span>
+      </div>
+      <div class="row items-center q-gutter-x-xs">
+        <q-icon name="mdi-broom" color="secondary" size="14px" />
+        <span class="dashboard-metric-value">{{ data?.total_services ?? 0 }}</span>
+      </div>
+    </div>
+
+    <div class="col-auto row justify-center">
+      <img :src="LogoDiariaColorida" alt="Diária" class="dashboard-logo" />
+    </div>
+
+    <div class="col row justify-end items-center">
+      <q-btn flat round dense icon="mdi-bell-outline" color="grey-7" size="sm" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg';
+
+defineProps({ data: { type: Object, default: () => null } });
+</script>
+
+<style scoped lang="scss">
+.dashboard-header {
+  padding-top: calc(env(safe-area-inset-top) + 8px);
+  width: 100%;
+  box-sizing: border-box;
+}
+.dashboard-logo {
+  width: 32px;
+  height: 32px;
+}
+.dashboard-metric-value {
+  font-family: "Inter", sans-serif;
+  font-size: 11px;
+  font-weight: 700;
+  color: #3a3a4a;
+  line-height: 1;
+}
+.dashboard-metric-meta {
+  font-family: "Inter", sans-serif;
+  font-size: 10px;
+  font-weight: 400;
+  color: #999;
+  line-height: 1;
+}
+</style>

+ 154 - 0
src/components/dashboard/DashboardNextSchedules.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <div class="section-title gradient-diarista q-mb-sm">{{ $t('provider.dashboard.next_schedules.title') }}</div>
+
+    <div class="scroll-wrapper">
+      <div class="scroll-track">
+        <q-card
+          v-for="item in data"
+          :key="item.id"
+          class="schedule-card card-border shadow-card bg-surface"
+          :flat="false"
+        >
+          <q-card-section class="q-pa-sm">
+            <div class="row no-wrap items-center q-gutter-x-sm">
+              <q-avatar size="48px">
+                <img :src="item.customer_photo || 'https://cdn.quasar.dev/img/avatar.png'">
+              </q-avatar>
+              <div class="column flex-1">
+                <div class="row items-center q-gutter-x-xs">
+                  <span class="text-name ellipsis">{{ item.customer_name ?? item.client_name }}</span>
+                  <div v-if="item.customer_rating" class="row items-center">
+                    <q-icon name="mdi-star" color="warning" size="14px" />
+                    <span class="text-rating text-text">{{ item.customer_rating }}</span>
+                  </div>
+                </div>
+                <div class="row items-center no-wrap">
+                  <span class="text-schedule-date-bold">{{ formatWeekday(item.date) }}</span>
+                  <span class="text-schedule-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+                </div>
+                <div class="text-schedule-date-regular">
+                  {{ $t('common.from') }}
+                  <span class="text-schedule-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
+                  {{ $t('common.to') }}
+                  <span class="text-schedule-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
+                </div>
+              </div>
+              <div class="column items-end text-text">
+                <div class="text-price">{{ formatCurrency(item.total_amount) }}</div>
+                <div class="text-type">{{ t(labelsPeriodTypes.find(label => label.value == item.period_type)?.label) }}</div>
+                <div class="text-distance">{{ item.distance || 0 }}{{ $t('common.km') }}</div>
+              </div>
+            </div>
+
+            <div class="row q-mt-md items-center text-text text-caption q-px-xs">
+              <div class="col ellipsis text-grey-7">
+                {{ item.address || 'bairro' }}
+              </div>
+              <q-icon name="mdi-content-copy" color="primary" size="16px" class="q-ml-xs" />
+            </div>
+
+            <div class="row q-mt-sm items-center">
+              <q-btn
+                flat
+                no-caps
+                color="primary"
+                size="sm"
+                class="col-auto q-px-none btn-details"
+                :label="$t('provider.dashboard.next_schedules.details')"
+              />
+              <q-space />
+              <div class="row items-center q-gutter-x-xs text-grey-7" style="font-size: 11px;">
+                <q-icon 
+                  :name="item.offers_meal ? 'mdi-silverware' : 'mdi-close-outline'" 
+                  :color="item.offers_meal ? 'secondary' : 'grey-5'" 
+                  size="14px" 
+                />
+                <span :class="item.offers_meal ? 'text-weight-medium' : ''">
+                  {{ item.offers_meal ? $t('provider.dashboard.next_schedules.offers_meal') : $t('provider.dashboard.next_schedules.no_meal') }}
+                </span>
+              </div>
+            </div>
+          </q-card-section>
+        </q-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useI18n } from 'vue-i18n';
+import { formatCurrency } from 'src/helpers/utils';
+import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
+defineProps({ data: { type: Array, default: () => [] } });
+
+const t = useI18n().t;
+
+const formatWeekday = (iso) => {
+  if (!iso) return '';
+  const d = new Date(iso);
+  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  return w.charAt(0).toUpperCase() + w.slice(1);
+};
+
+const formatDayMonth = (iso) => {
+  if (!iso) return '';
+  return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+};
+</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; }
+}
+.schedule-card {
+  min-width: 85vw;
+  border-radius: 12px;
+}
+.text-name {
+  font-weight: 700;
+  color: #3a3a4a;
+}
+.text-rating {
+  font-size: 12px;
+  font-weight: 600;
+}
+.text-schedule-date-bold {
+  font-family: "Inter", sans-serif;
+  font-size: 11px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+.text-schedule-date-regular {
+  font-family: "Inter", sans-serif;
+  font-size: 11px;
+  font-weight: 400;
+  color: #666;
+}
+.text-date, .text-time, .text-type, .text-region, .text-distance {
+  font-size: 11px;
+  color: #666;
+}
+.text-price {
+  font-weight: 700;
+  color: #3a3a4a;
+  font-size: 14px;
+}
+.btn-details {
+  font-weight: 700;
+  font-size: 13px;
+}
+.flex-1 {
+  flex: 1;
+  min-width: 0;
+}
+</style>

+ 137 - 0
src/components/dashboard/DashboardOpportunities.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <div class="row items-center justify-between q-mb-sm">
+      <div class="section-title gradient-diarista">{{ $t('provider.dashboard.opportunities.title') }}</div>
+      <q-btn flat no-caps color="grey-6" size="sm" :label="$t('common.see_all')" />
+    </div>
+
+    <div class="scroll-wrapper">
+      <div class="scroll-track row no-wrap q-gutter-x-md">
+        <q-card
+          v-for="item in data"
+          :key="item.id"
+          class="opportunity-card bg-surface shadow-card card-border"
+          :flat="false"
+        >
+          <q-card-section class="q-pa-sm">
+            <div class="row no-wrap items-center q-gutter-x-sm">
+              <q-avatar size="48px">
+                <img :src="item.customer_photo || 'https://cdn.quasar.dev/img/avatar.png'">
+              </q-avatar>
+              <div class="column flex-1">
+                <div class="row items-center q-gutter-x-xs">
+                  <span class="text-name ellipsis">{{ item.client_name }}</span>
+                  <div class="row items-center">
+                    <q-icon name="mdi-star" color="warning" size="14px" />
+                    <span class="text-rating text-text">{{ item.average_rating }}</span>
+                  </div>
+                </div>
+                <div class="row items-center no-wrap">
+                  <span class="text-schedule-date-bold">{{ formatWeekday(item.date) }}</span>
+                  <span class="text-schedule-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+                </div>
+                <div class="text-schedule-date-regular">
+                  {{ $t('common.from') }}
+                  <span class="text-schedule-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
+                  {{ $t('common.to') }}
+                  <span class="text-schedule-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
+                </div>
+              </div>
+              <div class="column items-end text-text">
+                <div class="text-price">{{ formatCurrency(item.total_amount) }}</div>
+                <div class="text-type">{{ $t(labelsPeriodTypes.find(label => label.value == item.period_type)?.label) }}</div>
+                <div class="text-region">{{ item.region || 'bairro' }}</div>
+                <div class="text-distance">{{ item.distance || 0 }}{{ $t('common.km') }}</div>
+              </div>
+            </div>
+
+            <div class="row q-mt-sm justify-end">
+              <q-btn
+                unelevated
+                rounded
+                no-caps
+                color="primary"
+                size="sm"
+                class="btn-details"
+                :label="$t('common.details')"
+              />
+            </div>
+          </q-card-section>
+        </q-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { formatCurrency } from 'src/helpers/utils';
+import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
+defineProps({
+  data: {
+    type: Array,
+    default: () => []
+  }
+});
+
+const formatWeekday = (iso) => {
+  if (!iso) return '';
+  const d = new Date(iso);
+  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  return w.charAt(0).toUpperCase() + w.slice(1);
+};
+
+const formatDayMonth = (iso) => {
+  if (!iso) return '';
+  return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+};
+</script>
+
+<style scoped lang="scss">
+.scroll-wrapper {
+  overflow-x: auto;
+  padding-bottom: 8px;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+}
+.opportunity-card {
+  min-width: 85vw;
+  border-radius: 12px;
+}
+.text-name {
+  font-weight: 700;
+  color: #3a3a4a;
+}
+.text-rating {
+  font-size: 12px;
+  font-weight: 600;
+}
+.text-schedule-date-bold {
+  font-family: "Inter", sans-serif;
+  font-size: 11px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+.text-schedule-date-regular {
+  font-family: "Inter", sans-serif;
+  font-size: 11px;
+  font-weight: 400;
+  color: #666;
+}
+.text-date, .text-time, .text-type, .text-region, .text-distance {
+  font-size: 11px;
+  color: #666;
+}
+.text-price {
+  font-weight: 700;
+  color: #3a3a4a;
+  font-size: 14px;
+}
+.btn-details {
+  font-weight: 700;
+}
+.flex-1 {
+  flex: 1;
+  min-width: 0;
+}
+</style>

+ 73 - 0
src/components/dashboard/DashboardPriceSuggest.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <q-card class="price-suggest-card bg-surface shadow-card card-border" :flat="false">
+      <q-card-section class="q-pa-md">
+        <div class="row items-center justify-between q-mb-sm">
+          <div class="row items-center q-gutter-x-sm">
+            <span class="text-suggest-label">{{ $t('provider.dashboard.price_suggest.region_label') }}</span>
+          </div>
+          <q-badge rounded class="price-badge q-px-md q-py-xs gradient-diarista-bg">
+            {{ formatCurrency(data?.average_price ?? 0) }}
+          </q-badge>
+        </div>
+        <div class="row items-center justify-between no-wrap">
+          <div class="row items-center q-gutter-x-sm">
+            <span class="text-suggest-label">{{ $t('provider.dashboard.price_suggest.my_price_label') }}</span>
+            <div class="row items-center no-wrap">
+              <span class="text-my-price q-mr-xs">{{ showMyPrice ? formatCurrency(data?.your_price ?? 0) : $t('common.price_masked') }}</span>
+              <q-btn icon="mdi-eye-off-outline" flat size="xs" color="grey-6" class="q-pa-none q-pl-sm" @click="showMyPrice = !showMyPrice"/>
+            </div>
+          </div>
+          <q-btn flat no-caps color="primary" padding="0" class="btn-alter">
+            <div class="row items-center q-gutter-x-xs">
+              <span class="text-weight-bold">{{ $t('common.alter') }}</span>
+              <q-icon name="mdi-pencil-outline" size="xs" />
+            </div>
+          </q-btn>
+        </div>
+      </q-card-section>
+    </q-card>
+  </div>
+</template>
+
+<script setup>
+import { formatCurrency } from 'src/helpers/utils';
+import { ref } from 'vue';
+
+const showMyPrice = ref(false);
+
+defineProps({
+  data: {
+    type: Object,
+    default: () => ({
+    })
+  }
+});
+
+</script>
+
+<style scoped lang="scss">
+.price-suggest-card {
+  border-radius: 12px;
+}
+.text-suggest-label {
+  font-family: "Inter", sans-serif;
+  font-size: 13px;
+  color: #3a3a4a;
+}
+.price-badge {
+  font-family: "Inter", sans-serif;
+  font-size: 14px;
+  font-weight: 700;
+  border-radius: 20px;
+}
+.text-my-price {
+  font-family: "Inter", sans-serif;
+  font-size: 13px;
+  font-weight: 600;
+  color: #3a3a4a;
+}
+.btn-alter {
+  font-size: 13px;
+}
+</style>

+ 61 - 0
src/components/dashboard/DashboardScrollAreaSchedules.vue

@@ -0,0 +1,61 @@
+<template>
+  <section class="promo-scroll-wrapper q-ma-md">
+    <div class="promo-scroll">
+      <div v-for="card in cards" :key="card.id" class="promo-card">
+        <img :src="card.image" :alt="card.alt" class="promo-card__img" />
+      </div>
+    </div>
+  </section>
+</template>
+
+<script setup>
+import Banner1 from 'src/assets/banner_1.svg';
+import Banner2 from 'src/assets/banner_2.svg';
+
+const cards = [
+  { id: 1, image: Banner1, alt: 'Diária sob medida' },
+  { id: 2, image: Banner2, alt: 'Escolha profissionais' },
+];
+</script>
+
+<style scoped lang="scss">
+.promo-scroll-wrapper {
+  width: 100%;
+  overflow: hidden;
+}
+
+.promo-scroll {
+  display: flex;
+  gap: 12px;
+  overflow-x: auto;
+  overscroll-behavior-x: contain;
+  padding: 0 0 4px 16px;
+  scroll-snap-type: x mandatory;
+
+  &::-webkit-scrollbar {
+    display: none;
+  }
+
+  &::after {
+    content: '';
+    flex: 0 0 16px;
+  }
+}
+
+.promo-card {
+  height: 120px;
+  border-radius: 10px;
+  overflow: hidden;
+  scroll-snap-align: start;
+  background: #e8e4f5;
+  flex-shrink: 0;
+}
+
+.promo-card__img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
+  border-radius: 10px;
+}
+</style>

+ 170 - 0
src/components/dashboard/DashboardSolicitations.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <div class="row items-center justify-between q-mb-sm">
+      <div class="section-title gradient-diarista ">{{ $t('provider.dashboard.solicitations.title') }}</div>
+      <q-btn flat no-caps color="grey-6" icon="mdi-chevron-right-outlined" size="sm" :label="$t('common.see_all')" />
+    </div>
+
+    <div class="scroll-wrapper">
+      <div class="scroll-track row no-wrap q-gutter-x-md">
+        <q-card
+          v-for="item in data"
+          :key="item.id"
+          class="solicitation-card bg-surface q-ma-md shadow-card card-border"
+          :flat="false"
+        >
+          <q-card-section class="q-pa-sm">
+            <div class="row no-wrap items-center q-gutter-x-sm">
+              <q-avatar size="48px">
+                <img :src="item.customer_photo || 'https://cdn.quasar.dev/img/avatar.png'">
+              </q-avatar>
+              <div class="column flex-1">
+                <div class="row items-center q-gutter-x-xs">
+                  <span class="text-name ellipsis">{{ item.client_name }}</span>
+                  <div class="row items-center">
+                    <q-icon name="mdi-star" color="warning" size="14px" />
+                    <span class="text-rating text-text">{{ item.average_rating }}</span>
+                  </div>
+                </div>
+                <div class="row items-center no-wrap">
+                  <span class="text-schedule-date-bold">{{ formatWeekday(item.date) }}</span>
+                  <span class="text-schedule-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+                </div>
+                <div class="text-schedule-date-regular">
+                  {{ $t('common.from') }}
+                  <span class="text-schedule-date-bold">
+                    {{ item.start_time?.slice(0, 5) }}
+                    
+                    {{ $t('common.to') }}
+
+                    {{ item.end_time?.slice(0, 5) }}
+                  </span>
+                </div>
+              </div>
+              <div class="column items-end text-text">
+                <div class="text-price">{{ formatCurrency(item.total_amount) }}</div>
+                <div class="text-type">{{ t(labelsPeriodTypes.find(label => label.value == item.period_type)?.label) }}</div>
+                <div class="text-region">{{ item.region || 'bairro' }}</div>
+                <div class="text-distance">
+                  <span class="q-pr-xs">{{ item.distance || 0 }}</span>
+                  {{ $t('common.km') }}
+                </div>
+              </div>
+            </div>
+
+            <div class="row q-mt-sm q-gutter-x-sm items-center">
+              <q-btn
+                flat
+                no-caps
+                color="primary"
+                size="sm"
+                class="col-auto q-px-none btn-details"
+                :label="$t('common.details')"
+              />
+              <q-space />
+              <q-btn
+                unelevated
+                rounded
+                no-caps
+                class="col-auto bg-grey-3 text-grey-8 btn-action"
+                size="sm"
+                :label="$t('common.refuse')"
+              />
+              <q-btn
+                unelevated
+                rounded
+                no-caps
+                color="secondary"
+                class="col-auto btn-action"
+                size="sm"
+                :label="$t('common.accept')"
+              />
+            </div>
+          </q-card-section>
+        </q-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { formatCurrency } from 'src/helpers/utils';
+import { useI18n } from 'vue-i18n';
+import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
+
+defineProps({
+  data: {
+    type: Array,
+    default: () => []
+  }
+});
+
+const t = useI18n().t;
+
+const formatWeekday = (iso) => {
+  if (!iso) return '';
+  const d = new Date(iso);
+  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  return w.charAt(0).toUpperCase() + w.slice(1);
+};
+
+const formatDayMonth = (iso) => {
+  if (!iso) return '';
+  return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+};
+
+</script>
+
+<style scoped lang="scss">
+.scroll-wrapper {
+  overflow-x: auto;
+  padding-bottom: 8px;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+}
+.solicitation-card {
+  min-width: 85vw;
+}
+.text-name {
+  font-weight: 700;
+  color: #3a3a4a;
+}
+.text-rating {
+  font-size: 12px;
+  font-weight: 600;
+}
+.text-schedule-date-bold {
+  font-family: "Inter", sans-serif;
+  font-size: 11px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+.text-schedule-date-regular {
+  font-family: "Inter", sans-serif;
+  font-size: 11px;
+  font-weight: 400;
+  color: #666;
+}
+.text-date, .text-time, .text-type, .text-region, .text-distance {
+  font-size: 11px;
+  color: #666;
+}
+.text-price {
+  font-weight: 700;
+  color: #3a3a4a;
+  font-size: 14px;
+}
+.btn-details {
+  font-weight: 700;
+  font-size: 13px;
+}
+.btn-action {
+  font-weight: 700;
+  min-width: 90px;
+}
+.flex-1 {
+  flex: 1;
+  min-width: 0;
+}
+</style>

+ 59 - 0
src/components/dashboard/DashboardSummaryInfos.vue

@@ -0,0 +1,59 @@
+<template>
+  <q-card class="summary-card shadow-card q-ma-md bg-surface card-border" :flat="false">
+    <q-card-section class="q-pa-md">
+      <div class="row items-center no-wrap q-gutter-x-md">
+        <div class="row items-center no-wrap q-gutter-x-sm col">
+          <q-avatar size="54px" :style="avatarStyle" class="text-weight-bold text-h6">
+            {{ data?.name?.slice(0, 2).toUpperCase() ?? '??' }}
+          </q-avatar>
+          <div class="column q-gutter-y-xs min-width-0">
+            <span class="summary-greeting text-greeting">{{ $t('provider.dashboard.summary.welcome') }}</span>
+            <span class="summary-name text-name text-primary">{{ data?.name ?? $t('provider.dashboard.summary.welcome') }}</span>
+          </div>
+        </div>
+        <div class="column items-end q-gutter-y-xs col-auto">
+          <span class="summary-label text-label-bold text-grey-6">{{ $t('provider.dashboard.summary.my_schedules') }}</span>
+          <span class="summary-count row">
+            <q-icon name="mdi-clock-check-outline" class="q-my-auto" size="sm" color="grey-6" />
+            <span class="q-my-auto q-ml-sm">{{ data?.pending_services ?? 0 }}</span>
+          </span>
+        </div>
+      </div>
+
+      <div class="row items-center justify-between no-wrap q-mt-xs">
+        <div class="summary-address text-address text-grey-6 ellipsis col">
+          {{ data?.address ?? 'bairro' }}
+        </div>
+        <q-icon name="mdi-chevron-down" color="secondary" size="18px" class="col-auto" />
+      </div>
+    </q-card-section>
+  </q-card>
+</template>
+
+<script setup>
+defineProps({ data: { type: Object, default: () => null } });
+
+const avatarStyle = {
+  background: 'linear-gradient(135deg, #ffd7e8 0%, #ff9acc 100%)',
+  color: '#7a154f',
+};
+</script>
+
+<style scoped lang="scss">
+.summary-card { overflow: hidden; }
+.min-width-0 { min-width: 0; }
+.summary-greeting { color: #8c8c98; display: block; }
+.summary-name { display: block; line-height: 1.1; }
+.summary-label { white-space: nowrap; }
+.summary-count {
+  font-family: "Inter", sans-serif;
+  font-size: 20px;
+  font-weight: 500;
+  color: #3a3a4a;
+}
+.summary-address {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 2 - 0
src/components/defaults/DefaultCurrencyInput.vue

@@ -9,6 +9,8 @@
     :label="newLabel"
     :disable
     :readonly
+    hide-bottom-space
+    no-error-icon
   >
   </q-input>
 </template>

+ 31 - 44
src/components/defaults/DefaultFilePicker.vue

@@ -39,10 +39,10 @@
           >
             {{
               isDragging
-                ? $t("common.ui.file.drag_and_drop")
-                : type == "image"
-                  ? $t("common.ui.file.click_select_image")
-                  : $t("common.ui.file.click_select")
+                ? $t('common.ui.file.drag_and_drop')
+                : type == 'image'
+                  ? $t('common.ui.file.click_select_image')
+                  : $t('common.ui.file.click_select')
             }}
           </div>
         </template>
@@ -86,12 +86,12 @@
 </template>
 
 <script setup>
-import { ref, watch, onUnmounted, useTemplateRef } from "vue";
+import { ref, watch, onUnmounted, useTemplateRef } from 'vue';
 
 const { label, rules, accept, type, initialImage } = defineProps({
   label: {
     type: String,
-    default: "Select Image",
+    default: 'Select Image',
   },
   rules: {
     type: Array,
@@ -99,11 +99,11 @@ const { label, rules, accept, type, initialImage } = defineProps({
   },
   accept: {
     type: String,
-    default: "image/*",
+    default: 'image/*',
   },
   type: {
     type: String,
-    default: "image",
+    default: 'image',
   },
   initialImage: {
     type: String,
@@ -115,15 +115,15 @@ const { label, rules, accept, type, initialImage } = defineProps({
   },
   errorMessage: {
     type: String,
-    default: "",
+    default: '',
   },
 });
 
 const model = defineModel({ type: [File, String, null], default: null });
-const base64File = defineModel("base64File", { type: String, default: null });
+const base64File = defineModel('base64File', { type: String, default: null });
 
 const isDragging = ref(false);
-const fileInputRef = useTemplateRef("fileInputRef");
+const fileInputRef = useTemplateRef('fileInputRef');
 const preview = ref(initialImage || null);
 let objectUrl = null;
 
@@ -140,12 +140,13 @@ const generateBase64 = (file) => {
     base64File.value = null;
     return;
   }
+
   const reader = new FileReader();
-  reader.onload = (e) => {
-    base64File.value = e.target.result;
+  reader.onload = (event) => {
+    base64File.value = event.target.result;
   };
   reader.onerror = () => {
-    console.error("FileReader failed to read file.");
+    console.error('FileReader failed to read file.');
     base64File.value = null;
   };
   reader.readAsDataURL(file);
@@ -155,11 +156,11 @@ watch(model, (newFile) => {
   cleanupObjectURL();
 
   if (newFile && newFile instanceof File) {
-    if (type === "image") {
+    if (type === 'image') {
       objectUrl = URL.createObjectURL(newFile);
       preview.value = objectUrl;
     } else {
-      preview.value = "file_selected";
+      preview.value = 'file_selected';
     }
     generateBase64(newFile);
   } else {
@@ -178,7 +179,7 @@ const clearFile = () => {
 
 const handleDragOver = (event) => {
   event.preventDefault();
-  event.dataTransfer.dropEffect = "copy";
+  event.dataTransfer.dropEffect = 'copy';
   isDragging.value = true;
 };
 
@@ -195,16 +196,16 @@ const handleDrop = (event) => {
 
   const acceptedMime = accept;
 
-  if (acceptedMime.endsWith("/*")) {
-    const baseMime = acceptedMime.replace("/*", "");
-    if (file.type.startsWith(baseMime + "/")) {
+  if (acceptedMime.endsWith('/*')) {
+    const baseMime = acceptedMime.replace('/*', '');
+    if (file.type.startsWith(baseMime + '/')) {
       model.value = file;
     }
   } else {
     if (
       acceptedMime
-        .split(",")
-        .map((m) => m.trim())
+        .split(',')
+        .map((mime) => mime.trim())
         .includes(file.type)
     ) {
       model.value = file;
@@ -214,40 +215,26 @@ const handleDrop = (event) => {
 </script>
 
 <style lang="scss" scoped>
-@use "sass:map";
-@use "src/css/quasar.variables.scss";
-
 .image-preview-container {
-  .body--dark & {
-    --image-bg-color: #{map.get($colors-dark, "surface")};
-    --image-border-color: #{map.get($colors-dark, "primary")};
-    --image-border-hover-color: #{map.get($colors-dark, "primary-dark")};
-  }
-
-  .body--light & {
-    --image-bg-color: #{map.get($colors, "surface")};
-    --image-border-color: #{map.get($colors, "primary")};
-    --image-border-hover-color: #{map.get($colors, "primary-dark")};
-  }
-
   width: 200px;
   height: 200px;
-  border: 2px dashed var(--image-border-color);
-  border-radius: 4px;
+  border: 2px dashed var(--q-primary);
+  border-radius: 8px;
   position: relative;
   overflow: hidden;
   transition: all 0.3s;
   cursor: pointer;
+  background-color: #fff;
 
   &.is-dragging {
-    border-color: var(--image-border-hover-color);
-    background-color: var(--image-bg-color);
-    opacity: 0.8;
+    border-color: var(--q-secondary, var(--q-primary));
+    background-color: rgba(37, 116, 252, 0.06);
+    opacity: 0.9;
   }
 
   &:hover {
-    border-color: var(--image-border-hover-color);
-    background-color: var(--image-bg-color);
+    border-color: var(--q-secondary, var(--q-primary));
+    background-color: rgba(37, 116, 252, 0.04);
   }
 
   &.has-image {

+ 195 - 0
src/components/login/LoginStepFivePanel.vue

@@ -0,0 +1,195 @@
+<template>
+  <q-card-section class="no-padding">
+    <div class="text-text q-mb-sm text-weight-medium">{{ $t('provider.login.steps.step_5.daily_price_title') }}</div>
+    <DefaultCurrencyInput
+      v-model="form.daily_price_8h"
+      rounded
+      no-error-icon
+      class="bg-surface q-mb-sm custom-currency-input"
+      input-class="text-text"
+      placeholder="R$ 0,00"
+      :error="!!priceError"
+      hide-bottom-space
+      label=""
+    />
+
+    <div class="text-caption text-center text-grey-7 q-mb-md">{{ $t('provider.login.steps.step_5.daily_price_min_max') }}</div>
+
+    <q-banner class="bg-blue-1 text-primary q-mb-lg q-pa-md bannerRound">
+      <template #avatar>
+        <q-icon name="mdi-alert-outline" color="primary" />
+      </template>
+      <span class="text-weight-bold">{{ $t('provider.login.steps.step_5.dont_worry') }}</span> {{ $t('provider.login.steps.step_5.change_anytime') }}
+    </q-banner>
+
+    <div class="text-text text-center q-mb-sm">{{ $t('provider.login.steps.step_5.shorter_services') }}</div>
+
+    <div class="row q-col-gutter-sm q-mb-lg q-mt-md">
+      <div class="col-4">
+        <div class="text-text text-center text-weight-bold">{{ $t('provider.login.steps.step_5.up_to_6h') }}</div>
+        <q-input :model-value="formatCurrency(form.daily_price_6h)" readonly class="bg-surface" input-class="text-text"/>
+      </div>
+      <div class="col-4">
+        <div class="text-text text-center text-weight-bold">{{ $t('provider.login.steps.step_5.up_to_4h') }}</div>
+        <q-input :model-value="formatCurrency(form.daily_price_4h)" readonly class="bg-surface" input-class="text-text"/>
+      </div>
+      <div class="col-4 column">
+        <div class="text-text text-center text-weight-bold">{{ $t('provider.login.steps.step_5.up_to_2h') }}</div>
+        <q-input :model-value="formatCurrency(form.daily_price_2h)" readonly class="bg-surface" input-class="text-text"/>
+      </div>
+    </div>
+
+    <div class="text-text text-weight-medium q-mb-xs text-center">{{ $t('provider.login.steps.step_5.other_services') }}</div>
+    <div class="text-caption text-grey-7 q-mb-md text-center">{{ $t('provider.login.steps.step_5.change_in_profile') }}</div>
+
+    <div v-if="loadingServiceTypes" class="row justify-center q-mb-lg">
+      <q-spinner color="primary" size="28px" />
+    </div>
+
+    <div v-else-if="serviceOptions.length" class="row q-col-gutter-sm q-mb-lg">
+      <div v-for="service in serviceOptions" :key="service.value" class="col-6">
+        <q-checkbox
+          :model-value="selectedServices.includes(service.value)"
+          :label="service.label"
+          color="primary"
+          class="q-mb-md text-text"
+          keep-color
+          @update:model-value="(checked) => onServiceToggle(service.value, checked)"
+        />
+      </div>
+    </div>
+
+    <div v-else class="text-caption text-grey-7 q-mb-lg">{{ $t('provider.login.steps.step_5.no_services') }}</div>
+
+    <div class="text-caption text-grey-7 text-center">{{ $t('provider.login.steps.step_5.search_visibility') }}</div>
+  </q-card-section>
+</template>
+
+<script setup>
+import { computed, onMounted, ref, watch } from 'vue';
+import { getPublicServiceTypes } from 'src/api/serviceType';
+import DefaultCurrencyInput from 'src/components/defaults/DefaultCurrencyInput.vue';
+
+const form = defineModel({ type: Object, required: true });
+
+const loadingServiceTypes = ref(false);
+const serviceOptions = ref([]);
+
+const priceError = computed(() => {
+  const value = form.value.daily_price_8h;
+  if (value === null || value === undefined || value === '') {
+    return 'Este campo é obrigatório';
+  }
+
+  const numericValue = Number(value);
+
+  if (Number.isNaN(numericValue)) {
+    return 'Valor inválido';
+  }
+
+  if (numericValue < 100) {
+    return 'Valor mínimo R$ 100,00';
+  }
+
+  if (numericValue > 500) {
+    return 'Valor máximo R$ 500,00';
+  }
+
+  return '';
+});
+
+const selectedServices = computed(() => {
+  if (!Array.isArray(form.value.services_types_ids)) {
+    return [];
+  }
+
+  return form.value.services_types_ids;
+});
+
+const formatCurrency = (value) => {
+  if (value === null || value === undefined || Number.isNaN(Number(value))) {
+    return 'R$ 0,00';
+  }
+
+  return Number(value).toLocaleString('pt-BR', {
+    style: 'currency',
+    currency: 'BRL',
+    minimumFractionDigits: 2,
+  });
+};
+
+const normalizeSelectedServiceIds = () => {
+  if (!Array.isArray(form.value.services_types_ids)) {
+    form.value.services_types_ids = [];
+  }
+
+  form.value.services_types_ids = form.value.services_types_ids
+    .map((id) => Number(id))
+    .filter((id) => Number.isInteger(id) && id > 0);
+};
+
+const onServiceToggle = (value, checked) => {
+  normalizeSelectedServiceIds();
+
+  if (checked) {
+    if (!form.value.services_types_ids.includes(value)) {
+      form.value.services_types_ids.push(value);
+    }
+    return;
+  }
+
+  form.value.services_types_ids = form.value.services_types_ids.filter((serviceId) => serviceId !== value);
+};
+
+const loadServiceTypes = async () => {
+  loadingServiceTypes.value = true;
+
+  try {
+    const serviceTypes = await getPublicServiceTypes();
+
+    serviceOptions.value = (Array.isArray(serviceTypes) ? serviceTypes : [])
+      .filter((item) => item?.is_active !== false)
+      .map((item) => ({
+        label: item.description,
+        value: Number(item.id),
+      }))
+      .filter((item) => Number.isInteger(item.value) && item.value > 0);
+  } catch (error) {
+    serviceOptions.value = [];
+    console.error(error);
+  } finally {
+    loadingServiceTypes.value = false;
+  }
+};
+
+watch(
+  () => form.value.daily_price_8h,
+  (value) => {
+    const price = Number(value);
+
+    if (!price || Number.isNaN(price)) {
+      form.value.daily_price_6h = null;
+      form.value.daily_price_4h = null;
+      form.value.daily_price_2h = null;
+      return;
+    }
+
+    form.value.daily_price_6h = Number((price * 0.85).toFixed(2));
+    form.value.daily_price_4h = Number((price * 0.55).toFixed(2));
+    form.value.daily_price_2h = Number((price * 0.30).toFixed(2));
+  },
+  { immediate: true },
+);
+
+onMounted(() => {
+  loadServiceTypes();
+});
+</script>
+<style scoped>
+.bannerRound {
+  border-radius: 18px;
+}
+:deep(.custom-currency-input .q-field__control) {
+  border-radius: 28px;
+}
+</style>

+ 129 - 81
src/components/login/LoginStepFourPanel.vue

@@ -1,99 +1,147 @@
 <template>
-  <div class="column col-12">
-    <q-card class="step4-card col-12">
-      <div class="bg-surface q-pa-lg">
-        <div class="text-center q-mb-md">
-          <div class="text-primary text-weight-bold">
-            {{ $t('auth.step4_title') }}
-          </div>
+  <q-card-section class="no-padding">
+    <div class="column q-gutter-y-lg q-pt-md">
+      <div 
+        class="bg-surface q-pa-lg rounded-borders flex flex-center column q-gutter-y-sm"
+        @click="openSubStep('id_front', 'document_front')"
+      >
+        <q-icon name="mdi-card-account-details-outline" size="48px" color="primary" />
+        <div class="text-subtitle1 text-weight-bold text-center">{{ $t('provider.login.steps.step_4.document_front') }}</div>
+        <div class="text-caption text-grey-7 text-center">{{ $t('provider.login.steps.step_4.document_front_desc') }}</div>
+        <div v-if="form.document_front" class="row items-center q-gutter-x-sm text-green text-weight-bold">
+          <q-icon name="check_circle" size="20px" />
+          <span>{{ $t('provider.login.steps.step_4.photo_captured') }}</span>
         </div>
+      </div>
 
-        <q-input
-          v-model="cep"
-          no-error-icon
-          outlined
-          rounded
-          class="bg-surface q-mb-md"
-          input-class="text-text"
-          placeholder="Digite seu CEP"
-          hide-bottom-space
-          :rules="[inputRules.requiredHideMessage, inputRules.cep]"
-          lazy-rules
-          mask="#####-###"
-          :loading="loadingCep"
-          :bottom-slots="false"
-          @update:model-value="onCepChange"
-        >
-          <template #prepend>
-            <q-icon name="mdi-map-marker-outline" color="grey-5" class="q-mr-sm" />
-          </template>
-        </q-input>
-
-        <q-btn
-          color="primary-button"
-          :label="$t('auth.use_location')"
-          rounded
-          padding="6px 16px"
-          class="full-width"
-          @click="useLocation"
-        />
+      <div 
+        class="bg-surface q-pa-lg rounded-borders flex flex-center column q-gutter-y-sm"
+        @click="openSubStep('id_back', 'document_back')"
+      >
+        <q-icon name="mdi-card-bulleted-outline" size="48px" color="primary" />
+        <div class="text-subtitle1 text-weight-bold text-center">{{ $t('provider.login.steps.step_4.document_back') }}</div>
+        <div class="text-caption text-grey-7 text-center">{{ $t('provider.login.steps.step_4.document_back_desc') }}</div>
+        <div v-if="form.document_back" class="row items-center q-gutter-x-sm text-green text-weight-bold">
+          <q-icon name="check_circle" size="20px" />
+          <span>{{ $t('provider.login.steps.step_4.photo_captured') }}</span>
+        </div>
+      </div>
 
+      <div 
+        class="bg-surface q-pa-lg rounded-borders flex flex-center column q-gutter-y-sm"
+        @click="openSubStep('selfie', 'selfie_with_document')"
+      >
+        <q-icon name="mdi-camera-account" size="48px" color="primary" />
+        <div class="text-subtitle1 text-weight-bold text-center">{{ $t('provider.login.steps.step_4.selfie') }}</div>
+        <div class="text-caption text-grey-7 text-center">{{ $t('provider.login.steps.step_4.selfie_desc') }}</div>
+        <div v-if="form.selfie_with_document" class="row items-center q-gutter-x-sm text-green text-weight-bold">
+          <q-icon name="check_circle" size="20px" />
+          <span>{{ $t('provider.login.steps.step_4.photo_captured') }}</span>
+        </div>
       </div>
-    </q-card>
-    <div class="text-center q-pt-sm">
-      <q-btn
-        flat
-        rounded
-        color="surface"
-        :label="$t('auth.back_to_register')"
-        :icon="'mdi-chevron-left-circle-outline'"
-        @click="emit('back')"
-      />
     </div>
-  </div>
+
+    <Teleport to="body">
+      <div v-if="showCamera" class="camera-overlay">
+        <div class="camera-header q-pa-md flex justify-between items-center bg-white text-black">
+          <q-btn flat round icon="arrow_back" color="black" @click="closeCamera" />
+          <div class="text-black text-weight-bold text-subtitle1">{{ currentSubStepTitle }}</div>
+          <div style="width: 32px"></div>
+        </div>
+
+        <div class="camera-container flex flex-center">
+          <div v-if="cameraMode === 'selfie'" class="selfie-guide"></div>
+          <div v-else class="document-guide"></div>
+        </div>
+
+        <div class="camera-footer q-pa-lg flex flex-center">
+          <q-btn round color="primary" size="22px" icon="photo_camera" @click="capturePhoto" />
+        </div>
+      </div>
+    </Teleport>
+  </q-card-section>
 </template>
 
 <script setup>
-import { ref } from 'vue';
-import { useInputRules } from 'src/composables/useInputRules';
-import axios from 'axios';
-
-const emit = defineEmits(['back']);
-const cep = defineModel('cep', { type: String, default: '' });
-
-const { inputRules } = useInputRules();
-const loadingCep = ref(false);
-
-const fetchCep = async (rawCep) => {
-  const cleaned = rawCep.replace(/\D/g, '');
-  if (cleaned.length !== 8) return;
-  loadingCep.value = true;
-  try {
-    const { data } = await axios.get(`https://viacep.com.br/ws/${cleaned}/json/`);
-    if (data.erro) cep.value = '';
-  } catch (error) {
-    console.log(error)
-  } finally {
-    loadingCep.value = false;
-  }
-};
+import { ref, watch } from 'vue';
 
-const onCepChange = (val) => {
-  const cleaned = val?.replace(/\D/g, '') ?? '';
-  if (cleaned.length === 8) fetchCep(val);
-};
+const form = defineModel({ type: Object, required: true });
+const emit = defineEmits(['update:show-sub-step']);
 
-const useLocation = () => {
-  // getlocationfromdevice
+const subStep = ref('main');
+
+watch(subStep, (val) => {
+  emit('update:show-sub-step', val !== 'main');
+});
+
+const openSubStep = (step) => {
+  subStep.value = step;
 };
+
 </script>
 
 <style lang="scss" scoped>
-.step4-card {
-  width: 100%;
-  max-width: 340px;
-  border-radius: 30px;
+.capture-screen {
+  background: #f8f8f8;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 9999;
+  display: flex;
+  flex-direction: column;
+}
+
+.capture-content {
+  flex: 1;
+  padding: 20px;
+}
+
+.capture-placeholder-container {
   position: relative;
-  z-index: 1;
+  width: 100%;
+  max-width: 320px;
+  background: #000;
+  aspect-ratio: 9/16;
+  border-radius: 20px;
+  overflow: hidden;
+}
+
+.capture-placeholder {
+  width: 100%;
+  height: 100%;
+  background: #1a1a1a;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.capture-preview {
+  width: 100%;
+  height: 100%;
+}
+
+.selfie-oval-overlay {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 70%;
+  height: 60%;
+  border: 4px solid var(--q-primary);
+  border-radius: 50% / 50%;
+  pointer-events: none;
+}
+
+.bg-primary-fade {
+  background-color: rgba(var(--q-primary-rgb), 0.2);
+}
+
+.capture-placeholder-container.column.flex-center {
+  &.doc {
+     aspect-ratio: 1/1.4; // Slightly wider for ID card
+  }
 }
 </style>

+ 7 - 2
src/components/login/LoginStepOnePanel.vue

@@ -12,7 +12,7 @@
       input-class="text-text"
       :placeholder="$t('common.terms.email')"
       hide-bottom-space
-      :rules="!phone ? [inputRules.requiredHideMessage, inputRules.email] : []"
+      :rules="!phone ? [inputRules.requiredHideMessage, inputRules.email] : [inputRules.email]"
       lazy-rules
       :bottom-slots="false"
     />
@@ -38,7 +38,7 @@
       :placeholder="$t('common.terms.phone')"
       input-class="text-text"
       hide-bottom-space
-      :rules="!email ? [inputRules.requiredHideMessage, inputRules.email] : []"
+      :rules="!email ? [inputRules.requiredHideMessage, validatePhone] : [validatePhone]"
       lazy-rules
       mask="(##) #####-####"
       :bottom-slots="false"
@@ -53,4 +53,9 @@ const email = defineModel('email', { type: String, required: true });
 const phone = defineModel('phone', { type: String, required: true });
 
 const { inputRules } = useInputRules();
+
+const validatePhone = (value) => {
+  if (!value) return true;
+  return value.replace(/\D/g, '').length >= 10 || 'Telefone inválido';
+};
 </script>

+ 180 - 0
src/components/login/LoginStepSixPanel.vue

@@ -0,0 +1,180 @@
+<template>
+  <q-card-section class="no-padding">
+    <div class="text-subtitle1 text-center text-weight-bold text-grey-8 q-mb-md">
+      {{ $t('provider.login.steps.step_6.title') }}
+    </div>
+
+    <q-card flat class="q-pa-lg q-mb-lg availability-card shadow-card bg-surface">
+      <div class="row items-start q-mb-md">
+        <q-icon name="mdi-lock-outline" size="20px" color="grey-6" class="q-mr-sm" />
+        <div class="col">
+          <div class="text-caption text-weight-bold text-grey-8 line-height-tight">
+            {{ $t('provider.login.steps.step_6.lock_hint') }} <span class="text-weight-regular text-grey-6">{{ $t('provider.login.steps.step_6.lock_description') }}</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="days-grid q-mb-lg">
+        <div v-for="day in days" :key="day.value" class="day-column">
+          <div
+            class="day-header q-mb-sm text-center"
+            :class="isDayAvailable(day.value) ? 'gradient-diarista-bg text-white' : 'bg-grey-3 text-grey-6'"
+            @click="toggleDay(day.value)"
+          >
+            {{ day.label }}
+          </div>
+
+          <div
+            class="period-button q-mb-sm flex flex-center relative-position"
+            :class="isSelected(day.value, 'morning') ? 'active-morning' : 'inactive-period'"
+            @click="togglePeriod(day.value, 'morning')"
+          >
+            <q-icon v-if="!isSelected(day.value, 'morning')" name="mdi-lock-outline" size="24px" color="grey-6" class="absolute-center lock-icon" />
+            <span class="period-label">{{ $t('provider.login.steps.step_6.morning') }}</span>
+          </div>
+
+          <div
+            class="period-button flex flex-center relative-position"
+            :class="isSelected(day.value, 'afternoon') ? 'active-afternoon' : 'inactive-period'"
+            @click="togglePeriod(day.value, 'afternoon')"
+          >
+            <q-icon v-if="!isSelected(day.value, 'afternoon')" name="mdi-lock-outline" size="24px" color="grey-6" class="absolute-center lock-icon" />
+            <span class="period-label">{{ $t('provider.login.steps.step_6.afternoon') }}</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="text-caption text-grey-6 text-center">
+        {{ $t('provider.login.steps.step_6.instruction') }}
+      </div>
+    </q-card>
+
+    <q-banner rounded class="bg-blue-1 text-primary q-mb-md">
+      <template #avatar>
+        <q-icon name="mdi-alert-outline" color="primary" />
+      </template>
+      <span class="text-weight-bold">{{ $t('provider.login.steps.step_6.dont_worry') }}</span> {{ $t('provider.login.steps.step_6.change_anytime') }}
+    </q-banner>
+  </q-card-section>
+</template>
+
+<script setup>
+import { onMounted } from 'vue';
+
+const form = defineModel({ type: Object, required: true });
+
+const days = [
+  { value: 0, label: 'DOM' },
+  { value: 1, label: 'SEG' },
+  { value: 2, label: 'TER' },
+  { value: 3, label: 'QUA' },
+  { value: 4, label: 'QUI' },
+  { value: 5, label: 'SEX' },
+  { value: 6, label: 'SÁB' },
+];
+
+const initializeWorkingDays = () => {
+  if (!form.value.working_days || typeof form.value.working_days !== 'object') {
+    form.value.working_days = {};
+  }
+
+  for (const day of days) {
+    if (!form.value.working_days[day.value]) {
+      form.value.working_days[day.value] = {
+        morning: true,
+        afternoon: true,
+      };
+    }
+  }
+};
+
+const isSelected = (day, period) => {
+  return !!form.value.working_days?.[day]?.[period];
+};
+
+const isDayAvailable = (day) => {
+  const d = form.value.working_days?.[day];
+  return d?.morning || d?.afternoon;
+};
+
+const toggleDay = (day) => {
+  initializeWorkingDays();
+  const currentState = isDayAvailable(day);
+  form.value.working_days[day].morning = !currentState;
+  form.value.working_days[day].afternoon = !currentState;
+};
+
+const togglePeriod = (day, period) => {
+  initializeWorkingDays();
+  form.value.working_days[day][period] = !form.value.working_days[day][period];
+};
+
+onMounted(() => {
+  initializeWorkingDays();
+});
+</script>
+
+<style lang="scss" scoped>
+.availability-card {
+  border-radius: 20px;
+}
+
+.days-grid {
+  display: flex;
+  justify-content: space-between;
+  gap: 4px;
+}
+
+.day-column {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.day-header {
+  font-size: 10px;
+  font-weight: bold;
+  padding: 2px 0;
+  border-radius: 10px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.period-button {
+  font-size: 10px;
+  height: 36px;
+  width: 36px;
+  margin-left: auto;
+  margin-right: auto;
+  border-radius: 50%;
+  cursor: pointer;
+  transition: all 0.3s;
+  text-align: center;
+}
+
+.period-label {
+  z-index: 1;
+}
+
+.lock-icon {
+  opacity: 0.6;
+  z-index: 2;
+}
+
+.active-morning, .active-afternoon {
+  background-color: #00ff7f;
+  color: #004d40;
+  font-weight: bold;
+}
+
+.inactive-period {
+  background-color: #e4e4e4;
+  color: #c0c0c0;
+  border: none;
+  text-decoration: none;
+}
+
+.line-height-tight {
+  line-height: 1.2;
+}
+</style>

+ 177 - 145
src/components/login/LoginStepThreePanel.vue

@@ -1,186 +1,216 @@
 <template>
   <q-card-section class="no-padding">
-    <div class="">
-      <div class="text-text">
-        <span class="text-weight-medium">{{ $t('auth.full_name') }}</span>
-      </div>
-      <q-input
-        v-model="form.name"
-        no-error-icon
-        outlined
-        rounded
-        class="bg-surface q-mt-sm q-mb-md"
-        input-class="text-text"
-        :placeholder="$t('auth.full_name')"
-        hide-bottom-space
-        :rules="[inputRules.required]"
-        lazy-rules
-      />
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('provider.login.steps.step_3.full_name') }}</span>
     </div>
+    <q-input
+      v-model="form.name"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      :placeholder="$t('provider.login.steps.step_3.full_name')"
+      hide-bottom-space
+      :rules="[inputRules.required]"
+      lazy-rules
+    />
 
-    <div class="">
-      <div class="text-text">
-        <span class="text-weight-medium">{{ $t('common.terms.cpf') }}</span>
-      </div>
-      <q-input
-        v-model="form.document"
-        no-error-icon
-        outlined
-        rounded
-        class="bg-surface q-mt-sm q-mb-md"
-        input-class="text-text"
-        placeholder="000.000.000-00"
-        hide-bottom-space
-        :rules="[inputRules.required, inputRules.cpf]"
-        lazy-rules
-        mask="###.###.###-##"
-      />
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('common.terms.phone') }}</span>
     </div>
+    <q-input
+      v-model="form.phone"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      placeholder="(00) 00000-0000"
+      hide-bottom-space
+      :rules="[inputRules.required]"
+      lazy-rules
+      mask="(##) #####-####"
+    />
 
-    <div class="">
-      <div class="text-text">
-        <span class="text-weight-medium">{{ $t('common.terms.cep') }}</span>
-      </div>
-      <q-input
-        v-model="form.zip_code"
-        no-error-icon
-        outlined
-        rounded
-        class="bg-surface q-mt-sm q-mb-md"
-        input-class="text-text"
-        placeholder="00000-00"
-        hide-bottom-space
-        :rules="[inputRules.required, inputRules.cep]"
-        lazy-rules
-        mask="#####-###"
-        :loading="loadingCep"
-        @update:model-value="onCepChange"
-      />
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('common.terms.email') }}</span>
     </div>
+    <q-input
+      v-model="form.email"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      placeholder="nome@email.com"
+      hide-bottom-space
+      readonly
+      :rules="[inputRules.required, inputRules.email]"
+      lazy-rules
+      type="email"
+    />
 
-    <div class="">
-      <div class="text-text">
-        <span class="text-weight-medium">{{ $t('common.terms.address') }}</span>
-      </div>
-      <q-input
-        v-model="form.address"
-        no-error-icon
-        outlined
-        rounded
-        class="bg-surface q-mt-sm q-mb-md"
-        input-class="text-text"
-        :placeholder="`${$t('common.terms.address')}...`"
-        hide-bottom-space
-        :rules="[inputRules.required]"
-        lazy-rules
-        readonly
-      />
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('provider.login.steps.step_3.rg') }}</span>
     </div>
+    <q-input
+      v-model="form.rg"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      placeholder="00.000.000-0"
+      hide-bottom-space
+      :rules="[inputRules.required, validateRG]"
+      lazy-rules
+      mask="##.###.###-#"
+    />
 
-    <div class="">
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('common.terms.cpf') }}</span>
+    </div>
+    <q-input
+      v-model="form.document"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      placeholder="000.000.000-00"
+      hide-bottom-space
+      :rules="[inputRules.required, inputRules.cpf]"
+      lazy-rules
+      mask="###.###.###-##"
+    />
+
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('provider.login.steps.step_3.birth_date') }}</span>
+    </div>
+    <q-input
+      v-model="form.birth_date"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      :placeholder="$t('provider.login.steps.step_3.birth_date_placeholder')"
+      hide-bottom-space
+      :rules="[inputRules.required, validateBirthDate]"
+      lazy-rules
+      mask="##/##/####"
+    />
+
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('common.terms.cep') }}</span>
+    </div>
+    <q-input
+      v-model="form.zip_code"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      placeholder="00000-000"
+      hide-bottom-space
+      :rules="[inputRules.required, inputRules.cep]"
+      lazy-rules
+      mask="#####-###"
+      :loading="loadingCep"
+      @update:model-value="onCepChange"
+    />
+
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('common.terms.address') }}</span>
+    </div>
+    <q-input
+      v-model="form.address"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm q-mb-md"
+      input-class="text-text"
+      :placeholder="$t('provider.login.steps.step_3.address_placeholder')"
+      hide-bottom-space
+      :rules="[inputRules.required]"
+      lazy-rules
+    />
+    <div class="text-center">
       <q-checkbox
         v-model="form.no_complement"
-        :label="$t('auth.no_complement')"
-        color="primary"
+        :label="$t('provider.login.steps.step_3.no_complement')"
         class="q-mb-md text-text"
-      />
-    </div>
-    <div class="">
-      <template v-if="!form.no_complement">
-        <div class="text-text">
-          <span class="text-weight-medium">{{ $t('common.terms.complement') }}</span>
-        </div>
-        <q-input
-          v-model="form.complement"
-          no-error-icon
-          outlined
-          rounded
-          class="bg-surface q-mt-sm q-mb-md"
-          input-class="text-text"
-          :placeholder="`${$t('common.ui.misc.example')}: Apartamento, Conjunto, Casa`"
-          hide-bottom-space
-          :rules="!form.no_complement ? [inputRules.required] : []"
-          lazy-rules
-        />
-      </template>
-    </div>
-    <div class="">
-      <div class="text-text">
-        <span class="text-weight-medium">{{ $t('auth.address_nickname') }}</span>
-      </div>
-      <q-input
-        v-model="form.nickname"
-        no-error-icon
-        outlined
-        rounded
-        class="bg-surface q-mt-sm q-mb-md"
-        input-class="text-text"
-        :placeholder="`${$t('common.ui.misc.example')}: Casa`"
-        hide-bottom-space
-        lazy-rules
+        color="primary"
+        keep-color
       />
     </div>
 
-    <div class="">
+    <template v-if="!form.no_complement">
       <div class="text-text">
-        <span class="text-weight-medium">{{ $t('auth.address_instructions') }}</span>
+        <span class="text-weight-medium">{{ $t('common.terms.complement') }}</span>
       </div>
       <q-input
-        v-model="form.instructions"
+        v-model="form.complement"
         no-error-icon
         outlined
         rounded
         class="bg-surface q-mt-sm q-mb-md"
         input-class="text-text"
-        type="textarea"
-        rows="3"
-        autogrow
+        :placeholder="$t('provider.login.steps.step_3.complement_placeholder')"
         hide-bottom-space
+        :rules="[inputRules.required]"
         lazy-rules
       />
-    </div>
-    <div class="">
-      <div class="row q-gutter-sm q-mt-xs">
-        <q-chip
-          v-for="type in addressTypes"
-          :key="type.value"
-          :selected="form.address_type === type.value"
-          clickable
-          color="primary"
-          :outline="form.address_type !== type.value"
-          text-color="surface"
-          :icon="type.icon"
-          :icon-selected="type.icon"
-          @click="form.address_type = type.value"
-        >
-          {{ $t(type.label) }}
-        </q-chip>
-      </div>
-    </div>
+    </template>
   </q-card-section>
 </template>
 
 <script setup>
 import { ref } from 'vue';
-import { useInputRules } from 'src/composables/useInputRules';
 import axios from 'axios';
+import { useI18n } from 'vue-i18n';
+import { useInputRules } from 'src/composables/useInputRules';
 
+const { t } = useI18n();
 const form = defineModel({ type: Object, required: true });
-
 const { inputRules } = useInputRules();
 const loadingCep = ref(false);
 
-const addressTypes = [
-  { value: 'home', label: 'auth.address_type_home', icon: 'mdi-home-outline' },
-  { value: 'commercial', label: 'auth.address_type_commercial', icon: 'mdi-briefcase-variant-outline' },
-  { value: 'other', label: 'auth.address_type_other', icon: 'mdi-map-marker-outline' },
-];
+const validateRG = (value) => {
+  const cleanedValue = value?.replace(/\D/g, '') || '';
+  return cleanedValue.length >= 8 || t('provider.login.steps.step_3.rg_invalid');
+};
+
+const validateBirthDate = (value) => {
+  if (!value) return t('validation.rules.required');
+
+  const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value);
+  if (!matches) return t('provider.login.steps.step_3.date_invalid');
+
+  const day = Number(matches[1]);
+  const month = Number(matches[2]);
+  const year = Number(matches[3]);
+
+  const date = new Date(year, month - 1, day);
+  const isValidDate =
+    date.getFullYear() === year &&
+    date.getMonth() === month - 1 &&
+    date.getDate() === day;
+
+  if (!isValidDate) return t('provider.login.steps.step_3.date_invalid');
+  if (date > new Date()) return t('provider.login.steps.step_3.date_invalid');
+
+  return true;
+};
+// ...existing code...
 
 const fetchCep = async (rawCep) => {
   const cleaned = rawCep.replace(/\D/g, '');
   if (cleaned.length !== 8) return;
 
   loadingCep.value = true;
+
   try {
     const { data } = await axios.get(`https://viacep.com.br/ws/${cleaned}/json/`);
     if (!data.erro) {
@@ -189,20 +219,22 @@ const fetchCep = async (rawCep) => {
       form.value.state = data.uf;
     } else {
       form.value.address = '';
+      form.value.city = '';
+      form.value.state = '';
     }
   } catch {
     form.value.address = '';
+    form.value.city = '';
+    form.value.state = '';
   } finally {
     loadingCep.value = false;
   }
 };
 
-const onCepChange = (val) => {
-  const cleaned = val?.replace(/\D/g, '') ?? '';
+const onCepChange = (value) => {
+  const cleaned = value?.replace(/\D/g, '') || '';
   if (cleaned.length === 8) {
-    fetchCep(val);
-  } else {
-    form.value.address = '';
+    fetchCep(value);
   }
 };
-</script>
+</script>

+ 1 - 1
src/composables/useAuth.js

@@ -56,7 +56,7 @@ export const useAuth = () => {
         return Promise.reject(new Error("No refresh token available"));
       }
 
-      const response = await api.post("/refresh", {
+      const response = await api.post("/refresh-app", {
         refresh_token: refreshToken,
       });
 

+ 14 - 12
src/css/app.scss

@@ -116,12 +116,7 @@ input[type="number"]::-webkit-outer-spin-button {
 }
 
 .gradient-diarista {
-  background: linear-gradient(
-    -90deg,
-    #ec48d1 5%,
-    #6b11cb 65%,
-    #2574fc 100%
-  );
+  background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
 
   -webkit-background-clip: text;
   -webkit-text-fill-color: transparent;
@@ -131,10 +126,17 @@ input[type="number"]::-webkit-outer-spin-button {
 }
 
 .gradient-diarista-bg {
-  background: linear-gradient(
-    -90deg,
-    #ec48d1 1%,
-    #6b11cbcb 65%,
-    #2574fcbd 100%
-  );
+  background: linear-gradient(-90deg, #ec48d1 1%, #6b11cbcb 65%, #2574fcbd 100%);
+}
+
+
+.shadow-card {
+box-shadow: 1px 4px 4px 0px rgba(0,0,0,0.2);
+-webkit-box-shadow: 1px 4px 4px 0px rgba(0,0,0,0.2);
+-moz-box-shadow: 1px 4px 4px 0px rgba(0,0,0,0.2);
+}
+
+.section-title {
+  font-size: 18px;
+  font-weight: 700;
 }

+ 18 - 0
src/helpers/arraysOptions/labelsPeriodTypes.js

@@ -0,0 +1,18 @@
+export const labelsPeriodTypes = [
+  {
+    value: 8,
+    label: 'provider.dashboard.solicitations.until_8h',
+  },
+  {
+    value: 6,
+    label: 'provider.dashboard.solicitations.until_6h',
+  },
+  {
+    value: 4,
+    label: 'provider.dashboard.solicitations.until_4h',
+  },
+  {
+    value: 2,
+    label: 'provider.dashboard.solicitations.until_2h',
+  },
+];

+ 116 - 57
src/i18n/locales/en.json

@@ -1,5 +1,14 @@
 {
   "common": {
+    "see_all": "See all",
+    "details": "View details",
+    "accept": "Accept",
+    "refuse": "Refuse",
+    "from": "From",
+    "to": "To",
+    "alter": "Alter",
+    "price_masked": "R$ ***,**",
+    "km": "km",
     "actions": {
       "save": "Save",
       "cancel": "Cancel",
@@ -127,29 +136,115 @@
     }
   },
   "auth": {
+    "validation_code": "Validation code",
     "login": "Login",
-    "logout": "Logout",
-    "registration": "Registration",
-    "confirm": "Confirm",
-    "continue": "Continue",
-    "confirm_password": "Confirm Password",
-    "agreed_terms": "I agree with the terms",
-    "agreed_privacy": "I agree with the privacy policy",
-    "enter_code": "Enter the code sent to your email",
-    "code_placeholder": "Code",
-    "validation_code": "Validation Code",
-    "register_later": "Register later",
     "register": "Register",
-    "full_name": "Full Name",
-    "no_complement": "Address without complement",
-    "address_nickname": "Address nickname",
-    "address_instructions": "Instructions (optional)",
-    "address_type_home": "Home",
-    "address_type_commercial": "Commercial",
-    "address_type_other": "Other",
-    "step4_title": "Add your ZIP code and see the nearest cleaners",
-    "use_location": "use my location",
-    "back_to_register": "Back to registration"
+    "logout": "Logout",
+    "forgot_password": "Forgot my password",
+    "confirm_password": "Confirm password"
+  },
+  "provider": {
+    "login": {
+      "steps": {
+        "step_1": {
+          "action": "send code"
+        },
+        "step_2": {
+          "action": "validate code"
+        },
+        "step_3": {
+          "full_name": "Full Name",
+          "birth_date": "Date of birth",
+          "birth_date_placeholder": "mm/dd/yyyy",
+          "no_complement": "Address without complement",
+          "complement_placeholder": "Ex: Apartment, Suite, House.",
+          "address_placeholder": "Street, Number, Neighborhood, City - ST",
+          "action": "continue",
+          "rg_invalid": "Invalid RG",
+          "date_invalid": "Invalid date",
+          "rg": "RG"
+        },
+        "step_4": {
+            "document_front": "Side 1 (Front)",
+            "document_front_desc": "Photo of the front of your document (ID or Driver License).",
+            "document_back": "Side 2 (Back)",
+            "document_back_desc": "Photo of the back of your document (ID or Driver License).",
+            "selfie": "Selfie with document",
+            "selfie_desc": "Take a selfie holding your document near your face.",
+            "photo_captured": "Photo captured!",
+            "action": "send documents"
+        },
+        "step_5": {
+            "daily_price_title": "What is the value of your daily rate for up to 8 hours?",
+            "daily_price_min_max": "Minimum value $ 100.00. Maximum value $ 500.00.",
+            "dont_worry": "Don't worry!",
+            "change_anytime": "You can change your daily rate value whenever you want ;)",
+            "shorter_services": "Based on the value above, the values for shorter services will be:",
+            "up_to_6h": "up to 6 hours",
+            "up_to_4h": "up to 4 hours",
+            "up_to_2h": "up to 2 hours",
+            "other_services": "Besides basic cleaning, what other services do you also perform?",
+            "change_in_profile": "(can be changed in profile)",
+            "no_services": "No service types available at the moment.",
+            "search_visibility": "This information will appear to those searching for your profile in the search.",
+            "action": "continue"
+        },
+        "step_6": {
+            "title": "Now let's choose the best times for you to receive service requests!",
+            "lock_hint": "Tap and block days or periods",
+            "lock_description": "if you do not wish to receive service requests in this period.",
+            "morning": "morning",
+            "afternoon": "afternoon",
+            "instruction": "To block individual days, click on the day, to block only periods, click on morning or afternoon.",
+            "dont_worry": "Don't worry!",
+            "change_anytime": "Afterwards you can change the availability of your preference at any time in the app ;)",
+            "select_at_least_one": "Select at least one work period.",
+            "action": "finish registration"
+        }
+      }
+    },
+    "dashboard": {
+      "summary": {
+        "welcome": "Hello,",
+        "my_schedules": "My schedules"
+      },
+      "solicitations": {
+        "title": "Requests for you",
+        "until_8h": "Full day (up to 8h)",
+        "until_6h": "Standard (up to 6h)",
+        "until_4h": "Medium (up to 4h)",
+        "until_2h": "Quick (up to 2h)"
+      },
+      "favorites": {
+        "title": "Your favorites",
+        "view_schedule": "View schedule"
+      },
+      "next_schedules": {
+        "title": "Next services",
+        "custom": "Custom",
+        "default": "Default cleaning",
+        "no_provider": "Provider not defined",
+        "from": "from",
+        "to": "to",
+        "to_combine": "To be combined",
+        "tag_custom": "Custom",
+        "tag_default": "Default",
+        "details": "View details",
+        "place_home": "Residential",
+        "place_apartment": "Apartment",
+        "place_unknown": "Address"
+      },
+      "last_schedules": {
+        "title": "Last ones performed",
+        "reschedule": "Reschedule"
+      },
+      "providers_close": {
+        "title": "Close to you",
+        "until_8h": "Up to 8h",
+        "place_home": "Residential",
+        "schedule": "Schedule"
+      }
+    }
   },
   "business": {
     "advertise": "Advertise",
@@ -313,41 +408,5 @@
       "passives": "Passives",
       "detractors": "Detractors"
     }
-  },
-  "dashboard": {
-    "currency_format": "$ {value}",
-    "cards": {
-      "total_earnings": "Total Earnings",
-      "tickets_sold": "Tickets Sold",
-      "registrations": "Registrations"
-    },
-    "charts": {
-      "tickets_by_type": {
-        "title": "Total tickets by type",
-        "labels": {
-          "vip": "VIP",
-          "track": "Track",
-          "box": "Box",
-          "courtesy": "Courtesy"
-        }
-      },
-      "participants_by_document": {
-        "title": "Percentage of CNPJ and CPF in registrations"
-      },
-      "sales_over_time": {
-        "title": "Sales Over Period",
-        "y_label": "Value ({currency})"
-      },
-      "registration_source": {
-        "title": "Registration Source",
-        "source": "Source",
-        "sources": {
-          "instagram": "Instagram",
-          "facebook": "Facebook",
-          "google": "Google",
-          "referral": "Referral"
-        }
-      }
-    }
   }
 }

+ 117 - 22
src/i18n/locales/es.json

@@ -1,5 +1,14 @@
 {
   "common": {
+    "see_all": "ver todo",
+    "details": "ver detalles",
+    "accept": "aceptar",
+    "refuse": "recusar",
+    "from": "De",
+    "to": "A",
+    "alter": "alterar",
+    "price_masked": "R$ ***,**",
+    "km": "km",
     "actions": {
       "save": "Guardar",
       "cancel": "Cancelar",
@@ -127,29 +136,115 @@
     }
   },
   "auth": {
-    "login": "Iniciar sesión",
+    "validation_code": "Código de validación",
+    "login": "Ingresar",
+    "register": "Registrarse",
     "logout": "Cerrar sesión",
-    "registration": "Registro",
-    "confirm": "Confirmar",
-    "continue": "Continuar",
-    "confirm_password": "Confirmar contraseña",
-    "agreed_terms": "Acepto los términos",
-    "agreed_privacy": "Acepto la política de privacidad",
-    "enter_code": "Ingresa el código enviado a tu correo",
-    "code_placeholder": "Código",
-    "validation_code": "Código de Validación",
-    "register_later": "Registrar más tarde",
-    "register": "Registrar",
-    "full_name": "Nombre Completo",
-    "no_complement": "Dirección sin complemento",
-    "address_nickname": "Apodo de la dirección",
-    "address_instructions": "Instrucciones (opcional)",
-    "address_type_home": "Casa",
-    "address_type_commercial": "Comercial",
-    "address_type_other": "Otro",
-    "step4_title": "Agrega tu código postal y ve los limpiadores más cercanos",
-    "use_location": "usar mi ubicación",
-    "back_to_register": "Volver al registro"
+    "forgot_password": "Olvidé mi contraseña",
+    "confirm_password": "Confirmar contraseña"
+  },
+  "provider": {
+    "login": {
+      "steps": {
+        "step_1": {
+          "action": "enviar código"
+        },
+        "step_2": {
+          "action": "validar código"
+        },
+        "step_3": {
+          "full_name": "Nombre Completo",
+          "birth_date": "Fecha de nacimiento",
+          "birth_date_placeholder": "dd/mm/aaaa",
+          "no_complement": "Dirección sin complemento",
+          "complement_placeholder": "Ej: Apartamento, Conjunto, Casa.",
+          "address_placeholder": "Calle, Número, Barrio, Ciudad - UF",
+          "action": "continuar",
+          "rg_invalid": "RG inválido",
+          "date_invalid": "Fecha inválida",
+          "rg": "RG"
+        },
+        "step_4": {
+            "document_front": "Lado 1 (Frente)",
+            "document_front_desc": "Foto del frente de su documento (RG o Licencia).",
+            "document_back": "Lado 2 (Dorso)",
+            "document_back_desc": "Foto del dorso de su documento (RG o Licencia).",
+            "selfie": "Selfie con documento",
+            "selfie_desc": "Tome una selfie sosteniendo su documento cerca de su rostro.",
+            "photo_captured": "¡Foto capturada!",
+            "action": "enviar documentos"
+        },
+        "step_5": {
+            "daily_price_title": "¿Cuál es el valor de su jornada de hasta 8 horas?",
+            "daily_price_min_max": "Valor mínimo R$ 100,00. Valor máximo R$ 500,00.",
+            "dont_worry": "¡No se preocupe!",
+            "change_anytime": "Puede cambiar el valor de su jornada cuando quiera ;)",
+            "shorter_services": "Basado en el valor anterior, los valores de servicios más cortos serán:",
+            "up_to_6h": "hasta 6 horas",
+            "up_to_4h": "hasta 4 horas",
+            "up_to_2h": "hasta 2 horas",
+            "other_services": "Además de la limpieza básica, ¿qué otros servicios realiza?",
+            "change_in_profile": "(podrá ser cambiado en el perfil)",
+            "no_services": "No hay tipos de servicio disponibles en este momento.",
+            "search_visibility": "Esta información aparecerá para quienes busquen su perfil en la búsqueda.",
+            "action": "continuar"
+        },
+        "step_6": {
+            "title": "¡Ahora vamos a elegir qué mejores horarios para que recibas solicitudes de servicios!",
+            "lock_hint": "Toque y bloquee los días o períodos",
+            "lock_description": "si no desea recibir solicitudes de servicios en este período.",
+            "morning": "mañana",
+            "afternoon": "tarde",
+            "instruction": "Para bloquear días individuales, haga clic en el día, para bloquear solo períodos, haga clic en mañana o tarde.",
+            "dont_worry": "¡No se preocupe!",
+            "change_anytime": "Después podrá cambiar la disponibilidad de su preferencia en cualquier momento en la aplicación ;)",
+            "select_at_least_one": "Seleccione al menos un período de trabajo.",
+            "action": "finalizar registro"
+        }
+      }
+    },
+    "dashboard": {
+      "summary": {
+        "welcome": "Hola,",
+        "my_schedules": "Mis citas"
+      },
+      "solicitations": {
+        "title": "Solicitudes para ti",
+        "until_8h": "Integral (hasta 8h)",
+        "until_6h": "Padrón (Hasta 6h)",
+        "until_4h": "Medio Período (Hasta 4h)",
+        "until_2h": "Diaria Rápida (Hasta 2h)"
+      },
+      "favorites": {
+        "title": "Tus favoritos",
+        "view_schedule": "Ver agenda"
+      },
+      "next_schedules": {
+        "title": "Próximos servicios",
+        "custom": "Personalizado",
+        "default": "Limpieza estándar",
+        "no_provider": "Prestador no definido",
+        "from": "de",
+        "to": "hasta",
+        "to_combine": "A convenir",
+        "tag_custom": "Personalizado",
+        "tag_default": "Estándar",
+        "details": "Ver detalles",
+        "place_home": "Residencial",
+        "place_apartment": "Apartamento",
+        "place_unknown": "Dirección"
+      },
+      "last_schedules": {
+        "title": "Últimas realizadas",
+        "reschedule": "Reprogramar"
+      },
+      "providers_close": {
+        "title": "Cerca de ti",
+        "until_8h": "Hasta 8h",
+        "place_home": "Residencial",
+        "schedule": "Agendar"
+      }
+    }
   },
   "business": {
     "advertise": "Anunciar",

+ 126 - 22
src/i18n/locales/pt.json

@@ -1,5 +1,14 @@
 {
   "common": {
+    "see_all": "ver todas",
+    "details": "ver detalhes",
+    "accept": "aceitar",
+    "refuse": "recusar",
+    "from": "Das",
+    "to": "às",
+    "alter": "alterar",
+    "price_masked": "R$ ***,**",
+    "km": "km",
     "actions": {
       "save": "Salvar",
       "cancel": "Cancelar",
@@ -127,29 +136,124 @@
     }
   },
   "auth": {
-    "login": "Login",
-    "logout": "Sair",
-    "registration": "Cadastro",
-    "confirm": "Confirmar",
-    "continue": "Continuar",
-    "confirm_password": "Confirmar Senha",
-    "agreed_terms": "Eu concordo com os termos",
-    "agreed_privacy": "Eu concordo com a política de privacidade",
-    "enter_code": "Digite o código enviado para seu e-mail",
-    "code_placeholder": "Código",
-    "validation_code": "Código de Validação",
-    "register_later": "Cadastrar mais tarde",
+    "validation_code": "Código de validação",
+    "login": "Entrar",
     "register": "Cadastrar",
-    "full_name": "Nome Completo",
-    "no_complement": "Endereço sem complemento",
-    "address_nickname": "Apelido do endereço",
-    "address_instructions": "Instruções (opcional)",
-    "address_type_home": "Casa",
-    "address_type_commercial": "Comercial",
-    "address_type_other": "Outro",
-    "step4_title": "Adicione seu CEP e veja os diaristas mais próximos",
-    "use_location": "usar minha localização",
-    "back_to_register": "Voltar para o cadastro"
+    "logout": "Sair",
+    "forgot_password": "Esqueci minha senha",
+    "confirm_password": "Confirmar senha"
+  },
+  "provider": {
+    "login": {
+      "steps": {
+        "step_1": {
+          "action": "enviar código"
+        },
+        "step_2": {
+          "action": "validar código"
+        },
+        "step_3": {
+          "full_name": "Nome Completo",
+          "birth_date": "Data de nascimento",
+          "birth_date_placeholder": "dd/mm/aaaa",
+          "no_complement": "Endereço sem complemento",
+          "complement_placeholder": "Ex: Apartamento, Conjunto, Casa.",
+          "address_placeholder": "Rua, Número, Bairro, Cidade - UF",
+          "action": "continuar",
+          "rg_invalid": "RG inválido",
+          "date_invalid": "Data inválida",
+          "rg": "RG"
+        },
+        "step_4": {
+            "document_front": "Lado 1 (Frente)",
+            "document_front_desc": "Foto da frente do seu documento (RG ou CNH).",
+            "document_back": "Lado 2 (Verso)",
+            "document_back_desc": "Foto do verso do seu documento (RG ou CNH).",
+            "selfie": "Selfie com documento",
+            "selfie_desc": "Tire uma selfie segurando o seu documento próximo ao rosto.",
+            "photo_captured": "Foto capturada!",
+            "action": "enviar documentos"
+        },
+        "step_5": {
+            "daily_price_title": "Qual valor da sua diária de até 8 horas?",
+            "daily_price_min_max": "Valor mínimo R$ 100,00. Valor máximo R$ 500,00.",
+            "dont_worry": "Não se preocupe!",
+            "change_anytime": "Você pode alterar o valor da sua diária quando quiser ;)",
+            "shorter_services": "Baseado no valor acima, os valores de serviços mais curtos serão:",
+            "up_to_6h": "até 6 horas",
+            "up_to_4h": "até 4 horas",
+            "up_to_2h": "até 2 horas",
+            "other_services": "Além da limpeza básica, quais serviços você também realiza?",
+            "change_in_profile": "(poderá ser alterado no perfil)",
+            "no_services": "Nenhum tipo de serviço disponível no momento.",
+            "search_visibility": "Essas informações aparecerão para quem buscar o seu perfil na busca.",
+            "action": "continuar"
+        },
+        "step_6": {
+            "title": "Agora vamos escolher quais melhor horários para você receber solicitações de serviços!",
+            "lock_hint": "Toque e bloqueie os dias ou períodos",
+            "lock_description": "se você não deseja receber solicitações de serviços neste período.",
+            "morning": "manhã",
+            "afternoon": "tarde",
+            "instruction": "Para bloquear dias individuais, clique no dia, para bloquear apenas períodos, clique em manhã ou tarde.",
+            "dont_worry": "Não se preocupe!",
+            "change_anytime": "Depois você poderá alterar a disponibilidade de sua preferência a qualquer momento no app ;)",
+            "select_at_least_one": "Selecione ao menos um período de trabalho.",
+            "action": "concluir cadastro"
+        }
+      }
+    },
+    "dashboard": {
+      "summary": {
+        "welcome": "Olá,",
+        "my_schedules": "Minhas diárias"
+      },
+      "price_suggest": {
+        "region_label": "Preço de diária sugerido na região",
+        "my_price_label": "Preço da minha diária"
+      },
+      "solicitations": {
+        "title": "Solicitações para você",
+        "until_8h": "Integral (até 8h)",
+        "until_6h": "Padrão (Até 6h)",
+        "until_4h": "Meio Período (Até 4h)",
+        "until_2h": "Diária Rápida (Até 2h)"
+      },
+      "opportunities": {
+        "title": "Oportunidades"
+      },
+      "favorites": {
+        "title": "Seus favoritos",
+        "view_schedule": "Ver agenda"
+      },
+      "next_schedules": {
+        "title": "Próximos serviços",
+        "custom": "Personalizada",
+        "default": "Faxina padrão",
+        "no_provider": "Prestador não definido",
+        "from": "de",
+        "to": "até",
+        "to_combine": "A combinar",
+        "tag_custom": "Personalizada",
+        "tag_default": "Padrão",
+        "details": "Ver detalhes",
+        "offers_meal": "Oferece refeição",
+        "no_meal": "Não oferece refeição",
+        "place_home": "Residencial",
+        "place_apartment": "Apartamento",
+        "place_unknown": "Endereço"
+      },
+      "last_schedules": {
+        "title": "Últimas realizadas",
+        "reschedule": "Reagendar"
+      },
+      "providers_close": {
+        "title": "Próximos a você",
+        "until_8h": "Até 8h",
+        "place_home": "Residencial",
+        "schedule": "Agendar"
+      }
+    }
   },
   "business": {
     "advertise": "Anunciar",

+ 54 - 109
src/layouts/MainLayout.vue

@@ -1,7 +1,5 @@
 <template>
   <q-layout class="main-layout relative" view="hHh lpR fFf">
-    <!-- <LeftMenuLayout v-if="!$q.screen.lt.sm" />
-    <LeftMenuLayoutMobile v-else v-model="leftDrawerOpen" /> -->
     <q-header
       v-if="$q.screen.lt.sm"
       class="bg-surface column justify-end"
@@ -9,71 +7,22 @@
         height: `calc(50px + env(safe-area-inset-top))`,
       }"
     >
-      <!-- <q-toolbar
-        class="flex justify-between bg-primary"
-        style="border-radius: 0 0 6px 6px !important"
-      > -->
-        <!-- <q-btn dense flat @click="toggleLeftDrawer">
-          <q-icon name="menu" :color="$q.dark.isActive ? 'white' : 'black'" />
-        </q-btn> -->
-        <!-- <q-btn dense flat>
-          <img
-            :src="someAvatar()"
-            alt="avatar"
-            style="width: 20px; height: 20px; border-radius: 50%"
-          />
-          <q-menu anchor="center right" self="top start">
-            <q-list class="column no-wrap overflow-hidden">
-              <q-item
-                v-ripple
-                v-close-popup
-                clickable
-                :to="{ name: 'ProfilePage' }"
-                exact
-                exact-active-class="menu-selected"
-              >
-                <div class="flex">
-                  <q-item-section avatar>
-                    <q-icon
-                      name="account_circle"
-                      color="primary"
-                      style="font-size: 18px"
-                    />
-                  </q-item-section>
-                  <q-item-section>{{
-                    $t("user.profile.singular")
-                  }}</q-item-section>
-                </div>
-              </q-item>
-              <q-item v-ripple clickable @click="logoutFn">
-                <div class="flex">
-                  <q-item-section avatar>
-                    <q-icon
-                      name="logout"
-                      color="negative"
-                      style="font-size: 18px"
-                    />
-                  </q-item-section>
-                  <q-item-section>{{ $t("auth.logout") }}</q-item-section>
-                </div>
-              </q-item>
-            </q-list>
-          </q-menu>
-        </q-btn> -->
-      <!-- </q-toolbar> -->
     </q-header>
     <q-page-container>
-      <q-page class="bg-surface">
+      <q-page class="bg-surface main-layout-page" style="overflow: hidden;">
         <q-scroll-area
           ref="scrollAreaRef"
-          :style="scrollAreaStyle"
+          class="main-layout-scroll-area"
+          :style="scrollAreaHeight"
+          :content-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
+          :content-active-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
         >
           <router-view v-slot="{ Component }">
             <Transition mode="out-in">
               <component
                 :is="Component"
-                class="main-layout__view"
-                :class="{ 'main-layout__view--mobile': $q.screen.lt.sm }"
+                class="main-layout-view"
+                :class="{ 'main-layout-view--mobile': $q.screen.lt.sm }"
               />
             </Transition>
           </router-view>
@@ -81,16 +30,16 @@
       </q-page>
     </q-page-container>
     <q-footer v-if="$q.screen.lt.sm" class="provider-bottom-nav bg-white">
-      <nav class="provider-bottom-nav__inner">
+      <nav class="provider-bottom-nav-inner">
         <router-link
           v-for="item in navItems"
           :key="item.name"
           :to="{ name: item.name }"
-          class="provider-bottom-nav__item"
-          :class="{ 'provider-bottom-nav__item--active': isNavItemActive(item) }"
+          class="provider-bottom-nav-item"
+          :class="{ 'provider-bottom-nav-item--active': isNavItemActive(item) }"
         >
-          <q-icon :name="item.icon" class="provider-bottom-nav__icon" />
-          <span class="provider-bottom-nav__label">{{ item.label }}</span>
+          <q-icon :name="item.icon" class="provider-bottom-nav-icon" />
+          <span class="provider-bottom-nav-label">{{ item.label }}</span>
         </router-link>
       </nav>
     </q-footer>
@@ -101,24 +50,14 @@
 import { computed, useTemplateRef, watch } from "vue";
 import { useQuasar } from "quasar";
 import { useRoute } from "vue-router";
-// import { useAuth } from "src/composables/useAuth";
-// import { useRouter } from "vue-router";
-// import LeftMenuLayout from "src/components/layout/LeftMenuLayout.vue";
-// import LeftMenuLayoutMobile from "src/components/layout/LeftMenuLayoutMobile.vue";
 
 defineOptions({
   name: "MainLayout",
 });
 
-// const { logout } = useAuth();
 const $q = useQuasar();
 const route = useRoute();
-// const leftDrawerOpen = ref(false);
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
-// const router = useRouter();
-
-const MOBILE_HEADER_HEIGHT = 68;
-const MOBILE_BOTTOM_NAV_HEIGHT = 102;
 
 let oldValue = route.path;
 
@@ -129,9 +68,9 @@ const navItems = [
     icon: "mdi-home-outline",
   },
   {
-    name: "SearchPage",
-    label: "Busca",
-    icon: "mdi-magnify",
+    name: "PagamentosPage",
+    label: "Pagamentos",
+    icon: "mdi-credit-card-outline",
   },
   {
     name: "AgendaPage",
@@ -145,30 +84,15 @@ const navItems = [
   },
 ];
 
-const scrollAreaStyle = computed(() => {
+const isNavItemActive = (item) => route.name === item.name;
+
+const scrollAreaHeight = computed(() => {
   if ($q.screen.lt.sm) {
-    return `height: calc(100dvh - ${MOBILE_HEADER_HEIGHT}px - env(safe-area-inset-top) - ${MOBILE_BOTTOM_NAV_HEIGHT}px - env(safe-area-inset-bottom)) !important;`;
+    return 'height: calc(100dvh - 50px - env(safe-area-inset-top) - 80px - env(safe-area-inset-bottom)) !important;';
   }
-
-  return "height: calc(100dvh - env(safe-area-inset-top)) !important;";
+  return 'height: 100dvh !important;';
 });
 
-const isNavItemActive = (item) => route.name === item.name;
-
-// const someAvatar = () => {
-//   let random = Math.floor(Math.random() * 5) + 1;
-//   return "https://cdn.quasar.dev/img/avatar" + random + ".jpg";
-// };
-
-// const logoutFn = async () => {
-//   await logout();
-//   router.push({ name: "LoginPage" });
-// };
-
-// const toggleLeftDrawer = () => {
-//   leftDrawerOpen.value = !leftDrawerOpen.value;
-// };
-
 watch(
   () => route.path,
   (value) => {
@@ -181,14 +105,35 @@ watch(
 );
 </script>
 <style scoped>
-.main-layout__view {
-  padding: 20px !important;
-  padding-right: 10px !important;
+.main-layout {
+  width: 100%;
+  max-width: 100%;
+  overflow-x: hidden;
+}
+
+.main-layout-page {
+  width: 100%;
+  max-width: 100%;
+  overflow: hidden;
+}
+
+.main-layout-scroll-area {
+  width: 100%;
+  max-width: 100%;
+  overflow: hidden;
+}
+
+.main-layout-view {
+  width: 100%;
+  max-width: 100%;
+  min-width: 0;
+  padding: 0 !important;
+  box-sizing: border-box;
+  overflow-x: hidden;
 }
 
-.main-layout__view--mobile {
-  padding-left: 10px !important;
-  padding-bottom: 18px !important;
+.main-layout-view--mobile {
+  padding: 0 !important;
 }
 
 .v-enter-active {
@@ -216,15 +161,15 @@ watch(
   box-shadow: 0 -12px 30px rgba(38, 27, 52, 0.1);
 }
 
-.provider-bottom-nav__inner {
+.provider-bottom-nav-inner {
   display: grid;
   grid-template-columns: repeat(4, minmax(0, 1fr));
   align-items: end;
-  min-height: 56px;
+  height: 80px;
   padding: 12px 10px calc(12px + env(safe-area-inset-bottom));
 }
 
-.provider-bottom-nav__item {
+.provider-bottom-nav-item {
   display: flex;
   flex-direction: column;
   align-items: center;
@@ -236,23 +181,23 @@ watch(
   transition: color 0.2s ease;
 }
 
-.provider-bottom-nav__item--active {
+.provider-bottom-nav-item--active {
   color: #ff00ea;
 }
 
-.provider-bottom-nav__icon {
+.provider-bottom-nav-icon {
   font-size: 30px;
   line-height: 1;
 }
 
-.provider-bottom-nav__label {
+.provider-bottom-nav-label {
   font-size: 12px;
   font-weight: 400;
   line-height: 1.1;
   letter-spacing: -0.02em;
 }
 
-.provider-bottom-nav__item--active .provider-bottom-nav__label {
+.provider-bottom-nav-item--active .provider-bottom-nav-label {
   font-weight: 700;
 }
 </style>

+ 198 - 80
src/pages/LoginPage.vue

@@ -1,39 +1,16 @@
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
 <template>
   <q-page class="login-page bg-surface-dark">
     <Transition name="fade-slide" mode="out-in">
-
       <div v-if="!clicked" key="splash" class="splash-screen" @click="clicked = true">
         <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
         <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
         <img :src="LogoLogin" class="splash-layer splash-layer--logo" />
       </div>
 
-      <div v-else-if="steps === 4" key="step4" class="splash-screen">
-        <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
-        <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
-        <img :src="LogoLogin" class="splash-layer splash-layer--logo-small" />
-        <div class="step4-card-wrapper">
-          <LoginStepFourPanel v-model:cep="stepFourCep" @back="steps = 3" />
-        </div>
-      </div>
-
       <div v-else key="flow" class="flow-screen">
-
-        <div class="flow-header">
-          <q-btn
-            v-if="steps === 3"
-            flat
-            dense
-            color="primary"
-            :label="$t('auth.register_later')"
-            icon-right="mdi-chevron-right-circle-outline"
-            class="text-caption"
-            @click="steps = 4"
-          />
-        </div>
-
-        <div class="flow-logo">
-          <q-img :src="LogoDiariaCampos" style="max-width: 180px;" />
+        <div v-if="!showSubStep" class="flow-logo q-my-xl">
+          <q-img :src="LogoDiariaCampos" style="max-width: 180px" />
         </div>
 
         <q-form
@@ -45,18 +22,25 @@
           spellcheck="false"
           @submit="onSubmit"
         >
-          <div class="flow-content" :class="{ 'flow-content--centered': steps < 3 }">
+          <div class="flow-content" :class="{ 'flow-content--centered': steps <= 2 && !showSubStep }">
             <LoginStepOnePanel v-if="steps === 1" v-model:email="email" v-model:phone="phone" />
             <LoginStepTwoPanel v-else-if="steps === 2" v-model:code="code" />
             <LoginStepThreePanel v-else-if="steps === 3" v-model="stepThreeForm" />
+            <LoginStepFourPanel
+              v-else-if="steps === 4"
+              v-model="stepFourForm"
+              @update:show-sub-step="showSubStep = $event"
+            />
+            <LoginStepFivePanel v-else-if="steps === 5" v-model="stepFiveForm" />
+            <LoginStepSixPanel v-else-if="steps === 6" v-model="stepSixForm" />
           </div>
 
-          <div class="flow-footer">
+          <div v-if="!showSubStep" class="flow-footer">
             <q-btn
               color="primary-button"
-              :label="$t('auth.continue')"
+              :label="actionLabel"
               rounded
-              padding="14px 16px"
+              padding="8px 16px"
               type="submit"
               class="full-width"
               :loading="submitting"
@@ -67,18 +51,18 @@
             </q-btn>
           </div>
         </q-form>
-
       </div>
-
     </Transition>
   </q-page>
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
+import { useQuasar } from 'quasar';
+import { useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
 import { createUserAndProvider, sendCode, validateCode } from 'src/api/user';
 import { useAuth } from 'src/composables/useAuth';
-import { useRouter } from 'vue-router';
 
 import BackgroundLogin from 'src/assets/background-login.svg';
 import FotoDiarista from 'src/assets/foto_diarista_login.svg';
@@ -89,76 +73,223 @@ import LoginStepOnePanel from 'src/components/login/LoginStepOnePanel.vue';
 import LoginStepTwoPanel from 'src/components/login/LoginStepTwoPanel.vue';
 import LoginStepThreePanel from 'src/components/login/LoginStepThreePanel.vue';
 import LoginStepFourPanel from 'src/components/login/LoginStepFourPanel.vue';
+import LoginStepFivePanel from 'src/components/login/LoginStepFivePanel.vue';
+import LoginStepSixPanel from 'src/components/login/LoginStepSixPanel.vue';
 
+const { t } = useI18n();
+const $q = useQuasar();
 const router = useRouter();
 const { setAuthDataFromPayload } = useAuth();
 
+const clicked = ref(false);
+const showSubStep = ref(false);
+const steps = ref(1);
+const submitting = ref(false);
+const loginForm = ref(null);
+const isLogin = ref(false);
+
 const email = ref('');
 const phone = ref('');
 const code = ref('');
-const stepFourCep = ref('');
 
 const stepThreeForm = ref({
   name: '',
+  phone: '',
+  email: '',
+  rg: '',
   document: '',
+  birth_date: '',
   zip_code: '',
   address: '',
   complement: '',
-  nickname: '',
-  instructions: '',
-  address_type: 'home',
   no_complement: false,
   city: '',
   state: '',
+  address_type: 'home',
+  nickname: 'Principal',
+  instructions: '',
 });
 
-const steps = ref(1); // 1 = credentials | 2 = code | 3 = user fields | 4 = cep only
-const clicked = ref(false);
-const submitting = ref(false);
-const loginForm = ref(null);
+const stepFourForm = ref({
+  selfie_file: null,
+  selfie_base64: '',
+  document_front_file: null,
+  document_front_base64: '',
+  document_back_file: null,
+  document_back_base64: '',
+});
+
+const stepFiveForm = ref({
+  daily_price_8h: null,
+  daily_price_6h: null,
+  daily_price_4h: null,
+  daily_price_2h: null,
+  services_types_ids: [],
+});
+
+const stepSixForm = ref({
+  working_days: {},
+});
+
+const actionLabel = computed(() => {
+  if (steps.value === 1) return t('provider.login.steps.step_1.action');
+  if (steps.value === 2) return t('provider.login.steps.step_2.action');
+  if (steps.value === 6) return t('provider.login.steps.step_6.action');
+  return t('provider.login.steps.step_3.action');
+});
+
+const toISODate = (value) => {
+  const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value || '');
+  if (!matches) return null;
+
+  return `${matches[3]}-${matches[2]}-${matches[1]}`;
+};
+
+const mapWorkingDays = () => {
+  const mapped = [];
+  const workingDays = stepSixForm.value.working_days || {};
+
+  Object.entries(workingDays).forEach(([dayKey, periods]) => {
+    if (periods?.morning) {
+      mapped.push({ day: Number(dayKey), period: 'morning' });
+    }
+    if (periods?.afternoon) {
+      mapped.push({ day: Number(dayKey), period: 'afternoon' });
+    }
+  });
+
+  return mapped;
+};
+
+const hasWorkingDaySelected = () => {
+  return mapWorkingDays().length > 0;
+};
+
+const validateCurrentStep = async () => {
+  const isValid = await loginForm.value?.validate();
+
+  if (!isValid) {
+    return false;
+  }
+
+  if (steps.value === 6 && !hasWorkingDaySelected()) {
+    $q.notify({
+      type: 'negative',
+      message: t('provider.login.steps.step_6.select_at_least_one'),
+    });
+    return false;
+  }
+
+  return true;
+};
 
 const sendValidationCode = async () => {
   const response = await sendCode(email.value, phone.value);
   if (response.status === 201) {
     steps.value = 2;
-  } else {
-    console.error('Failed to send validation code');
   }
+  return response;
 };
 
 const validateCodeInput = async () => {
-  const response = await validateCode(email.value, phone.value, code.value);
+  const response = await validateCode(email.value, phone.value, code.value, isLogin.value);
+
   if (response.status === 200) {
+    if (isLogin.value === true) {
+      await setAuthDataFromPayload(response.data.payload);
+      router.push({ name: 'DashboardPage' });
+      return;
+    }
+
+    stepThreeForm.value.email = email.value;
+    stepThreeForm.value.phone = phone.value;
     steps.value = 3;
-  } else {
-    console.error('Invalid validation code');
   }
 };
 
 const registerUserAndProvider = async () => {
+  const workingDays = mapWorkingDays();
+
   const payload = {
     ...stepThreeForm.value,
-    email: email.value,
-    phone: phone.value,
+    email: stepThreeForm.value.email || email.value,
+    phone: stepThreeForm.value.phone || phone.value,
     code: code.value,
+    birth_date: toISODate(stepThreeForm.value.birth_date),
     has_complement: !stepThreeForm.value.no_complement,
+    complement: stepThreeForm.value.no_complement ? null : stepThreeForm.value.complement,
+
+    selfie_base64: stepFourForm.value.selfie_base64,
+    document_front_base64: stepFourForm.value.document_front_base64,
+    document_back_base64: stepFourForm.value.document_back_base64,
+
+    daily_price_8h: Number(stepFiveForm.value.daily_price_8h),
+    daily_price_6h: Number(stepFiveForm.value.daily_price_6h),
+    daily_price_4h: Number(stepFiveForm.value.daily_price_4h),
+    daily_price_2h: Number(stepFiveForm.value.daily_price_2h),
+    services_types_ids: stepFiveForm.value.services_types_ids,
+
+    working_days: workingDays,
+
+    is_approved: false,
   };
 
   const response = await createUserAndProvider(payload);
   if (response.status === 200) {
     await setAuthDataFromPayload(response.data.payload);
     router.push({ name: 'DashboardPage' });
-  } else {
-    console.error('Failed to create user and provider');
   }
 };
 
 const onSubmit = async () => {
-  switch (steps.value) {
-    case 1: await sendValidationCode(); break;
-    case 2: await validateCodeInput(); break;
-    case 3: await registerUserAndProvider(); break;
-    default: break;
+  if (showSubStep.value) return; // Não submete o form principal se estiver em um sub-passo
+
+  const isValid = await loginForm.value.validate();
+  if (!isValid) return;
+
+  submitting.value = true;
+
+  try {
+    switch (steps.value) {
+      case 1: {
+        const response = await sendValidationCode();
+        isLogin.value = response?.data?.payload?.isLogin === true;
+        break;
+      }
+      case 2:
+        await validateCodeInput();
+        break;
+      case 3: {
+        if (await validateCurrentStep()) {
+          steps.value = 4;
+        }
+        break;
+      }
+      case 4: {
+        if (await validateCurrentStep()) {
+          steps.value = 5;
+        }
+        break;
+      }
+      case 5: {
+        if (await validateCurrentStep()) {
+          steps.value = 6;
+        }
+        break;
+      }
+      case 6: {
+        if (await validateCurrentStep()) {
+          await registerUserAndProvider();
+        }
+        break;
+      }
+      default:
+        break;
+    }
+  } catch (error) {
+    console.error(error);
+  } finally {
+    submitting.value = false;
   }
 };
 </script>
@@ -168,8 +299,14 @@ const onSubmit = async () => {
 .fade-slide-leave-active {
   transition: opacity 0.35s ease, transform 0.35s ease;
 }
-.fade-slide-enter-from { opacity: 0; transform: translateY(6px); }
-.fade-slide-leave-to   { opacity: 0; transform: translateY(-6px); }
+.fade-slide-enter-from {
+  opacity: 0;
+  transform: translateY(6px);
+}
+.fade-slide-leave-to {
+  opacity: 0;
+  transform: translateY(-6px);
+}
 
 .login-page {
   min-height: 100vh;
@@ -211,27 +348,9 @@ const onSubmit = async () => {
       width: 180px;
       z-index: 1;
     }
-
-    &--logo-small {
-      top: 18%;
-      left: 50%;
-      transform: translate(-50%, -50%);
-      width: 100px;
-      z-index: 1;
-    }
   }
 }
 
-.step4-card-wrapper {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -30%);
-  z-index: 2;
-  width: 90%;
-  max-width: 360px;
-}
-
 .flow-screen {
   display: flex;
   flex-direction: column;
@@ -244,25 +363,24 @@ const onSubmit = async () => {
 
 .flow-header {
   min-height: 36px;
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
 }
 
 .flow-logo {
   display: flex;
   justify-content: center;
-  padding: 12px 0 24px;
+  padding: 12px 0 20px;
 }
 
 .flow-form {
   flex: 1;
   display: flex;
   flex-direction: column;
+  min-height: 0;
 }
 
 .flow-content {
   flex: 1;
+  overflow-y: auto;
 
   &--centered {
     display: flex;

+ 47 - 13
src/pages/dashboard/DashboardPage.vue

@@ -1,25 +1,59 @@
 <template>
-  <div>
-    <!-- <DefaultHeaderPage> -->
-
-    <!-- </DefaultHeaderPage> -->
-    {{ userStore().user.name + ' é o usuario logado'  }}<br>
-    {{ userStore().user }}
-
+  <div class="dashboard-page bg-page">
+    <template v-if="loading">
+      <div class="row items-center justify-center full-width bg-surface" style="height: 80vh">
+        <q-spinner-dots color="primary" />
+      </div>
+    </template>
+    <template v-else>
+      <DashboardHeaderBar :data="headerBar" />
+      <DashboardSummaryInfos :data="summaryInfos" />
+      <DashboardPriceSuggest :data="priceSuggestion"/>
+      <DashboardScrollAreaSchedules />
+      <DashboardSolicitations :data="solicitations"/>
+      <DashboardNextSchedules :data="nextSchedules" />
+      <DashboardOpportunities :data="opportunities"/>
+    </template>
   </div>
 </template>
 
 <script setup>
-import { onMounted } from "vue";
-// import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
-import { userStore } from 'src/stores/user';
+import DashboardHeaderBar from 'src/components/dashboard/DashboardHeaderBar.vue';
+import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
+import DashboardPriceSuggest from 'src/components/dashboard/DashboardPriceSuggest.vue';
+import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
+import DashboardSolicitations from 'src/components/dashboard/DashboardSolicitations.vue';
+import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
+import DashboardOpportunities from 'src/components/dashboard/DashboardOpportunities.vue';
+import { onMounted, ref } from 'vue';
+import { dadosDashboard } from 'src/api/dashboard';
+
+const headerBar = ref({});
+const summaryInfos = ref({});
+const priceSuggestion = ref({});
+const solicitations = ref([]);
+const nextSchedules = ref([]);
+const opportunities = ref([]);
 
-onMounted(async () => {
+const loading = ref(true);
+onMounted( async () => {
+  const response = await dadosDashboard();
+  if(response) {
+    headerBar.value = response.headerBar;
+    summaryInfos.value = response.summaryInfos;
+    priceSuggestion.value = response.priceSuggested;
+    solicitations.value = response.solicitations;
+    nextSchedules.value = response.nextSchedules;
+    opportunities.value = response.opportunities;
+  }
+  loading.value = false;
 });
 </script>
 
 <style scoped>
-.gap {
-  gap: 16px;
+.dashboard-page {
+  width: 100%;
+  min-height: 100%;
+  box-sizing: border-box;
 }
 </style>

+ 1 - 1
src/pages/search/SearchPage.vue → src/pages/search/PagamentosPage.vue

@@ -6,7 +6,7 @@
     </div>
     <h1 class="mobile-placeholder__title">Busca</h1>
     <p class="mobile-placeholder__description">
-      Área reservada para a busca de diárias e oportunidades próximas.
+      Área reservada para a parte de pagamentos, que ainda está em desenvolvimento
     </p>
   </section>
 </template>

+ 4 - 4
src/router/routes.js

@@ -29,9 +29,9 @@ const routes = [
         },
       },
       {
-        path: "busca",
-        name: "SearchPage",
-        component: () => import("src/pages/search/SearchPage.vue"),
+        path: "pagamentos",
+        name: "PagamentosPage",
+        component: () => import("src/pages/search/PagamentosPage.vue"),
         meta: {
           title: "Busca",
           requireAuth: true,
@@ -41,7 +41,7 @@ const routes = [
               title: "ui.navigation.dashboard",
             },
             {
-              name: "SearchPage",
+              name: "PagamentosPage",
               title: "Busca",
             },
           ],

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov