- 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.
750 lines
38 KiB
Vue
750 lines
38 KiB
Vue
<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> |