|
@@ -1,5 +1,6 @@
|
|
|
<script>
|
|
<script>
|
|
|
- import { onMount } from 'svelte';
|
|
|
|
|
|
|
+ import { browser } from '$app/environment';
|
|
|
|
|
+ import { onDestroy, onMount, tick } from 'svelte';
|
|
|
import { get } from 'svelte/store';
|
|
import { get } from 'svelte/store';
|
|
|
import Header from '$lib/layout/Header.svelte';
|
|
import Header from '$lib/layout/Header.svelte';
|
|
|
import Tabs from '$lib/components/Tabs.svelte';
|
|
import Tabs from '$lib/components/Tabs.svelte';
|
|
@@ -147,7 +148,6 @@
|
|
|
'cpr_place_name',
|
|
'cpr_place_name',
|
|
|
'cpr_deliveryPlace_state_acronym',
|
|
'cpr_deliveryPlace_state_acronym',
|
|
|
'cpr_deliveryPlace_city_name',
|
|
'cpr_deliveryPlace_city_name',
|
|
|
- 'cpr_deliveryPlace_ibge_code',
|
|
|
|
|
'cpr_issuer_legal_nature_code',
|
|
'cpr_issuer_legal_nature_code',
|
|
|
'cpr_issuers_person_type_acronym',
|
|
'cpr_issuers_person_type_acronym',
|
|
|
'cpr_issuers_document_number',
|
|
'cpr_issuers_document_number',
|
|
@@ -274,11 +274,30 @@
|
|
|
let submitSuccess = '';
|
|
let submitSuccess = '';
|
|
|
let isSubmitting = false;
|
|
let isSubmitting = false;
|
|
|
|
|
|
|
|
|
|
+ const PAYMENT_STORAGE_KEY = 'tooeasy_cpr_payment';
|
|
|
|
|
+ const PAYMENT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
|
|
|
+ const PAYMENT_POLL_INTERVAL_MS = 10_000;
|
|
|
|
|
+ let paymentModalVisible = false;
|
|
|
|
|
+ let paymentCode = '';
|
|
|
|
|
+ let paymentId = null;
|
|
|
|
|
+ let paymentError = '';
|
|
|
|
|
+ let paymentStatusMessage = '';
|
|
|
|
|
+ let paymentCopyFeedback = '';
|
|
|
|
|
+ let paymentCountdownMs = PAYMENT_TIMEOUT_MS;
|
|
|
|
|
+ let paymentStartedAt = null;
|
|
|
|
|
+ let paymentQrContainer;
|
|
|
|
|
+ let paymentQrInstance = null;
|
|
|
|
|
+ let paymentPollIntervalId = null;
|
|
|
|
|
+ let paymentCountdownIntervalId = null;
|
|
|
|
|
+ let paymentCopyTimeoutId = null;
|
|
|
|
|
+ let isCheckingPayment = false;
|
|
|
|
|
+
|
|
|
const breadcrumb = [{ label: 'Início' }, { label: 'CPR', active: true }];
|
|
const breadcrumb = [{ label: 'Início' }, { label: 'CPR', active: true }];
|
|
|
let activeTab = 4;
|
|
let activeTab = 4;
|
|
|
const tabs = ['Contrato', 'Registro', 'Emissão'];
|
|
const tabs = ['Contrato', 'Registro', 'Emissão'];
|
|
|
|
|
|
|
|
const historyEndpoint = `${apiUrl}/cpr/history`;
|
|
const historyEndpoint = `${apiUrl}/cpr/history`;
|
|
|
|
|
+ const paymentConfirmEndpoint = `${apiUrl}/b3/payment/confirm`;
|
|
|
const historyColumns = [
|
|
const historyColumns = [
|
|
|
{ key: 'cpr_id', label: 'CPR ID' },
|
|
{ key: 'cpr_id', label: 'CPR ID' },
|
|
|
{ key: 'cpr_product_class_name', label: 'Produto' },
|
|
{ key: 'cpr_product_class_name', label: 'Produto' },
|
|
@@ -429,6 +448,312 @@
|
|
|
return numeric.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
|
return numeric.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function formatCountdown(ms) {
|
|
|
|
|
+ if (ms == null || Number.isNaN(ms)) return '--:--';
|
|
|
|
|
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
|
|
|
+ const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
|
|
|
|
+ const seconds = String(totalSeconds % 60).padStart(2, '0');
|
|
|
|
|
+ return `${minutes}:${seconds}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function persistPaymentState() {
|
|
|
|
|
+ if (!browser || !paymentId || !paymentCode) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ paymentId,
|
|
|
|
|
+ paymentCode,
|
|
|
|
|
+ startedAt: paymentStartedAt ?? Date.now()
|
|
|
|
|
+ };
|
|
|
|
|
+ localStorage.setItem(PAYMENT_STORAGE_KEY, JSON.stringify(payload));
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('[CPR] Não foi possível salvar o estado do pagamento', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function removePaymentStateFromStorage() {
|
|
|
|
|
+ if (!browser) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ localStorage.removeItem(PAYMENT_STORAGE_KEY);
|
|
|
|
|
+ } catch {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function stopPaymentPolling() {
|
|
|
|
|
+ if (paymentPollIntervalId) {
|
|
|
|
|
+ clearInterval(paymentPollIntervalId);
|
|
|
|
|
+ paymentPollIntervalId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function stopPaymentCountdown() {
|
|
|
|
|
+ if (paymentCountdownIntervalId) {
|
|
|
|
|
+ clearInterval(paymentCountdownIntervalId);
|
|
|
|
|
+ paymentCountdownIntervalId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function stopPaymentCopyTimeout() {
|
|
|
|
|
+ if (paymentCopyTimeoutId) {
|
|
|
|
|
+ clearTimeout(paymentCopyTimeoutId);
|
|
|
|
|
+ paymentCopyTimeoutId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function startPaymentCountdown(startTimestamp = Date.now()) {
|
|
|
|
|
+ if (!browser) return;
|
|
|
|
|
+ paymentStartedAt = startTimestamp;
|
|
|
|
|
+ updatePaymentCountdown();
|
|
|
|
|
+ stopPaymentCountdown();
|
|
|
|
|
+ paymentCountdownIntervalId = window.setInterval(updatePaymentCountdown, 1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function updatePaymentCountdown() {
|
|
|
|
|
+ if (!paymentStartedAt) {
|
|
|
|
|
+ paymentCountdownMs = PAYMENT_TIMEOUT_MS;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const elapsed = Date.now() - paymentStartedAt;
|
|
|
|
|
+ const remaining = PAYMENT_TIMEOUT_MS - elapsed;
|
|
|
|
|
+ paymentCountdownMs = remaining > 0 ? remaining : 0;
|
|
|
|
|
+ if (remaining <= 0) {
|
|
|
|
|
+ handlePaymentExpired();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handlePaymentExpired() {
|
|
|
|
|
+ if (!paymentId) return;
|
|
|
|
|
+ stopPaymentPolling();
|
|
|
|
|
+ stopPaymentCountdown();
|
|
|
|
|
+ removePaymentStateFromStorage();
|
|
|
|
|
+ paymentStatusMessage = '';
|
|
|
|
|
+ paymentError = 'O tempo limite para pagamento expirou. Gere uma nova CPR para emitir outro QR Code.';
|
|
|
|
|
+ paymentId = null;
|
|
|
|
|
+ paymentCode = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function startPaymentPolling(immediate = true) {
|
|
|
|
|
+ if (!browser || !paymentId) return;
|
|
|
|
|
+ stopPaymentPolling();
|
|
|
|
|
+ if (immediate) {
|
|
|
|
|
+ void pollPaymentStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ paymentPollIntervalId = window.setInterval(() => {
|
|
|
|
|
+ void pollPaymentStatus();
|
|
|
|
|
+ }, PAYMENT_POLL_INTERVAL_MS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function pollPaymentStatus() {
|
|
|
|
|
+ if (!paymentId || isCheckingPayment) return;
|
|
|
|
|
+ isCheckingPayment = true;
|
|
|
|
|
+ paymentError = '';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await requestPaymentConfirmation();
|
|
|
|
|
+ if (result.state === 'success') {
|
|
|
|
|
+ handlePaymentConfirmationSuccess(result.data);
|
|
|
|
|
+ } else if (result.state === 'pending') {
|
|
|
|
|
+ paymentStatusMessage = result.message ?? 'Pagamento pendente. Continuaremos monitorando.';
|
|
|
|
|
+ } else if (result.state === 'error') {
|
|
|
|
|
+ paymentStatusMessage = '';
|
|
|
|
|
+ paymentError = result.message ?? 'Falha ao confirmar pagamento.';
|
|
|
|
|
+ stopPaymentPolling();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[CPR] Erro ao confirmar pagamento', err);
|
|
|
|
|
+ paymentError = err?.message ?? 'Não foi possível verificar o pagamento.';
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isCheckingPayment = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function requestPaymentConfirmation() {
|
|
|
|
|
+ if (!paymentId) {
|
|
|
|
|
+ return { state: 'error', message: 'Pagamento inválido. Gere um novo QR Code.' };
|
|
|
|
|
+ }
|
|
|
|
|
+ const { token, company_id } = ensureAuthContext();
|
|
|
|
|
+ const res = await fetch(paymentConfirmEndpoint, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'content-type': 'application/json',
|
|
|
|
|
+ Authorization: `Bearer ${token}`
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({ payment_id: paymentId, company_id })
|
|
|
|
|
+ });
|
|
|
|
|
+ const raw = await res.text();
|
|
|
|
|
+ let body = null;
|
|
|
|
|
+ if (raw) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ body = JSON.parse(raw);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[CPR] Resposta inválida ao confirmar pagamento:', err, raw);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (res.ok && body?.success) {
|
|
|
|
|
+ return { state: 'success', data: body };
|
|
|
|
|
+ }
|
|
|
|
|
+ const code = body?.code ?? body?.error;
|
|
|
|
|
+ const fallbackMessage = body?.data?.message ?? body?.message ?? body?.msg ?? raw ?? 'Falha ao confirmar pagamento.';
|
|
|
|
|
+ if (code === 'E_PAYMENT_PENDING' || body?.data?.status === 0) {
|
|
|
|
|
+ return { state: 'pending', message: 'Pagamento pendente. Assim que confirmado, você será avisado.' };
|
|
|
|
|
+ }
|
|
|
|
|
+ const message = mapPaymentError(code, res.status, fallbackMessage);
|
|
|
|
|
+ return { state: 'error', message };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function mapPaymentError(code, status, fallback) {
|
|
|
|
|
+ const normalized = code?.toUpperCase?.();
|
|
|
|
|
+ switch (normalized) {
|
|
|
|
|
+ case 'E_VALIDATE':
|
|
|
|
|
+ return 'Payment_id inválido. Registre uma nova CPR para gerar outro QR Code.';
|
|
|
|
|
+ case 'E_NOT_FOUND':
|
|
|
|
|
+ return 'Pagamento não encontrado. Gere um novo QR Code.';
|
|
|
|
|
+ case 'E_PAYMENT_STATUS':
|
|
|
|
|
+ return 'Pagamento em status inválido. Gere um novo QR Code.';
|
|
|
|
|
+ case 'E_CPR_NOT_FOUND':
|
|
|
|
|
+ return 'CPR não vinculada ao pagamento. Entre em contato com o suporte.';
|
|
|
|
|
+ case 'E_EXTERNAL':
|
|
|
|
|
+ return 'Erro externo ao confirmar pagamento. Tente novamente em instantes.';
|
|
|
|
|
+ default:
|
|
|
|
|
+ if (status === 502) {
|
|
|
|
|
+ return 'Erro externo ao confirmar pagamento. Tente novamente em instantes.';
|
|
|
|
|
+ }
|
|
|
|
|
+ return fallback ?? 'Não foi possível confirmar o pagamento.';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handlePaymentConfirmationSuccess(payload) {
|
|
|
|
|
+ const message = payload?.data?.message ?? payload?.message ?? 'Pagamento confirmado com sucesso.';
|
|
|
|
|
+ submitSuccess = message;
|
|
|
|
|
+ paymentError = '';
|
|
|
|
|
+ paymentStatusMessage = '';
|
|
|
|
|
+ resetPaymentTracking({ hideModal: true });
|
|
|
|
|
+ void fetchHistory();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function resetPaymentTracking({ hideModal = true } = {}) {
|
|
|
|
|
+ stopPaymentPolling();
|
|
|
|
|
+ stopPaymentCountdown();
|
|
|
|
|
+ stopPaymentCopyTimeout();
|
|
|
|
|
+ removePaymentStateFromStorage();
|
|
|
|
|
+ paymentId = null;
|
|
|
|
|
+ paymentCode = '';
|
|
|
|
|
+ paymentStartedAt = null;
|
|
|
|
|
+ paymentCountdownMs = PAYMENT_TIMEOUT_MS;
|
|
|
|
|
+ paymentCopyFeedback = '';
|
|
|
|
|
+ if (hideModal) {
|
|
|
|
|
+ paymentModalVisible = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleCancelPaymentFlow() {
|
|
|
|
|
+ resetPaymentTracking({ hideModal: true });
|
|
|
|
|
+ paymentStatusMessage = '';
|
|
|
|
|
+ paymentError = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleRetryPaymentCheck() {
|
|
|
|
|
+ if (!paymentId) return;
|
|
|
|
|
+ paymentError = '';
|
|
|
|
|
+ paymentStatusMessage = 'Verificando status do pagamento...';
|
|
|
|
|
+ startPaymentPolling(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function renderPaymentQrCode(link) {
|
|
|
|
|
+ if (!browser || !link) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!paymentQrInstance) {
|
|
|
|
|
+ const module = await import('qr-code-styling');
|
|
|
|
|
+ const QRCodeStyles = module.default ?? module;
|
|
|
|
|
+ paymentQrInstance = new QRCodeStyles({
|
|
|
|
|
+ width: 240,
|
|
|
|
|
+ height: 240,
|
|
|
|
|
+ type: 'svg',
|
|
|
|
|
+ data: link,
|
|
|
|
|
+ dotsOptions: {
|
|
|
|
|
+ color: '#0f172a',
|
|
|
|
|
+ type: 'rounded'
|
|
|
|
|
+ },
|
|
|
|
|
+ backgroundOptions: {
|
|
|
|
|
+ color: '#ffffff'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ paymentQrInstance.update({ data: link });
|
|
|
|
|
+ }
|
|
|
|
|
+ await tick();
|
|
|
|
|
+ if (paymentQrContainer) {
|
|
|
|
|
+ paymentQrContainer.innerHTML = '';
|
|
|
|
|
+ paymentQrInstance.append(paymentQrContainer);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('[CPR] Falha ao renderizar QR Code de pagamento', error);
|
|
|
|
|
+ paymentError = 'Não foi possível gerar o QR Code. Use o código Pix copiado para pagar.';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function openPaymentModalWithPayment(data, startedAt = Date.now(), immediatePoll = true) {
|
|
|
|
|
+ if (!data?.payment_id || !data?.payment_code) return;
|
|
|
|
|
+ paymentModalVisible = true;
|
|
|
|
|
+ paymentId = Number(data.payment_id);
|
|
|
|
|
+ paymentCode = data.payment_code;
|
|
|
|
|
+ paymentError = '';
|
|
|
|
|
+ paymentStatusMessage = 'Aguardando pagamento via Pix.';
|
|
|
|
|
+ paymentCopyFeedback = '';
|
|
|
|
|
+ paymentStartedAt = startedAt;
|
|
|
|
|
+ const remaining = PAYMENT_TIMEOUT_MS - (Date.now() - startedAt);
|
|
|
|
|
+ paymentCountdownMs = remaining > 0 ? remaining : PAYMENT_TIMEOUT_MS;
|
|
|
|
|
+ persistPaymentState();
|
|
|
|
|
+ await tick();
|
|
|
|
|
+ await renderPaymentQrCode(paymentCode);
|
|
|
|
|
+ startPaymentCountdown(startedAt);
|
|
|
|
|
+ startPaymentPolling(immediatePoll);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function restorePersistedPayment() {
|
|
|
|
|
+ if (!browser) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const raw = localStorage.getItem(PAYMENT_STORAGE_KEY);
|
|
|
|
|
+ if (!raw) return;
|
|
|
|
|
+ const stored = JSON.parse(raw);
|
|
|
|
|
+ if (!stored?.paymentId || !stored?.paymentCode) {
|
|
|
|
|
+ removePaymentStateFromStorage();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const startedAt = stored.startedAt ?? Date.now();
|
|
|
|
|
+ const remaining = PAYMENT_TIMEOUT_MS - (Date.now() - startedAt);
|
|
|
|
|
+ if (remaining <= 0) {
|
|
|
|
|
+ removePaymentStateFromStorage();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await openPaymentModalWithPayment(
|
|
|
|
|
+ { payment_id: stored.paymentId, payment_code: stored.paymentCode },
|
|
|
|
|
+ startedAt,
|
|
|
|
|
+ false
|
|
|
|
|
+ );
|
|
|
|
|
+ paymentStatusMessage = 'Retomamos a verificação do pagamento pendente.';
|
|
|
|
|
+ startPaymentPolling(true);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[CPR] Não foi possível restaurar o pagamento pendente', err);
|
|
|
|
|
+ removePaymentStateFromStorage();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function handleCopyPaymentCode() {
|
|
|
|
|
+ if (!browser || !paymentCode) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(paymentCode);
|
|
|
|
|
+ paymentCopyFeedback = 'Código Pix copiado!';
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ paymentCopyFeedback = 'Não foi possível copiar automaticamente. Copie manualmente.';
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ stopPaymentCopyTimeout();
|
|
|
|
|
+ if (browser) {
|
|
|
|
|
+ paymentCopyTimeoutId = window.setTimeout(() => {
|
|
|
|
|
+ paymentCopyFeedback = '';
|
|
|
|
|
+ paymentCopyTimeoutId = null;
|
|
|
|
|
+ }, 2000);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async function fetchHistory() {
|
|
async function fetchHistory() {
|
|
|
historyLoading = true;
|
|
historyLoading = true;
|
|
|
historyError = '';
|
|
historyError = '';
|
|
@@ -515,6 +840,13 @@
|
|
|
|
|
|
|
|
onMount(() => {
|
|
onMount(() => {
|
|
|
void fetchHistory();
|
|
void fetchHistory();
|
|
|
|
|
+ void restorePersistedPayment();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ onDestroy(() => {
|
|
|
|
|
+ stopPaymentPolling();
|
|
|
|
|
+ stopPaymentCountdown();
|
|
|
|
|
+ stopPaymentCopyTimeout();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
function getMissingRequiredFields() {
|
|
function getMissingRequiredFields() {
|
|
@@ -616,14 +948,24 @@
|
|
|
|
|
|
|
|
const serverStatus = response?.status?.toLowerCase?.() ?? '';
|
|
const serverStatus = response?.status?.toLowerCase?.() ?? '';
|
|
|
if (!res.ok || (response?.status && serverStatus !== 'ok')) {
|
|
if (!res.ok || (response?.status && serverStatus !== 'ok')) {
|
|
|
- throw new Error(response?.msg ?? 'Falha ao registrar CPR.');
|
|
|
|
|
|
|
+ console.error('[CPR] Falha ao registrar CPR', {
|
|
|
|
|
+ status: res.status,
|
|
|
|
|
+ statusText: res.statusText,
|
|
|
|
|
+ raw,
|
|
|
|
|
+ response
|
|
|
|
|
+ });
|
|
|
|
|
+ throw new Error((response?.msg ?? raw) || 'Falha ao registrar CPR.');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
submitSuccess = response?.msg ?? 'CPR registrada com sucesso.';
|
|
submitSuccess = response?.msg ?? 'CPR registrada com sucesso.';
|
|
|
|
|
+ const paymentData = response?.data;
|
|
|
cprForm = createInitialForm();
|
|
cprForm = createInitialForm();
|
|
|
repeatingGroups = createInitialRepeatingGroups();
|
|
repeatingGroups = createInitialRepeatingGroups();
|
|
|
- activeTab = 4;
|
|
|
|
|
|
|
+ if (paymentData?.payment_id && paymentData?.payment_code) {
|
|
|
|
|
+ await openPaymentModalWithPayment(paymentData);
|
|
|
|
|
+ }
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
|
|
+ console.error('[CPR] Erro inesperado ao registrar CPR', err);
|
|
|
submitError = err?.message ?? 'Falha ao registrar CPR.';
|
|
submitError = err?.message ?? 'Falha ao registrar CPR.';
|
|
|
} finally {
|
|
} finally {
|
|
|
isSubmitting = false;
|
|
isSubmitting = false;
|
|
@@ -827,4 +1169,96 @@
|
|
|
{/if}
|
|
{/if}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {#if paymentModalVisible}
|
|
|
|
|
+ <div class="fixed inset-0 z-40 flex items-center justify-center bg-black/60 px-4">
|
|
|
|
|
+ <div class="w-full max-w-xl rounded-2xl bg-white dark:bg-gray-900 p-6 shadow-2xl relative">
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="absolute top-4 right-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
|
|
|
|
+ aria-label="Fechar"
|
|
|
|
|
+ on:click={handleCancelPaymentFlow}
|
|
|
|
|
+ >
|
|
|
|
|
+ ✕
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div class="space-y-4">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-sm uppercase tracking-wider text-blue-500 font-semibold">Pagamento pendente</p>
|
|
|
|
|
+ <h3 class="text-2xl font-bold text-gray-900 dark:text-white">Finalize a CPR via Pix</h3>
|
|
|
|
|
+ <p class="text-sm text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ Utilize o QR Code ou copie o código Pix para concluir o pagamento. O QR Code expira em
|
|
|
|
|
+ <span class="font-semibold">{formatCountdown(paymentCountdownMs)}</span>.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex flex-col gap-6 md:flex-row">
|
|
|
|
|
+ <div class="flex-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/80 p-4 flex flex-col items-center gap-4">
|
|
|
|
|
+ <div class="text-center">
|
|
|
|
|
+ <p class="text-xs uppercase tracking-wider text-gray-500">QR Code Pix</p>
|
|
|
|
|
+ <p class="text-sm text-gray-600 dark:text-gray-300">Escaneie com o app do banco</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="w-56 h-56 flex items-center justify-center bg-white rounded-xl shadow-inner"
|
|
|
|
|
+ bind:this={paymentQrContainer}
|
|
|
|
|
+ aria-label="QR Code do pagamento"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="w-full rounded-lg bg-blue-600 hover:bg-blue-700 text-white py-2 text-sm font-semibold transition disabled:opacity-60"
|
|
|
|
|
+ on:click={handleCopyPaymentCode}
|
|
|
|
|
+ disabled={!paymentCode}
|
|
|
|
|
+ >
|
|
|
|
|
+ {paymentCopyFeedback ? paymentCopyFeedback : 'Copiar código Pix'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex-1 space-y-4">
|
|
|
|
|
+ <div class="rounded-lg border border-amber-200 bg-amber-50 text-amber-900 px-3 py-2 text-sm">
|
|
|
|
|
+ <p class="font-semibold text-amber-900">Tempo restante</p>
|
|
|
|
|
+ <p class="text-3xl font-mono tracking-widest">{formatCountdown(paymentCountdownMs)}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="space-y-2">
|
|
|
|
|
+ <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Código Pix copia e cola</label>
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ class="w-full text-sm rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-2 resize-none"
|
|
|
|
|
+ rows="4"
|
|
|
|
|
+ readonly
|
|
|
|
|
+ value={paymentCode}
|
|
|
|
|
+ ></textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {#if paymentStatusMessage}
|
|
|
|
|
+ <div class="rounded-lg border border-blue-200 bg-blue-50 text-blue-900 px-3 py-2 text-sm">
|
|
|
|
|
+ {paymentStatusMessage}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/if}
|
|
|
|
|
+ {#if paymentError}
|
|
|
|
|
+ <div class="rounded-lg border border-red-200 bg-red-50 text-red-900 px-3 py-2 text-sm">
|
|
|
|
|
+ {paymentError}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/if}
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex flex-wrap gap-2">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="flex-1 min-w-[140px] inline-flex items-center justify-center rounded-lg border border-gray-300 dark:border-gray-700 px-3 py-2 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-60"
|
|
|
|
|
+ on:click={handleRetryPaymentCheck}
|
|
|
|
|
+ disabled={isCheckingPayment}
|
|
|
|
|
+ >
|
|
|
|
|
+ {isCheckingPayment ? 'Verificando...' : 'Reverificar agora'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="flex-1 min-w-[140px] inline-flex items-center justify-center rounded-lg border border-red-300 px-3 py-2 text-sm font-semibold text-red-600 hover:bg-red-50"
|
|
|
|
|
+ on:click={handleCancelPaymentFlow}
|
|
|
|
|
+ >
|
|
|
|
|
+ Cancelar pagamento
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/if}
|
|
|
</div>
|
|
</div>
|