Ver Fonte

Merge branch 'development' of gogs.softpar.inf.br:Softpar/sfp_front_vue_diarista_cliente into development

Gustavo Zanatta há 1 semana atrás
pai
commit
8871e8e6ce

+ 158 - 42
src/components/dashboard/DashboardNextSchedules.vue

@@ -7,17 +7,18 @@
         <q-card
           v-for="item in data"
           :key="item.id"
-          class="schedule-card card-border shadow-card bg-surface"
           :flat="false"
-        >
-          <q-card-section class="q-pa-md row col-12 no-wrap">
-            <div class="col-3 column text-center">
+          class="schedule-card card-border shadow-card bg-surface"
+          >
+          <q-card-section class="q-pa-md row col-12 no-wrap schedule-card-section">
+            <div class="column text-center schedule-avatar">
               <div class="col-7">
                 <q-avatar :style="avatarColors[item.id % avatarColors.length]" class="text-weight-bold q-mx-auto">
                   <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover;border-radius:50%;" />
                   <span v-else>{{ item.provider_name?.slice(0,1) ?? '—' }}</span>
                 </q-avatar>
               </div>
+
               <div class="col-5 column justify-end">
                 <span class="text-pill text-primary customColor">
                   {{ item.schedule_type === 'custom' ? $t('dashboard_client.next_schedules.tag_custom') : $t('dashboard_client.next_schedules.tag_default') }}
@@ -25,47 +26,72 @@
               </div>
             </div>
 
-            <div class="col-5 column text-text q-px-sm">
-              <div class="col-7 column">
-                <div class="col-4">
-                  <span class="text-provider-name">{{ item.provider_name ?? $t('dashboard_client.next_schedules.no_provider') }}</span>
+            <div class="column text-text q-px-sm schedule-main">
+              <div class="column schedule-info">
+                <div>
+                  <span class="text-provider-name provider-name-ellipsis">
+                    {{ item.provider_name ?? $t('dashboard_client.next_schedules.no_provider') }}
+                  </span>
                 </div>
-                <div class="col-4 column">
-                  <div class="col-6">
-                    <span class="text-schedule-date-bold">{{ formatWeekday(item.date) }}</span><span class="text-schedule-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+
+                <div class="column schedule-date-time">
+                  <div class="schedule-line">
+                    <span class="text-schedule-date-bold">
+                      {{ formatWeekday(item.date) }}
+                    </span>
+
+                    <span class="text-schedule-date-regular">
+                      {{ ', ' + formatDayMonth(item.date) }}
+                    </span>
                   </div>
-                  <div class="col-6 q-pt-sm">
-                    <span class="text-schedule-date-regular">{{ $t('dashboard_client.next_schedules.from') }} </span>
-                    <span class="text-schedule-date-bold">{{ item.start_time?.slice(0,5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ item.end_time?.slice(0,5) }}</span>
+
+                  <div class="schedule-line q-pt-xs">
+                    <span class="text-schedule-date-regular">
+                      {{ $t('dashboard_client.next_schedules.from') }}&nbsp;
+                    </span>
+
+                    <span class="text-schedule-date-bold">
+                      {{ item.start_time?.slice(0,5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ item.end_time?.slice(0,5) }}
+                    </span>
                   </div>
                 </div>
               </div>
-              <div class="col-5">
-                <div class="full-height column justify-end">
-                  <div class="row text-pill-place">
-                    <q-icon :name="addressIcon(item.address?.address_type ?? item.custom_address_type)" size="15px" color="primary" />
-                    <span class="row items-end">{{ addressLabel(item.address?.address_type ?? item.custom_address_type) }}</span>
+
+              <div class="schedule-place">
+                <div class="column justify-end">
+                  <div class="row items-center no-wrap text-pill-place schedule-place-content">
+                    <q-icon
+                      :name="addressIcon(item.address?.address_type ?? item.custom_address_type)"
+                      color="primary"
+                      size="15px"
+                    />
+
+                    <span>
+                      {{ addressLabel(item.address?.address_type ?? item.custom_address_type) }}
+                    </span>
                   </div>
                 </div>
               </div>
             </div>
 
-            <div class="col-4 column text-text">
-              <div class="column col-5">
-                <span class="text-price-main col-6">
+            <div class="column text-text schedule-price">
+              <div class="column schedule-price-info">
+                <span class="text-price-main">
                   {{ item.total_amount && item.total_amount !== '0.00' ? formatCurrency(item.total_amount) : $t('dashboard_client.next_schedules.to_combine') }}
                 </span>
-                <span class="text-price-label col-6">
+
+                <span class="text-price-label schedule-price-label">
                   {{ formatLabelByPeriodType(item.period_type) }}
                 </span>
               </div>
-              <div class="col-7 column justify-end items-end">
+
+              <div class="column justify-end items-end">
                 <q-btn
-                  unelevated rounded no-caps
+                  class="full-width"
                   color="primary"
                   padding="1px 5px"
                   size="sm"
-                  class="full-width"
+                  unelevated rounded no-caps
                   :label="$t('dashboard_client.next_schedules.details')"
                   @click="emit('view-details', item)"
                 />
@@ -79,11 +105,12 @@
 </template>
 
 <script setup>
-import { useI18n } from 'vue-i18n';
 import { formatCurrency } from 'src/helpers/utils';
 import { formatLabelByPeriodType } from 'src/helpers/utils';
+import { useI18n } from 'vue-i18n';
 
 defineProps({ data: { type: Array, default: () => [] } });
+
 const emit = defineEmits(['view-details']);
 
 const { t } = useI18n();
@@ -95,33 +122,36 @@ const avatarColors = [
   { background: '#ffe5cc', color: '#8a4500' },
 ];
 
-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 addressIcon = (type) => type === 'home' ? 'mdi-home-outline' : 'mdi-office-building-outline';
+
+const addressLabel = (type) => {
+  if (type === 'home')       return t('address.types.commercial.home');
+  if (type === 'apartment')  return t('dashboard_client.next_schedules.place_apartment');
+  if (type === 'commercial') return t('address.types.commercial.commercial');
+
+  return t('dashboard_client.next_schedules.place_unknown');
 };
 
 const formatDayMonth = (iso) => {
   if (!iso) return '';
+
   return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
 };
 
-const addressIcon = (type) => type === 'home' ? 'mdi-home-outline' : 'mdi-office-building-outline';
-
-const addressLabel = (type) => {
-  if (type === 'home') return t('address.types.commercial.home');
-  if (type === 'apartment') return t('dashboard_client.next_schedules.place_apartment');
-  if (type === 'commercial') return t('address.types.commercial.commercial');
-  return t('dashboard_client.next_schedules.place_unknown');
-};
+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);
+};
 </script>
 
 <style scoped lang="scss">
 .scroll-wrapper { overflow: hidden; }
+
 .scroll-track {
   display: flex;
   flex-direction: row;
@@ -133,9 +163,95 @@ const addressLabel = (type) => {
   &::-webkit-scrollbar { display: none; }
   &::after { content: ''; flex: 0 0 1px; }
 }
+
 .schedule-card {
-  min-width: 80%;
-  min-height: 90px;
+  min-width: 90%;
+  min-height: 112px;
+}
+
+.schedule-card-section {
+  align-items: stretch;
+  gap: 8px;
+}
+
+.schedule-avatar {
+  flex: 0 0 52px;
+  max-width: 52px;
+  min-width: 52px;
+}
+
+.schedule-main {
+  flex: 1 1 auto;
+  min-width: 0;
+  overflow: hidden;
+  gap: 8px;
+}
+
+.schedule-info {
+  flex: 0 1 auto;
+  gap: 6px;
+  min-height: 0;
+}
+
+.schedule-date-time {
+  min-width: 0;
+}
+
+.schedule-line {
+  display: block;
+  max-width: 100%;
+  line-height: 1.15;
+  overflow: visible;
+  white-space: normal;
+}
+
+.schedule-price {
+  flex: 0 0 92px;
+  max-width: 92px;
+  min-width: 92px;
+  gap: 8px;
+  justify-content: space-between;
+}
+
+.schedule-price-info {
+  gap: 2px;
+}
+
+.schedule-price-label {
+  align-self: stretch;
+  line-height: 1.15;
+  white-space: normal;
+}
+
+.schedule-place {
+  margin-top: auto;
+  min-width: 0;
+}
+
+.schedule-place-content {
+  max-width: 100%;
+  min-width: 0;
+  overflow: hidden;
+
+  .q-icon {
+    flex: 0 0 auto;
+  }
+
+  span {
+    flex: 1 1 auto;
+    min-width: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
+
+.provider-name-ellipsis {
+  display: block;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 .customColor {

+ 6 - 1
src/components/dashboard/ScheduleAcceptedDialog.vue

@@ -74,7 +74,9 @@
 import { computed } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
+import { usePaymentStore } from 'src/stores/payment'
 import SchedulePaymentDialog from './SchedulePaymentDialog.vue'
+import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 
 const props = defineProps({
   schedule: {
@@ -88,6 +90,7 @@ defineEmits([...useDialogPluginComponent.emits])
 const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
 
 const $q = useQuasar()
+const paymentStore = usePaymentStore()
 
 const SERVICE_FEE_RATE = 0.11
 
@@ -95,8 +98,10 @@ const serviceFee = computed(() => Number(props.schedule.total_amount) * SERVICE_
 const total = computed(() => Number(props.schedule.total_amount) + serviceFee.value)
 
 const onGoToPayment = () => {
+  const hasValidPixPayment = !!paymentStore.getValidPixPayment(props.schedule.id)
+
   $q.dialog({
-    component: SchedulePaymentDialog,
+    component: hasValidPixPayment ? SchedulePaymentPixDialog : SchedulePaymentDialog,
     componentProps: {
       schedule: props.schedule,
       total: total.value,

+ 17 - 6
src/components/dashboard/SchedulePaymentDialog.vue

@@ -102,6 +102,7 @@ import { ref, computed, onMounted } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 import { userStore } from 'src/stores/user'
+import { usePaymentStore } from 'src/stores/payment'
 import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
 import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
@@ -124,6 +125,7 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginC
 const $q = useQuasar()
 const { t } = useI18n()
 const store = userStore()
+const paymentStore = usePaymentStore()
 
 const selectedMethod = ref(null)
 const agreedToTerms = ref(false)
@@ -171,14 +173,18 @@ const openAddCard = () => {
   })
 }
 
+const openPixPayment = () => {
+  $q.dialog({
+    component: SchedulePaymentPixDialog,
+    componentProps: { schedule: props.schedule, total: props.total },
+  }).onOk(() => {
+    onDialogOK()
+  })
+}
+
 const onConfirm = () => {
   if (selectedMethod.value === 'pix') {
-    $q.dialog({
-      component: SchedulePaymentPixDialog,
-      componentProps: { schedule: props.schedule, total: props.total },
-    }).onOk(() => {
-      onDialogOK()
-    })
+    openPixPayment()
     return
   }
 
@@ -193,6 +199,11 @@ const onConfirm = () => {
 }
 
 onMounted(() => {
+  if (paymentStore.getValidPixPayment(props.schedule.id)) {
+    openPixPayment()
+    return
+  }
+
   loadCards()
 })
 </script>

+ 15 - 0
src/components/dashboard/SchedulePaymentPixDialog.vue

@@ -106,6 +106,7 @@ import { computed, ref, onMounted, onUnmounted } from 'vue'
 import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
 import { paySchedule } from 'src/api/payment'
+import { usePaymentStore } from 'src/stores/payment'
 
 const props = defineProps({
   schedule: { type: Object, required: true },
@@ -116,6 +117,7 @@ defineEmits([...useDialogPluginComponent.emits])
 
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
+const paymentStore = usePaymentStore()
 
 const payment = ref(null)
 const success = ref(false)
@@ -155,6 +157,10 @@ const updateCountdown = () => {
   const s = totalSeconds.value % 60
   countdown.value = `${m} min, ${String(s).padStart(2, '0')} seg`
   if (!pixExpiresAt.value && totalSeconds.value > 0) totalSeconds.value--
+
+  if (pixExpiresAt.value && totalSeconds.value <= 0) {
+    paymentStore.clearPixPayment(props.schedule.id)
+  }
 }
 
 onMounted(async () => {
@@ -162,7 +168,16 @@ onMounted(async () => {
   timer = setInterval(updateCountdown, 1000)
 
   try {
+    const cachedPayment = paymentStore.getValidPixPayment(props.schedule.id)
+
+    if (cachedPayment) {
+      payment.value = cachedPayment
+      updateCountdown()
+      return
+    }
+
     payment.value = await paySchedule(props.schedule.id, { payment_method: 'pix' })
+    paymentStore.setPixPayment(props.schedule.id, payment.value)
     updateCountdown()
   } catch (e) {
     console.error('Erro ao criar pagamento Pix:', e)

+ 49 - 0
src/stores/payment.js

@@ -0,0 +1,49 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+
+const getPixExpiresAt = (payment) => payment?.pix?.expires_at ?? payment?.expires_at ?? null;
+
+const isFutureDate = (date) => {
+  if (!date) return false;
+  const time = new Date(date).getTime();
+  return Number.isFinite(time) && time > Date.now();
+};
+
+export const usePaymentStore = defineStore('payment', () => {
+  const pixPaymentsByScheduleId = ref({});
+
+  const getValidPixPayment = (scheduleId) => {
+    const payment = pixPaymentsByScheduleId.value[scheduleId] ?? null;
+    if (!payment) return null;
+
+    if (!isFutureDate(getPixExpiresAt(payment))) {
+      clearPixPayment(scheduleId);
+      return null;
+    }
+
+    return payment;
+  };
+
+  const setPixPayment = (scheduleId, payment) => {
+    if (!scheduleId || !payment) return;
+    pixPaymentsByScheduleId.value = {
+      ...pixPaymentsByScheduleId.value,
+      [scheduleId]: payment,
+    };
+  };
+
+  const clearPixPayment = (scheduleId) => {
+    if (!scheduleId || !pixPaymentsByScheduleId.value[scheduleId]) return;
+
+    const payments = { ...pixPaymentsByScheduleId.value };
+    delete payments[scheduleId];
+    pixPaymentsByScheduleId.value = payments;
+  };
+
+  return {
+    pixPaymentsByScheduleId,
+    getValidPixPayment,
+    setPixPayment,
+    clearPixPayment,
+  };
+});