Browse Source

feat: :sparkles: feat(agendamento-sob-medida) adicionando listagem, navegação e tela de detalhes

Ajustado o fluxo de oportunidades do prestador com criação da navegação pelo banner da dashboard, implementação da tela de listagem de oportunidades, exibição dinâmica das informações principais do serviço como cliente, data, horário, distância, tipo de oportunidade e valor

fase:dev | origin:escopo
kayo henrique 2 tuần trước cách đây
mục cha
commit
348917deba

+ 19 - 3
src/components/dashboard/DashboardScrollAreaSchedules.vue

@@ -1,21 +1,36 @@
 <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">
+      <div
+        v-for="card in cards"
+        :key="card.id"
+        class="promo-card"
+        @click="goToCard(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';
 
+import { useRouter } from 'vue-router';
+
+const router = useRouter();
+
 const cards = [
-  { id: 1, image: Banner1, alt: 'Diária sob medida' },
+  { id: 1, image: Banner1, alt: 'Diária sob medida', routeName: 'OpportunitiesPage' },
   { id: 2, image: Banner2, alt: 'Escolha profissionais' },
 ];
+
+const goToCard = (card) => {
+  if (card.routeName) {
+    router.push({ name: card.routeName })
+  }
+}
+
 </script>
 
 <style scoped lang="scss">
@@ -49,6 +64,7 @@ const cards = [
   scroll-snap-align: start;
   background: #e8e4f5;
   flex-shrink: 0;
+  cursor: pointer;
 }
 
 .promo-card__img {

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

@@ -241,7 +241,10 @@
         "until_2h": "Diária Rápida (Até 2h)"
       },
       "opportunities": {
-        "title": "Oportunidades"
+        "title": "Oportunidades",
+        "banner_text": "Pedidos de clientes para os profissionais do Diário. Veja os detalhes do pedido, se candidate ao serviço e se o cliente aceitar você receberá uma nova diária.",
+        "full_day": "Integral (8h)",
+        "details": "ver detalhes"
       },
       "favorites": {
         "title": "Seus favoritos",
@@ -631,4 +634,4 @@
       "commercial": "Comercial"
     }
   }
-}
+}

+ 232 - 0
src/pages/opportunities/OpportunitiesPage.vue

@@ -0,0 +1,232 @@
+<template>
+  <q-page class="opportunities-page">
+    <!-- HEADER -->
+    <div class="page-header">
+      <q-btn
+        flat
+        round
+        dense
+        icon="chevron_left"
+        class="back-btn"
+        @click="router.back()"
+      />
+      <div class="page-title">
+        {{ $t('provider.dashboard.opportunities.title') }}
+      </div>
+    </div>
+
+    <!-- BANNER -->
+    <q-card flat class="info-banner">
+      <q-icon name="mdi-auto-fix" size="22px" class="banner-icon" />
+      <div class="banner-text">
+        {{ $t('provider.dashboard.opportunities.banner_text') }}
+      </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>
+  </q-page>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+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 goToOpportunityDetails = (item) => {
+  router.push({
+    name: 'OpportunityDetailsPage',
+    params: {
+      id: item.id
+    }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.opportunities-page {
+  padding: 16px;
+  background: #f7f7fb;
+  min-height: 100vh;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  margin-bottom: 16px;
+}
+
+.back-btn {
+  position: absolute;
+  left: 0;
+}
+
+.page-title {
+  font-size: 16px;
+  font-weight: 700;
+  color: #7c5cff;
+}
+
+.info-banner {
+  display: flex;
+  gap: 12px;
+  padding: 14px;
+  border-radius: 14px;
+  background: #a78bfa;
+  color: white;
+  margin-bottom: 16px;
+}
+
+.banner-text {
+  font-size: 12px;
+  line-height: 1.4;
+}
+
+.opportunity-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.opportunity-card {
+  display: flex;
+  align-items: center;
+  padding: 12px;
+  border-radius: 16px;
+  background: white;
+}
+
+.client-avatar {
+  width: 54px;
+  height: 54px;
+  border-radius: 50%;
+  object-fit: cover;
+}
+
+.card-content {
+  flex: 1;
+  margin-left: 12px;
+}
+
+.client-name-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.rating {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  color: #ffb800;
+  font-size: 12px;
+}
+
+.service-date,
+.service-address,
+.service-type {
+  font-size: 11px;
+  color: #777;
+}
+
+.price-column {
+  text-align: right;
+}
+
+.price {
+  font-size: 15px;
+  font-weight: 700;
+}
+
+.hours,
+.distance {
+  font-size: 10px;
+  color: #777;
+}
+
+.details-btn {
+  margin-top: 8px;
+  font-size: 11px;
+}
+</style>

+ 216 - 0
src/pages/opportunities/components/OpportunityDetailsPage.vue

@@ -0,0 +1,216 @@
+<template>
+  <q-page class="details-page">
+    <!-- HEADER -->
+    <div class="page-header">
+      <q-btn
+        flat
+        round
+        dense
+        icon="chevron_left"
+        class="back-btn"
+        @click="router.back()"
+      />
+      <div class="page-title">{{ mockDetails.title }}</div>
+    </div>
+
+    <!-- CLIENTE -->
+    <div class="client-section">
+      <img :src="AvatarMock" class="client-avatar" />
+      <div class="client-name">{{ mockDetails.clientName }}</div>
+      <div class="client-price">{{ mockDetails.price }}</div>
+    </div>
+
+    <!-- INFOS -->
+    <div class="details-info">
+      <div>{{ mockDetails.date }}</div>
+      <div>{{ mockDetails.hour }}</div>
+      <div>{{ mockDetails.address }}</div>
+      <div>{{ mockDetails.distance }}</div>
+    </div>
+
+    <!-- TAGS -->
+    <div class="tags-row">
+      <q-chip dense color="grey-3">
+        {{ mockDetails.tags[0] }}
+      </q-chip>
+
+      <q-chip dense color="grey-3">
+        {{ mockDetails.tags[1] }}
+      </q-chip>
+    </div>
+
+    <!-- DESCRIÇÃO -->
+    <div class="description-box">
+      {{ mockDetails.description }}
+    </div>
+
+    <!-- BOTÃO -->
+    <q-btn
+      unelevated
+      rounded
+      no-caps
+      color="secondary"
+      :label="mockDetails.buttonLabel"
+      class="full-width q-mt-md"
+      @click="goToProposalFlow"
+    />
+
+    <!-- ALERTA -->
+    <q-card flat class="bottom-alert">
+      {{ mockDetails.alertText }}
+    </q-card>
+  </q-page>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router'
+import AvatarMock from 'src/assets/foto_diarista_login.svg'
+
+const router = useRouter()
+
+const mockDetails = {
+  title: 'Detalhes do serviço',
+  clientName: 'Helena',
+  price: 'R$245,00',
+  date: 'Domingo, 04/10',
+  hour: 'Das 08h00 às 17h00',
+  address: 'Rua Teste, 123',
+  distance: '4,2 km da sua localização',
+  tags: ['Comercial', 'Refeição no local'],
+  description: 'Limpeza pós obra detalhada para salão comercial.',
+  buttonLabel: 'Quero atender',
+  alertText:
+    'Se seu pedido for aceito pelo cliente você receberá um aviso confirmando o agendamento e aparecerá nos seus próximos serviços.'
+}
+
+const goToProposalFlow = () => {
+  console.log('seguir fluxo')
+}
+</script>
+
+<style scoped lang="scss">
+.details-page {
+  padding: 16px;
+  background: #f7f7fb;
+  min-height: 100vh;
+}
+
+.page-header {
+  display: flex;
+  justify-content: center;
+  position: relative;
+  margin-bottom: 24px;
+}
+
+.back-btn {
+  position: absolute;
+  left: 0;
+  top: -4px;
+}
+
+.page-title {
+  font-size: 16px;
+  font-weight: 700;
+  color: #7c5cff;
+}
+
+.client-section {
+  text-align: center;
+  margin-top: 8px;
+}
+
+.client-avatar {
+  width: 84px;
+  height: 84px;
+  border-radius: 50%;
+  object-fit: cover;
+}
+
+.client-name {
+  margin-top: 8px;
+  font-size: 18px;
+  font-weight: 500;
+  color: #666;
+}
+
+.client-price {
+  margin-top: 8px;
+  color: #7c5cff;
+  font-size: 32px;
+  font-weight: 700;
+}
+
+.details-info {
+  margin-top: 12px;
+  text-align: center;
+  font-size: 13px;
+  line-height: 1.6;
+  color: #666;
+}
+
+.distance-info {
+  margin-top: 12px;
+  text-align: center;
+  font-size: 12px;
+  color: #999;
+}
+
+.service-highlight {
+  margin-top: 16px;
+  text-align: center;
+  font-size: 13px;
+  color: #666;
+}
+
+.highlight-text {
+  color: #7c5cff;
+  font-weight: 700;
+}
+
+.tags-row {
+  display: flex;
+  justify-content: center;
+  gap: 8px;
+  margin: 18px 0;
+}
+
+.tags-row .q-chip {
+  border: 1px solid #7c5cff;
+  color: #7c5cff;
+  background: white;
+  font-size: 12px;
+}
+
+.info-title {
+  text-align: center;
+  color: #7c5cff;
+  font-size: 18px;
+  font-weight: 700;
+  margin-bottom: 12px;
+}
+
+.description-box {
+  text-align: center;
+  font-size: 13px;
+  line-height: 1.6;
+  color: #666;
+}
+
+.full-width {
+  margin-top: 20px;
+  height: 48px;
+  font-size: 16px;
+  background: #8f6dfc !important;
+}
+
+.bottom-alert {
+  margin-top: 18px;
+  padding: 14px;
+  border-radius: 14px;
+  background: #dfeeff;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #5c6b8a;
+  text-align: center;
+}
+</style>

+ 41 - 0
src/router/routes/appointments.route.js

@@ -0,0 +1,41 @@
+export default [
+  {
+    path: 'opportunities',
+    name: 'OpportunitiesPage',
+    component: () => import('src/pages/opportunities/OpportunitiesPage.vue'),
+    meta: {
+      title: 'Oportunidades',
+      requireAuth: true,
+      breadcrumbs: [
+        {
+          name: 'DashboardPage',
+          title: 'ui.navigation.dashboard'
+        },
+        {
+          name: 'OpportunitiesPage',
+          title: 'provider.dashboard.opportunities.title'
+        }
+      ]
+    }
+  },
+  {
+    path: 'opportunities/:id',
+    name: 'OpportunityDetailsPage',
+    component: () =>
+      import('src/pages/opportunities/components/OpportunityDetailsPage.vue'),
+    meta: {
+      title: 'Detalhes da oportunidade',
+      requireAuth: true,
+      breadcrumbs: [
+        {
+          name: 'DashboardPage',
+          title: 'ui.navigation.dashboard'
+        },
+        {
+          name: 'OpportunitiesPage',
+          title: 'provider.dashboard.opportunities.title'
+        }
+      ]
+    }
+  }
+]