2026-01-16 17:11:24 -06:00

320 lines
12 KiB
Vue

<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import { apiURL } from '@Services/Api';
import GoogleIcon from '@Shared/GoogleIcon.vue';
import Loader from '@Shared/Loader.vue';
import Input from '@Holos/Form/Input.vue';
import PrimaryButton from '@Holos/Button/Primary.vue';
/** Definidores */
const route = useRoute();
/** Estado */
const loading = ref(true);
const submitting = ref(false);
const submitted = ref(false);
const error = ref(null);
const saleData = ref(null);
const formErrors = ref({});
const form = ref({
name: '',
email: '',
phone: '',
address: '',
rfc: '',
razon_social: '',
regimen_fiscal: '',
cp_fiscal: '',
uso_cfdi: ''
});
/** Computed */
const invoiceNumber = computed(() => route.params.invoiceNumber);
/** Métodos */
const fetchSaleData = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(apiURL(`facturacion/${invoiceNumber.value}`), {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
if (response.status === 404) {
error.value = 'No se encontró la venta con el folio proporcionado.';
} else if (response.status === 400) {
const data = await response.json();
error.value = data.message || 'Esta venta ya fue facturada.';
} else {
error.value = 'Error al obtener los datos de la venta.';
}
return;
}
const data = await response.json();
saleData.value = data.sale;
} catch (err) {
console.error('Error:', err);
error.value = 'Error de conexión. Por favor intente más tarde.';
} finally {
loading.value = false;
}
};
const submitForm = async () => {
submitting.value = true;
formErrors.value = {};
try {
const response = await fetch(apiURL(`facturacion/${invoiceNumber.value}`), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(form.value)
});
const data = await response.json();
if (!response.ok) {
if (data.errors) {
formErrors.value = data.errors;
} else {
error.value = data.message || 'Error al enviar los datos.';
}
return;
}
submitted.value = true;
} catch (err) {
console.error('Error:', err);
error.value = 'Error de conexión. Por favor intente más tarde.';
} finally {
submitting.value = false;
}
};
const formatCurrency = (value) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN'
}).format(value || 0);
};
/** Ciclos */
onMounted(() => {
fetchSaleData();
});
</script>
<template>
<div class="min-h-screen bg-gray-100 dark:bg-gray-900 py-8 px-4">
<div class="max-w-2xl mx-auto">
<!-- Header -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary dark:bg-primary-d mb-4">
<GoogleIcon name="receipt_long" class="text-3xl text-white" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Solicitud de Factura
</h1>
<p class="text-gray-600 dark:text-gray-400 mt-2">
Complete sus datos fiscales para generar su factura
</p>
</div>
<!-- Loading -->
<div v-if="loading" class="flex justify-center py-12">
<Loader />
</div>
<!-- Error -->
<div v-else-if="error" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
<GoogleIcon name="error" class="text-3xl text-red-500" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
No se puede procesar
</h2>
<p class="text-gray-600 dark:text-gray-400">
{{ error }}
</p>
</div>
<!-- Submitted Success -->
<div v-else-if="submitted" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 dark:bg-green-900/30 mb-4">
<GoogleIcon name="check_circle" class="text-3xl text-green-500" />
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Solicitud Enviada
</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Sus datos han sido recibidos correctamente. Recibirá su factura en el correo electrónico proporcionado.
</p>
<p class="text-sm text-gray-500 dark:text-gray-500">
Folio: <span class="font-mono font-semibold">{{ invoiceNumber }}</span>
</p>
</div>
<!-- Form -->
<div v-else class="space-y-6">
<!-- Sale Info Card -->
<div v-if="saleData" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<GoogleIcon name="shopping_cart" class="text-xl" />
Datos de la Venta
</h2>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500 dark:text-gray-400">Folio:</span>
<span class="ml-2 font-semibold text-gray-900 dark:text-white">{{ saleData.invoice_number }}</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Fecha:</span>
<span class="ml-2 font-semibold text-gray-900 dark:text-white">
{{ new Date(saleData.created_at).toLocaleDateString('es-MX') }}
</span>
</div>
<div class="col-span-2">
<span class="text-gray-500 dark:text-gray-400">Total:</span>
<span class="ml-2 font-bold text-lg text-green-600 dark:text-green-400">
{{ formatCurrency(saleData.total) }}
</span>
</div>
</div>
</div>
<!-- Billing Form -->
<form @submit.prevent="submitForm" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<GoogleIcon name="description" class="text-xl" />
Datos de Facturación
</h2>
<div class="grid gap-4 grid-cols-1 md:grid-cols-2">
<Input
v-model="form.name"
id="name"
title="name Completo"
placeholder="Juan Pérez García"
:onError="formErrors.name"
/>
<Input
v-model="form.email"
id="email"
title="Correo Electrónico"
type="email"
placeholder="correo@ejemplo.com"
:onError="formErrors.email"
/>
<Input
v-model="form.phone"
id="phone"
title="Teléfono"
type="tel"
placeholder="55 1234 5678"
:onError="formErrors.phone"
/>
<Input
v-model="form.rfc"
id="rfc"
title="RFC"
placeholder="XAXX010101000"
maxlength="13"
:onError="formErrors.rfc"
/>
<Input
v-model="form.razon_social"
id="razon_social"
title="Razón Social"
placeholder="Como aparece en la Constancia Fiscal"
:onError="formErrors.razon_social"
/>
<Input
v-model="form.regimen_fiscal"
id="regimen_fiscal"
title="Régimen Fiscal"
placeholder="Ej: 601 - General de Ley"
:onError="formErrors.regimen_fiscal"
/>
<Input
v-model="form.cp_fiscal"
id="cp_fiscal"
title="Código Postal Fiscal"
placeholder="06600"
maxlength="5"
:onError="formErrors.cp_fiscal"
/>
<Input
v-model="form.uso_cfdi"
id="uso_cfdi"
title="Uso de CFDI"
placeholder="Ej: G03 - Gastos en general"
:onError="formErrors.uso_cfdi"
/>
<div class="md:col-span-2">
<Input
v-model="form.address"
id="address"
title="Dirección"
placeholder="Calle, Número, Colonia, Ciudad, Estado"
:onError="formErrors.address"
/>
</div>
</div>
<!-- Submit Button -->
<div class="mt-6 flex justify-center">
<PrimaryButton
:class="{ 'opacity-25': submitting }"
:disabled="submitting"
>
<template v-if="submitting">
<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Enviando...
</template>
<template v-else>
<GoogleIcon name="send" class="mr-2" />
Solicitar Factura
</template>
</PrimaryButton>
</div>
<!-- Info Note -->
<p class="mt-4 text-xs text-gray-500 dark:text-gray-400 text-center">
Al enviar este formulario, acepta que sus datos serán utilizados únicamente para la emisión de su factura fiscal.
</p>
</form>
</div>
<!-- Footer -->
<div class="mt-8 text-center text-sm text-gray-500 dark:text-gray-400">
<p>¿Tiene dudas? Contáctenos</p>
</div>
</div>
</div>
</template>