Gustavo Zanatta 1 天之前
父節點
當前提交
89e825ad0e

+ 3 - 36
src/api/opportunities.js

@@ -1,40 +1,7 @@
 import api from 'src/api'
 
 export const getProviderOpportunities = async (providerId) => {
-  try {
-    const { data } = await api.get(
-      `/custom-schedule-available`,
-      {
-        params: {
-          provider_id: providerId
-        }
-      }
-    )
+  const { data } = await api.get(`/custom-schedule-available?provider_id=${providerId}`)
 
-    return data?.payload || []
-  } catch (error) {
-    console.error('[API] getProviderOpportunities error:', error)
-    return []
-  }
-}
-
-export const getOpportunityById = async (id) => {
-  try {
-    const { data } = await api.get(`/custom-schedule/${id}`)
-    return data?.payload || null
-  } catch (error) {
-    console.error('[API] getOpportunityById error:', error)
-    return null
-  }
-}
-
-
-export const proposalOpportunity = async (scheduleId, providerId) => {
-  try {
-    const { data } = await api.post(`/custom-schedule/${scheduleId}/propose`, { provider_id: providerId })
-    return data.payload || null
-  } catch (error) {
-    console.error('[API] proposalOpportunity error:', error)
-    return null
-  }
-} 
+  return data.payload
+}

+ 1 - 37
src/helpers/utils.js

@@ -225,40 +225,6 @@ const formatAddress = (address) => {
   return parts.join(', ');
 };
 
-const calculateDailyPrices = (dailyPrice8h) => {
-  if (!dailyPrice8h || dailyPrice8h <= 0) {
-    return {
-      daily_price_8h: null,
-      daily_price_6h: null,
-      daily_price_4h: null,
-      daily_price_2h: null,
-    };
-  }
-
-  return {
-    daily_price_8h: dailyPrice8h, 
-    daily_price_6h: dailyPrice8h * 0.85,
-    daily_price_4h: dailyPrice8h * 0.55,
-    daily_price_2h: dailyPrice8h * 0.30,
-  };
-};
-
-const chooseprice = (periodType, daily_price_8h) => {
-  let alldaily_prices = calculateDailyPrices(daily_price_8h);
-  switch (periodType) {
-    case "8":
-      return daily_price_8h
-    case "6":
-      return alldaily_prices.daily_price_6h
-    case "4":
-      return alldaily_prices.daily_price_4h
-    case "2":
-      return alldaily_prices.daily_price_2h
-    default:
-      return 0
-  }
-}
-
 export {
   formatDateDMYtoYMD,
   formatDateYMDtoDMY,
@@ -272,7 +238,5 @@ export {
   validaDataHora,
   formatQuantity,
   formatCurrency,
-  formatAddress,
-  calculateDailyPrices,
-  chooseprice,
+  formatAddress
 };

+ 0 - 17
src/i18n/locales/en.json

@@ -275,23 +275,6 @@
         "address_not_found": "Address not available",
         "currency": "$ {value}"
       },
-      "opportunity_details": {
-        "title": "Service details",
-        "client_default": "Client",
-        "price_label": "Full day (up to 8h)",
-        "distance_text": "It is {distance} away from your registered address.",
-        "distance_default": "0 km",
-        "sob_medida": "Request made",
-        "sob_medida_highlight": "custom",
-        "para": "for",
-        "info_title": "Service information",
-        "description_not_found": "No description provided",
-        "address_not_found": "Address not provided",
-        "hour_not_found": "Time not provided",
-        "button_accept": "accept job",
-        "alert_text": "If your request is accepted by the client, you will receive a notification confirming the booking.",
-        "offers_meal": "On-site meal"
-      },
       "favorites": {
         "title": "Your favorites",
         "view_schedule": "View schedule"

+ 0 - 17
src/i18n/locales/es.json

@@ -275,23 +275,6 @@
         "address_not_found": "Dirección no informada",
         "currency": "R$ {value}"
       },
-      "opportunity_details": {
-        "title": "Detalles del servicio",
-        "client_default": "Cliente",
-        "price_label": "Jornada completa (hasta 8h)",
-        "distance_text": "Está a {distance} de distancia de su dirección registrada.",
-        "distance_default": "0 km",
-        "sob_medida": "Solicitud hecha",
-        "sob_medida_highlight": "a medida",
-        "para": "para",
-        "info_title": "Información del servicio",
-        "description_not_found": "Sin descripción proporcionada",
-        "address_not_found": "Dirección no informada",
-        "hour_not_found": "Horario no informado",
-        "button_accept": "quiero atender",
-        "alert_text": "Si el cliente acepta tu solicitud, recibirás una notificación confirmando el servicio.",
-        "offers_meal": "Comida en el lugar"
-      },
       "favorites": {
         "title": "Tus favoritos",
         "view_schedule": "Ver agenda"

+ 0 - 17
src/i18n/locales/pt.json

@@ -275,23 +275,6 @@
         "address_not_found": "Endereço não informado",
         "currency": "R$ {value}"
       },
-      "opportunity_details": {
-        "title": "Detalhes do serviço",
-        "client_default": "Cliente",
-        "price_label": "Integral (até 8h)",
-        "distance_text": "Há {distance} de distância do seu endereço cadastrado.",
-        "distance_default": "0 km",
-        "sob_medida": "Pedido feito",
-        "sob_medida_highlight": "sob medida",
-        "para": "para",
-        "info_title": "Informações do serviço",
-        "description_not_found": "Sem descrição informada",
-        "address_not_found": "Endereço não informado",
-        "hour_not_found": "Horário não informado",
-        "button_accept": "quero atender",
-        "alert_text": "Se seu pedido for aceito pelo cliente você receberá um aviso confirmando o agendamento.",
-        "offers_meal": "Refeição no local"
-      },
       "favorites": {
         "title": "Seus favoritos",
         "view_schedule": "Ver agenda"

+ 71 - 48
src/pages/opportunities/OpportunitiesPage.vue

@@ -1,7 +1,14 @@
 <template>
   <q-page class="opportunities-page">
     <div class="page-header">
-      <q-btn flat round dense icon="chevron_left" class="back-btn" @click="router.back()" />
+      <q-btn
+        flat
+        round
+        dense
+        icon="chevron_left"
+        class="back-btn"
+        @click="router.back()"
+      />
       <div class="page-title">
         {{ $t('provider.dashboard.opportunities.title') }}
       </div>
@@ -18,60 +25,68 @@
       <q-spinner-dots color="secondary" size="32px" />
     </div>
 
-    <div v-else-if="!opportunities.length" class="text-center q-pa-md text-grey">
+    <div
+      v-else-if="!opportunities.length"
+      class="text-center q-pa-md text-grey"
+    >
       {{ $t('provider.dashboard.opportunities.empty') }}
     </div>
 
     <div v-else class="opportunity-list">
-      <q-card v-for="item in opportunities" :key="item.id" flat class="opportunity-card">
+      <q-card
+        v-for="item in opportunities"
+        :key="item.id"
+        flat
+        class="opportunity-card"
+      >
         <div class="avatar-column">
           <img :src="item.avatar" class="client-avatar" />
           <div class="service-type">
-            {{ item.custom_schedule?.service_type.description }}
+            {{ item.serviceType }}
           </div>
         </div>
 
         <div class="center-content">
           <div class="client-name-row">
-            <span class="client-name">{{ item.client?.user.name }}</span>
+            <span class="client-name">{{ item.clientName }}</span>
 
-            <!-- campo de avaliação -->
             <span class="rating">
               <q-icon name="star" size="11px" />
-              {{ item.client?.average_rating }}
+              {{ item.rating }}
             </span>
           </div>
 
           <div class="service-date">
-            {{ formatDate(item.date) }}
+            {{ item.date }}
           </div>
 
           <div class="service-hour">
-           {{ `Das ${formatHour(item.start_time)} às ${formatHour(item.end_time)}` }}
+            {{ item.hour }}
           </div>
         </div>
 
         <div class="right-content">
           <div class="price">
-            {{ $t('provider.dashboard.opportunities.currency', { value: chooseprice(item.period_type, user.user.provider.daily_price_8h) }) }}
+            {{ $t('provider.dashboard.opportunities.currency', { value: item.price }) }}
           </div>
 
           <div class="service-address">
-            {{ item.custom_schedule?.address_type }}
+            {{ item.address }}
           </div>
 
-          <div class="district">
-             {{ item.address?.district }}
-          </div>
-
-
           <div class="distance">
             {{ $t('provider.dashboard.opportunities.distance_km', { distance: item.distance }) }}
           </div>
 
-          
-
-          <q-btn unelevated rounded no-caps color="secondary" :label="$t('provider.dashboard.opportunities.details')" class="details-btn" @click="goToOpportunityDetails(item)" />
+          <q-btn
+            unelevated
+            rounded
+            no-caps
+            color="secondary"
+            :label="$t('provider.dashboard.opportunities.details')"
+            class="details-btn"
+            @click="goToOpportunityDetails(item)"
+          />
         </div>
       </q-card>
     </div>
@@ -80,45 +95,59 @@
 <script setup>
 import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
-import { chooseprice } from 'src/helpers/utils'
+import { useI18n } from 'vue-i18n'
+
 import { getProviderOpportunities } from 'src/api/opportunities'
 import { userStore } from 'src/stores/user'
 
 const router = useRouter()
+const { t } = useI18n()
 const user = userStore()
 
 const opportunities = ref([])
 const loading = ref(false)
 
+const formatHour = (time) =>
+  time ? time.slice(0, 5).replace(':', 'h') : ''
 
-const goToOpportunityDetails = (item) => {
 
-  const id = item.custom_schedule?.id || item.id
+const normalizeOpportunity = (item) => ({
+  id: item.id,
 
-  router.push({
-    name: 'OpportunityDetailsPage',
-    params: { id },
-    state: { opportunity: item }
-  })
-}
+  avatar: item.client?.user?.photo || '/icons/avatar.svg',
 
+  clientName:
+    item.client?.user?.name ||
+    t('provider.dashboard.opportunities.client_default'),
 
+  rating: item.client?.average_rating || 5.0,
 
-// formatando a data
-const formatDate = (date) => {
-  if (!date) return ''
+  date: new Date(
+    item.custom_schedule?.created_at || item.created_at
+  ).toLocaleDateString(),
 
-  return new Date(date).toLocaleDateString('pt-BR', {
-    weekday: 'long',
-    day: '2-digit',
-    month: '2-digit'
-  })
-}
+  hour: `${t('common.from')} ${formatHour(item.start_time)} ${t('common.to')} ${formatHour(item.end_time)}`,
+
+  address:
+    item.address?.address ||
+    t('provider.dashboard.opportunities.address_not_found'),
 
-// formatando hora para exibir só HH:mm
-const formatHour = (time) => {
-  if (!time) return ''
-  return time.slice(0, 5) 
+  serviceType:
+    item.custom_schedule?.service_type?.descritpion ||
+    t('provider.dashboard.opportunities.client_default'),
+
+  price: Number(
+    item.custom_schedule?.max_price || 0
+  ).toFixed(2),
+
+  distance: 0
+})
+
+const goToOpportunityDetails = (item) => {
+  router.push({
+    name: 'OpportunityDetailsPage',
+    params: { id: item.id }
+  })
 }
 
 const loadOpportunities = async () => {
@@ -129,7 +158,7 @@ const loadOpportunities = async () => {
       user.user.provider.id
     )
 
-    opportunities.value = (response || [])
+    opportunities.value = (response || []).map(normalizeOpportunity)
   } catch (error) {
     console.error('Erro ao buscar oportunidades:', error)
     opportunities.value = []
@@ -238,12 +267,6 @@ onMounted(loadOpportunities)
   color: #2d2d2d;
 }
 
-.district {
-  margin-top: 4px;
-  font-size: 11px;
-  color: #666;
-}
-
 .rating {
   display: flex;
   align-items: center;

+ 153 - 211
src/pages/opportunities/components/OpportunityDetailsPage.vue

@@ -1,183 +1,169 @@
 <template>
-  <q-page v-if="details" class="details-page">
-
+  <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">
-        {{ $t('provider.dashboard.opportunity_details.title') }}
-      </div>
+      <q-btn
+        flat
+        round
+        dense
+        icon="chevron_left"
+        class="back-btn"
+        @click="router.back()"
+      />
+      <div class="page-title">{{ details.title }}</div>
     </div>
 
     <!-- CLIENTE -->
     <div class="client-section">
-      <img :src="details.avatar" class="client-avatar" />
-
-      <div class="client-name">
-        {{ details.schedule?.client_name }}
-        <span class="rating"> {{ details.schedule?.rating }}</span>
-      </div>
-
-      <div class="client-price">{{ $t('provider.dashboard.opportunities.currency', { value: chooseprice(details.schedule?.period_type) }) }}</div>
-
-      <div class="date">  {{ formatDate(details.schedule?.date) }}</div>
-      <div class="hour">{{ formatHour(details.schedule?.start_time ) }} {{ formatHour(details.schedule?.end_time ) }}</div>
-    </div>
-
-    <!-- ENDEREÇO -->
-    <div class="address">
-      <q-icon name="place" size="16px" />
-      {{ details.schedule?.address?.district }}
+      <img :src="AvatarMock" class="client-avatar" />
+      <div class="client-name">{{ details.clientName }}</div>
+      <div class="client-price">{{ details.price }}</div>
     </div>
 
-    <div class="distance">
-      {{ $t('provider.dashboard.opportunity_details.distance_text', { distance: details.schedule?.distance }) }}
+    <!-- INFOS -->
+    <div class="details-info">
+      <div>{{ details.date }}</div>
+      <div>{{ details.hour }}</div>
+      <div>{{ details.address }}</div>
+      <div>{{ details.distance }}</div>
     </div>
 
     <!-- TAGS -->
-    <div v-if="details.tags?.length" class="tags-row">
-      <q-chip v-for="(tag, index) in details.tags" :key="index" outline class="chip">
-        {{ tag }}
+    <div class="tags-row">
+      <q-chip dense color="grey-3">
+        {{ details.tags[0] }}
+      </q-chip>
+
+      <q-chip dense color="grey-3">
+        {{ details.tags[1] }}
       </q-chip>
     </div>
 
-    <!-- INFO -->
-    <div class="service-type">
-  {{ $t('provider.dashboard.opportunity_details.sob_medida') }}
-  <span>
-    {{ $t('provider.dashboard.opportunity_details.sob_medida_highlight') }}
-  </span>
-  <br />
-  {{ $t('provider.dashboard.opportunity_details.para') }}
-  <strong class="highlight-service">
-    {{ details.service_type_name }}
-  </strong>
-</div>
-    
-
-
-    <div class="address-type">
-  <span class="chip-type">
-    {{ (details.address_type) }}
-  </span> 
-  <span v-if="details.offers_meal" class="chip-type">
-    {{ $t('provider.dashboard.opportunity_details.offers_meal') }}
-  </span>    
-</div>
-    
-
-
-     <div v-if="details.description" class="service-type gradient-diarista">
-  {{ $t('provider.dashboard.opportunity_details.info_title') }}
-  </div>
     <!-- DESCRIÇÃO -->
     <div class="description-box">
       {{ details.description }}
     </div>
 
     <!-- BOTÃO -->
-    <q-btn unelevated rounded no-caps class="accept-btn" :label="$t('provider.dashboard.opportunity_details.button_accept')" @click="goToProposalFlow" />
+    <q-btn
+      unelevated
+      rounded
+      no-caps
+      color="secondary"
+      :label="details.buttonLabel"
+      class="full-width q-mt-md"
+      @click="goToProposalFlow"
+    />
 
     <!-- ALERTA -->
-    <div class="alert-box">
-      <q-icon name="warning" size="18px" class="alert-icon" />
-      <span class="alert-text">
-        {{ $t('provider.dashboard.opportunity_details.alert_text') }}
-      </span>
-    </div>
-
+    <q-card flat class="bottom-alert">
+      {{ details.alertText }}
+    </q-card>
   </q-page>
 </template>
 
 <script setup>
 import { ref, onMounted } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
-import { userStore } from 'src/stores/user'
-import { getOpportunityById, proposalOpportunity } from 'src/api/opportunities'
 
+import AvatarMock from 'src/assets/foto_diarista_login.svg'
+import { getOpportunityById } from 'src/api/opportunities'
 
+// router
 const router = useRouter()
 const route = useRoute()
 
-const user = userStore()
+// state
 const details = ref(null)
-const loading = ref(true)
-
-const chooseprice = (periodType) => {
-  switch (periodType) {
-    case "8":
-      return user.user.provider_daily_price_8h
-    case "6":
-      return user.user.provider_daily_price_6h
-    case "4":
-      return user.user.provider_daily_price_4h
-    case "2":
-      return user.user.provider_daily_price_2h
-    default:
-      return 0
-  }
-}
+const loading = ref(false)
 
-// formatando a data
-const formatDate = (date) => {
-  if (!date) return ''
+// params
+const opportunityId = route.params.id
 
-  const [day, month, year] = date.split('/')
+// helpers
+const formatHour = (time) =>
+  time ? time.slice(0, 5).replace(':', 'h') : ''
 
-  const parsedDate = new Date(`${year}-${month}-${day}`)
+// normalize (PADRÃO EMPRESA)
+const normalizeDetails = (item) => ({
+  title: 'Detalhes do serviço',
 
-  const formatted = parsedDate.toLocaleDateString('pt-BR', {
-    weekday: 'long',
-    day: '2-digit',
-    month: '2-digit'
-  })
+  avatar: item.client?.user?.photo || AvatarMock,
 
-  return formatted.charAt(0).toUpperCase() + formatted.slice(1)
-}
+  clientName:
+    item.client?.user?.name || 'Cliente',
 
-// formatando hora para exibir só HH:mm
-const formatHour = (time) => {
-  if (!time) return ''
-  return time.slice(0, 5) 
-}
+  price: `R$${Number(
+    item.custom_schedule?.max_price || 0
+  ).toFixed(2)}`,
+
+  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',
+
+  distance: '0 km',
+
+  tags: [
+    item.custom_schedule?.service_type?.description,
+    item.custom_schedule?.offers_meal
+      ? 'Refeição no local'
+      : null
+  ].filter(Boolean),
+
+  description:
+    item.custom_schedule?.description || '',
+
+  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.'
+})
+
+// load
+const loadDetails = async () => {
+  loading.value = true
 
-onMounted(async () => {
   try {
-    const id = route.params.id
-    const response = await getOpportunityById(id)
-
-    if (response) {
-      details.value = response
-    } else {
-      console.warn('Nenhum dado retornado')
-    }
+    const response = await getOpportunityById(opportunityId)
+
+    console.log('DETAILS RESPONSE:', response)
+
+    details.value = normalizeDetails(response)
   } catch (error) {
     console.error('Erro ao carregar detalhes:', error)
+    details.value = null
   } finally {
     loading.value = false
   }
-})
-
-const goToProposalFlow = async () => {
+}
 
-  await proposalOpportunity(details.value.schedule_id, user.user.provider.id)
-  router.push({ name: 'DashboardPage' })
+// actions
+const goToProposalFlow = () => {
+  console.log('Ir para proposta', details.value)
 }
+
+// lifecycle
+onMounted(loadDetails)
+
 </script>
 
 <style scoped lang="scss">
 .details-page {
   padding: 16px;
-  background: #f4f5f7;
+  background: #f7f7fb;
   min-height: 100vh;
 }
 
-/* HEADER */
 .page-header {
   display: flex;
   justify-content: center;
   position: relative;
-  margin-bottom: 16px;
+  margin-bottom: 24px;
 }
 
 .back-btn {
@@ -187,152 +173,108 @@ const goToProposalFlow = async () => {
 }
 
 .page-title {
-  font-size: 15px;
-  font-weight: 600;
+  font-size: 16px;
+  font-weight: 700;
   color: #7c5cff;
 }
 
-/* CLIENTE */
 .client-section {
   text-align: center;
+  margin-top: 8px;
 }
 
 .client-avatar {
-  width: 88px;
-  height: 88px;
+  width: 84px;
+  height: 84px;
   border-radius: 50%;
   object-fit: cover;
 }
 
 .client-name {
-  margin-top: 6px;
-  font-size: 14px;
+  margin-top: 8px;
+  font-size: 18px;
+  font-weight: 500;
   color: #666;
 }
 
-.rating {
-  color: #ffb800;
-  font-size: 12px;
-  margin-left: 4px;
-}
-
 .client-price {
   margin-top: 8px;
-  font-size: 24px;
-  font-weight: 700;
   color: #7c5cff;
+  font-size: 32px;
+  font-weight: 700;
 }
 
-/* DATA */
-.date {
-  margin-top: 6px;
-  font-size: 12px;
-  font-weight: 600;
-  color: #555;
+.details-info {
+  margin-top: 12px;
+  text-align: center;
+  font-size: 13px;
+  line-height: 1.6;
+  color: #666;
 }
 
-.hour {
+.distance-info {
+  margin-top: 12px;
+  text-align: center;
   font-size: 12px;
-  color: #777;
-}
-
-.highlight-service{
-  color: #7c5cff;
-  font-weight: 600;
+  color: #999;
 }
 
-/* ENDEREÇO */
-.address {
-  margin-top: 12px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  gap: 4px;
-  color: #7c5cff;
+.service-highlight {
+  margin-top: 16px;
+  text-align: center;
   font-size: 13px;
-  font-weight: 600;
+  color: #666;
 }
 
-.distance {
-  text-align: center;
-  font-size: 11px;
-  color: #999;
-  margin-top: 4px;
+.highlight-text {
+  color: #7c5cff;
+  font-weight: 700;
 }
 
-/* ADDRESS TYPE (CHIPS) */
-.address-type {
+.tags-row {
   display: flex;
   justify-content: center;
-  gap: 10px;
-  margin-top: 10px;
+  gap: 8px;
+  margin: 18px 0;
 }
 
-.chip-type {
-  border: 1.5px solid #7c5cff;
+.tags-row .q-chip {
+  border: 1px solid #7c5cff;
   color: #7c5cff;
-  padding: 6px 14px;
-  border-radius: 999px;
-  font-size: 12px;
-  font-weight: 600;
   background: white;
-  text-transform: lowercase;
-}
-
-/* TEXTO SOB MEDIDA */
-.service-type {
-  text-align: center;
   font-size: 12px;
-  margin-top: 12px;
-  color: #666;
 }
 
-.service-type span {
-  color: #7c5cff;
-  font-weight: 600;
-}
-
-/* INFO */
 .info-title {
   text-align: center;
-  font-weight: 700;
   color: #7c5cff;
-  margin-top: 14px;
-  font-size: 13px;
+  font-size: 18px;
+  font-weight: 700;
+  margin-bottom: 12px;
 }
 
-/* DESCRIÇÃO */
 .description-box {
   text-align: center;
-  font-size: 12px;
+  font-size: 13px;
+  line-height: 1.6;
   color: #666;
-  margin: 10px 0 20px;
-  line-height: 1.4;
 }
 
-/* BOTÃO */
-.accept-btn {
-  width: 100%;
+.full-width {
+  margin-top: 20px;
   height: 48px;
-  border-radius: 25px;
-  background: linear-gradient(90deg, #7c5cff, #9f7aea);
-  color: white;
-  font-weight: 600;
-  font-size: 14px;
+  font-size: 16px;
+  background: #8f6dfc !important;
 }
 
-/* ALERTA */
-.alert-box {
-  margin-top: 12px;
-  background: #e9f0ff;
-  padding: 10px;
-  border-radius: 12px;
-  font-size: 11px;
-  text-align: center;
+.bottom-alert {
+  margin-top: 18px;
+  padding: 14px;
+  border-radius: 14px;
+  background: #dfeeff;
+  font-size: 12px;
+  line-height: 1.5;
   color: #5c6b8a;
-  display: flex;
-  gap: 6px;
-  align-items: center;
-  justify-content: center;
+  text-align: center;
 }
 </style>