| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- <template>
- <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
- <div class="bg-page full-height column">
- <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
- <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
- <q-space />
- <span class="font16 fontbold gradient-diarista">
- {{ $t('payment.pix_title') }}
- </span>
- <q-space />
- <div style="width: 32px" />
- </div>
- <div v-if="success" class="col column items-center justify-center q-px-xl">
- <q-btn
- flat round icon="mdi-close" color="grey-5"
- class="self-end q-mb-md"
- @click="onDialogOK"
- />
- <div class="success-icon-wrapper q-mb-lg">
- <q-icon name="mdi-check-circle" size="100px" color="primary" />
- </div>
- <div class="success-title text-primary text-center q-mb-sm">
- {{ $t('payment.success_title') }}
- </div>
- <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center">
- <template #nextServices>
- <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
- </template>
- <template #agenda>
- <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
- </template>
- </i18n-t>
- </div>
- <div v-else-if="processing" class="col column items-center justify-center q-px-xl">
- <q-spinner-oval color="primary" size="72px" class="q-mb-lg" />
- <div class="processing-title text-primary text-center q-mb-sm">
- {{ $t('payment.processing_title') }}
- </div>
- <div class="processing-message text-grey-6 text-center">
- {{ $t('payment.processing_message') }}
- </div>
- </div>
- <div v-else class="pix-payment-content col scroll q-px-lg q-pt-lg q-pb-xl column">
- <div class="row items-center justify-between q-mb-sm">
- <span class="pix-label font14 fontbold">{{ $t('payment.pix_total') }}</span>
- <span class="text-primary font14 fontbold">{{ formatCurrency(total) }}</span>
- </div>
- <q-separator />
- <div class="row items-center justify-between q-mt-sm q-mb-lg">
- <span class="pix-label font14 fontbold">{{ $t('payment.pix_expires') }}</span>
- <span class="text-primary font14 fontbold">{{ countdown }}</span>
- </div>
- <div class="flex flex-center q-mb-md">
- <q-icon name="mdi-cash-fast" size="48px" color="teal" />
- </div>
- <div class="flex flex-center q-mb-md">
- <q-img
- v-if="pixQrCodeUrl"
- :src="pixQrCodeUrl"
- width="180px"
- height="180px"
- fit="contain"
- class="qrcode-wrapper"
- />
- <div v-else class="qrcode-wrapper column items-center justify-center">
- <q-icon name="mdi-qrcode" size="80px" color="grey-6" />
- </div>
- </div>
- <div class="pix-code-text q-mb-md">{{ pixCode || 'Código Pix indisponível.' }}</div>
- <q-btn
- unelevated
- rounded
- no-caps
- color="primary"
- padding="4px 12px"
- class="full-width q-mb-lg"
- :label="$t('payment.pix_copy_btn')"
- :loading="processing"
- @click="copyCode"
- />
- <i18n-t keypath="payment.pix_instructions" tag="p" class="pix-instructions-text font14 q-mb-sm">
- <template #copyCode>
- <span class="fontbold">{{ $t('payment.pix_copy_code') }}</span>
- </template>
- <template #pasteCode>
- <span class="fontbold">{{ $t('payment.pix_paste_code') }}</span>
- </template>
- <template #finalize>
- <span class="fontbold">{{ $t('payment.pix_finalize') }}</span>
- </template>
- </i18n-t>
- <p class="pix-instructions-text font14">{{ $t('payment.pix_email_note') }}</p>
- </div>
- </div>
- </q-dialog>
- </template>
- <script setup>
- import { computed, ref, onMounted, onUnmounted } from 'vue'
- import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
- import { formatCurrency } from 'src/helpers/utils'
- import { getSchedulePixPayment, paySchedule } from 'src/api/payment'
- import { usePaymentStore } from 'src/stores/payment'
- const props = defineProps({
- schedule: { type: Object, required: true },
- total: { type: Number, required: true },
- })
- defineEmits([...useDialogPluginComponent.emits])
- const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
- const $q = useQuasar()
- const paymentStore = usePaymentStore()
- const payment = ref(null)
- const success = ref(false)
- const processing = ref(true)
- const pixData = computed(() => payment.value?.pix ?? {})
- const pixCode = computed(() => pixData.value?.qr_code ?? '')
- const pixQrCodeUrl = computed(() => pixData.value?.qr_code_url ?? '')
- const pixExpiresAt = computed(() => pixData.value?.expires_at ?? payment.value?.expires_at ?? null)
- const copyCode = async () => {
- try {
- if (!pixCode.value) {
- $q.notify({ type: 'negative', message: 'Código Pix indisponível.' })
- return
- }
- await copyToClipboard(pixCode.value)
- $q.notify({ type: 'positive', message: 'Código copiado!' })
- } catch {
- $q.notify({ type: 'negative', message: 'Erro ao copiar.' })
- return
- }
- }
- const totalSeconds = ref(20 * 60)
- const countdown = ref('')
- let countdownTimer = null
- let pollingTimer = null
- let pollingInFlight = false
- const stopPolling = () => {
- clearInterval(pollingTimer)
- pollingTimer = null
- }
- const applyPaymentStatus = (nextPayment) => {
- if (!nextPayment) return
- payment.value = nextPayment
- if (nextPayment.status === 'paid') {
- success.value = true
- processing.value = false
- paymentStore.clearPixPayment(props.schedule.id)
- stopPolling()
- return
- }
- if (['failed', 'cancelled'].includes(nextPayment.status)) {
- processing.value = false
- paymentStore.clearPixPayment(props.schedule.id)
- stopPolling()
- $q.notify({ type: 'negative', message: nextPayment.failure_message || 'Pagamento Pix não confirmado.' })
- onDialogCancel()
- return
- }
- paymentStore.setPixPayment(props.schedule.id, nextPayment)
- }
- const checkPaymentStatus = async () => {
- if (pollingInFlight || success.value || totalSeconds.value <= 0) return
- pollingInFlight = true
- try {
- applyPaymentStatus(await getSchedulePixPayment(props.schedule.id))
- updateCountdown()
- } catch (e) {
- console.error('Erro ao verificar pagamento Pix:', e)
- } finally {
- pollingInFlight = false
- }
- }
- const startPolling = () => {
- stopPolling()
- if (success.value || ['failed', 'cancelled'].includes(payment.value?.status) || totalSeconds.value <= 0) return
- checkPaymentStatus()
- pollingTimer = setInterval(checkPaymentStatus, 5000)
- }
- const updateCountdown = () => {
- if (pixExpiresAt.value) {
- totalSeconds.value = Math.max(0, Math.floor((new Date(pixExpiresAt.value).getTime() - Date.now()) / 1000))
- }
- const m = Math.floor(totalSeconds.value / 60)
- 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)
- stopPolling()
- }
- }
- onMounted(async () => {
- updateCountdown()
- countdownTimer = setInterval(updateCountdown, 1000)
- try {
- const cachedPayment = paymentStore.getValidPixPayment(props.schedule.id)
- if (cachedPayment) {
- applyPaymentStatus(cachedPayment)
- updateCountdown()
- startPolling()
- return
- }
- applyPaymentStatus(await paySchedule(props.schedule.id, { payment_method: 'pix' }))
- updateCountdown()
- startPolling()
- } catch (e) {
- console.error('Erro ao criar pagamento Pix:', e)
- $q.notify({ type: 'negative', message: e?.response?.data?.message ?? 'Erro ao criar pagamento Pix.' })
- onDialogCancel()
- } finally {
- processing.value = false
- }
- })
- onUnmounted(() => {
- clearInterval(countdownTimer)
- stopPolling()
- })
- </script>
- <style scoped lang="scss">
- .shadow-header {
- box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
- }
- .pix-label {
- color: #3a3a4a;
- }
- .qrcode-wrapper {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 12px;
- padding: 12px;
- }
- .pix-code-text {
- color: #5a5a6a;
- text-align: center;
- word-break: break-all;
- line-height: 1.5;
- background: #f5f5f8;
- border-radius: 8px;
- padding: 10px;
- }
- .pix-instructions-text {
- align-self: stretch;
- line-height: 1.5;
- color: #3a3a4a;
- margin-left: 0;
- margin-right: 0;
- max-width: 100%;
- overflow-wrap: anywhere;
- text-align: left;
- width: 100%;
- }
- .success-icon-wrapper {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .success-title {
- line-height: 1.3;
- max-width: 280px;
- }
- .success-message {
- line-height: 1.6;
- max-width: 280px;
- }
- </style>
|