Selaa lähdekoodia

Merge branch 'feature/diariaapp-kay-agendamentos-sob-medida-apps' into development

Gustavo Zanatta 2 viikkoa sitten
vanhempi
commit
46755f9276

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

+ 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

@@ -265,7 +265,10 @@
         "help": "ajuda"
       },
       "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",
@@ -681,4 +684,4 @@
     "agenda": "Agenda",
     "profile": "Perfil"
   }
-}
+}

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

@@ -0,0 +1,328 @@
+<template>
+  <q-page class="opportunities-page">
+    <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>
+
+    <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>
+
+    <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 = 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({
+    name: 'OpportunityDetailsPage',
+    params: {
+      id: item.id
+    }
+  })
+}
+
+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">
+.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: 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;
+  width: 58px;
+}
+
+.client-avatar {
+  width: 54px;
+  height: 54px;
+  border-radius: 50%;
+  object-fit: cover;
+}
+
+.service-type {
+  margin-top: 8px;
+  font-size: 11px;
+  color: #7c5cff;
+  font-weight: 500;
+  text-align: center;
+}
+
+.center-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.client-name-row {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.client-name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #2d2d2d;
+}
+
+.rating {
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  font-size: 11px;
+  color: #ffb800;
+}
+
+.service-date,
+.service-hour {
+  font-size: 11px;
+  color: #666;
+  line-height: 1.35;
+}
+
+.right-content {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  min-width: 110px;
+}
+
+.price {
+  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;
+}
+
+.distance {
+  margin-top: 4px;
+  font-size: 11px;
+  color: #999;
+}
+
+.details-btn {
+  margin-top: 10px;
+  min-height: 30px;
+  padding: 0 18px;
+  font-size: 11px;
+  font-weight: 600;
+}
+</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('aqui')
+}
+</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>

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

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