ソースを参照

feat: :sparkles: feat: ✨ feat(agendamento-sob-medida): finalizando fluxo frontend com dashboard, modal e i18n

Realizada a finalização do fluxo Sob Medida no frontend, incluindo criação do componente FinalSuccessModal para exibição do modal de sucesso após a criação do agendamento e integração do redirecionamento para dashboard, além da refatoração do script setup, organização do template, melhorias de CSS e implementação da internacionalização (PT, EN e ES).

fase:dev | origin:escopo
kayo henrique 2 週間 前
コミット
4cffa47391

+ 7 - 0
src/api/opportunities.js

@@ -0,0 +1,7 @@
+import api from 'src/api'
+
+export const getProviderOpportunities = async (providerId) => {
+  const { data } = await api.get(`/custom-schedule-available?provider_id=${providerId}`)
+
+  return data.payload
+}

+ 195 - 99
src/pages/opportunities/OpportunitiesPage.vue

@@ -1,6 +1,5 @@
 <template>
   <q-page class="opportunities-page">
-    <!-- HEADER -->
     <div class="page-header">
       <q-btn
         flat
@@ -15,7 +14,6 @@
       </div>
     </div>
 
-    <!-- BANNER -->
     <q-card flat class="info-banner">
       <q-icon name="mdi-auto-fix" size="22px" class="banner-icon" />
       <div class="banner-text">
@@ -23,92 +21,131 @@
       </div>
     </q-card>
 
-    <!-- LISTA -->
-    <div class="opportunity-list">
-      <q-card
-        v-for="item in opportunities"
-        :key="item.id"
-        flat
-        class="opportunity-card"
-      >
-        <img :src="item.avatar" class="client-avatar" />
-
-        <div class="card-content">
-          <div class="client-name-row">
-            <span class="client-name">{{ item.name }}</span>
-
-            <span class="rating">
-              <q-icon name="star" size="12px" />
-              {{ item.rating }}
-            </span>
-          </div>
-
-          <div class="service-date">
-            {{ item.date }}
-          </div>
-
-          <div class="service-address">
-            {{ item.address }}
-          </div>
-
-          <div class="service-type">
-            {{ item.service }}
-          </div>
-        </div>
-
-        <div class="price-column">
-          <div class="price">{{ item.price }}</div>
-
-          <div class="hours">
-            {{ $t('provider.dashboard.opportunities.full_day') }}
-          </div>
-
-          <div class="distance">
-            {{ item.distance }}
-          </div>
-
-          <q-btn
-            unelevated
-            rounded
-            no-caps
-            color="secondary"
-            :label="$t('provider.dashboard.opportunities.details')"
-            class="details-btn"
-            @click="goToOpportunityDetails(item)"
-          />
-        </div>
-      </q-card>
+    <div v-if="loading" class="flex flex-center q-pa-lg">
+      <q-spinner-dots color="secondary" size="32px" />
+    </div>
+
+    <div
+      v-else-if="!opportunities.length"
+      class="text-center q-pa-md text-grey"
+    >
+    </div>
+
+<div v-else class="opportunity-list">
+  <q-card
+    v-for="item in opportunities"
+    :key="item.id"
+    flat
+    class="opportunity-card"
+  >
+    <!-- coluna avatar -->
+    <div class="avatar-column">
+      <img :src="item.avatar" class="client-avatar" />
+
+      <div class="service-type">
+        {{ item.serviceType }}
+      </div>
     </div>
+
+    <!-- conteúdo central -->
+    <div class="center-content">
+      <div class="client-name-row">
+        <span class="client-name">{{ item.clientName }}</span>
+
+        <span class="rating">
+          <q-icon name="star" size="11px" />
+          {{ item.rating }}
+        </span>
+      </div>
+
+      <div class="service-date">
+        {{ item.date }}
+      </div>
+
+      <div class="service-hour">
+        {{ item.hour }}
+      </div>
+    </div>
+
+    <!-- lado direito -->
+    <div class="right-content">
+      <div class="price">
+        {{ `R$${item.price}` }}
+      </div>
+
+      <div class="service-address">
+        {{ item.address }}
+      </div>
+
+      <div class="distance">
+        {{ item.distance }}
+      </div>
+
+      <q-btn
+        unelevated
+        rounded
+        no-caps
+        color="secondary"
+        label="ver detalhes"
+        class="details-btn"
+        @click="goToOpportunityDetails(item)"
+      />
+    </div>
+  </q-card>
+</div>
   </q-page>
 </template>
 
 <script setup>
+import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
+import { getProviderOpportunities } from 'src/api/opportunities'
+import { userStore } from 'src/stores/user'
 
 const router = useRouter()
+const user = userStore()
 
-const opportunities = [
-  {
-    id: 1,
-    name: 'Nathalia',
-    rating: '5,0',
-    date: 'Segunda-feira, 04/10',
-    address: 'Das 09h30 às 17h30',
-    service: 'Evento',
-    price: 'R$245,00',
-    distance: '7 km',
-  },
-  {
-    id: 2,
-    name: 'Nathalia',
-    rating: '5,0',
-    date: 'Segunda-feira, 04/10',
-    address: 'Das 09h30 às 17h30',
-    service: 'Limpeza padrão',
-    price: 'R$245,00',
-    distance: '7 km',
-  }
-]
+const opportunities = ref([])
+const loading = ref(false)
+
+const formatHour = (time) =>
+  time ? time.slice(0, 5).replace(':', 'h') : ''
+
+const normalizeOpportunity = (item) => ({
+  id: item.id,
+
+  avatar: item.client?.user?.photo || '/icons/avatar.svg',
+
+  clientName:
+    item.client?.user?.name || 'Cliente',
+
+  rating:
+    item.client?.average_rating || 5.0,
+
+ date: new Date(
+  item.custom_schedule?.created_at ||
+  item.created_at
+).toLocaleDateString('pt-BR'),
+
+hour: `Das ${formatHour(
+  item.start_time
+)} às ${formatHour(
+  item.end_time
+)}`,
+
+
+  address:
+    item.address?.address || 'Endereço não informado',
+
+  serviceType:
+    item.custom_schedule?.service_type?.name || 'Serviço',
+
+  price: Number(
+    item.custom_schedule?.max_price || 0
+  ).toFixed(2),
+
+  distance: '0 km'
+})
 
 const goToOpportunityDetails = (item) => {
   router.push({
@@ -118,6 +155,27 @@ const goToOpportunityDetails = (item) => {
     }
   })
 }
+
+const loadOpportunities = async () => {
+  loading.value = true
+  try {
+    const response = await getProviderOpportunities(
+      user.user.provider.id
+    )
+    console.log('DETALHE DA OPORTUNIDADE', response)
+
+    console.log('Oportunidades recebidas:', response)
+    opportunities.value = (response || []).map(normalizeOpportunity)
+  } catch (error) {
+    console.error('Erro ao buscar oportunidades:', error)
+    opportunities.value = []
+    
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(loadOpportunities)
 </script>
 
 <style scoped lang="scss">
@@ -164,15 +222,24 @@ const goToOpportunityDetails = (item) => {
 .opportunity-list {
   display: flex;
   flex-direction: column;
-  gap: 12px;
+  gap: 14px;
 }
 
 .opportunity-card {
   display: flex;
+  gap: 14px;
+  padding: 14px;
+  border-radius: 18px;
+  background: #fff;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
+  align-items: flex-start;
+}
+
+.avatar-column {
+  display: flex;
+  flex-direction: column;
   align-items: center;
-  padding: 12px;
-  border-radius: 16px;
-  background: white;
+  width: 58px;
 }
 
 .client-avatar {
@@ -182,51 +249,80 @@ const goToOpportunityDetails = (item) => {
   object-fit: cover;
 }
 
-.card-content {
+.service-type {
+  margin-top: 8px;
+  font-size: 11px;
+  color: #7c5cff;
+  font-weight: 500;
+  text-align: center;
+}
+
+.center-content {
   flex: 1;
-  margin-left: 12px;
+  display: flex;
+  flex-direction: column;
 }
 
 .client-name-row {
   display: flex;
   align-items: center;
-  gap: 6px;
-  font-size: 14px;
+  gap: 4px;
+}
+
+.client-name {
+  font-size: 15px;
   font-weight: 600;
+  color: #2d2d2d;
 }
 
 .rating {
   display: flex;
   align-items: center;
-  gap: 4px;
+  gap: 2px;
+  font-size: 11px;
   color: #ffb800;
-  font-size: 12px;
 }
 
 .service-date,
-.service-address,
-.service-type {
+.service-hour {
   font-size: 11px;
-  color: #777;
+  color: #666;
+  line-height: 1.35;
 }
 
-.price-column {
-  text-align: right;
+.right-content {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  min-width: 110px;
 }
 
 .price {
-  font-size: 15px;
+  font-size: 22px;
   font-weight: 700;
+  color: #2d2d2d;
+}
+
+.service-address {
+  margin-top: 6px;
+  font-size: 11px;
+  color: #666;
+  text-align: right;
+  line-height: 1.3;
+  max-width: 120px;
 }
 
-.hours,
 .distance {
-  font-size: 10px;
-  color: #777;
+  margin-top: 4px;
+  font-size: 11px;
+  color: #999;
 }
 
 .details-btn {
-  margin-top: 8px;
+  margin-top: 10px;
+  min-height: 30px;
+  padding: 0 18px;
   font-size: 11px;
+  font-weight: 600;
 }
 </style>

+ 1 - 1
src/pages/opportunities/components/OpportunityDetailsPage.vue

@@ -84,7 +84,7 @@ const mockDetails = {
 }
 
 const goToProposalFlow = () => {
-  console.log('seguir fluxo')
+  console.log('aqui')
 }
 </script>
 

+ 13 - 38
src/router/routes.js

@@ -32,46 +32,26 @@ const routes = [
         path: "pagamentos",
         name: "PagamentosPage",
         component: () => import("src/pages/search/PagamentosPage.vue"),
-        meta: {
-          title: "Busca",
-          requireAuth: true,
-          breadcrumbs: [
-            {
-              name: "DashboardPage",
-              title: "ui.navigation.dashboard",
-            },
-            {
-              name: "PagamentosPage",
-              title: "Busca",
-            },
-          ],
-        },
       },
       {
         path: "agenda",
         name: "AgendaPage",
         component: () => import("src/pages/agenda/AgendaPage.vue"),
-        meta: {
-          title: "Agenda",
-          requireAuth: true,
-          breadcrumbs: [
-            {
-              name: "DashboardPage",
-              title: "ui.navigation.dashboard",
-            },
-            {
-              name: "AgendaPage",
-              title: "Agenda",
-            },
-          ],
-        },
       },
       {
         path: "perfil",
         name: "ProfilePage",
         component: () => import("src/pages/profile/ProfilePage.vue"),
+      },
+
+      
+      {
+        path: "opportunities",
+        name: "OpportunitiesPage",
+        component: () =>
+          import("src/pages/opportunities/OpportunitiesPage.vue"),
         meta: {
-          title: "Perfil",
+          title: "Oportunidades",
           requireAuth: true,
           breadcrumbs: [
             {
@@ -79,12 +59,13 @@ const routes = [
               title: "ui.navigation.dashboard",
             },
             {
-              name: "ProfilePage",
-              title: "Perfil",
+              name: "OpportunitiesPage",
+              title: "Oportunidades",
             },
           ],
         },
       },
+
       ...sub_routes,
     ],
   },
@@ -96,19 +77,13 @@ const routes = [
         path: "",
         name: "LoginPage",
         component: () => import("pages/LoginPage.vue"),
-        meta: {
-          title: "Login",
-        },
       },
     ],
   },
-
-  // Always leave this as last one,
-  // but you can also remove it
   {
     path: "/:catchAll(.*)*",
     component: () => import("pages/ErrorNotFound.vue"),
   },
 ];
 
-export default routes;
+export default routes;