Sfoglia il codice sorgente

feat: :sparkles: feat (pedidos e solicitacoes) adicionada listagem de solicitacoes do cliente na aba pedidos

foi adicionada a listagem de solicitacoes do cliente e as propostas recebidas dos prestadores na aba pedidos. Tambem foi feita correcao gerao das datas nos cards de agendamentos

fase:dev | origin:escopo
Gustavo Zanatta 2 settimane fa
parent
commit
369b9779d4

+ 5 - 0
src/api/dashboard.js

@@ -5,6 +5,11 @@ export const dadosDashboard = async () => {
   return data.payload;
 }
 
+export const dadosPedidosCliente = async () => {
+  const { data } = await api.get("/dados-pedidos-cliente");
+  return data.payload;
+}
+
 export const getScheduleClienteDetails = async (id) => {
   const { data } = await api.get(`/dados-dashboard-cliente/schedule/${id}/detalhes`);
   return data.payload;

+ 1 - 19
src/components/dashboard/DashboardNextSchedules.vue

@@ -145,6 +145,7 @@
 
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 
 import {
   formatCurrency,
@@ -196,25 +197,6 @@ const formatScheduleTotal = (item) => {
   );
 };
 
-const formatDayMonth = (iso) => {
-  if (!iso) return "";
-
-  return new Date(iso).toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "2-digit",
-  });
-};
-
-const formatWeekday = (iso) => {
-  if (!iso) return "";
-
-  const d = new Date(iso);
-
-  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
-
-  return w.charAt(0).toUpperCase() + w.slice(1);
-};
-
 onMounted(() => {
   loadPlatformFees().catch(() => {});
 });

+ 2 - 1
src/components/dashboard/DashboardPendingSchedules.vue

@@ -174,6 +174,7 @@
 import { computed, ref } from "vue";
 import { avatarColors } from "src/helpers/avatarColors";
 import { getFirstName } from "src/helpers/utils";
+import { parseLocalDate } from "src/helpers/scheduleDate";
 import { useI18n } from "vue-i18n";
 
 const props = defineProps({
@@ -258,7 +259,7 @@ const displayDate = (item) => {
 
   if (!schedule?.date) return "—";
 
-  const date = new Date(schedule.date);
+  const date = parseLocalDate(schedule.date);
 
   return date.toLocaleDateString("pt-BR", {
     day: "2-digit",

+ 22 - 0
src/components/dashboard/NextSchedulesDetailsDialog.vue

@@ -117,6 +117,19 @@
           </span>
         </div>
 
+        <div
+          v-if="priceRangeLabel"
+          class="detail-row"
+        >
+          <span class="detail-label text-primary q-pr-sm">
+            {{ $t("dashboard_client.pending_schedules.detail_price_range") }}
+          </span>
+
+          <span class="detail-value">
+            {{ priceRangeLabel }}
+          </span>
+        </div>
+
         <div class="detail-row-total font16 fontbold">
           <span class="detail-label text-primary q-pr-sm">
             {{ $t("dashboard_client.pending_schedules.detail_total") }}
@@ -274,6 +287,15 @@ const total = computed(() =>
   ),
 );
 
+const priceRangeLabel = computed(() => {
+  const minPrice = details.value?.min_price;
+  const maxPrice = details.value?.max_price;
+
+  if (minPrice == null || maxPrice == null) return null;
+
+  return `${formatCurrency(minPrice)} - ${formatCurrency(maxPrice)}`;
+});
+
 const openCancelDialog = () => {
   $q.dialog({
     component: ScheduleCancelDialog,

+ 24 - 0
src/helpers/scheduleDate.js

@@ -0,0 +1,24 @@
+const parseLocalDate = (dateStr) => {
+  if (!dateStr) return null;
+
+  const iso = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
+  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
+  const dmy = dateStr.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
+  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
+  return null;
+};
+
+const formatWeekday = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return "";
+  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
+  return w.charAt(0).toUpperCase() + w.slice(1);
+};
+
+const formatDayMonth = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return "";
+  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
+};
+
+export { parseLocalDate, formatWeekday, formatDayMonth };

+ 4 - 1
src/i18n/locales/en.json

@@ -499,7 +499,8 @@
       "btn_reschedule": "reschedule",
       "empty_upcoming": "No scheduled services",
       "empty_completed": "No completed services",
-      "waiting_proposals": "Waiting for proposals"
+      "waiting_proposals": "Waiting for proposals",
+      "proposals_received_count": "{count} proposal(s) received"
     },
     "favorites": {
       "title": "Favorites",
@@ -542,6 +543,7 @@
       "detail_service_fee_pix": "Service fee (Pix):",
       "detail_service_fee_credit_card": "Service fee (card):",
       "detail_total": "Total:",
+      "detail_price_range": "Price range:",
       "detail_package_total": "Package total (Pix):",
       "detail_pix_total": "Pix total:",
       "detail_credit_card_total": "Credit card total:",
@@ -834,6 +836,7 @@
     "other_provider_blocked": "Your order already has time slots with another professional. Finish or empty the current order before booking with another professional.",
     "local_cart_title": "Orders",
     "orders_title": "Orders",
+    "pending_custom_title": "Requests in progress",
     "no_orders": "No orders created.",
     "order_title": "Order #{id}",
     "view_favorites": "view favorites",

+ 4 - 1
src/i18n/locales/es.json

@@ -496,7 +496,8 @@
       "btn_reschedule": "reprogramar",
       "empty_upcoming": "Sin servicios programados",
       "empty_completed": "Sin servicios completados",
-      "waiting_proposals": "Esperando propuestas"
+      "waiting_proposals": "Esperando propuestas",
+      "proposals_received_count": "{count} propuesta(s) recibida(s)"
     },
     "favorites": {
       "title": "Favoritos",
@@ -539,6 +540,7 @@
       "detail_service_fee_pix": "Tarifa de servicio (Pix):",
       "detail_service_fee_credit_card": "Tarifa de servicio (tarjeta):",
       "detail_total": "Total:",
+      "detail_price_range": "Rango de precio:",
       "detail_package_total": "Total del paquete (Pix):",
       "detail_pix_total": "Total en Pix:",
       "detail_credit_card_total": "Total en tarjeta de crédito:",
@@ -831,6 +833,7 @@
     "other_provider_blocked": "Tu pedido ya tiene horarios con otro profesional. Finaliza o vacía el pedido actual antes de reservar con otro profesional.",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
+    "pending_custom_title": "Solicitudes en curso",
     "no_orders": "Ningún pedido creado.",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",

+ 4 - 1
src/i18n/locales/pt.json

@@ -499,7 +499,8 @@
       "btn_reschedule": "reagendar",
       "empty_upcoming": "Nenhum serviço agendado",
       "empty_completed": "Nenhum serviço concluído",
-      "waiting_proposals": "Aguardando propostas"
+      "waiting_proposals": "Aguardando propostas",
+      "proposals_received_count": "{count} proposta(s) recebida(s)"
     },
     "favorites": {
       "title": "Favoritos",
@@ -542,6 +543,7 @@
       "detail_service_fee_pix": "Taxa de serviço (Pix):",
       "detail_service_fee_credit_card": "Taxa de serviço (cartão):",
       "detail_total": "Total:",
+      "detail_price_range": "Faixa de preço:",
       "detail_package_total": "Total do pacote (Pix):",
       "detail_pix_total": "Total no Pix:",
       "detail_credit_card_total": "Total no cartão de crédito:",
@@ -842,6 +844,7 @@
     "other_provider_blocked": "Seu pedido já possui horários com outro profissional. Finalize ou esvazie o pedido atual antes de agendar com outro profissional.",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
+    "pending_custom_title": "Solicitações em andamento",
     "no_orders": "Nenhum pedido criado.",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",

+ 1 - 23
src/pages/agenda/CalendarPage.vue

@@ -305,6 +305,7 @@
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { getClientCalendar } from "src/api/clientCalendar";
 
 import {
@@ -331,29 +332,6 @@ const loading = ref(true);
 const upcomingSchedules = ref([]);
 const completedSchedules = ref([]);
 
-const parseLocalDate = (dateStr) => {
-  if (!dateStr) return null;
-
-  const iso = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
-  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
-  const dmy = dateStr.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
-  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
-  return null;
-};
-
-const formatWeekday = (dateStr) => {
-  const d = parseLocalDate(dateStr);
-  if (!d) return "";
-  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
-  return w.charAt(0).toUpperCase() + w.slice(1);
-};
-
-const formatDayMonth = (dateStr) => {
-  const d = parseLocalDate(dateStr);
-  if (!d) return "";
-  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
-};
-
 const periodLabel = (periodType) => {
   const key = `period_types.${periodType}`;
   const translated = t(key);

+ 13 - 25
src/pages/dashboard/components/DashboardClientProposals.vue

@@ -157,6 +157,7 @@
 <script setup>
 import { refuseProposal } from "src/api/customSchedules";
 import { avatarColors } from "src/helpers/avatarColors";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { useQuasar } from "quasar";
 import SchedulePaymentDialog from "src/components/dashboard/SchedulePaymentDialog.vue";
 
@@ -204,25 +205,6 @@ const $q = useQuasar();
 //   return `${weekday}, ${day}-${month}`;
 // };
 
-const formatDayMonth = (iso) => {
-  if (!iso) return "";
-
-  return new Date(iso).toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "2-digit",
-  });
-};
-
-const formatWeekday = (iso) => {
-  if (!iso) return "";
-
-  const d = new Date(iso);
-
-  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
-
-  return w.charAt(0).toUpperCase() + w.slice(1);
-};
-
 const handleAcceptProposal = (item) => {
   const servicePackage = {
     id: item.id,
@@ -258,17 +240,23 @@ const handleRefuseProposal = async (proposalId) => {
 
 <style scoped lang="scss">
 .scroll-wrapper {
-  overflow-x: auto;
-  scrollbar-width: none;
+  overflow: hidden;
 }
 
 .scroll-track {
   display: flex;
   gap: 12px;
-}
-
-.scroll-wrapper::-webkit-scrollbar {
-  display: none;
+  overflow-x: auto;
+  overscroll-behavior-x: contain;
+  scroll-snap-type: x proximity;
+  scrollbar-width: none;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  &::after {
+    content: "";
+    flex: 0 0 1px;
+  }
 }
 
 .proposal-card {

+ 9 - 1
src/pages/dashboard/components/DashboardPendingCustomSchedules.vue

@@ -1,5 +1,10 @@
 <template>
-  <q-card :flat="false" bordered class="pending-request-card shadow-card">
+  <q-card
+    :flat="false"
+    bordered
+    class="pending-request-card shadow-card cursor-pointer"
+    @click="router.push({ name: 'ScheduleCartPage' })"
+  >
     <div class="request-wrapper">
       <!-- informações -->
       <div class="request-info">
@@ -34,6 +39,9 @@
 </template>
 
 <script setup>
+import { useRouter } from "vue-router";
+
+const router = useRouter();
 </script>
 
 <style scoped lang="scss">

+ 140 - 68
src/pages/orders/ServicePackagePage.vue

@@ -20,74 +20,7 @@
       <div style="width: 36px"></div>
     </div>
 
-    <div
-      v-if="servicePackage.items.length === 0"
-      class="q-px-md q-pt-md"
-    >
-      <q-card
-        :flat="false"
-        class="empty-card bg-surface shadow-card card-border"
-      >
-        <q-card-section class="column items-center text-center q-pa-lg">
-          <q-icon
-            color="grey-5"
-            name="mdi-shopping-outline"
-            size="42px"
-          />
-
-          <span class="font14 fontbold text-text q-mt-sm">
-            {{ $t("service_package.empty_title") }}
-          </span>
-
-          <span class="font12 text-grey-7 q-mt-xs">
-            {{ $t("service_package.empty_message") }}
-          </span>
-
-          <q-btn
-            class="q-mt-md"
-            color="primary"
-            no-caps
-            padding="8px 18px"
-            rounded
-            unelevated
-            :label="$t('service_package.view_favorites')"
-            @click="router.push({ name: 'ProfilePage' })"
-          />
-        </q-card-section>
-      </q-card>
-
-      <q-card
-        v-if="servicePackage.lastCreatedGroup"
-        :flat="false"
-        class="created-card bg-surface shadow-card card-border q-mt-md"
-      >
-        <q-card-section class="q-pa-md">
-          <div class="row items-center no-wrap">
-            <q-icon
-              color="positive"
-              name="mdi-check-circle-outline"
-              size="24px"
-            />
-
-            <div class="column q-ml-sm">
-              <span class="font14 fontbold text-text">
-                {{ $t("service_package.last_request_title") }}
-              </span>
-
-              <span class="font12 text-grey-7">
-                {{
-                  $t("service_package.created_count", {
-                    count: servicePackage.lastCreatedGroup.schedule_ids.length,
-                  })
-                }}
-              </span>
-            </div>
-          </div>
-        </q-card-section>
-      </q-card>
-    </div>
-
-    <template v-else>
+    <template v-if="servicePackage.items.length > 0">
       <div class="q-px-md q-pt-md column q-gutter-y-sm">
         <div class="service-package-discount-banner row items-center no-wrap q-pa-sm">
           <span class="font12 fontbold text-positive q-ml-sm">
@@ -300,13 +233,106 @@
         </q-card>
       </div>
     </template>
+
+    <DashboardClientProposals
+      v-if="schedulesProposals.length > 0"
+      :data="schedulesProposals"
+      class="q-pt-md"
+      @refresh-data="loadCustomRequests"
+    />
+
+    <div
+      v-if="pendingCustomRequests.length > 0"
+      class="q-px-md q-pt-md column q-gutter-y-sm"
+    >
+      <div class="section-title gradient-diarista q-mb-sm font16 fontbold">
+        {{ $t("service_package.pending_custom_title") }}
+      </div>
+
+      <CustomScheduleRequestCard
+        v-for="entry in pendingCustomRequests"
+        :key="entry.schedule.id"
+        :proposals-count="entry.proposalsCount"
+        :schedule="entry.schedule"
+      />
+    </div>
+
+    <div
+      v-if="servicePackage.items.length === 0 && !hasPendingCustomRequests"
+      class="q-px-md q-pt-md"
+    >
+      <q-card
+        :flat="false"
+        class="empty-card bg-surface shadow-card card-border"
+      >
+        <q-card-section class="column items-center text-center q-pa-lg">
+          <q-icon
+            color="grey-5"
+            name="mdi-shopping-outline"
+            size="42px"
+          />
+
+          <span class="font14 fontbold text-text q-mt-sm">
+            {{ $t("service_package.empty_title") }}
+          </span>
+
+          <span class="font12 text-grey-7 q-mt-xs">
+            {{ $t("service_package.empty_message") }}
+          </span>
+
+          <q-btn
+            class="q-mt-md"
+            color="primary"
+            no-caps
+            padding="8px 18px"
+            rounded
+            unelevated
+            :label="$t('service_package.view_favorites')"
+            @click="router.push({ name: 'ProfilePage' })"
+          />
+        </q-card-section>
+      </q-card>
+
+      <q-card
+        v-if="servicePackage.lastCreatedGroup"
+        :flat="false"
+        class="created-card bg-surface shadow-card card-border q-mt-md"
+      >
+        <q-card-section class="q-pa-md">
+          <div class="row items-center no-wrap">
+            <q-icon
+              color="positive"
+              name="mdi-check-circle-outline"
+              size="24px"
+            />
+
+            <div class="column q-ml-sm">
+              <span class="font14 fontbold text-text">
+                {{ $t("service_package.last_request_title") }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{
+                  $t("service_package.created_count", {
+                    count: servicePackage.lastCreatedGroup.schedule_ids.length,
+                  })
+                }}
+              </span>
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+    </div>
   </q-page>
 </template>
 
 <script setup>
 import { computed, onMounted, ref } from "vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
+import DashboardClientProposals from "src/pages/dashboard/components/DashboardClientProposals.vue";
+import CustomScheduleRequestCard from "src/pages/orders/components/CustomScheduleRequestCard.vue";
 import { createScheduleServicePackage } from "src/api/servicePackage";
+import { dadosPedidosCliente } from "src/api/dashboard";
 import { getAddresses } from "src/api/address";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { useI18n } from "vue-i18n";
@@ -327,6 +353,42 @@ const store = userStore();
 const addressLoading = ref(false);
 const primaryAddress = ref(null);
 const submitting = ref(false);
+const customSchedulesNoProposals = ref([]);
+const schedulesProposals = ref([]);
+
+const pendingCustomRequests = computed(() => {
+  const groups = new Map();
+
+  for (const schedule of customSchedulesNoProposals.value) {
+    groups.set(schedule.id, { schedule, proposalsCount: 0 });
+  }
+
+  for (const item of schedulesProposals.value) {
+    const existing = groups.get(item.schedule_id);
+
+    if (existing) {
+      existing.proposalsCount += 1;
+
+      continue;
+    }
+
+    groups.set(item.schedule_id, {
+      schedule: {
+        id: item.schedule_id,
+        date: item.date,
+        start_time: item.start_time,
+        end_time: item.end_time,
+        address: item.address,
+        schedule_type: "custom",
+      },
+      proposalsCount: 1,
+    });
+  }
+
+  return [...groups.values()];
+});
+
+const hasPendingCustomRequests = computed(() => pendingCustomRequests.value.length > 0);
 
 const isValidFee = (fee) =>
   fee !== null && fee !== undefined && Number.isFinite(Number(fee));
@@ -606,9 +668,19 @@ const submitServicePackage = async () => {
   }
 };
 
+const loadCustomRequests = async () => {
+  const response = await dadosPedidosCliente();
+
+  if (response) {
+    customSchedulesNoProposals.value = response.customSchedulesNoProposals ?? [];
+    schedulesProposals.value = response.schedulesProposals ?? [];
+  }
+};
+
 onMounted(() => {
   loadPrimaryAddress();
   loadPlatformFees().catch(() => {});
+  loadCustomRequests();
 });
 </script>
 

+ 115 - 0
src/pages/orders/components/CustomScheduleRequestCard.vue

@@ -0,0 +1,115 @@
+<template>
+  <q-card :flat="false" class="request-card bg-surface shadow-card q-mb-sm">
+    <q-card-section class="q-pa-sm">
+      <div class="row no-wrap items-start q-gutter-x-sm">
+        <q-avatar size="44px" class="request-avatar">
+          <q-icon
+            :name="hasProposals ? 'mdi-format-list-bulleted' : 'mdi-clock-outline'"
+            size="20px"
+            color="white"
+          />
+        </q-avatar>
+
+        <div class="col column text-text">
+          <span class="font12 fontbold">
+            {{
+              hasProposals
+                ? $t("dashboard_client.agenda.proposals_received_count", { count: proposalsCount })
+                : $t("dashboard_client.agenda.waiting_proposals")
+            }}
+          </span>
+
+          <div class="row items-center no-wrap">
+            <span class="font10 fontbold">{{ formatWeekday(schedule.date) }}</span>
+
+            <span class="font10 fontmedium">{{ ", " + formatDayMonth(schedule.date) }}</span>
+          </div>
+
+          <span>
+            <span class="font10 fontmedium">
+              {{ $t("dashboard_client.next_schedules.from") }}
+            </span>
+
+            <span class="font10 fontbold q-ml-xs">
+              {{ schedule.start_time?.slice(0, 5) }}
+              {{ $t("dashboard_client.next_schedules.to") }}
+              {{ schedule.end_time?.slice(0, 5) }}
+            </span>
+          </span>
+        </div>
+
+        <div class="col-auto column items-end">
+          <q-chip
+            class="font10"
+            color="warning-bg"
+            dense
+            square
+            text-color="warning"
+            :label="$t('dashboard_client.agenda.status_pending')"
+          />
+        </div>
+      </div>
+
+      <div class="row items-center no-wrap q-mt-xs">
+        <span class="type-label text-primary font10 fontmedium">
+          {{ $t("dashboard_client.agenda.type_custom") }}
+        </span>
+
+        <q-space />
+
+        <q-btn
+          color="primary"
+          no-caps
+          padding="4px 8px"
+          rounded
+          size="sm"
+          :label="$t('dashboard_client.agenda.btn_view_details')"
+          @click="openDetailsDialog"
+        />
+      </div>
+    </q-card-section>
+  </q-card>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { useQuasar } from "quasar";
+import NextSchedulesDetailsDialog from "src/components/dashboard/NextSchedulesDetailsDialog.vue";
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true,
+  },
+  proposalsCount: {
+    type: Number,
+    default: 0,
+  },
+});
+
+const $q = useQuasar();
+
+const hasProposals = computed(() => props.proposalsCount > 0);
+
+const openDetailsDialog = () => {
+  $q.dialog({
+    component: NextSchedulesDetailsDialog,
+    componentProps: { schedule: props.schedule },
+  });
+};
+</script>
+
+<style scoped lang="scss">
+.request-card {
+  border-radius: 8px;
+}
+
+.request-avatar {
+  background: linear-gradient(135deg, #8b5cf6, #ec4899);
+}
+
+.type-label {
+  line-height: 1.2;
+}
+</style>