Bläddra i källkod

last commit before new pr pattern | fluxo dashboad integrada na API + perfil com abas funcionais

Gustavo Zanatta 3 veckor sedan
förälder
incheckning
04e9c65ef9

+ 13 - 2
src/components/dashboard/DashboardNextSchedules.vue

@@ -43,9 +43,9 @@
 
             <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' }}
+                {{ formatAddress(item.address) || 'N/A' }}
               </div>
-              <q-icon name="mdi-content-copy" color="primary" size="16px" class="q-ml-xs" />
+              <q-icon name="mdi-content-copy" color="primary" size="16px" class="q-ml-xs" @click="copyAddress(item.address)"/>
             </div>
 
             <div class="row q-mt-sm items-center">
@@ -80,9 +80,12 @@
 import { useI18n } from 'vue-i18n';
 import { formatCurrency } from 'src/helpers/utils';
 import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
+import { useQuasar } from 'quasar';
+import { formatAddress } from 'src/helpers/utils';
 defineProps({ data: { type: Array, default: () => [] } });
 
 const t = useI18n().t;
+const $q = useQuasar();
 
 const formatWeekday = (iso) => {
   if (!iso) return '';
@@ -95,6 +98,14 @@ const formatDayMonth = (iso) => {
   if (!iso) return '';
   return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
 };
+
+const copyAddress = (address) => {
+  const formatted = formatAddress(address);
+  if (formatted) {
+    navigator.clipboard.writeText(formatted);
+  }
+  $q.notify({ message: t('provider.dashboard.next_schedules.address_copied'), color: 'positive' });
+};
 </script>
 
 <style scoped lang="scss">

+ 18 - 2
src/components/dashboard/DashboardOpportunities.vue

@@ -40,12 +40,15 @@
               <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-region text-weight-bold">{{ item.address?.district || 'N/A' }}</div>
                 <div class="text-distance">{{ item.distance || 0 }}{{ $t('common.km') }}</div>
               </div>
             </div>
 
-            <div class="row q-mt-sm justify-end">
+            <div class="row q-mt-sm justify-between">
+              <div class="text-text text-primary">
+                  {{ chooseAddressType(item.address_type) }}
+              </div>
               <q-btn
                 unelevated
                 rounded
@@ -66,12 +69,14 @@
 <script setup>
 import { formatCurrency } from 'src/helpers/utils';
 import { labelsPeriodTypes } from 'src/helpers/arraysOptions/labelsPeriodTypes.js';
+import { useI18n } from 'vue-i18n';
 defineProps({
   data: {
     type: Array,
     default: () => []
   }
 });
+const { t } = useI18n();
 
 const formatWeekday = (iso) => {
   if (!iso) return '';
@@ -84,6 +89,17 @@ const formatDayMonth = (iso) => {
   if (!iso) return '';
   return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
 };
+
+const chooseAddressType = (type) => {
+  switch(type) {
+    case 'home':
+      return t("address.types.home");
+    case 'commercial':
+      return t("address.types.commercial");
+    default:
+      return 'N/A';
+  }
+};
 </script>
 
 <style scoped lang="scss">

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

@@ -4,7 +4,8 @@
       <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>
+            <span class="text-suggest-label">{{ $t('provider.dashboard.price_suggest.region_label_1') }}</span>
+            <span class="text-suggest-label text-weight-bold">{{ $t('provider.dashboard.price_suggest.region_label_2') }}</span>
           </div>
           <q-badge rounded class="price-badge q-px-md q-py-xs gradient-diarista-bg">
             {{ formatCurrency(data?.average_price ?? 0) }}
@@ -13,10 +14,10 @@
         <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">
+            <q-chip class="row items-center no-wrap bg-surface" outline color="text">
               <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>
+            </q-chip>
           </div>
           <q-btn flat no-caps color="primary" padding="0" class="btn-alter">
             <div class="row items-center q-gutter-x-xs">

+ 19 - 9
src/components/dashboard/DashboardSolicitations.vue

@@ -44,7 +44,7 @@
               <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-region text-weight-bold">{{ item.address?.district || 'N/A' }}</div>
                 <div class="text-distance">
                   <span class="q-pr-xs">{{ item.distance || 0 }}</span>
                   {{ $t('common.km') }}
@@ -53,14 +53,19 @@
             </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')"
-              />
+              <div>
+                <q-btn
+                  flat
+                  no-caps
+                  color="primary"
+                  size="sm"
+                  class="col-auto q-px-none btn-details"
+                  :label="$t('common.details')"
+                />
+                <div class="text-schedule-date-bold text-text text-weight-bold">
+                  {{ item.time_since_request }}
+                </div>
+              </div>
               <q-space />
               <q-btn
                 unelevated
@@ -113,6 +118,11 @@ const formatDayMonth = (iso) => {
   return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
 };
 
+// const openTimeSynceCreatedSolicitation = (id) => {
+//   // Implementar lógica para abrir detalhes da solicitação
+  
+// };
+
 </script>
 
 <style scoped lang="scss">

+ 7 - 10
src/components/dashboard/DashboardSummaryInfos.vue

@@ -12,17 +12,17 @@
           </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-label text-label-bold text-text">{{ $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>
+            <q-icon name="mdi-clock-check-outline" class="q-my-auto" size="xs" color="grey-6" />
+            <span class="q-my-auto q-ml-sm text-caption text-weight-bold">{{ 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 class="text-text col text-caption">
+          {{ formatAddress(data.address) }}
         </div>
         <q-icon name="mdi-chevron-down" color="secondary" size="18px" class="col-auto" />
       </div>
@@ -31,12 +31,14 @@
 </template>
 
 <script setup>
+import { formatAddress } from 'src/helpers/utils';
 defineProps({ data: { type: Object, default: () => null } });
 
 const avatarStyle = {
   background: 'linear-gradient(135deg, #ffd7e8 0%, #ff9acc 100%)',
   color: '#7a154f',
 };
+
 </script>
 
 <style scoped lang="scss">
@@ -51,9 +53,4 @@ const avatarStyle = {
   font-weight: 500;
   color: #3a3a4a;
 }
-.summary-address {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
 </style>

+ 382 - 82
src/components/login/LoginStepFourPanel.vue

@@ -1,147 +1,447 @@
 <template>
   <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>
+    <div class="column items-center q-gutter-y-xl q-pt-md">
 
-      <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 class="column items-center q-gutter-y-sm">
+        <div
+          class="photo-circle"
+          :class="selfieDone ? 'photo-circle--done' : 'photo-circle--pending'"
+          @click="subStep = 'selfie_camera'"
+        >
+          <q-icon name="photo_camera" size="52px" color="white" />
         </div>
+
+        <template v-if="!selfieDone">
+          <q-btn
+            color="primary-button"
+            :label="t('provider.login.steps.step_4.btn_selfie')"
+            rounded
+            no-caps
+            padding="10px 28px"
+            @click="subStep = 'selfie_camera'"
+          />
+          <div class="text-caption text-grey-6 text-center">{{ t('provider.login.steps.step_4.selfie_hint') }}</div>
+        </template>
+        <template v-else>
+          <div class="row items-center q-gutter-x-xs">
+            <q-icon name="check_circle" color="positive" size="18px" />
+            <span class="text-body2 text-weight-bold text-primary">{{ t('provider.login.steps.step_4.selfie_sent') }}</span>
+          </div>
+        </template>
       </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 class="column items-center q-gutter-y-sm">
+        <div
+          class="photo-circle"
+          :class="docDone ? 'photo-circle--done' : 'photo-circle--pending'"
+          @click="subStep = 'doc_front_camera'"
+        >
+          <q-icon name="mdi-card-account-details-outline" size="52px" color="white" />
         </div>
+
+        <template v-if="!docDone">
+          <q-btn
+            color="primary-button"
+            :label="t('provider.login.steps.step_4.btn_document')"
+            rounded
+            no-caps
+            padding="10px 28px"
+            @click="subStep = 'doc_front_camera'"
+          />
+          <div class="text-caption text-grey-6 text-center">{{ t('provider.login.steps.step_4.document_hint') }}</div>
+        </template>
+        <template v-else>
+          <div class="row items-center q-gutter-x-xs">
+            <q-icon name="check_circle" color="positive" size="18px" />
+            <span class="text-body2 text-weight-bold text-primary">{{ t('provider.login.steps.step_4.document_sent') }}</span>
+          </div>
+        </template>
       </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 v-if="subStep !== 'main'" class="fs-overlay">
+        <div v-if="isCameraState" class="fs-camera">
+          <div class="fs-header">
+            <q-btn flat dense round icon="chevron_left" color="black" @click="goBack" />
+            <span class="text-weight-bold text-body1">{{ t('common.actions.back') }}</span>
+          </div>
+          <div class="fs-viewport">
+            <video ref="videoRef" autoplay playsinline muted class="fs-video" />
+            <div v-if="subStep === 'selfie_camera'" class="guide-oval" />
+            <div v-else class="guide-doc" />
+          </div>
+          <div class="fs-footer">
+            <q-btn
+              color="primary-button"
+              :label="captureLabel"
+              rounded
+              no-caps
+              padding="14px 32px"
+              class="full-width"
+              @click="capturePhoto"
+            />
+          </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 v-else-if="isPreviewState" class="fs-camera">
+          <div class="fs-header">
+            <q-btn flat dense round icon="chevron_left" color="black" @click="goBack" />
+            <span class="text-weight-bold text-body1">{{ t('common.actions.back') }}</span>
+          </div>
+          <div class="fs-viewport">
+            <img :src="currentPreview" class="fs-video fs-preview-img" />
+            <div v-if="subStep === 'selfie_preview'" class="guide-oval" />
+            <div v-else class="guide-doc" />
+          </div>
+          <div class="fs-footer column q-gutter-y-sm">
+            <q-btn
+              outline
+              color="primary-button"
+              :label="t('provider.login.steps.step_4.btn_retake')"
+              rounded
+              no-caps
+              padding="12px 32px"
+              class="full-width"
+              @click="retakePhoto"
+            />
+            <q-btn
+              color="primary-button"
+              :label="subStep === 'selfie_preview' ? t('provider.login.steps.step_4.btn_confirm') : t('provider.login.steps.step_4.btn_use_photo')"
+              rounded
+              no-caps
+              padding="12px 32px"
+              class="full-width"
+              @click="usePhoto"
+            />
+          </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 v-else class="fs-result">
+          <q-img :src="LogoDiaria" class="q-mb-xl" style="max-width: 150px" />
+
+          <div
+            class="result-circle"
+            :class="docResult === 'error' ? 'result-circle--error' : 'result-circle--done'"
+          >
+            <q-icon
+              :name="subStep === 'selfie_result' ? 'photo_camera' : 'mdi-card-account-details-outline'"
+              size="56px"
+              color="white"
+            />
+          </div>
+
+          <template v-if="docResult === 'error'">
+            <q-icon name="cancel" color="negative" size="28px" class="q-mt-md" />
+            <div class="text-body1 text-weight-bold text-primary text-center q-mt-xs q-px-lg">
+              {{ t('provider.login.steps.step_4.error_message') }}
+            </div>
+          </template>
+          <div v-else class="text-body1 text-weight-bold text-primary text-center q-mt-md q-px-lg">
+            {{ subStep === 'selfie_result' ? t('provider.login.steps.step_4.selfie_success') : t('provider.login.steps.step_4.document_success') }}
+          </div>
+
+          <q-btn
+            color="primary-button"
+            :label="docResult === 'error' ? t('provider.login.steps.step_4.btn_retry') : t('provider.login.steps.step_4.btn_continue')"
+            rounded
+            no-caps
+            padding="14px 48px"
+            class="q-mt-xl"
+            style="min-width: 200px"
+            @click="handleResult"
+          />
+
+          <div v-if="subStep === 'doc_result' && docResult !== 'error'" class="text-caption text-grey-6 text-center q-mt-sm">
+            {{ t('provider.login.steps.step_4.document_hint_result') }}
+          </div>
         </div>
+
       </div>
     </Teleport>
+
+    <canvas ref="canvasRef" style="display: none" />
   </q-card-section>
 </template>
 
 <script setup>
-import { ref, watch } from 'vue';
+import { ref, computed, watch, onUnmounted, nextTick } from 'vue';
+import { useI18n } from 'vue-i18n';
+import LogoDiaria from 'src/assets/logo_diaria_campos_login.svg';
+
+const { t } = useI18n();
 
 const form = defineModel({ type: Object, required: true });
 const emit = defineEmits(['update:show-sub-step']);
 
 const subStep = ref('main');
+const docResult = ref(null); // null | 'success' | 'error'
+
+const tempDocFront = ref(null);
+const tempDocBack = ref(null);
+const tempSelfie = ref(null);
+
+const stream = ref(null);
+const videoRef = ref(null);
+const canvasRef = ref(null);
+
+const CAMERA_STATES = ['selfie_camera', 'doc_front_camera', 'doc_back_camera'];
+const PREVIEW_STATES = ['selfie_preview', 'doc_front_preview', 'doc_back_preview'];
+
+const isCameraState = computed(() => CAMERA_STATES.includes(subStep.value));
+const isPreviewState = computed(() => PREVIEW_STATES.includes(subStep.value));
+
+const selfieDone = computed(() => !!form.value.selfie_base64);
+const docDone = computed(() => !!form.value.document_front_base64 && !!form.value.document_back_base64);
+
+const currentPreview = computed(() => {
+  if (subStep.value === 'selfie_preview') return tempSelfie.value;
+  if (subStep.value === 'doc_front_preview') return tempDocFront.value;
+  if (subStep.value === 'doc_back_preview') return tempDocBack.value;
+  return null;
+});
+
+const captureLabel = computed(() => {
+  if (subStep.value === 'selfie_camera') return t('provider.login.steps.step_4.btn_capture_selfie');
+  if (subStep.value === 'doc_front_camera') return t('provider.login.steps.step_4.btn_capture_front');
+  return t('provider.login.steps.step_4.btn_capture_back');
+});
+
+const stopCamera = () => {
+  if (stream.value) {
+    stream.value.getTracks().forEach((t) => t.stop());
+    stream.value = null;
+  }
+  if (videoRef.value) {
+    videoRef.value.srcObject = null;
+  }
+};
+
+const startCameraStream = async (facingMode) => {
+  try {
+    stopCamera();
+    stream.value = await navigator.mediaDevices.getUserMedia({
+      video: { facingMode, width: { ideal: 1280 }, height: { ideal: 720 } },
+    });
+    await nextTick();
+    if (videoRef.value) {
+      videoRef.value.srcObject = stream.value;
+      await videoRef.value.play().catch(() => {});
+    }
+    return true;
+  } catch {
+    stopCamera();
+    return false;
+  }
+};
+
+watch(subStep, async (newStep) => {
+  emit('update:show-sub-step', newStep !== 'main');
 
-watch(subStep, (val) => {
-  emit('update:show-sub-step', val !== 'main');
+  if (!CAMERA_STATES.includes(newStep)) {
+    stopCamera();
+  }
+
+  if (newStep === 'selfie_camera') {
+    const ok = await startCameraStream('user');
+    if (!ok) subStep.value = 'main';
+  } else if (newStep === 'doc_front_camera' || newStep === 'doc_back_camera') {
+    const ok = await startCameraStream('environment');
+    if (!ok) {
+      docResult.value = 'error';
+      subStep.value = 'doc_result';
+    }
+  }
 });
 
-const openSubStep = (step) => {
-  subStep.value = step;
+onUnmounted(stopCamera);
+
+const capturePhoto = () => {
+  const video = videoRef.value;
+  const canvas = canvasRef.value;
+  if (!video || !canvas) return;
+
+  canvas.width = video.videoWidth || 1280;
+  canvas.height = video.videoHeight || 720;
+  canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
+  const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
+
+  if (subStep.value === 'selfie_camera') {
+    tempSelfie.value = dataUrl;
+    subStep.value = 'selfie_preview';
+  } else if (subStep.value === 'doc_front_camera') {
+    tempDocFront.value = dataUrl;
+    subStep.value = 'doc_front_preview';
+  } else if (subStep.value === 'doc_back_camera') {
+    tempDocBack.value = dataUrl;
+    subStep.value = 'doc_back_preview';
+  }
+};
+
+const usePhoto = () => {
+  if (subStep.value === 'selfie_preview') {
+    form.value.selfie_base64 = tempSelfie.value;
+    subStep.value = 'selfie_result';
+  } else if (subStep.value === 'doc_front_preview') {
+    subStep.value = 'doc_back_camera';
+  } else if (subStep.value === 'doc_back_preview') {
+    form.value.document_front_base64 = tempDocFront.value;
+    form.value.document_back_base64 = tempDocBack.value;
+    docResult.value = 'success';
+    subStep.value = 'doc_result';
+  }
+};
+
+const retakePhoto = () => {
+  const retakeMap = {
+    selfie_preview: 'selfie_camera',
+    doc_front_preview: 'doc_front_camera',
+    doc_back_preview: 'doc_back_camera',
+  };
+  subStep.value = retakeMap[subStep.value] || 'main';
 };
 
+const goBack = () => {
+  const backMap = {
+    selfie_camera: 'main',
+    selfie_preview: 'selfie_camera',
+    doc_front_camera: 'main',
+    doc_front_preview: 'doc_front_camera',
+    doc_back_camera: 'doc_front_preview',
+    doc_back_preview: 'doc_back_camera',
+  };
+  subStep.value = backMap[subStep.value] || 'main';
+};
+
+const handleResult = () => {
+  if (docResult.value === 'error') {
+    docResult.value = null;
+    tempDocFront.value = null;
+    tempDocBack.value = null;
+    subStep.value = 'doc_front_camera';
+  } else {
+    subStep.value = 'main';
+  }
+};
 </script>
 
 <style lang="scss" scoped>
-.capture-screen {
-  background: #f8f8f8;
+.photo-circle {
+  width: 130px;
+  height: 130px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: transform 0.15s ease;
+
+  &:active { transform: scale(0.95); }
+
+  &--pending {
+    background-color: rgba(139, 92, 246, 0.18);
+  }
+
+  &--done {
+    background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
+  }
+}
+
+.fs-overlay {
   position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
+  inset: 0;
   z-index: 9999;
+  background: white;
+}
+
+.fs-camera {
+  height: 100%;
   display: flex;
   flex-direction: column;
 }
 
-.capture-content {
-  flex: 1;
-  padding: 20px;
+.fs-header {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 8px 12px;
+  background: white;
+  flex-shrink: 0;
 }
 
-.capture-placeholder-container {
-  position: relative;
-  width: 100%;
-  max-width: 320px;
+.fs-viewport {
+  flex: 1;
   background: #000;
-  aspect-ratio: 9/16;
-  border-radius: 20px;
+  position: relative;
   overflow: hidden;
 }
 
-.capture-placeholder {
+.fs-video {
   width: 100%;
   height: 100%;
-  background: #1a1a1a;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
+  object-fit: cover;
+  display: block;
 }
 
-.capture-preview {
-  width: 100%;
-  height: 100%;
+.fs-preview-img {
+  object-fit: contain;
+}
+
+.fs-footer {
+  padding: 16px 20px;
+  background: white;
+  flex-shrink: 0;
+}
+
+.guide-oval {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 62%;
+  height: 70%;
+  border: 3px solid #8b5cf6;
+  border-radius: 50%;
+  pointer-events: none;
 }
 
-.selfie-oval-overlay {
+.guide-doc {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
-  width: 70%;
-  height: 60%;
-  border: 4px solid var(--q-primary);
-  border-radius: 50% / 50%;
+  width: 86%;
+  height: 52%;
+  border: 2px solid rgba(255, 255, 255, 0.65);
+  border-radius: 14px;
   pointer-events: none;
 }
 
-.bg-primary-fade {
-  background-color: rgba(var(--q-primary-rgb), 0.2);
+.fs-result {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 28px;
+  background: white;
 }
 
-.capture-placeholder-container.column.flex-center {
-  &.doc {
-     aspect-ratio: 1/1.4; // Slightly wider for ID card
+.result-circle {
+  width: 150px;
+  height: 150px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &--done {
+    background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
+  }
+
+  &--error {
+    background-color: rgba(139, 92, 246, 0.18);
   }
 }
 </style>

+ 8 - 9
src/components/profile/ProfileAddressDialog.vue

@@ -1,18 +1,18 @@
 <template>
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height no-shadow">
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+      <div class="row items-center q-pa-md q-pb-sm bg-white shadow-profile bg-surface">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-subtitle1 text-weight-bold text-primary">{{ $t('profile.address.title') }}</span>
         <q-space />
         <div style="width: 32px"></div>
       </div>
-
-      <q-card-section class="col" >
-        <div class="q-px-md q-pt-lg text-text">
+      <q-card-section class="col q-pa-lg">
+        <div class="text-text">
           <div class="text-h6 text-weight-bold gradient-diarista">{{ $t('profile.address.address_subtitle') }}</div>
           <div class="text-caption text-grey-7 q-mb-lg">{{ $t('profile.address.address_description') }}</div>
+ 
 
           <q-card class="q-pa-lg bg-white shadow-card" style="border-radius: 25px;" :flat="false">
             <q-input
@@ -113,7 +113,7 @@ defineEmits([...useDialogPluginComponent.emits]);
 const { dialogRef, onDialogOK } = useDialogPluginComponent();
 const $q = useQuasar();
 const user = userStore();
-
+const providerId = user.user.provider.id;
 const {
   form,
   hasUpdatedFields,
@@ -130,7 +130,7 @@ const {
   city: null,
   state: null,
   source: 'provider',
-  source_id: user.user.id,
+  source_id: providerId,
   address_type: 'home',
 });
 
@@ -182,11 +182,10 @@ const save = async () => {
 };
 
 onMounted(async () => {
-  const providersAddresses = await getAddresses('provider', user.user.id);
+  const providersAddresses = await getAddresses('provider', providerId);
   if (providersAddresses && providersAddresses.length > 0) {
     const current = providersAddresses[0];
     addressId.value = current.id;
-
     const initialData = {
       zip_code: current.zip_code || '',
       address: current.address || '',
@@ -198,7 +197,7 @@ onMounted(async () => {
       city: current.city || null,
       state: current.state || null,
       source: 'provider',
-      source_id: user.user.id,
+      source_id: providerId,
       address_type: current.address_type || 'home',
     };
 

+ 4 - 11
src/components/profile/ProfileAvailabilityDialog.vue

@@ -60,8 +60,10 @@
           </div>
 
 
-          <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
-            {{ $t('profile.availability.agenda_title') }}
+          <div class="text-h6 text-weight-bold q-mb-xs q-pt-md">
+            <span class="gradient-diarista">
+              {{ $t('profile.availability.agenda_title') }}
+            </span>
           </div>
           <div class="text-caption text-text q-mb-md">
             {{ $t('profile.availability.agenda_subtitle') }}
@@ -529,28 +531,24 @@ onMounted(async () => {
     background-color: white !important;
   }
 
-  // Dias fora do mês atual (mais apagados)
   :deep(.q-date__calendar-item--out) {
     .q-btn__content {
       color: #CBD5E1 !important;
     }
   }
 
-  // Dias do mês atual
   :deep(.q-date__calendar-days .q-btn__content) {
     font-family: 'Inter', sans-serif;
     font-weight: 500;
     color: #1E293B;
   }
 
-  // Dias da semana (Dom, Seg...) no header do calendário
   :deep(.q-date__calendar-weekdays > div) {
     color: #6366F1;
     font-weight: 700;
     opacity: 0.8;
   }
 
-  // Header de navegação do minimal (< Mês Ano >)
   :deep(.q-date__navigation) {
     .q-btn {
       color: #1E293B !important;
@@ -560,14 +558,12 @@ onMounted(async () => {
     }
   }
 
-  // Texto do mês/ano no header de navegação
   :deep(.q-date__nav-btn-month),
   :deep(.q-date__nav-btn-year) {
     color: #6366F1 !important;
     font-weight: 700;
   }
 
-  // Evento (ponto abaixo do dia bloqueado)
   :deep(.q-date__event) {
     bottom: 4px;
     height: 6px;
@@ -575,7 +571,6 @@ onMounted(async () => {
     border-radius: 50%;
   }
 
-  // Dia de hoje
   :deep(.q-date__today) {
     .q-btn__content {
       color: #7c4dff !important;
@@ -584,7 +579,6 @@ onMounted(async () => {
     }
   }
 
-  // Dia selecionado
   :deep(.q-date__selected) {
     .q-btn__content {
       background: #6366F1 !important;
@@ -594,7 +588,6 @@ onMounted(async () => {
     }
   }
 
-  // Views de meses e anos (quando clica no nome do mês/ano para selecionar)
   :deep(.q-date__view--months),
   :deep(.q-date__view--years) {
     .q-btn {

+ 47 - 52
src/components/profile/ProfileBankDataDialog.vue

@@ -10,11 +10,11 @@
       </div>
 
       <q-card-section class="col">
-        <div class="q-px-md q-pt-lg text-text">
+        <div class="text-text">
           <div class="text-h6 text-weight-bold gradient-diarista">{{ $t('profile.bank_data.my_bank_data_title') }}</div>
           <div class="text-caption text-grey-7 q-mb-md">{{ $t('profile.bank_data.subtitle') }}</div>
 
-          <div class="pix-warning q-pa-md q-mb-lg row items-start q-col-gutter-sm">
+          <div class="pix-warning q-pa-md q-mb-md row items-start q-col-gutter-sm">
             <q-icon name="mdi-alert-outline" color="warning" size="20px" class="q-mt-xs" />
             <div class="col text-caption">
               <span class="text-weight-bold">{{ $t('profile.bank_data.warning_title') }}</span>
@@ -22,11 +22,11 @@
             </div>
           </div>
 
-          <div class="text-subtitle1 text-weight-bold text-center text-text q-mb-md">
+          <div class="text-subtitle1 text-weight-bold text-center text-text q-mb-sm">
             {{ $t('profile.bank_data.pix_title') }}
           </div>
 
-          <q-card class="q-pa-lg bg-white shadow-card q-mb-lg" style="border-radius: 25px;" :flat="false">
+          <q-card class="q-pa-md bg-white shadow-card q-mb-md" style="border-radius: 20px;" :flat="false">
             <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.bank_data.pix_key') }}</div>
             <q-input
               v-model="pixForm.pix_key"
@@ -34,7 +34,7 @@
               dense
               input-class="text-text"
               :placeholder="$t('profile.bank_data.pix_key_placeholder')"
-              class="q-mb-md"
+              class="q-mb-sm"
             />
 
             <q-btn
@@ -42,27 +42,27 @@
               rounded
               no-caps
               color="primary"
-              class="full-width q-py-md text-weight-bold"
-              style="font-size: 1.1rem;"
-              :label="$t('profile.bank_data.save_pix')"
+              class="full-width text-weight-bold"
+              padding="8px 16px"
+              :label="pixAlreadyRegistered ? $t('profile.bank_data.att_pix') : $t('profile.bank_data.save_pix')"
               :loading="savingPix"
               :disable="!hasPixUpdatedFields"
               @click="savePix"
             />
           </q-card>
 
-          <div class="text-subtitle1 text-weight-bold text-center text-text q-mb-md">
-            {{ $t('profile.bank_data.bank_account_title') }}
-          </div>
 
-          <q-card class="q-pa-lg bg-white shadow-card q-mb-lg" style="border-radius: 25px;" :flat="false">
-            <div class="text-weight-bold q-mb-sm text-text">{{ $t('profile.bank_data.account_type') }}</div>
-            <div class="row q-mb-md">
+          <q-card class="q-pa-md bg-white row shadow-card q-mb-md" style="border-radius: 20px;" :flat="false">
+            <div class="text-subtitle1 text-weight-bold text-center text-text q-mb-sm col-12">
+              {{ $t('profile.bank_data.bank_account_title') }}
+            </div>
+            <div class="row q-mb-sm q-my-auto col-12 row items-center">
               <q-radio
                 v-model="bankForm.bank_account_type"
                 val="checking"
                 :label="$t('profile.bank_data.checking')"
                 color="primary"
+                keep-color
                 class="q-mr-lg text-text"
               />
               <q-radio
@@ -70,54 +70,46 @@
                 val="savings"
                 :label="$t('profile.bank_data.savings')"
                 color="primary"
+                keep-color
                 class="text-text"
               />
             </div>
 
-            <div class="row q-col-gutter-sm q-mb-sm">
-              <div class="col-5">
-                <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.bank_data.agency') }}</div>
-                <q-input
-                  v-model="bankForm.agency"
-                  outlined
-                  dense
-                  input-class="text-text"
-                  placeholder="0001"
-                />
-              </div>
-              <div class="col-7">
-                <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.bank_data.account') }}</div>
-                <q-input
-                  v-model="bankForm.account"
-                  outlined
-                  dense
-                  input-class="text-text"
-                  placeholder="Ex: 0000000-0"
-                />
-              </div>
-            </div>
+            <q-input
+              v-model="bankForm.agency"
+              outlined
+              dense
+              input-class="text-text"
+              :placeholder="$t('profile.bank_data.agency')"
+              class="q-mb-sm col-12"
+            />
 
-            <div class="row q-mb-md">
-              <div class="col-4">
-                <div class="text-weight-bold q-mb-xs text-text">{{ $t('profile.bank_data.digit') }}</div>
-                <q-input
-                  v-model="bankForm.digit"
-                  outlined
-                  dense
-                  input-class="text-text"
-                  :placeholder="$t('profile.bank_data.digit')"
-                />
-              </div>
-            </div>
+            <q-input
+              v-model="bankForm.account"
+              outlined
+              dense
+              input-class="text-text"
+              :placeholder="$t('profile.bank_data.account')"
+              class="q-mb-sm col-12"
+            />
+
+            <q-input
+              v-model="bankForm.digit"
+              outlined
+              dense
+              input-class="text-text"
+              :placeholder="$t('profile.bank_data.digit')"
+              class="q-mb-sm col-3"
+            />
 
             <q-btn
               unelevated
               rounded
               no-caps
               color="primary"
-              class="full-width q-py-md text-weight-bold"
-              style="font-size: 1.1rem;"
-              :label="$t('profile.bank_data.save_account')"
+              padding="8px 16px"
+              class="full-width text-weight-bold"
+              :label="bankAlreadyRegistered ? $t('profile.bank_data.att_bank') : $t('profile.bank_data.save_account')"
               :loading="savingBank"
               :disable="!hasBankUpdatedFields"
               @click="saveBank"
@@ -153,7 +145,8 @@ const pixId = ref(null);
 const bankId = ref(null);
 const savingPix = ref(false);
 const savingBank = ref(false);
-
+const pixAlreadyRegistered = ref(false);
+const bankAlreadyRegistered = ref(false);
 const {
   form: pixForm,
   hasUpdatedFields: hasPixUpdatedFields,
@@ -250,6 +243,7 @@ onMounted(async () => {
       pixId.value = pix.id;
       pixForm.pix_key = pix.pix_key || '';
       setPixOriginal();
+      pixAlreadyRegistered.value = true;
     }
 
     const bank = methods.find((m) => m.account_type === 'bank_account');
@@ -260,6 +254,7 @@ onMounted(async () => {
       bankForm.account = bank.account || '';
       bankForm.digit = bank.digit || '';
       setBankOriginal();
+      bankAlreadyRegistered.value = true;
     }
   } catch (error) {
     console.error('Erro ao carregar dados bancários:', error);

+ 38 - 39
src/components/profile/ProfileServiceDataDialog.vue

@@ -1,7 +1,6 @@
 <template>
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height column no-shadow">
-
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
@@ -11,23 +10,19 @@
       </div>
 
       <div class="col overflow-auto">
-        <div class="q-px-md q-pt-lg q-pb-xl">
-
-          <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
-            {{ $t('profile.service_data.pricing_title') }}
-          </div>
-          <div class="text-caption text-text q-mb-md">
-            {{ $t('profile.service_data.pricing_subtitle') }}
+        <div class="q-px-md q-pt-lg q-pb-xl justify-between column full-height">
+          <div class="text-center q-mb-lg">
+            <q-img :src="LogoDiariaCampos" style="max-width: 160px;" />
           </div>
 
-          <q-card class="q-pa-lg bg-surface shadow-card q-mb-lg" style="border-radius: 20px;" :flat="false">
+          <div class="q-mb-lg">
             <div class="text-weight-medium text-text q-mb-sm">
               {{ $t('profile.service_data.price_8h_label') }}
             </div>
 
             <DefaultCurrencyInput
               v-model="form.daily_price_8h"
-              class="q-mb-xs"
+              class="q-mb-xs bg-surface"
               input-class="text-text"
               :error="!!priceError"
               :error-message="priceError"
@@ -53,6 +48,7 @@
                   outlined
                   dense
                   readonly
+                  class="bg-surface"
                   input-class="text-text text-center"
                 />
               </div>
@@ -65,6 +61,7 @@
                   outlined
                   dense
                   readonly
+                  class="bg-surface"
                   input-class="text-text text-center"
                 />
               </div>
@@ -77,54 +74,55 @@
                   outlined
                   dense
                   readonly
+                  class="bg-surface"
                   input-class="text-text text-center"
                 />
               </div>
             </div>
-          </q-card>
-
-          <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
-            {{ $t('profile.service_data.services_title') }}
-          </div>
-          <div class="text-caption text-text q-mb-md">
-            {{ $t('profile.service_data.services_subtitle') }}
           </div>
 
-          <q-card class="q-pa-lg bg-surface shadow-card q-mb-lg" style="border-radius: 20px;" :flat="false">
-            <div v-if="loadingServiceTypes" class="row justify-center q-py-md">
-              <q-spinner color="primary" size="28px" />
+          <div class="text-caption text-weight-bold text-text text-center">
+            {{ $t('profile.service_data.services_subtitle') }}
+            <div class="text-caption text-text q-mb-md text-center">
+              {{ $t('profile.service_data.change_in_profile') }}
             </div>
+            <div class="q-mb-lg">
+              <div v-if="loadingServiceTypes" class="row justify-center q-py-md">
+                <q-spinner color="primary" size="28px" />
+              </div>
 
-            <div v-else-if="serviceOptions.length" class="row q-col-gutter-sm">
-              <div v-for="service in serviceOptions" :key="service.value" class="col-6">
-                <q-checkbox
-                  :model-value="isServiceSelected(service.value)"
-                  :label="service.label"
-                  :disable="togglingService[service.value]"
-                  color="primary"
-                  class="text-text"
-                  keep-color
-                  @update:model-value="(checked) => onServiceToggle(service, checked)"
-                />
+              <div v-else-if="serviceOptions.length" class="row q-col-gutter-sm">
+                <div v-for="service in serviceOptions" :key="service.value" class="col-6">
+                  <q-checkbox
+                    :model-value="isServiceSelected(service.value)"
+                    :label="service.label"
+                    :disable="togglingService[service.value]"
+                    color="primary"
+                    class="text-text"
+                    keep-color
+                    @update:model-value="(checked) => onServiceToggle(service, checked)"
+                  />
+                </div>
               </div>
-            </div>
 
-            <div v-else class="text-caption text-grey-7 text-center q-py-md">
-              {{ $t('profile.service_data.no_services') }}
-            </div>
+              <div v-else class="text-caption text-grey-7 text-center q-py-md">
+                {{ $t('profile.service_data.no_services') }}
+              </div>
 
-            <div class="text-caption text-grey-6 text-center q-mt-md">
-              {{ $t('profile.service_data.services_hint') }}
+              <div class="text-caption text-grey-6 text-center q-mt-md">
+                {{ $t('profile.service_data.services_hint') }}
+              </div>
             </div>
-          </q-card>
+          </div>
+
 
           <q-btn
             unelevated
             rounded
             no-caps
             color="primary"
+            padding="8px 16px"
             class="full-width q-py-md text-weight-bold"
-            style="font-size: 1.05rem; border-radius: 14px;"
             :label="$t('common.actions.update')"
             :loading="saving"
             :disable="!!priceError || saving"
@@ -148,6 +146,7 @@ import { getUser } from 'src/api/user';
 import { updateProvider } from 'src/api/provider';
 import { userStore } from 'src/stores/user';
 import DefaultCurrencyInput from 'src/components/defaults/DefaultCurrencyInput.vue';
+import LogoDiariaCampos from 'src/assets/logo_diaria_campos_login.svg';
 
 defineEmits([...useDialogPluginComponent.emits]);
 

+ 0 - 1
src/composables/useInputRules.js

@@ -63,7 +63,6 @@ export const useInputRules = () => {
 };
 
 function isValidCPF(cpf) {
-  console.log("isValidCPF", cpf);
   if (!cpf) return false;
   cpf = cpf.replace(/[^\d]+/g, "");
   if (cpf.length !== 11) return false;

+ 13 - 0
src/helpers/utils.js

@@ -213,6 +213,18 @@ const formatCurrency = (value) => {
   return value;
 };
 
+const formatAddress = (address) => {
+  if (!address) return '';
+  const { address: street, number, district, city, state } = address;
+  let parts = [];
+  if(street && street !== null && street != 'null') parts.push(street);
+  if(number && number !== null && number != 'null') parts.push(number);
+  if(district && district !== null && district != 'null') parts.push(district);
+  if(city?.name && city.name !== null && city.name != 'null') parts.push(city.name);
+  if(state?.code && state.code !== null && state.code != 'null') parts.push(state.code);
+  return parts.join(', ');
+};
+
 export {
   formatDateDMYtoYMD,
   formatDateYMDtoDMY,
@@ -226,4 +238,5 @@ export {
   validaDataHora,
   formatQuantity,
   formatCurrency,
+  formatAddress
 };

+ 35 - 5
src/i18n/locales/en.json

@@ -173,7 +173,26 @@
           "selfie": "Selfie with document",
           "selfie_desc": "Take a selfie holding your document near your face.",
           "photo_captured": "Photo captured!",
-          "action": "send documents"
+          "action": "send documents",
+          "upload_all_photos": "Upload all photos to continue.",
+          "btn_selfie": "take a selfie",
+          "selfie_hint": "Choose a well-lit environment",
+          "selfie_sent": "Photo sent.",
+          "btn_document": "take a photo of the document",
+          "document_hint": "ID with CPF, front and back.",
+          "document_sent": "Document sent.",
+          "btn_capture_selfie": "take photo",
+          "btn_capture_front": "take photo of the front",
+          "btn_capture_back": "take photo of the back",
+          "btn_retake": "take another photo",
+          "btn_confirm": "confirm",
+          "btn_use_photo": "use this photo",
+          "btn_retry": "take the photo",
+          "btn_continue": "continue",
+          "error_message": "Oops, something went wrong! Take a new photo of your document.",
+          "selfie_success": "Your photo was sent successfully!",
+          "document_success": "Your document was sent successfully!",
+          "document_hint_result": "ID with CPF, front and back."
         },
         "step_5": {
           "daily_price_title": "What is the value of your daily rate for up to 8 hours?",
@@ -210,7 +229,8 @@
         "my_schedules": "My schedules"
       },
       "price_suggest": {
-        "region_label": "Suggested daily rate in the region",
+        "region_label_1": "Suggested daily rate",
+        "region_label_2": "in the region",
         "my_price_label": "My daily rate"
       },
       "solicitations": {
@@ -242,7 +262,8 @@
         "no_meal": "Does not offer meal",
         "place_home": "Residential",
         "place_apartment": "Apartment",
-        "place_unknown": "Address"
+        "place_unknown": "Address",
+        "address_copied": "Address copied to clipboard"
       },
       "last_schedules": {
         "title": "Last ones performed",
@@ -300,7 +321,9 @@
       "bank_fields_required": "Please enter agency and account",
       "save_account": "register account",
       "account_saved": "Bank account saved successfully!",
-      "save_error": "Error saving. Please try again."
+      "save_error": "Error saving. Please try again.",
+      "att_pix": "Update pix",
+      "att_bank": "Update bank account"
     },
     "availability": {
       "title": "Availability",
@@ -360,7 +383,8 @@
       "services_title": "Other services",
       "services_subtitle": "In addition to basic cleaning, what other services do you also provide?",
       "services_hint": "This information will appear to those searching your profile.",
-      "no_services": "No services available at the moment."
+      "no_services": "No services available at the moment.",
+      "change_in_profile": "(you can change it in your profile)"
     },
     "address": {
       "title": "Address",
@@ -600,5 +624,11 @@
         }
       }
     }
+  },
+  "address": {
+    "types": {
+      "home": "Residential",
+      "commercial": "Commercial"
+    }
   }
 }

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

@@ -173,7 +173,26 @@
           "selfie": "Selfie con documento",
           "selfie_desc": "Tome una selfie sosteniendo su documento cerca de su rostro.",
           "photo_captured": "¡Foto capturada!",
-          "action": "enviar documentos"
+          "action": "enviar documentos",
+          "upload_all_photos": "Suba todas las fotos para continuar.",
+          "btn_selfie": "tomar una selfie",
+          "selfie_hint": "Elija un ambiente bien iluminado",
+          "selfie_sent": "Foto enviada.",
+          "btn_document": "tomar foto del documento",
+          "document_hint": "RG con CPF, frente y dorso.",
+          "document_sent": "Documento enviado.",
+          "btn_capture_selfie": "tomar foto",
+          "btn_capture_front": "tomar foto del frente",
+          "btn_capture_back": "tomar foto del dorso",
+          "btn_retake": "tomar otra foto",
+          "btn_confirm": "confirmar",
+          "btn_use_photo": "usar esta foto",
+          "btn_retry": "tomar la foto",
+          "btn_continue": "continuar",
+          "error_message": "¡Ups, algo salió mal! Tome una nueva foto de su documento.",
+          "selfie_success": "¡Su foto fue enviada con éxito!",
+          "document_success": "¡Su documento fue enviado con éxito!",
+          "document_hint_result": "RG con CPF, frente y dorso."
         },
         "step_5": {
           "daily_price_title": "¿Cuál es el valor de su jornada de hasta 8 horas?",
@@ -210,7 +229,8 @@
         "my_schedules": "Mis citas"
       },
       "price_suggest": {
-        "region_label": "Precio de jornada sugerido en la región",
+        "region_label_1": "Precio de jornada",
+        "region_label_2": "sugerido en la región",
         "my_price_label": "Precio de mi jornada"
       },
       "solicitations": {
@@ -242,7 +262,8 @@
         "no_meal": "No ofrece comida",
         "place_home": "Residencial",
         "place_apartment": "Apartamento",
-        "place_unknown": "Dirección"
+        "place_unknown": "Dirección",
+        "address_copied": "Dirección copiada al portapapeles"
       },
       "last_schedules": {
         "title": "Últimas realizadas",
@@ -300,7 +321,9 @@
       "bank_fields_required": "Por favor ingrese agencia y cuenta",
       "save_account": "registrar cuenta",
       "account_saved": "¡Cuenta bancaria guardada con éxito!",
-      "save_error": "Error al guardar. Inténtelo de nuevo."
+      "save_error": "Error al guardar. Inténtelo de nuevo.",
+      "att_pix": "Actualizar pix",
+      "att_bank": "Actualizar cuenta bancaria"
     },
     "availability": {
       "title": "Disponibilidad",
@@ -360,7 +383,8 @@
       "services_title": "Otros servicios",
       "services_subtitle": "Además de la limpieza básica, ¿qué otros servicios también realizas?",
       "services_hint": "Esta información aparecerá para quienes busquen tu perfil.",
-      "no_services": "No hay servicios disponibles por el momento."
+      "no_services": "No hay servicios disponibles por el momento.",
+      "change_in_profile": "(podrás cambiarlo en tu perfil)"
     },
     "address": {
       "title": "Dirección",
@@ -600,5 +624,11 @@
         }
       }
     }
+  },
+  "address": {
+    "types": {
+      "home": "Residencial",
+      "commercial": "Comercial"
+    }
   }
 }

+ 65 - 35
src/i18n/locales/pt.json

@@ -166,41 +166,60 @@
           "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"
+          "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",
+          "upload_all_photos": "Faça upload de todas as fotos para continuar.",
+          "btn_selfie": "tirar uma selfie",
+          "selfie_hint": "Escolha um ambiente iluminado",
+          "selfie_sent": "Foto enviada.",
+          "btn_document": "tirar foto do documento",
+          "document_hint": "RG com CPF, frente e verso.",
+          "document_sent": "Documento enviado.",
+          "btn_capture_selfie": "tirar foto",
+          "btn_capture_front": "tirar foto da frente",
+          "btn_capture_back": "tirar foto do verso",
+          "btn_retake": "tirar outra foto",
+          "btn_confirm": "confirmar",
+          "btn_use_photo": "usar esta foto",
+          "btn_retry": "tirar a foto",
+          "btn_continue": "continuar",
+          "error_message": "Ops, algo deu errado! Tire uma nova foto do seu documento.",
+          "selfie_success": "Sua foto foi enviada com sucesso!",
+          "document_success": "Seu documento foi enviado com sucesso!",
+          "document_hint_result": "RG com CPF, frente e verso."
         },
         "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"
+          "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"
+          "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"
         }
       }
     },
@@ -210,7 +229,8 @@
         "my_schedules": "Minhas diárias"
       },
       "price_suggest": {
-        "region_label": "Preço de diária sugerido na região",
+        "region_label_1": "Preço de diária",
+        "region_label_2": "sugerido na região",
         "my_price_label": "Preço da minha diária"
       },
       "solicitations": {
@@ -242,7 +262,8 @@
         "no_meal": "Não oferece refeição",
         "place_home": "Residencial",
         "place_apartment": "Apartamento",
-        "place_unknown": "Endereço"
+        "place_unknown": "Endereço",
+        "address_copied": "Endereço copiado!"
       },
       "last_schedules": {
         "title": "Últimas realizadas",
@@ -300,7 +321,9 @@
       "bank_fields_required": "Informe agência e conta",
       "save_account": "cadastrar conta",
       "account_saved": "Conta bancária salva com sucesso!",
-      "save_error": "Erro ao salvar. Tente novamente."
+      "save_error": "Erro ao salvar. Tente novamente.",
+      "att_pix": "Atualizar pix",
+      "att_bank": "Atualizar conta bancária"
     },
     "availability": {
       "title": "Disponibilidade",
@@ -360,7 +383,8 @@
       "services_title": "Outros serviços",
       "services_subtitle": "Além da limpeza básica, quais serviços você também realiza?",
       "services_hint": "Essas informações aparecerão para quem buscar o seu perfil na busca.",
-      "no_services": "Nenhum serviço disponível no momento."
+      "no_services": "Nenhum serviço disponível no momento.",
+      "change_in_profile": "(poderá ser alterado no perfil)"
     },
     "address": {
       "title": "Endereço",
@@ -600,5 +624,11 @@
         }
       }
     }
+  },
+  "address": {
+    "types": {
+      "home": "Casa",
+      "commercial": "Comercial"
+    }
   }
 }

+ 14 - 0
src/pages/LoginPage.vue

@@ -172,6 +172,20 @@ const validateCurrentStep = async () => {
     return false;
   }
 
+  if(steps.value === 4) {
+    const hasSelfie = !!stepFourForm.value.selfie_base64;
+    const hasDocumentFront = !!stepFourForm.value.document_front_base64;
+    const hasDocumentBack = !!stepFourForm.value.document_back_base64;
+
+    if (!hasSelfie || !hasDocumentFront || !hasDocumentBack) {
+      $q.notify({
+        type: 'negative',
+        message: t('provider.login.steps.step_4.upload_all_photos'),
+      });
+      return false;
+    }
+  }
+
   if (steps.value === 6 && !hasWorkingDaySelected()) {
     $q.notify({
       type: 'negative',