Juan Felipe Zapata Moreno 99f190f61b feat: unidades de medida, validación de series y WhatsApp
- Integra selección de unidad y restringe series en cantidades decimales.
- Implementa servicio de mensajería y facturación por WhatsApp.
- Agrega componentes de sidebar y actualiza vistas de inventario.
2026-02-10 00:06:42 -06:00

750 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import { apiURL } from '@Services/Api';
import { formatDate, formatCurrency } from '@/utils/formatters';
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 clientData = ref(null);
const existingInvoiceRequest = ref(null);
const formErrors = ref({});
const searchingRfc = ref(false);
const rfcSearch = ref('');
const rfcSearchError = ref('');
const form = ref({
name: '',
email: '',
phone: '',
address: '',
rfc: '',
razon_social: '',
regimen_fiscal: '',
cp_fiscal: '',
uso_cfdi: ''
});
const paymentMethods = [
{ value: 'cash', label: 'Efectivo' },
{ value: 'credit_card', label: 'Tarjeta de Crédito' },
{ value: 'debit_card', label: 'Tarjeta de Débito' }
];
const usoCfdiOptions = [
{ value: 'G01', label: 'G01 - Adquisición de mercancías' },
{ value: 'G02', label: 'G02 - Devoluciones, descuentos o bonificaciones' },
{ value: 'G03', label: 'G03 - Gastos en general' },
{ value: 'I01', label: 'I01 - Construcciones' },
{ value: 'I02', label: 'I02 - Mobiliario y equipo de oficina por inversiones' },
{ value: 'I03', label: 'I03 - Equipo de transporte' },
{ value: 'I04', label: 'I04 - Equipo de computo y accesorios' },
{ value: 'I05', label: 'I05 - Dados, troqueles, moldes, matrices y herramental' },
{ value: 'I06', label: 'I06 - Comunicaciones telefónicas' },
{ value: 'I07', label: 'I07 - Comunicaciones satelitales' },
{ value: 'I08', label: 'I08 - Otra maquinaria y equipo' },
{ value: 'S01', label: 'S01 - Sin efectos fiscales' }
];
const regimenFiscalOptions = [
{ value: '601', label: '601 - General de Ley Personas Morales' },
{ value: '603', label: '603 - Personas Morales con Fines no Lucrativos' },
{ value: '610', label: '610 - Residentes en el Extranjero sin Establecimiento Permanente en México' },
{ value: '620', label: '620 - Sociedades Cooperativas de Producción que optan por diferir sus ingresos' },
{ value: '622', label: '622 - Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras' },
{ value: '623', label: '623 - Opcional para Grupos de Sociedades' },
{ value: '624', label: '624 - Coordinados' },
{ value: '626', label: '626 - Régimen Simplificado de Confianza' }
];
const paymentMethodLabel = computed(() => {
const method = paymentMethods.find(m => saleData.value?.payment_method?.includes(m.value));
return method?.label || saleData.value?.payment_method || 'N/A';
});
/** Computed */
const invoiceNumber = computed(() => route.params.invoiceNumber);
const hasClient = computed(() => !!clientData.value);
const hasExistingRequest = computed(() => {
if (!saleData.value?.invoice_requests) return false;
return saleData.value.invoice_requests.length > 0;
});
const latestRequest = computed(() => {
if (!hasExistingRequest.value) return null;
return saleData.value.invoice_requests[0];
});
const canRequestInvoice = computed(() => {
if (!latestRequest.value) return true;
return latestRequest.value.status === 'rejected';
});
/** Helpers para mostrar labels legibles */
const getRegimenFiscalLabel = (value) => {
const option = regimenFiscalOptions.find(o => o.value === value);
return option ? option.label : value || 'No registrado';
};
const getUsoCfdiLabel = (value) => {
const option = usoCfdiOptions.find(o => o.value === value);
return option ? option.label : value || 'No registrado';
};
/** Métodos */
const fetchSaleData = () => {
loading.value = true;
error.value = null;
window.axios.get(apiURL(`facturacion/${invoiceNumber.value}`))
.then(({ data }) => {
if (data.status === 'success') {
saleData.value = data.data.sale;
// Si la venta ya tiene un cliente asociado, cargar sus datos
if (data.data.client) {
clientData.value = data.data.client;
fillFormWithClient(data.data.client);
}
}
})
.catch(({ response }) => {
if (response?.status === 404) {
error.value = 'No se encontró la venta con el folio proporcionado.';
} else if (response?.status === 400 || response?.status === 422) {
error.value = response.data?.data?.message || response.data?.message || 'Esta venta ya tiene una solicitud de facturación.';
existingInvoiceRequest.value = response.data?.data?.invoice_request || null;
} else {
error.value = response?.data?.message || 'Error al obtener los datos de la venta.';
}
})
.finally(() => {
loading.value = false;
});
};
/**
* Llenar el formulario con los datos del cliente
*/
const fillFormWithClient = (client) => {
form.value = {
name: client.name || '',
email: client.email || '',
phone: client.phone || '',
address: client.address || '',
rfc: client.rfc || '',
razon_social: client.razon_social || '',
regimen_fiscal: client.regimen_fiscal || '',
cp_fiscal: client.cp_fiscal || '',
uso_cfdi: client.uso_cfdi || ''
};
};
/**
* Buscar cliente por RFC
*/
const searchClientByRfc = () => {
const rfc = rfcSearch.value?.trim().toUpperCase();
if (!rfc) {
rfcSearchError.value = 'Por favor ingrese un RFC';
return;
}
if (rfc.length < 12 || rfc.length > 13) {
rfcSearchError.value = 'El RFC debe tener entre 12 y 13 caracteres';
return;
}
searchingRfc.value = true;
rfcSearchError.value = '';
window.axios.get(apiURL(`facturacion/check-rfc?rfc=${rfc}`))
.then(({ data }) => {
// La respuesta viene: { status: 'success', data: { exists: true, client: {...} } }
if (data.status === 'success' && data.data?.exists && data.data?.client) {
clientData.value = data.data.client;
fillFormWithClient(data.data.client);
rfcSearch.value = '';
window.Notify.success('Cliente encontrado. Verifica los datos antes de continuar.');
} else if (data.status === 'success' && !data.data?.exists) {
rfcSearchError.value = 'No se encontró ningún cliente con este RFC';
window.Notify.warning('RFC no encontrado. Complete el formulario manualmente.');
} else {
rfcSearchError.value = 'No se encontró ningún cliente con este RFC';
}
})
.catch(() => {
rfcSearchError.value = 'Error al buscar el RFC. Intente nuevamente.';
})
.finally(() => {
searchingRfc.value = false;
});
};
/**
* Manejar Enter en el input de búsqueda
*/
const handleSearchKeypress = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
searchClientByRfc();
}
};
/**
* Limpiar datos del cliente y volver al formulario limpio
*/
const clearFoundClient = () => {
clientData.value = null;
rfcSearchError.value = '';
form.value = {
name: '',
email: '',
phone: '',
address: '',
rfc: '',
razon_social: '',
regimen_fiscal: '',
cp_fiscal: '',
uso_cfdi: ''
};
window.Notify.info('Formulario limpio. Puede ingresar nuevos datos.');
};
const submitForm = () => {
submitting.value = true;
formErrors.value = {};
window.axios.post(apiURL(`facturacion/${invoiceNumber.value}`), form.value)
.then(({ data }) => {
submitted.value = true;
})
.catch(({ response }) => {
if (response?.status === 422 && response.data?.errors) {
formErrors.value = response.data.errors;
} else if (response?.data?.data?.errors) {
formErrors.value = response.data.data.errors;
} else {
error.value = response?.data?.message || response?.data?.data?.message || 'Error al enviar los datos.';
}
})
.finally(() => {
submitting.value = false;
});
};
/** 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-3xl 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-xl md: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 con detalles de solicitud existente -->
<div v-else-if="error" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 md: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 mb-4">
{{ error }}
</p>
<div v-if="existingInvoiceRequest" class="mt-6 p-4 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50">
<h3 class="text-sm font-semibold mb-3 text-gray-800 dark:text-gray-200">
Detalles de la solicitud:
</h3>
<div class="text-sm space-y-2 text-gray-700 dark:text-gray-300">
<div class="flex items-center justify-center gap-2">
<span class="font-medium">Estado:</span>
<span class="px-3 py-1 rounded-full text-xs font-semibold">
{{ existingInvoiceRequest.status }}
</span>
</div>
<p>
<span class="font-medium">Solicitada:</span>
<span class="ml-1">{{ formatDate(existingInvoiceRequest.requested_at) }}</span>
</p>
<p v-if="existingInvoiceRequest.processed_at">
<span class="font-medium">Procesada:</span>
<span class="ml-1">{{ formatDate(existingInvoiceRequest.processed_at) }}</span>
</p>
<p v-if="existingInvoiceRequest.notes" class="mt-2 pt-2 border-t border-gray-300 dark:border-gray-600 italic">
{{ existingInvoiceRequest.notes }}
</p>
</div>
</div>
</div>
<!-- Success State -->
<div v-else-if="submitted" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 md: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. Su solicitud será procesada a la brevedad y 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>
<!-- Content -->
<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-1 sm: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 font-mono">{{ 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', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) }}
</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Método de pago:</span>
<span class="ml-2 font-semibold text-gray-900 dark:text-white capitalize">
{{ paymentMethodLabel }}
</span>
</div>
<div>
<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>
<!-- Items de la venta -->
<div v-if="saleData.items && saleData.items.length > 0" class="mt-6">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
Artículos Comprados
</h3>
<div class="space-y-2">
<div v-for="item in saleData.items" :key="item.id"
class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-sm">
<div class="flex justify-between items-start">
<div class="flex-1">
<p class="font-medium text-gray-900 dark:text-white">
{{ item.product_name }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ item.category }} SKU: {{ item.sku }}
</p>
<div v-if="item.serial_numbers && item.serial_numbers.length > 0"
class="mt-1 text-xs text-gray-600 dark:text-gray-400">
<span class="font-medium">Serie(s):</span>
<span v-for="(serial, idx) in item.serial_numbers" :key="idx"
class="ml-1 font-mono">
{{ serial.serial_number }}{{ idx < item.serial_numbers.length - 1 ? ',' : '' }}
</span>
</div>
</div>
<div class="text-right ml-4">
<p class="font-semibold text-gray-900 dark:text-white">
{{ formatCurrency(item.subtotal) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ item.quantity }} × {{ formatCurrency(item.unit_price) }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Historial de solicitudes -->
<div v-if="hasExistingRequest" 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="history" class="text-xl" />
Historial de Solicitudes
</h2>
<div class="space-y-3">
<div v-for="request in saleData.invoice_requests" :key="request.id"
class="p-4 border rounded-lg border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-semibold text-gray-800 dark:text-gray-200">
Solicitud #{{ request.id }}
</span>
<span class="px-3 py-1 rounded-full text-xs font-semibold bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200">
{{ request.status }}
</span>
</div>
<div class="text-xs space-y-1 text-gray-700 dark:text-gray-300">
<p class="flex items-center gap-2">
<GoogleIcon name="event" class="text-sm" />
<span class="font-medium">Solicitada:</span>
<span>{{ formatDate(request.requested_at) }}</span>
</p>
<p v-if="request.processed_at" class="flex items-center gap-2">
<GoogleIcon name="check" class="text-sm" />
<span class="font-medium">Procesada:</span>
<span>{{ formatDate(request.processed_at) }}</span>
</p>
<p v-if="request.notes" class="mt-2 pt-2 border-t border-gray-300 dark:border-gray-600 italic">
<GoogleIcon name="note" class="text-sm inline mr-1" />
{{ request.notes }}
</p>
</div>
</div>
</div>
<div class="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm text-blue-800 dark:text-blue-200">
<GoogleIcon name="info" class="inline mr-1" />
<span v-if="latestRequest?.status === 'pending'">
Su solicitud está siendo procesada. Recibirá su factura por correo electrónico.
</span>
<span v-else-if="latestRequest?.status === 'processed'">
Su factura ya fue emitida y enviada. Revise su correo electrónico.
</span>
<span v-else-if="latestRequest?.status === 'rejected'">
Su solicitud fue rechazada. Puede enviar una nueva solicitud corrigiendo los datos.
</span>
</div>
</div>
<!-- No puede solicitar factura -->
<div v-if="!canRequestInvoice && !submitted"
class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-300 dark:border-yellow-700 rounded-lg p-6 text-center">
<GoogleIcon name="info" class="text-3xl text-yellow-600 dark:text-yellow-400 mb-2" />
<p class="text-yellow-800 dark:text-yellow-200 font-medium">
Ya existe una solicitud de factura en proceso para esta venta.
</p>
<p class="text-sm text-yellow-700 dark:text-yellow-300 mt-2">
No es posible crear una nueva solicitud hasta que la actual sea procesada o rechazada.
</p>
</div>
<!-- Buscador de cliente por RFC -->
<div v-if="canRequestInvoice && !hasClient" class="bg-gradient-to-br from-gray-50 to-indigo-50 dark:from-gray-900/20 dark:to-indigo-900/20 rounded-lg shadow-lg p-6 border border-gray-200 dark:border-gray-800">
<div class="flex items-center gap-2 mb-4">
<GoogleIcon name="person_search" class="text-2xl text-gray-600 dark:text-gray-400" />
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
¿Ya eres cliente?
</h2>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Busca tu RFC para autocompletar tus datos fiscales
</p>
<div class="flex gap-2">
<div class="flex-1">
<input
v-model="rfcSearch"
type="text"
placeholder="Ingresa tu RFC (ej: XAXX010101000)"
maxlength="13"
@keypress="handleSearchKeypress"
:disabled="searchingRfc"
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 uppercase"
/>
</div>
<button
@click="searchClientByRfc"
type="button"
:disabled="searchingRfc || !rfcSearch"
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white rounded-lg transition-colors font-medium flex items-center gap-2 shadow-md hover:shadow-lg"
>
<GoogleIcon
:name="searchingRfc ? 'sync' : 'search'"
:class="{ 'animate-spin': searchingRfc }"
class="text-xl"
/>
<span class="hidden sm:inline">
{{ searchingRfc ? 'Buscando...' : 'Buscar' }}
</span>
</button>
</div>
<p v-if="rfcSearchError" class="mt-3 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
<GoogleIcon name="error" class="text-lg" />
{{ rfcSearchError }}
</p>
<div class="mt-4 p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<p class="text-xs text-blue-800 dark:text-blue-300 flex items-start gap-2">
<GoogleIcon name="info" class="text-sm mt-0.5" />
<span>Si no encuentras tu RFC, no te preocupes. Podrás llenar el formulario manualmente más abajo.</span>
</p>
</div>
</div>
<!-- Cliente encontrado -->
<div v-if="hasClient && canRequestInvoice" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<GoogleIcon name="check_circle" class="text-xl text-green-600 dark:text-green-400" />
Cliente Identificado
</h2>
<button
@click="clearFoundClient"
type="button"
class="text-sm text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 flex items-center gap-1 font-medium"
>
<GoogleIcon name="close" class="text-lg" />
Usar otros datos
</button>
</div>
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 mb-6">
<p class="text-sm text-green-800 dark:text-green-300 flex items-center gap-2">
<GoogleIcon name="verified" class="text-lg" />
<span class="font-medium">Datos fiscales encontrados. Verifica que sean correctos antes de continuar.</span>
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm mb-6">
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">Nombre:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ form.name }}</span>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">RFC:</span>
<span class="font-semibold font-mono text-gray-900 dark:text-white">{{ form.rfc || 'No registrado' }}</span>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">Email:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ form.email || 'No registrado' }}</span>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">Teléfono:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ form.phone || 'No registrado' }}</span>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">Razón Social:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ form.razon_social || 'No registrado' }}</span>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">Régimen Fiscal:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ getRegimenFiscalLabel(form.regimen_fiscal) }}</span>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">C.P. Fiscal:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ form.cp_fiscal || 'No registrado' }}</span>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">Uso CFDI:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ getUsoCfdiLabel(form.uso_cfdi) }}</span>
</div>
<div class="sm:col-span-2 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-gray-500 dark:text-gray-400 block mb-1">Dirección:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ form.address || 'No registrado' }}</span>
</div>
</div>
<div class="flex justify-center">
<PrimaryButton
@click="submitForm"
:class="{ 'opacity-25': submitting }"
:disabled="submitting"
class="px-8 py-3"
>
<template v-if="submitting">
<GoogleIcon name="sync" class="animate-spin mr-2" />
Enviando solicitud...
</template>
<template v-else>
<GoogleIcon name="send" class="mr-2" />
Confirmar y Solicitar Factura
</template>
</PrimaryButton>
</div>
<p class="mt-4 text-xs text-gray-500 dark:text-gray-400 text-center">
La factura se generará con los datos mostrados y se enviará a tu correo electrónico.
</p>
</div>
<!-- Formulario manual -->
<form v-else-if="canRequestInvoice && !hasClient" @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.rfc"
id="rfc"
title="RFC *"
placeholder="XAXX010101000"
maxlength="13"
:onError="formErrors.rfc"
/>
<Input
v-model="form.name"
id="name"
title="Nombre 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.razon_social"
id="razon_social"
title="Razón Social *"
placeholder="Como aparece en la Constancia Fiscal"
:onError="formErrors.razon_social"
/>
<!-- Régimen Fiscal Select -->
<div>
<label for="regimen_fiscal" class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
Régimen Fiscal *
</label>
<select
v-model="form.regimen_fiscal"
id="regimen_fiscal"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
:class="{ 'border-red-500': formErrors.regimen_fiscal }"
>
<option value="" disabled>Seleccionar régimen fiscal</option>
<option
v-for="option in regimenFiscalOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<p v-if="formErrors.regimen_fiscal" class="mt-1 text-xs text-red-600 dark:text-red-400">
{{ formErrors.regimen_fiscal[0] }}
</p>
</div>
<Input
v-model="form.cp_fiscal"
id="cp_fiscal"
title="Código Postal Fiscal *"
placeholder="06600"
maxlength="5"
:onError="formErrors.cp_fiscal"
/>
<!-- Uso CFDI Select -->
<div>
<label for="uso_cfdi" class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
Uso de CFDI *
</label>
<select
v-model="form.uso_cfdi"
id="uso_cfdi"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
:class="{ 'border-red-500': formErrors.uso_cfdi }"
>
<option value="" disabled>Seleccionar uso de CFDI</option>
<option
v-for="option in usoCfdiOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<p v-if="formErrors.uso_cfdi" class="mt-1 text-xs text-red-600 dark:text-red-400">
{{ formErrors.uso_cfdi[0] }}
</p>
</div>
<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>
<div class="mt-6 flex justify-center">
<PrimaryButton
:class="{ 'opacity-25': submitting }"
:disabled="submitting"
>
<template v-if="submitting">
<GoogleIcon name="sync" class="animate-spin mr-2" />
Enviando...
</template>
<template v-else>
<GoogleIcon name="send" class="mr-2" />
Enviar Solicitud de Factura
</template>
</PrimaryButton>
</div>
<p class="mt-4 text-xs text-gray-500 dark:text-gray-400 text-center">
* Campos obligatorios. 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>