ソースを参照

style: detalhes da solicitacao + listagem de itens no card do dashboard

Gustavo Mantovani 5 日 前
コミット
87673c1d1f

+ 51 - 9
src/components/dashboard/DashboardSolicitations.vue

@@ -18,21 +18,21 @@
 
     <div
       class="scroll-wrapper"
-      :class="{ 'scroll-wrapper--single': groupedSolicitations.length === 1 }"
+      :class="{ 'scroll-wrapper--single': visibleSolicitations.length === 1 }"
     >
       <div
         class="no-wrap row scroll-track"
         :class="{
-          'q-gutter-x-md': groupedSolicitations.length > 1,
-          'scroll-track--single': groupedSolicitations.length === 1,
+          'q-gutter-x-md': visibleSolicitations.length > 1,
+          'scroll-track--single': visibleSolicitations.length === 1,
         }"
       >
         <q-card
-          v-for="item in groupedSolicitations"
+          v-for="item in visibleSolicitations"
           :key="item.id"
           :class="{
-            'solicitation-card--single': groupedSolicitations.length === 1,
-            'solicitation-card--multiple': groupedSolicitations.length > 1,
+            'solicitation-card--single': visibleSolicitations.length === 1,
+            'solicitation-card--multiple': visibleSolicitations.length > 1,
           }"
           :flat="false"
           class="bg-surface card-border q-ma-md shadow-card solicitation-card"
@@ -159,8 +159,7 @@
               />
 
               <q-btn
-                class="btn-action col-auto"
-                color="secondary"
+                class="btn-action btn-accept"
                 no-caps
                 rounded
                 size="sm"
@@ -212,6 +211,10 @@ const groupedSolicitations = computed(() => {
   return [...groups.values()];
 });
 
+const visibleSolicitations = computed(() =>
+  groupedSolicitations.value.slice(0, 3)
+);
+
 const packageTotal = (item) =>
   item.package_items.reduce(
     (total, packageItem) => total + (Number(packageItem.total_amount) || 0),
@@ -399,7 +402,6 @@ const formatTimeSinceRequest = (value) => {
 }
 
 .solicitation-address-line {
-  border-bottom: 1px solid #dcdce5;
   display: block;
   margin: 8px -8px 0;
   padding: 8px 8px 6px;
@@ -425,4 +427,44 @@ const formatTimeSinceRequest = (value) => {
   color: #666;
   line-height: 1.35;
 }
+.btn-accept {
+  position: relative;
+  overflow: hidden;
+  color: #fff !important;
+
+  background: linear-gradient(
+    90deg,
+    #3b5cff,
+    #7a4dff,
+    #d94cff
+  );
+}
+
+.btn-accept::after {
+  content: "";
+  position: absolute;
+  inset: 0;
+  left: -40%;
+
+  width: 40%;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255,255,255,.35),
+    transparent
+  );
+
+  transform: skewX(-20deg);
+  animation: shine 2s linear infinite;
+}
+
+@keyframes shine {
+  from {
+    left: -40%;
+  }
+
+  to {
+    left: 140%;
+  }
+}
 </style>

+ 131 - 58
src/components/dashboard/DashboardTodayServices.vue

@@ -1,18 +1,21 @@
 <template>
-  <div v-if="props.data.length > 0" class="q-mx-md q-mb-md">
+  <div
+    v-if="visibleItems.length > 0"
+    class="q-mx-md q-mb-md"
+  >
     <div class="scroll-wrapper">
       <div
         class="scroll-track"
-        :class="{ 'scroll-track--single': props.data.length === 1 }"
+        :class="{ 'scroll-track--single': visibleItems.length === 1 }"
       >
         <q-card
-          v-for="item in props.data"
+          v-for="item in visibleItems"
           :key="item.id"
-          :class="{ 'today-card--single': props.data.length === 1 }"
+          :class="{ 'today-card--single': visibleItems.length === 1 }"
           :flat="false"
           class="today-card card-border shadow-card bg-surface"
         >
-          <q-card-section class="column justify-between q-pa-md full-height">
+          <q-card-section class="column justify-between q-pa-sm full-height">
             <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">
@@ -143,7 +146,11 @@
                     @click.stop="emit('rate', item)"
                   >
                     <div class="column items-center">
-                      <q-icon name="mdi-star-outline" size="14px" class="q-mr-xs" />
+                      <q-icon
+                        class="q-mr-xs"
+                        name="mdi-star-outline"
+                        size="14px"
+                      />
   
                       {{ $t('provider.dashboard.today_services.rate_btn') }}
                     </div>
@@ -153,7 +160,7 @@
             </div>
           </div>
 
-          <div class="progress-track q-mb-sm">
+          <div class="progress-track q-mb-xs">
             <div
               class="progress-fill"
               :class="cardState(item) === 'finished' ? 'progress-fill--finished' : ''"
@@ -161,55 +168,57 @@
             />
           </div>
 
-          <div class="row items-center no-wrap">
-            <q-btn
-              class="flex-shrink-0"
-              color="primary"
-              flat no-caps dense
-              size="sm"
-              :label="$t('provider.dashboard.today_services.help')"
-              @click.stop="openHelp"
-            />
-
-            <q-space />
-
-            <template v-if="cardState(item) === 'in_progress'">
+          <template v-if="cardState(item) === 'in_progress'">
+            <div class="row items-center no-wrap">
               <q-icon
                 class="q-mr-xs flex-shrink-0 gradient-diarista"
                 name="mdi-clock-outline"
                 size="13px"
               />
-
-              <span class="text-grey-6 text-right text-no-wrap font11">
+  
+              <span class="text-grey-6 text-right text-no-wrap font9 fontmedium">
                 {{ $t('provider.dashboard.today_services.end_time_label') }}
-
+  
                 <span class="gradient-diarista">
                   {{ calculateTimeUntilEnd(item.end_time) }}
                 </span>
               </span>
- 
-            </template>
-              <template v-else-if="cardState(item) === 'awaiting_code'">
-                <q-icon
-                  class="q-mr-xs flex-shrink-0"
-                  color="grey-6"
-                  name="mdi-map-marker-outline"
-                  size="13px"
-                />
-
-                <span class="text-grey-6 address-text ellipsis text-right font9 fontmedium">
-                  {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
-                </span>
+            </div>
+          </template>
 
-                <q-icon
-                  class="q-ml-xs"
-                  color="primary"
-                  name="mdi-content-copy"
-                  size="16px"
-                  @click="copyAddress(item.address)"
-                />
-              </template>
+          <template v-else-if="cardState(item) === 'awaiting_code'">
+            <div class="address-row">
+              <q-icon
+                class="flex-shrink-0"
+                color="grey-6"
+                name="mdi-map-marker-outline"
+                size="13px"
+              />
+  
+              <span class="text-grey-6 address-text text-right font9 fontmedium">
+                {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
+  
+              </span>
+              <q-icon
+                class="flex-shrink-0 q-ml-sm"
+                color="primary"
+                name="mdi-content-copy"
+                size="16px"
+                @click="copyAddress(item.address)"
+              />
             </div>
+          </template>
+
+          <div class="row items-center no-wrap">
+            <q-btn
+              class="flex-shrink-0"
+              color="primary"
+              flat no-caps dense
+              size="sm"
+              :label="$t('provider.dashboard.today_services.help')"
+              @click.stop="openHelp"
+            />
+          </div>
           </q-card-section>
         </q-card>
       </div>
@@ -219,7 +228,7 @@
 
 <script setup>
 import { avatarColors, formatAddress, getFirstName } from 'src/helpers/utils';
-import { reactive } from 'vue'
+import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { useQuasar } from 'quasar'
 import { verifyScheduleCode } from 'src/api/schedule'
@@ -232,6 +241,17 @@ const emit  = defineEmits(['rate', 'refresh'])
 const { t } = useI18n()
 const $q    = useQuasar()
 
+const now = ref(Date.now())
+let timer = null
+
+onMounted(() => {
+  timer = setInterval(() => { now.value = Date.now() }, 1000)
+})
+
+onUnmounted(() => {
+  if (timer) clearInterval(timer)
+})
+
 const codeInputs    = reactive({})
 const codeRefs      = reactive({})
 const codeError     = reactive({})
@@ -324,21 +344,21 @@ const submitCode = async (item) => {
 const calculateTimeUntilEnd = (endTimeStr) => {
   const [h, m] = (endTimeStr || '23:59').slice(0, 5).split(':').map(Number)
 
-  const now = new Date()
-
   const endTime = new Date()
 
   endTime.setHours(h, m, 0, 0)
 
-  const diffMs = endTime - now
+  const diffMs = endTime.getTime() - now.value
 
-  if (diffMs <= 0) return '00:00'
+  if (diffMs <= 0) return '00:00:00'
 
   const diffH = Math.floor(diffMs / (1000 * 60 * 60))
 
   const diffM = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
 
-  return `${String(diffH).padStart(2, '0')}h:${String(diffM).padStart(2, '0')}`
+  const diffS = Math.floor((diffMs % (1000 * 60)) / 1000)
+
+  return `${String(diffH).padStart(2, '0')}:${String(diffM).padStart(2, '0')}:${String(diffS).padStart(2, '0')}`
 }
 
 const cardState = (item) => {
@@ -352,9 +372,13 @@ const cardState = (item) => {
 
   endTime.setHours(h, m, 0, 0)
 
-  return new Date() >= endTime ? 'finished' : 'in_progress'
+  return now.value >= endTime.getTime() ? 'finished' : 'in_progress'
 }
 
+const visibleItems = computed(() =>
+  props.data.filter((item) => cardState(item) !== 'finished')
+)
+
 const copyAddress = (address) => {
   const formatted = formatAddress(address);
 
@@ -370,10 +394,23 @@ const copyAddress = (address) => {
 const progressByState = (item) => {
   const state = cardState(item)
 
-  if (state === 'awaiting_code') return 60
-  if (state === 'in_progress')   return 80
+  if (state === 'awaiting_code') return 50
+  if (state === 'finished') return 100
+
+  const [sh, sm] = (item.start_time || '00:00').slice(0, 5).split(':').map(Number)
+  const [eh, em] = (item.end_time || '23:59').slice(0, 5).split(':').map(Number)
+
+  const startTime = new Date()
+  startTime.setHours(sh, sm, 0, 0)
+
+  const endTime = new Date()
+  endTime.setHours(eh, em, 0, 0)
+
+  const total = endTime.getTime() - startTime.getTime()
+  if (total <= 0) return 80
 
-  return 100
+  const elapsed = now.value - startTime.getTime()
+  return Math.min(Math.max((elapsed / total) * 100, 10), 100)
 }
 
 //
@@ -448,8 +485,17 @@ const openHelp = () => {
   line-height: 1.2;
 }
 
+.address-row {
+  display: grid;
+  grid-template-columns: auto 1fr auto;
+  align-items: center;
+  gap: 4px;
+}
+
 .address-text {
-  max-width: 110px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
   min-width: 0;
 }
 
@@ -478,15 +524,17 @@ const openHelp = () => {
 
 .progress-track {
   width: 100%;
-  height: 5px;
+  height: 8px;
   background: #E2E8F0;
-  border-radius: 3px;
+  border-radius: 4px;
   overflow: hidden;
 }
 
 .progress-fill {
   height: 100%;
-  border-radius: 3px;
+  border-radius: 4px;
+  position: relative;
+  overflow: hidden;
   background: linear-gradient(90deg, #8B5CF6, #EC4899);
   transition: width 0.4s ease;
 
@@ -495,4 +543,29 @@ const openHelp = () => {
   }
 }
 
+.progress-fill::after {
+  content: "";
+  position: absolute;
+  inset: 0;
+  left: -40%;
+  width: 40%;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255,255,255,.35),
+    transparent
+  );
+  transform: skewX(-20deg);
+  animation: shine 2s linear infinite;
+}
+
+.progress-fill--finished::after {
+  display: none;
+}
+
+@keyframes shine {
+  from { left: -40%; }
+  to { left: 140%; }
+}
+
 </style>

+ 38 - 42
src/components/dashboard/SolicitationDetailsDialog.vue

@@ -224,46 +224,44 @@
             :key="idx"
             class="package-item-row"
           >
-            <div class="row items-center no-wrap justify-between">
-              <div>
+            <div class="full-width">
+              <div class="row items-center no-wrap justify-between">
                 <span class="fontbold font12">
-                  {{ formatItemWeekday(pkgItem.date) }}
+                  {{ formatItemWeekday(pkgItem.date) }}, {{ formatItemDayMonth(pkgItem.date) }}
                 </span>
 
-                <span class="font12">
-                  {{ ", " + formatItemDayMonth(pkgItem.date) }}
+                <span class="fontbold font12">
+                  {{ formatCurrency(Number(pkgItem.total_amount) || 0) }}
                 </span>
               </div>
 
-              <span class="fontbold font12">
-                {{ formatCurrency(Number(pkgItem.total_amount) || 0) }}
-              </span>
-            </div>
+              <div class="row items-center no-wrap justify-between">
+                <span class="text-grey-6 font12">
+                  {{ pkgItem.start_time?.slice(0, 5) }} {{ $t("common.to") }} {{ pkgItem.end_time?.slice(0, 5) }}
+                </span>
 
-            <div class="text-grey-6 font12">
-              {{ pkgItem.start_time?.slice(0, 5) }} {{ $t("common.to") }} {{ pkgItem.end_time?.slice(0, 5) }}
+                <div class="row items-center no-wrap">
+                  <!--
+                  <q-icon
+                    :color="pkgItem.offers_meal ? 'positive' : 'grey-5'"
+                    :name="pkgItem.offers_meal ? 'mdi-food' : 'mdi-food-off'"
+                    size="14px"
+                    class="q-mr-xs"
+                  />
+                  !-->
+                  <span class="text-grey-7 font12">
+                    {{
+                      pkgItem.offers_meal
+                        ? $t("provider.dashboard.solicitations.offers_meal")
+                        : $t("provider.dashboard.solicitations.not_offers_meal")
+                    }}
+                  </span>
+                </div>
+              </div>
             </div>
           </div>
         </q-card-section>
 
-        <q-card-section class="column items-center q-pt-none q-pb-xs">
-          <div class="row items-center q-gutter-x-xs">
-            <q-icon
-              :color="solicitation.offers_meal ? 'positive' : 'grey-5'"
-              :name="solicitation.offers_meal ? 'mdi-food' : 'mdi-food-off'"
-              size="18px"
-            />
-
-            <span class="text-grey-7 font14">
-              {{
-                solicitation.offers_meal
-                  ? $t("provider.dashboard.solicitations.offers_meal")
-                  : $t("provider.dashboard.solicitations.not_offers_meal")
-              }}
-            </span>
-          </div>
-        </q-card-section>
-
         <q-card-section class="column items-center q-pt-xs q-pb-sm text-text">
           <div class="row items-start no-wrap self-start q-gutter-x-xs">
             <q-icon
@@ -445,7 +443,7 @@ const fullDateLabel = computed(() => {
 
   return date.toLocaleDateString("pt-BR", {
     day: "2-digit",
-    month: "long",
+    month: "2-digit",
     year: "numeric",
   });
 });
@@ -525,14 +523,10 @@ const servicesList = computed(() => {
   return services;
 });
 
+//
 
 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 backFromConfirm = () => {
   if (props.initialView !== "details") {
     onDialogCancel();
@@ -578,13 +572,11 @@ const formatItemDayMonth = (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}`;
+  return date.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+  });
 };
 
 const formatAddressShort = (address) => {
@@ -679,4 +671,8 @@ const parseAmount = (value) => {
     border-bottom: none;
   }
 }
+
+.min-width-0 {
+  min-width: 0;
+}
 </style>

+ 2 - 1
src/pages/dashboard/DashboardPage.vue

@@ -92,9 +92,10 @@ const handleScheduleAction = async (id, status, ids = [], servicePackageId = nul
         scheduleIds.map((scheduleId) => updateScheduleStatus(scheduleId, status)),
       );
     }
-    await loadDashboard();
   } catch (e) {
     console.log(e);
+  } finally {
+    await loadDashboard();
   }
 };