Forráskód Böngészése

refactor: exibindo schedules agrupados de acordo com service_packages e ajustando interfaces

Gustavo Mantovani 1 hónapja
szülő
commit
932305c72c

+ 1 - 0
src/api/address.js

@@ -28,6 +28,7 @@ export const deleteAddress = async (id) => {
 export const searchAddressByCEP = async (cep) => {
   try {
     const responseCep = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
+
     const data = await responseCep.json();
 
     if (data.erro) {

+ 5 - 0
src/api/review.js

@@ -5,18 +5,23 @@ export const createReview = async (reviewData) => {
 
   if (photos && photos.length > 0) {
     const form = new FormData()
+
     Object.entries(rest).forEach(([key, val]) => {
       if (val === null || val === undefined) return
       if (Array.isArray(val)) val.forEach(v => form.append(`${key}[]`, v))
       else if (typeof val === 'boolean') form.append(key, val ? '1' : '0')
       else form.append(key, val)
     })
+
     photos.forEach(file => form.append('photos[]', file))
+
     const { data } = await api.post('/reviews', form)
+
     return data.payload
   }
 
   const { data } = await api.post('/reviews', rest)
+
   return data.payload
 }
 

+ 17 - 0
src/api/schedule.js

@@ -14,3 +14,20 @@ export const verifyScheduleCode = async (scheduleId, code) => {
   const response = await api.post(`/custom-schedule-verify-code/${scheduleId}`, { code })
   return response.data
 }
+
+export const acceptServicePackage = async (packageId) => {
+  const { data } = await api.patch(`/service-packages/${packageId}/accept`)
+  return data.payload
+}
+
+export const rejectServicePackage = async (packageId) => {
+  const { data } = await api.patch(`/service-packages/${packageId}/refuse`)
+  return data.payload
+}
+
+export const payServicePackage = async (packageId, paymentMethod = 'pix') => {
+  const { data } = await api.post(`/payment/service-package/${packageId}/pay`, {
+    payment_method: paymentMethod,
+  })
+  return data.payload
+}

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

@@ -24,12 +24,16 @@
       v-else
       class="scroll-wrapper"
     >
-      <div class="scroll-track">
+      <div
+        class="scroll-track"
+        :class="{ 'scroll-track--single': data.length === 1 }"
+      >
         <q-card
           v-for="item in data"
           :key="item.id"
           :flat="false"
           class="schedule-card card-border shadow-card bg-surface"
+          :class="{ 'schedule-card--single': data.length === 1 }"
         >
           <q-card-section class="q-pa-sm">
             <div class="row no-wrap items-center q-gutter-x-sm">
@@ -275,6 +279,10 @@ const formatWeekday = (iso) => {
   border-radius: 12px;
   min-width: 85vw;
 }
+.schedule-card--single {
+  min-width: 100%;
+  width: 100%;
+}
 .text-name {
   color: #3a3a4a;
 }

+ 278 - 74
src/components/dashboard/DashboardSolicitations.vue

@@ -1,3 +1,4 @@
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
 <template>
   <div class="q-mb-md q-mx-md">
     <div class="items-center justify-between q-mb-sm row">
@@ -15,106 +16,133 @@
       />
     </div>
 
-    <div class="scroll-wrapper">
-      <div class="no-wrap q-gutter-x-md row scroll-track">
+    <div
+      class="scroll-wrapper"
+      :class="{ 'scroll-wrapper--single': groupedSolicitations.length === 1 }"
+    >
+      <div
+        class="no-wrap row scroll-track"
+        :class="{
+          'q-gutter-x-md': groupedSolicitations.length > 1,
+          'scroll-track--single': groupedSolicitations.length === 1,
+        }"
+      >
         <q-card
-          v-for="item in data"
+          v-for="item in groupedSolicitations"
           :key="item.id"
+          :class="{
+            'solicitation-card--single': groupedSolicitations.length === 1,
+            'solicitation-card--multiple': groupedSolicitations.length > 1,
+          }"
           :flat="false"
           class="bg-surface card-border q-ma-md shadow-card solicitation-card"
         >
-          <q-card-section class="q-pa-sm">
-            <div class="items-center no-wrap q-gutter-x-sm row text-text">
-              <q-avatar size="48px">
-                <img
-                  v-if="item.customer_photo"
-                  :src="item.customer_photo"
-                  style="object-fit: cover"
-                />
+          <q-card-section class="q-pa-sm solicitation-card-section">
+            <div class="items-start no-wrap q-gutter-x-sm row text-text">
+              <div class="solicitation-avatar-column">
+                <!--
+                <div class="font9 fontmedium text-text solicitation-time">
+                  {{ formatTimeSinceRequest(item.time_since_request) }}
+                </div>
+                !-->
+
+                <q-avatar size="48px">
+                  <img
+                    v-if="item.customer_photo"
+                    :src="item.customer_photo"
+                    style="object-fit: cover"
+                  />
+
+                  <span
+                    v-else
+                    class="flex flex-center full-height full-width"
+                    style="border-radius: 50%"
+                    :style="avatarColors[item.id % avatarColors.length]"
+                  >
+                    {{
+                      getFirstName(item.client_name)
+                        ?.slice(0, 2)
+                        .toUpperCase() ?? "??"
+                    }}
+                  </span>
+                </q-avatar>
 
-                <span
-                  v-else
-                  class="flex flex-center full-height full-width"
-                  style="border-radius: 50%"
-                  :style="avatarColors[item.id % avatarColors.length]"
-                >
-                  {{
-                    getFirstName(item.client_name)
-                      ?.slice(0, 2)
-                      .toUpperCase() ?? "??"
-                  }}
-                </span>
-              </q-avatar>
+              </div>
 
-              <div class="column flex-1">
-                <div class="items-center q-gutter-x-xs row">
-                  <span class="ellipsis font9 fontmedium">
-                    {{ getFirstName(item.client_name) || "—" }}
-                  </span>
+              <div class="column flex-1 solicitation-info">
+                <div class="info-line solicitation-top-line no-wrap row">
+                  <div class="items-center no-wrap row info-line__main">
+                    <span class="ellipsis font12 fontbold">
+                      {{ getFirstName(item.client_name) || "—" }}
+                    </span>
 
-                  <div class="items-center row">
                     <q-icon
+                      class="q-ml-xs"
                       color="warning"
                       name="mdi-star"
                       size="12px"
                     />
 
-                    <span class="font9 text-text">
+                    <span class="font9 text-text no-wrap">
                       {{ item.average_rating }}
                     </span>
                   </div>
+
+                  <div class="column items-end no-wrap q-ml-xs solicitation-price">
+                    <span class="font12 fontbold no-wrap">
+                      {{ formatCurrency(packageTotal(item)) }}
+                    </span>
+                  </div>
                 </div>
 
-                <div class="font9 items-center no-wrap row">
-                  <span class="fontbold">
-                    {{ formatWeekday(item.date) }}
-                  </span>
+                <div class="schedule-list font9">
+                  <div
+                    v-if="item.package_items.length > 1"
+                    class="package-summary font8"
+                  >
+                    {{ $t("provider.dashboard.solicitations.package_count", { count: item.package_items.length }) }}
+                  </div>
 
-                  <span>{{ `, ${formatDayMonth(item.date)}` }}</span>
-                </div>
+                  <div
+                    v-for="scheduleItem in item.package_items"
+                    :key="scheduleItem.id"
+                    class="solicitation-date-time"
+                  >
+                    <span class="schedule-weekday">
+                      {{ formatWeekday(scheduleItem.date) }}
+                    </span>
 
-                <div class="font9">
-                  {{ $t("common.from") }}
+                    <span class="fontbold schedule-day-month">
+                      {{ formatDayMonth(scheduleItem.date) }}
+                    </span>
 
-                  <span class="fontbold">
-                    {{ item.start_time?.slice(0, 5) }}
-                    {{ $t("common.to") }}
-                    {{ item.end_time?.slice(0, 5) }}
-                  </span>
+                    <span class="fontbold schedule-time">
+                      {{ $t("common.from") }}
+                      {{ scheduleItem.start_time?.slice(0, 5) }}
+                      {{ $t("common.to") }}
+                      {{ scheduleItem.end_time?.slice(0, 5) }}
+                    </span>
+                  </div>
                 </div>
               </div>
+            </div>
 
-              <div class="column items-end text-text">
-                <div class="font12 fontbold">
-                  {{ formatCurrency(item.total_amount) }}
-                </div>
-
-                <div class="font9">
-                  {{ t(labelsPeriodTypes.find((label) => label.value == item.period_type)?.label) }}
-                </div>
+            <div class="font9 info-line solicitation-address-line row text-text">
+              {{ formatTimeSinceRequest(item.time_since_request) }} -
 
-                <div class="font9 fontbold">
-                  {{ item.address?.district || "N/A" }}
-                </div>
+              <span class="info-line__main solicitation-address">
+                {{ item.address?.district || "N/A" }}
 
-                <div class="font9">
-                  <span class="q-pr-xs">
-                    {{ item.distance_km ?? 0 }}
-                  </span>
-
-                  {{ $t("common.km") }}
-                </div>
-              </div>
+                <span class="fontbold no-wrap solicitation-distance">
+                  {{ item.distance_km ?? 0 }} {{ $t("common.km") }}
+                </span>
+              </span>
             </div>
 
-            <div class="items-center q-gutter-x-sm q-mt-sm row">
-              <div>
-                <div class="font9 fontmedium text-text">
-                  {{ item.time_since_request }}
-                </div>
-
+            <div class="items-center q-gutter-x-sm q-mt-sm row solicitation-actions">
+              <div class="solicitation-details">
                 <q-btn
-                  class="col-auto q-px-none"
+                  class="col-auto q-px-none solicitation-details-btn"
                   color="primary"
                   flat
                   no-caps
@@ -155,11 +183,11 @@
 </template>
 
 <script setup>
+import { computed } from "vue";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
-import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { useI18n } from "vue-i18n";
 
-defineProps({
+const props = defineProps({
   data: {
     default: () => [],
     type: Array,
@@ -168,6 +196,31 @@ defineProps({
 
 const emit = defineEmits(["accept", "reject", "view-details"]);
 
+const groupedSolicitations = computed(() => {
+  const groups = new Map();
+
+  props.data.forEach((item) => {
+    const key = item.service_package_id
+      ? `package:${item.service_package_id}`
+      : `schedule:${item.id}`;
+    const group = groups.get(key);
+
+    if (group) {
+      group.package_items.push(item);
+    } else {
+      groups.set(key, { ...item, package_items: [item] });
+    }
+  });
+
+  return [...groups.values()];
+});
+
+const packageTotal = (item) =>
+  item.package_items.reduce(
+    (total, packageItem) => total + (Number(packageItem.total_amount) || 0),
+    0,
+  );
+
 const { t } = useI18n();
 
 const avatarColors = [
@@ -207,20 +260,72 @@ const formatWeekday = (iso) => {
 
   return weekday.charAt(0).toUpperCase() + weekday.slice(1);
 };
+
+const formatTimeSinceRequest = (value) => {
+  if (!value) return "";
+
+  const normalizedValue = String(value).trim().replace(/\s+/g, " ");
+
+  if (/^just now$/i.test(normalizedValue)) {
+    return t("provider.dashboard.solicitations.time_ago.just_now");
+  }
+
+  const match = normalizedValue.match(
+    /^(-?\d+|an?|one)\s+(second|minute|hour|day|week|month|year)s?(?:\s+ago)?$/i,
+  );
+
+  if (!match) return normalizedValue;
+
+  const count = /^-?\d+$/.test(match[1]) ? Math.abs(Number(match[1])) : 1;
+
+  return t(
+    `provider.dashboard.solicitations.time_ago.${match[2].toLowerCase()}`,
+    count,
+  );
+};
 </script>
 
 <style lang="scss" scoped>
 .scroll-wrapper {
   overflow-x: auto;
-  padding-bottom: 8px;
+  padding: 0 0 8px 8px;
 
   &::-webkit-scrollbar {
     display: none;
   }
 }
 
+.scroll-wrapper--single {
+  overflow-x: visible;
+  padding: 0 0 8px 0;
+}
+
+.scroll-track {
+  align-items: flex-start;
+}
+
 .solicitation-card {
-  min-width: 85vw;
+  box-shadow: -4px 2px 12px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.06) !important;
+  overflow: visible;
+}
+
+.solicitation-card--single {
+  flex: 0 0 100%;
+  margin: 0 !important;
+  width: 100%;
+}
+
+.solicitation-card--multiple {
+  flex: 0 0 82vw;
+  margin-left: 8px !important;
+  margin-right: 8px !important;
+  max-width: 82vw;
+  min-width: 82vw;
+  width: 82vw;
+}
+
+.scroll-track--single {
+  width: 100%;
 }
 
 .btn-action {
@@ -231,4 +336,103 @@ const formatWeekday = (iso) => {
   flex: 1;
   min-width: 0;
 }
+
+.solicitation-avatar-column {
+  flex: 0 0 48px;
+  text-align: center;
+}
+
+.solicitation-time {
+  margin-bottom: 6px;
+  white-space: nowrap;
+}
+
+.solicitation-info {
+  align-self: stretch;
+  justify-content: space-around;
+  gap: 2px;
+}
+
+.info-line {
+  align-items: baseline;
+  justify-content: space-between;
+  min-width: 0;
+}
+
+.solicitation-top-line {
+  align-items: flex-start;
+}
+
+.info-line__main {
+  min-width: 0;
+}
+
+.solicitation-address {
+  max-width: 100%;
+  overflow-wrap: anywhere;
+  white-space: normal;
+}
+
+.solicitation-distance::before {
+  content: " - ";
+}
+
+.schedule-list {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  margin-bottom: 0;
+}
+
+.solicitation-date-time {
+  align-items: center;
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr);
+  border-bottom: 1px dotted #d5d5df;
+  padding: 1px 0;
+  width: 100%;
+  line-height: 1.35;
+}
+
+.schedule-weekday {
+  text-align: left;
+}
+
+.schedule-day-month {
+  text-align: center;
+}
+
+.schedule-time {
+  min-width: 0;
+  text-align: right;
+  white-space: nowrap;
+}
+
+.solicitation-address-line {
+  border-bottom: 1px solid #dcdce5;
+  display: block;
+  margin: 8px -8px 0;
+  padding: 8px 8px 6px;
+  text-align: center;
+
+  &::before {
+    color: var(--q-primary);
+    content: "●";
+    margin-right: 4px;
+  }
+}
+
+.solicitation-details {
+  align-items: center;
+  display: flex;
+}
+
+.solicitation-details-btn {
+  margin-top: 0;
+}
+
+.package-summary {
+  color: #666;
+  line-height: 1.35;
+}
 </style>

+ 25 - 3
src/components/dashboard/DashboardSummaryInfos.vue

@@ -21,11 +21,28 @@
         </div>
       </div>
 
-      <div class="row items-center justify-between no-wrap q-mt-xs">
-        <div class="text-text col font8 fontmedium ellipsis">
+      <div
+        class="row justify-between q-mt-xs"
+        :class="expanded ? 'items-start' : 'items-center no-wrap'"
+      >
+        <div
+          class="text-text col font8 fontmedium"
+          :class="{ ellipsis: !expanded }"
+        >
           {{ formatAddress(data.address) }}
         </div>
-        <q-icon name="mdi-chevron-down" color="secondary" size="18px" class="col-auto" />
+
+        <q-btn
+          :aria-label="$t(expanded ? 'common.collapse' : 'common.expand')"
+          class="col-auto expand-button"
+          color="secondary"
+          dense
+          flat
+          :icon="expanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
+          round
+          size="sm"
+          @click="expanded = !expanded"
+        />
       </div>
     </q-card-section>
   </q-card>
@@ -33,8 +50,12 @@
 
 <script setup>
 import { formatAddress, getFirstName } from 'src/helpers/utils';
+import { ref } from 'vue';
+
 defineProps({ data: { type: Object, default: () => null } });
 
+const expanded = ref(false);
+
 const avatarStyle = {
   background: 'linear-gradient(135deg, #ffd7e8 0%, #ff9acc 100%)',
   color: '#7a154f',
@@ -51,4 +72,5 @@ const avatarStyle = {
 .summary-count {
   color: #3a3a4a;
 }
+.expand-button { margin: -6px -6px -6px 2px; }
 </style>

+ 18 - 6
src/components/dashboard/DashboardTodayServices.vue

@@ -1,15 +1,19 @@
 <template>
   <div v-if="props.data.length > 0" class="q-mx-md q-mb-md">
     <div class="scroll-wrapper">
-      <div class="scroll-track">
+      <div
+        class="scroll-track"
+        :class="{ 'scroll-track--single': props.data.length === 1 }"
+      >
         <q-card
           v-for="item in props.data"
           :key="item.id"
           class="today-card card-border shadow-card bg-surface"
+          :class="{ 'today-card--single': props.data.length === 1 }"
           :flat="false"
         >
           <q-card-section class="q-pa-md">
-            <div class="row no-wrap items-start q-mb-xs">
+            <div class="row no-wrap items-center q-mb-xs">
               <div class="col-8 row">
                 <q-avatar size="40px" class="flex-shrink-0 q-mr-sm">
                   <img v-if="item.customer_photo" :src="item.customer_photo" style="object-fit:cover" />
@@ -44,8 +48,8 @@
                 </div>
               </div>
 
-              <div class="flex-shrink-0 row items-right justify-end col-4">
-                <div v-if="cardState(item) === 'awaiting_code'" class="col-12 row items-center justify-center q-pb-sm q-px-sm">
+                <div class="flex-shrink-0 row items-right justify-end col-4">
+                <div v-if="cardState(item) === 'awaiting_code'" class="col-12 row items-center justify-end q-pb-sm">
                   <div class="hint-text text-text text-center font9 q-mb-xs col-12">
                     {{ $t('provider.dashboard.today_services.code_hint') }}
                   </div>
@@ -151,7 +155,7 @@ import { reactive } from 'vue'
 import { useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
-import { verifyScheduleCode } from 'src/api/schedule'
+import { payServicePackage, verifyScheduleCode } from 'src/api/schedule'
 import { formatAddress, getFirstName } from 'src/helpers/utils';
 
 const props = defineProps({ data: { type: Array, default: () => [] } })
@@ -209,6 +213,10 @@ const submitCode = async (item) => {
   const code = codeInputs[id].join('')
   if (code.length < 4) return
   try {
+    if (item.service_package_id) {
+      await payServicePackage(item.service_package_id)
+    }
+
     const response = await verifyScheduleCode(id, code)
     console.log(response)
     if (response?.payload) {
@@ -295,6 +303,10 @@ const copyAddress = (address) => {
   scroll-snap-align: start;
   border-radius: 12px;
 }
+.today-card--single {
+  min-width: 100%;
+  width: 100%;
+}
 
 .hint-text {
   max-width: 100px;
@@ -311,7 +323,7 @@ const copyAddress = (address) => {
   padding: 4px 8px; 
   width: 110px; 
   height: 28px; 
-  margin: 0 auto; 
+  margin: 0 0 0 auto; 
 }
 
 .code-input-box {

+ 98 - 91
src/components/dashboard/SolicitationDetailsDialog.vue

@@ -204,35 +204,37 @@
           </div>
 
           <div class="text-price text-primary font16 fontbold q-mt-xs">
-            {{ formatCurrency(solicitation.total_amount) }}
+            {{ formatCurrency(packageTotal) }}
           </div>
 
-          <div class="text-grey-6 font12">
-            {{ periodLabel }}
+          <div
+            v-if="packageItems.length > 1"
+            class="text-grey-6 font12"
+          >
+            {{ packageCountLabel }}
           </div>
         </q-card-section>
 
-        <q-card-section
-          class="column items-center q-pt-none q-pb-sm text-text font14"
-        >
-          <div class="text-center">
-            <span class="fontbold">
-              {{ weekdayLabel + " ," + dayMonthLabel }}
-            </span>
-          </div>
-
-          <div class="text-center">
-            {{ $t("common.from") }}
-
-            <strong>
-              {{ solicitation.start_time?.slice(0, 5) }}
-            </strong>
-
-            {{ $t("common.to") }}
-
-            <strong>
-              {{ solicitation.end_time?.slice(0, 5) }}
-            </strong>
+        <q-card-section class="q-pt-none q-pb-sm q-px-md text-text">
+          <div
+            v-for="(pkgItem, idx) in packageItems"
+            :key="idx"
+            class="package-item-row"
+          >
+            <div class="row items-center no-wrap justify-between">
+              <div>
+                <span class="fontbold font12">{{ formatItemWeekday(pkgItem.date) }}</span>
+                <span class="font12">{{ ", " + formatItemDayMonth(pkgItem.date) }}</span>
+              </div>
+
+              <span class="fontbold font12">
+                {{ formatCurrency(Number(pkgItem.total_amount) || 0) }}
+              </span>
+            </div>
+
+            <div class="text-grey-6 font12">
+              {{ pkgItem.start_time?.slice(0, 5) }} {{ $t("common.to") }} {{ pkgItem.end_time?.slice(0, 5) }}
+            </div>
           </div>
         </q-card-section>
 
@@ -255,14 +257,15 @@
         </q-card-section>
 
         <q-card-section class="column items-center q-pt-xs q-pb-sm text-text">
-          <div class="row items-center q-gutter-x-xs">
+          <div class="row items-start no-wrap self-start q-gutter-x-xs">
             <q-icon
+              class="q-mt-xs"
               color="primary"
               name="mdi-map-marker"
               size="20px"
             />
 
-            <span class="font16 fontbold">
+            <span class="font16 fontbold no-wrap text-center">
               {{ solicitation.address?.district || "N/A" }}
             </span>
           </div>
@@ -311,7 +314,8 @@
               rounded
               unelevated
               :label="$t('common.accept')"
-              @click="view = 'confirm-accept'"
+              :loading="loading"
+              @click="confirmAccept"
             />
           </div>
 
@@ -342,39 +346,6 @@
         </div>
       </template>
 
-      <template v-else-if="view === 'confirm-accept'">
-        <q-card-section class="column items-center q-pt-xl q-pb-lg text-center">
-          <div class="confirm-title text-secondary fontbold">
-            {{ $t("provider.dashboard.solicitations.confirm_accept") }}
-          </div>
-        </q-card-section>
-
-        <q-card-section class="q-pt-none q-pb-xl">
-          <div class="row justify-center q-gutter-x-md">
-            <q-btn
-              class="btn-action"
-              color="purple-5"
-              no-caps
-              rounded
-              unelevated
-              :label="$t('common.actions.back')"
-              @click="backFromConfirm"
-            />
-
-            <q-btn
-              class="btn-action"
-              color="secondary"
-              no-caps
-              rounded
-              unelevated
-              :label="$t('common.accept')"
-              :loading="loading"
-              @click="confirmAccept"
-            />
-          </div>
-        </q-card-section>
-      </template>
-
       <template v-else-if="view === 'confirm-reject'">
         <q-card-section class="column items-center q-pt-xl q-pb-md text-center">
           <div class="confirm-title text-secondary fontbold">
@@ -422,7 +393,6 @@
 <script setup>
 import { computed, ref } from "vue";
 import { formatCurrency } from "src/helpers/utils";
-import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 
@@ -433,7 +403,7 @@ const props = defineProps({
     default: "details",
     type: String,
     validator: (v) =>
-      ["details", "confirm-accept", "confirm-reject"].includes(v),
+      ["details", "confirm-reject"].includes(v),
   },
   solicitation: {
     required: true,
@@ -460,17 +430,6 @@ const baseAmount = computed(() => {
   return parseAmount(amount);
 });
 
-const dayMonthLabel = computed(() => {
-  const date = parseLocalDate(props.solicitation.date);
-
-  if (!date) return "";
-
-  return date.toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "2-digit",
-  });
-});
-
 const fullDateLabel = computed(() => {
   const date = parseLocalDate(props.solicitation.date);
 
@@ -491,14 +450,31 @@ const paidTotalAmount = computed(() =>
   ),
 );
 
-const periodLabel = computed(() => {
-  const period = labelsPeriodTypes.find(
-    (label) => label.value == props.solicitation.period_type,
-  );
+const packageItems = computed(() => {
+  const items = props.solicitation?.package_items;
+
+  if (Array.isArray(items) && items.length) {
+    return items;
+  }
 
-  return period ? t(period.label) : "";
+  return [props.solicitation];
 });
 
+const packageCountLabel = computed(() =>
+  packageItems.value.length > 1
+    ? t("provider.dashboard.solicitations.package_count", {
+        count: packageItems.value.length,
+      })
+    : "",
+);
+
+const packageTotal = computed(() =>
+  packageItems.value.reduce(
+    (total, item) => total + parseAmount(item.total_amount),
+    0,
+  ),
+);
+
 const serviceFee = computed(() => {
   const fee =
     props.solicitation.platform_fee_amount ??
@@ -540,16 +516,6 @@ const servicesList = computed(() => {
   return services;
 });
 
-const weekdayLabel = computed(() => {
-  const date = parseLocalDate(props.solicitation.date);
-
-  if (!date) return "";
-
-  const weekday = date.toLocaleDateString("pt-BR", { weekday: "long" });
-
-  return weekday.charAt(0).toUpperCase() + weekday.slice(1);
-});
-
 const avatarColors = [
   { background: "#ffd5df", color: "#932e57" },
   { background: "#d7e8ff", color: "#2158a8" },
@@ -568,13 +534,45 @@ const backFromConfirm = () => {
 const confirmAccept = () => {
   loading.value = true;
 
-  onDialogOK({ action: "accept", id: props.solicitation.id });
+  onDialogOK({
+    action: "accept",
+    id: props.solicitation.id,
+    ids: packageItems.value.map((item) => item.id),
+    service_package_id: props.solicitation.service_package_id,
+  });
 };
 
 const confirmReject = () => {
   loading.value = true;
 
-  onDialogOK({ action: "reject", id: props.solicitation.id });
+  onDialogOK({
+    action: "reject",
+    id: props.solicitation.id,
+    ids: packageItems.value.map((item) => item.id),
+    service_package_id: props.solicitation.service_package_id,
+  });
+};
+
+const WEEKDAYS = ["Domingo", "Segunda-feira", "Terça-feira", "Quarta-feira", "Quinta-feira", "Sexta-feira", "Sábado"];
+
+const MONTHS = [
+  "janeiro", "fevereiro", "março", "abril", "maio", "junho",
+  "julho", "agosto", "setembro", "outubro", "novembro", "dezembro",
+];
+
+const formatItemWeekday = (iso) => {
+  const date = parseLocalDate(iso);
+  if (!date) return "";
+  return WEEKDAYS[date.getDay()];
+};
+
+const formatItemDayMonth = (iso) => {
+  const date = parseLocalDate(iso);
+  if (!date) return "";
+  const day = String(date.getDate()).padStart(2, "0");
+  const month = MONTHS[date.getMonth()];
+  const year = date.getFullYear();
+  return `${day} de ${month} de ${year}`;
 };
 
 const formatAddressShort = (address) => {
@@ -620,8 +618,8 @@ const parseAmount = (value) => {
 
   return Number.isFinite(amount) ? amount : 0;
 };
-</script>
 
+</script>
 <style scoped lang="scss">
 .solicitation-dialog-card {
   width: 320px;
@@ -658,4 +656,13 @@ const parseAmount = (value) => {
   align-items: baseline;
   margin-bottom: 4px;
 }
+
+.package-item-row {
+  border-bottom: 1px solid #ececf2;
+  padding: 6px 0;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
 </style>

+ 2 - 2
src/components/profile/ProfileAddressDialog.vue

@@ -30,7 +30,7 @@
         <div style="width: 32px"></div>
       </div>
 
-      <q-card-section class="col q-pa-lg">
+      <q-card-section class="col q-px-md q-py-lg">
         <div class="text-text">
           <div class="gradient-diarista font16 fontbold">
             {{ $t("profile.address.address_subtitle") }}
@@ -471,4 +471,4 @@ onMounted(async () => {
 });
 </script>
 
-<style scoped></style>
+<style scoped></style>

+ 1 - 1
src/components/profile/ProfileBankDataDialog.vue

@@ -240,7 +240,7 @@
             </div>
 
             <q-btn
-              class="full-width"
+              class="full-width q-mt-xl"
               color="primary"
               no-caps
               padding="8px 16px"

+ 11 - 3
src/components/profile/ProfileServiceDataDialog.vue

@@ -94,14 +94,14 @@
                 <q-spinner color="primary" size="28px" />
               </div>
 
-              <div v-else-if="serviceOptions.length" class="row q-col-gutter-sm">
-                <div v-for="service in serviceOptions" :key="service.value" class="col-6">
+              <div v-else-if="serviceOptions.length" class="row q-col-gutter-sm service-options">
+                <div v-for="service in serviceOptions" :key="service.value" class="col-6 service-option-cell">
                   <q-checkbox
                     :model-value="isServiceSelected(service.value)"
                     :label="service.label"
                     :disable="togglingService[service.value]"
                     color="primary"
-                    class="text-text"
+                    class="text-text full-width"
                     keep-color
                     @update:model-value="(checked) => onServiceToggle(service, checked)"
                   />
@@ -337,4 +337,12 @@ onMounted(async () => {
 :deep(.q-field--outlined .q-field__control) {
   border-radius: 10px;
 }
+
+.service-option-cell {
+  text-align: left;
+}
+
+:deep(.service-option-cell .q-checkbox) {
+  justify-content: flex-start;
+}
 </style>

+ 6 - 1
src/css/app.scss

@@ -190,4 +190,9 @@ box-shadow: 1px 4px 4px 0px rgba(0,0,0,0.2);
 .dialog-safe-header {
   padding-top: calc(env(safe-area-inset-top, 24px) + 12px) !important;
   min-height: calc(50px + env(safe-area-inset-top, 24px));
-}
+}
+
+.dialog-safe-header > .q-btn:first-child {
+  margin-left: -16px;
+  margin-right: 16px;
+}

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

@@ -11,6 +11,8 @@
     "details": "View details",
     "accept": "Accept",
     "refuse": "Refuse",
+    "expand": "expand",
+    "collapse": "collapse",
     "from": "From",
     "to": "To",
     "alter": "Alter",
@@ -287,6 +289,17 @@
       },
       "solicitations": {
         "title": "Requests for you",
+        "package_count": "Package with {count} services",
+        "time_ago": {
+          "just_now": "just now",
+          "second": "{count} second ago | {count} seconds ago",
+          "minute": "{count} minute ago | {count} minutes ago",
+          "hour": "{count} hour ago | {count} hours ago",
+          "day": "{count} day ago | {count} days ago",
+          "week": "{count} week ago | {count} weeks ago",
+          "month": "{count} month ago | {count} months ago",
+          "year": "{count} year ago | {count} years ago"
+        },
         "until_8h": "Full day (up to 8h)",
         "until_6h": "Standard (up to 6h)",
         "until_4h": "Medium (up to 4h)",
@@ -300,7 +313,7 @@
         "same_day_warning_before": "Notice: ",
         "same_day_warning_bold": "When receiving more than one request for the same day, check the schedule and distance",
         "same_day_warning_after": " to plan your travel time.",
-        "confirm_accept": "Request accepted!",
+        "confirm_accept": "Do you want to accept this request?",
         "confirm_reject": "Request declined",
         "tip_label": "Tip:",
         "reject_tip": "Declining requests frequently may reduce your visibility."

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

@@ -11,6 +11,8 @@
     "details": "ver detalles",
     "accept": "aceptar",
     "refuse": "rechazar",
+    "expand": "expandir",
+    "collapse": "contraer",
     "from": "De",
     "to": "A",
     "alter": "alterar",
@@ -287,6 +289,17 @@
       },
       "solicitations": {
         "title": "Solicitudes para ti",
+        "package_count": "Paquete con {count} servicios",
+        "time_ago": {
+          "just_now": "ahora mismo",
+          "second": "hace {count} segundo | hace {count} segundos",
+          "minute": "hace {count} minuto | hace {count} minutos",
+          "hour": "hace {count} hora | hace {count} horas",
+          "day": "hace {count} día | hace {count} días",
+          "week": "hace {count} semana | hace {count} semanas",
+          "month": "hace {count} mes | hace {count} meses",
+          "year": "hace {count} año | hace {count} años"
+        },
         "until_8h": "Integral (hasta 8h)",
         "until_6h": "Estándar (Hasta 6h)",
         "until_4h": "Medio Período (Hasta 4h)",
@@ -300,7 +313,7 @@
         "same_day_warning_before": "Aviso: ",
         "same_day_warning_bold": "Al recibir más de una solicitud para el mismo día, verifique el horario y la distancia",
         "same_day_warning_after": " para planificar su tiempo de desplazamiento.",
-        "confirm_accept": "¡Solicitud aceptada!",
+        "confirm_accept": "¿Desea aceptar esta solicitud?",
         "confirm_reject": "Solicitud rechazada",
         "tip_label": "Consejo:",
         "reject_tip": "Rechazar solicitudes con frecuencia puede reducir su visibilidad."

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

@@ -11,6 +11,8 @@
     "details": "ver detalhes",
     "accept": "aceitar",
     "refuse": "recusar",
+    "expand": "expandir",
+    "collapse": "recolher",
     "from": "Das",
     "to": "às",
     "alter": "alterar",
@@ -287,6 +289,17 @@
       },
       "solicitations": {
         "title": "Solicitações para você",
+        "package_count": "Pacote com {count} serviços",
+        "time_ago": {
+          "just_now": "agora mesmo",
+          "second": "há {count} segundo | há {count} segundos",
+          "minute": "há {count} minuto | há {count} minutos",
+          "hour": "há {count} hora | há {count} horas",
+          "day": "há {count} dia | há {count} dias",
+          "week": "há {count} semana | há {count} semanas",
+          "month": "há {count} mês | há {count} meses",
+          "year": "há {count} ano | há {count} anos"
+        },
         "until_8h": "Integral (até 8h)",
         "until_6h": "Padrão (Até 6h)",
         "until_4h": "Meio Período (Até 4h)",
@@ -300,8 +313,8 @@
         "same_day_warning_before": "Aviso: ",
         "same_day_warning_bold": "Ao receber mais de uma solicitação para o mesmo dia, verifique o horário e a distância",
         "same_day_warning_after": " para planejar o seu tempo de deslocamento.",
-        "confirm_accept": "Proposta aceita com sucesso!",
-        "confirm_reject": "Proposta recusada",
+        "confirm_accept": "Deseja aceitar esta proposta?",
+        "confirm_reject": "Deseja recusar esta proposta?",
         "tip_label": "Dica:",
         "reject_tip": "Recusar solicitações com frequência pode reduzir sua visibilidade."
       },

+ 2 - 1
src/pages/LoginPage.vue

@@ -752,7 +752,8 @@ onMounted(() => {
 }
 
 .flow-back-btn {
-  margin-left: -4px;
+  margin-left: -16px;
+  margin-right: 16px;
 }
 
 .flow-logo {

+ 17 - 5
src/pages/dashboard/DashboardPage.vue

@@ -29,7 +29,7 @@
         <DashboardSolicitations
           v-if="solicitations?.length > 0"
           :data="solicitations"
-          @accept="(item) => openDetailsDialog(item, 'confirm-accept')"
+          @accept="(item) => openDetailsDialog(item)"
           @reject="(item) => openDetailsDialog(item, 'confirm-reject')"
           @view-details="(item) => openDetailsDialog(item)"
         />
@@ -48,7 +48,7 @@
 <script setup>
 import { dadosDashboard } from "src/api/dashboard";
 import { onMounted, ref, watch } from "vue";
-import { updateScheduleStatus } from "src/api/schedule";
+import { acceptServicePackage, rejectServicePackage, updateScheduleStatus } from "src/api/schedule";
 import { useQuasar } from "quasar";
 import { useRoute, useRouter } from "vue-router";
 
@@ -79,9 +79,19 @@ const solicitations = ref([]);
 const summaryInfos = ref({});
 const todayServices = ref([]);
 
-const handleScheduleAction = async (id, status) => {
+const handleScheduleAction = async (id, status, ids = [], servicePackageId = null) => {
   try {
-    await updateScheduleStatus(id, status);
+    if (servicePackageId && status === "accepted") {
+      await acceptServicePackage(servicePackageId);
+    } else if (servicePackageId && status === "rejected") {
+      await rejectServicePackage(servicePackageId);
+    } else {
+      const scheduleIds = ids.length ? ids : [id];
+
+      await Promise.all(
+        scheduleIds.map((scheduleId) => updateScheduleStatus(scheduleId, status)),
+      );
+    }
     await loadDashboard();
   } catch (e) {
     console.log(e);
@@ -115,10 +125,12 @@ const openDetailsDialog = (solicitation, initialView = "details") => {
   $q.dialog({
     component: SolicitationDetailsDialog,
     componentProps: { initialView, solicitation },
-  }).onOk(async ({ action, id }) => {
+  }).onOk(async ({ action, id, ids, service_package_id }) => {
     await handleScheduleAction(
       id,
       action === "accept" ? "accepted" : "rejected",
+      ids,
+      service_package_id,
     );
   });
 };

+ 5 - 4
src/pages/payments/PaymentsPage.vue

@@ -99,7 +99,7 @@
       </q-card>
     </div>
 
-    <div class="q-mx-md q-mt-sm">
+    <div class="q-mx-md q-mt-md">
       <q-card
         class="balance-card bg-surface shadow-card"
         :flat="false"
@@ -139,7 +139,7 @@
       </q-card>
     </div>
 
-    <div class="q-mx-md q-mt-sm">
+    <div class="q-mx-md q-mt-md">
       <q-card
         class="balance-card bg-secondary shadow-card"
         :flat="false"
@@ -174,7 +174,7 @@
       </q-card>
     </div>
 
-    <div class="q-mx-md q-mt-md">
+    <div class="q-mx-md q-mt-lg">
       <div class="row items-center no-wrap q-mb-sm">
         <span class="section-title font16 fontbold gradient-diarista">
           {{ $t("provider.payments.services_title") }}
@@ -183,6 +183,7 @@
         <q-space />
 
         <q-btn
+          class="q-pr-none self-center"
           color="primary"
           flat
           icon-right="mdi-chevron-right"
@@ -726,4 +727,4 @@ onMounted(loadData);
   height: auto;
   padding: 2px;
 }
-</style>
+</style>