diff --git a/src/components/Holos/DocumentSection/ClientSection.vue b/src/components/Holos/DocumentSection/ClientSection.vue
index e6e52d6..1a789be 100644
--- a/src/components/Holos/DocumentSection/ClientSection.vue
+++ b/src/components/Holos/DocumentSection/ClientSection.vue
@@ -20,25 +20,35 @@ const props = defineProps({
DATOS FISCALES CLIENTE
-
+
Serie:
{{ data.serie }}
@@ -68,13 +68,17 @@ const getTipoFolioLabel = (documentType) => {
Fecha de Emisión:
{{ data.fechaEmision }}
+
+ Fecha de Remisión:
+ {{ data.fechaRemision }}
+
Fecha de Realización:
{{ data.fechaRealizacion }}
-
- Vigencia::
+
+ Vigencia:
{{ data.vigencia }}
diff --git a/src/components/Holos/DocumentSection/ProductsTableView.vue b/src/components/Holos/DocumentSection/ProductsTableView.vue
index e92e441..a3d28ab 100644
--- a/src/components/Holos/DocumentSection/ProductsTableView.vue
+++ b/src/components/Holos/DocumentSection/ProductsTableView.vue
@@ -1,4 +1,6 @@
-
+
+
+
+ | CANT |
+ UNIDAD |
+ CODIGO |
+ DESCRIPCION |
+ CANTIDAD |
+ PRECIO UNIT. |
+
+
+
+
+ | {{ producto.cantidad }} |
+ {{ producto.unidad || producto.unidadSAT }} |
+ {{ producto.codigo }} |
+ {{ producto.descripcion }} |
+ {{ formatCurrency(producto.precioUnitario) }} |
+ {{ formatCurrency(producto.cantidad * producto.precioUnitario) }} |
+
+
+
+
+import { ref, computed, watch } from 'vue';
+import GoogleIcon from '@Shared/GoogleIcon.vue';
+
+const props = defineProps({
+ modelValue: {
+ type: Array,
+ default: () => []
+ }
+});
+
+const emit = defineEmits(['update:modelValue', 'update:totals']);
+
+const productos = ref([...props.modelValue]);
+
+const unidades = [
+ { codigo: 'PIEZA', nombre: 'Pieza' },
+ { codigo: 'CAJA', nombre: 'Caja' },
+ { codigo: 'PAQUETE', nombre: 'Paquete' },
+ { codigo: 'KG', nombre: 'Kilogramo' },
+ { codigo: 'METRO', nombre: 'Metro' },
+ { codigo: 'LITRO', nombre: 'Litro' },
+];
+
+/**
+ * Agregar nuevo producto
+ */
+const agregarProducto = () => {
+ productos.value.push({
+ cantidad: 1,
+ unidad: 'PIEZA',
+ codigo: '',
+ descripcion: '',
+ precioUnitario: 0,
+ });
+ actualizarProductos();
+};
+
+/**
+ * Eliminar producto
+ */
+const eliminarProducto = (index) => {
+ productos.value.splice(index, 1);
+ actualizarProductos();
+};
+
+/**
+ * Calcular importe total
+ */
+const calcularImporte = (producto) => {
+ return producto.cantidad * producto.precioUnitario;
+};
+
+/**
+ * Calcular totales generales
+ */
+const calculos = computed(() => {
+ const total = productos.value.reduce((sum, producto) => {
+ return sum + calcularImporte(producto);
+ }, 0);
+
+ return {
+ total,
+ subtotal: total,
+ iva: 0,
+ descuentoTotal: 0,
+ };
+});
+
+/**
+ * Actualizar productos y emitir cambios
+ */
+const actualizarProductos = () => {
+ emit('update:modelValue', productos.value);
+ emit('update:totals', calculos.value);
+};
+
+// Watch para actualizar cuando cambian los productos
+watch(productos, () => {
+ actualizarProductos();
+}, { deep: true });
+
+// Watch para sincronizar con el v-model externo
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ if (JSON.stringify(newVal) !== JSON.stringify(productos.value)) {
+ productos.value = [...newVal];
+ }
+ },
+ { deep: true }
+);
+
+/**
+ * Formatear moneda
+ */
+const formatCurrency = (value) => {
+ return new Intl.NumberFormat('es-MX', {
+ style: 'currency',
+ currency: 'MXN'
+ }).format(value || 0);
+};
+
+
+
+
+
+
+
+
+ Productos / Servicios
+
+
+ Listado de productos en la remisión
+
+
+
+
+
+
+
+
+
+
+
+
+ #{{ index + 1 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Importe:
+
+ {{ formatCurrency(calcularImporte(producto)) }}
+
+
+
+
+
+
+
+
+
+
No hay productos agregados
+
Haz clic en "Agregar Producto" para comenzar
+
+
+
+
+
+
+
+
+ Total:
+
+
+ {{ formatCurrency(calculos.total) }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/composables/useDocument.js b/src/composables/useDocument.js
new file mode 100644
index 0000000..84d8961
--- /dev/null
+++ b/src/composables/useDocument.js
@@ -0,0 +1,464 @@
+import { ref, computed, watch } from 'vue';
+import { useForm, useApi } from '@/services/Api';
+import { documentService } from '@/services/documentService';
+
+/**
+ * Composable para manejar documentos
+ *
+ */
+export function useDocument(config = {}) {
+ // ============================================
+ // ESTADO
+ // ============================================
+ const documentId = ref(config.documentId || null);
+ const documentType = ref(config.documentType || 'COTIZACION');
+ const isLoading = ref(false);
+ const isSaving = ref(false);
+ const error = ref(null);
+
+ // Documento completo
+ const document = ref(null);
+
+ // Formulario de datos
+ const formData = ref({});
+ const branding = ref({
+ documentType: documentType.value,
+ primaryColor: '#2c50dd',
+ slogan: '',
+ logoUrl: null,
+ logoPreview: null
+ });
+ const productos = ref([]);
+ const totales = ref({
+ // Totales principales
+ subtotal: 0,
+ iva: 0,
+ total: 0,
+
+ // Totales adicionales
+ subtotal1: 0,
+ descuentoTotal: 0,
+ subtotal2: 0,
+ impuestosTrasladados: 0
+ });
+
+ // ============================================
+ // COMPUTED
+ // ============================================
+
+ /**
+ * Transforma datos
+ */
+ const documentPayload = computed(() => {
+ return {
+ tipo: documentType.value,
+ estado: 'BORRADOR',
+ templateConfig: {
+ primaryColor: branding.value.primaryColor,
+ logoUrl: branding.value.logoUrl,
+ slogan: branding.value.slogan
+ },
+ datos: {
+ // Datos de la empresa
+ empresa: {
+ nombre: formData.value.empresaNombre || '',
+ rfc: formData.value.empresaRFC || '',
+ email: formData.value.empresaEmail || '',
+ telefono: formData.value.empresaTelefono || '',
+ direccion: formData.value.empresaDireccion || '',
+ web: formData.value.empresaWeb || '',
+
+ // Campos específicos para facturas (CFDI)
+ lugar: formData.value.empresaLugar || '',
+ cfdi: formData.value.empresaCfdi || '',
+ regimen: formData.value.empresaRegimen || ''
+ },
+
+ // Datos bancarios (para cotizaciones)
+ bancos: {
+ banco: formData.value.bancoBanco || '',
+ tipoCuenta: formData.value.bancoTipoCuenta || '',
+ cuenta: formData.value.bancoCuenta || ''
+ },
+
+ // Datos del cliente
+ cliente: {
+ nombre: formData.value.clienteNombre || '',
+ rfc: formData.value.clienteRFC || '',
+ domicilio: formData.value.clienteDomicilio || '',
+ telefono: formData.value.clienteTelefono || '',
+
+ // Campo específico para facturas
+ regimen: formData.value.clienteRegimen || ''
+ },
+
+ // Datos del ejecutivo (para cotizaciones)
+ ejecutivo: {
+ nombre: formData.value.ejecutivoNombre || '',
+ correo: formData.value.ejecutivoCorreo || '',
+ celular: formData.value.ejecutivoCelular || ''
+ },
+
+ // Detalles del documento
+ documento: {
+ // Común para todos
+ folio: formData.value.folio || '',
+ observaciones: formData.value.observaciones || '',
+
+ // Específico para cotizaciones
+ fechaRealizacion: formData.value.fechaRealizacion || '',
+ vigencia: formData.value.vigencia || '',
+
+ // Específico para facturas
+ serie: formData.value.serie || '',
+ fechaEmision: formData.value.fechaEmision || '',
+ tipoComprobante: formData.value.tipoComprobante || '',
+
+ // Específico para remisiones
+ fechaRemision: formData.value.fechaRemision || ''
+ },
+
+ // Productos y totales
+ productos: productos.value,
+ totales: totales.value
+ }
+ };
+ });
+
+ // ============================================
+ // MÉTODOS
+ // ============================================
+
+ /**
+ * Cargar documento existente
+ */
+ const loadDocument = async (id) => {
+ isLoading.value = true;
+ error.value = null;
+
+ try {
+ const config = documentService.getById(id);
+ const api = useApi();
+
+ await api.load({
+ ...config,
+ options: {
+ onSuccess: (data) => {
+ document.value = data;
+ hydrateForm(data);
+ },
+ onFail: (err) => {
+ error.value = err.message || 'Error al cargar documento';
+ }
+ }
+ });
+ } catch (err) {
+ error.value = err.message;
+ } finally {
+ isLoading.value = false;
+ }
+ };
+
+ /**
+ * Hidratar formulario con datos del servidor
+ *
+ * Llena todos los campos del formulario con los datos de un documento cargado
+ *
+ */
+ const hydrateForm = (documentData) => {
+ documentType.value = documentData.tipo;
+
+ // Actualizar branding/template
+ branding.value = {
+ primaryColor: documentData.templateConfig.primaryColor || '#2c50dd',
+ slogan: documentData.templateConfig.slogan || '',
+ logoUrl: documentData.templateConfig.logoUrl || null,
+ logoPreview: documentData.templateConfig.logoUrl || null
+ };
+
+ // Llenar todos los campos del formulario
+ formData.value = {
+ // Datos de la empresa
+ empresaNombre: documentData.datos.empresa.nombre || '',
+ empresaRFC: documentData.datos.empresa.rfc || '',
+ empresaEmail: documentData.datos.empresa.email || '',
+ empresaTelefono: documentData.datos.empresa.telefono || '',
+ empresaDireccion: documentData.datos.empresa.direccion || '',
+ empresaWeb: documentData.datos.empresa.web || '',
+
+ // Campos específicos de facturas
+ empresaLugar: documentData.datos.empresa.lugar || '',
+ empresaCfdi: documentData.datos.empresa.cfdi || '',
+ empresaRegimen: documentData.datos.empresa.regimen || '',
+
+ // Datos bancarios (puede no existir en facturas)
+ bancoBanco: documentData.datos.bancos?.banco || '',
+ bancoTipoCuenta: documentData.datos.bancos?.tipoCuenta || '',
+ bancoCuenta: documentData.datos.bancos?.cuenta || '',
+
+ // Datos del cliente
+ clienteNombre: documentData.datos.cliente.nombre || '',
+ clienteRFC: documentData.datos.cliente.rfc || '',
+ clienteDomicilio: documentData.datos.cliente.domicilio || '',
+ clienteTelefono: documentData.datos.cliente.telefono || '',
+ clienteRegimen: documentData.datos.cliente.regimen || '',
+
+ // Datos del ejecutivo (puede no existir en facturas)
+ ejecutivoNombre: documentData.datos.ejecutivo?.nombre || '',
+ ejecutivoCorreo: documentData.datos.ejecutivo?.correo || '',
+ ejecutivoCelular: documentData.datos.ejecutivo?.celular || '',
+
+ // Detalles del documento
+ folio: documentData.datos.documento?.folio || '',
+ observaciones: documentData.datos.documento?.observaciones || '',
+
+ // Específico para cotizaciones
+ fechaRealizacion: documentData.datos.documento?.fechaRealizacion || '',
+ vigencia: documentData.datos.documento?.vigencia || '',
+
+ // Específico para facturas
+ serie: documentData.datos.documento?.serie || '',
+ fechaEmision: documentData.datos.documento?.fechaEmision || '',
+ tipoComprobante: documentData.datos.documento?.tipoComprobante || '',
+
+ // Específico para remisiones
+ fechaRemision: documentData.datos.documento?.fechaRemision || '',
+ };
+
+ // Productos y totales
+ productos.value = documentData.datos.productos || [];
+ totales.value = documentData.datos.totales || {
+ subtotal: 0,
+ iva: 0,
+ total: 0
+ };
+ };
+
+ /**
+ * Inicializar formulario vacío desde configuración de template
+ *
+ */
+ const initializeForm = (templateConfig) => {
+ const data = {};
+
+ templateConfig.campos.forEach((seccion) => {
+ seccion.campos.forEach((campo) => {
+ data[campo.key] = campo.defaultValue || '';
+ });
+ });
+
+ formData.value = data;
+ };
+
+ /**
+ * Guardar documento (crear o actualizar)
+ */
+ const saveDocument = async (options = {}) => {
+ isSaving.value = true;
+ error.value = null;
+
+ try {
+ const config = documentService.save(
+ documentPayload.value,
+ documentId.value
+ );
+
+ const form = useForm(documentPayload.value);
+
+ await form.load({
+ ...config,
+ options: {
+ onSuccess: (data) => {
+ document.value = data;
+ documentId.value = data.id;
+
+ if (options.onSuccess) {
+ options.onSuccess(data);
+ }
+ },
+ onFail: (err) => {
+ error.value = err.message || 'Error al guardar documento';
+
+ if (options.onFail) {
+ options.onFail(err);
+ }
+ }
+ }
+ });
+ } catch (err) {
+ error.value = err.message;
+ } finally {
+ isSaving.value = false;
+ }
+ };
+
+ /**
+ * Auto-guardar (debounced)
+ */
+ let autoSaveTimeout = null;
+ const autoSave = () => {
+ clearTimeout(autoSaveTimeout);
+ autoSaveTimeout = setTimeout(() => {
+ saveDocument();
+ }, 2000); // Guardar 2 segundos después del último cambio
+ };
+
+ /**
+ * Subir logo
+ *
+ * (usa FileReader temporalmente)
+ *
+ */
+ const uploadLogo = async (file) => {
+ if (!file || !(file instanceof File)) {
+ error.value = 'Archivo inválido';
+ return;
+ }
+
+ isLoading.value = true;
+ error.value = null;
+
+ try {
+ // Cuando el backend esté listo, descomentar esto
+ /*
+ const config = documentService.uploadLogo(file);
+ const form = useForm({ logo: file });
+
+ await form.load({
+ ...config,
+ options: {
+ onSuccess: (data) => {
+ branding.value.logoUrl = data.logoUrl;
+ branding.value.logoPreview = data.logoUrl;
+ },
+ onFail: (failData) => {
+ error.value = failData.message || 'Error al subir logo';
+ }
+ }
+ });
+ */
+
+ // FALLBACK: Mientras no hay backend, usar FileReader
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ branding.value.logoPreview = e.target.result;
+ branding.value.logo = file; // Guardar el archivo original
+ };
+
+ reader.onerror = () => {
+ error.value = 'Error al leer el archivo de imagen';
+ };
+
+ reader.readAsDataURL(file);
+ } catch (err) {
+ error.value = err.message || 'Error al procesar logo';
+ console.error('Error uploadLogo:', err);
+ } finally {
+ isLoading.value = false;
+ }
+ };
+
+ /**
+ * Generar PDF del documento
+ *
+ * Si el documento no está guardado, lo guarda primero
+ * y verifica que el guardado sea exitoso antes de generar PDF
+ */
+ const generatePDF = async () => {
+ isLoading.value = true;
+ error.value = null;
+
+ try {
+ // Si no hay documentId, guardar primero
+ if (!documentId.value) {
+ // Guardar y esperar confirmación
+ await new Promise((resolve, reject) => {
+ saveDocument({
+ onSuccess: () => {
+ resolve();
+ },
+ onFail: (failData) => {
+ reject(new Error(failData.message || 'Error al guardar documento'));
+ }
+ });
+ });
+
+ // Verificar que ahora sí tenemos documentId
+ if (!documentId.value) {
+ throw new Error('No se pudo obtener el ID del documento guardado');
+ }
+ }
+
+ // Ahora generar PDF
+ const config = documentService.generatePDF(documentId.value);
+ const api = useApi();
+
+ await api.load({
+ ...config,
+ options: {
+ onSuccess: (data) => {
+ if (data.pdfUrl) {
+ window.open(data.pdfUrl, '_blank');
+ } else {
+ error.value = 'No se recibió la URL del PDF';
+ }
+ },
+ onFail: (failData) => {
+ error.value = failData.message || 'Error al generar PDF';
+ }
+ }
+ });
+ } catch (err) {
+ error.value = err.message || 'Error al generar PDF';
+ console.error('Error generatePDF:', err);
+ } finally {
+ isLoading.value = false;
+ }
+ };
+
+ // ============================================
+ // WATCHERS
+ // ============================================
+
+ // Sincronizar documentType con branding.documentType
+ watch(documentType, (newType) => {
+ branding.value.documentType = newType;
+ });
+
+ // Auto-save cuando cambian los datos (solo si ya existe documentId)
+ watch([formData, productos, totales], () => {
+ if (documentId.value) {
+ autoSave();
+ }
+ }, { deep: true });
+
+ // ============================================
+ // RETORNO
+ // ============================================
+
+ return {
+ // Estado
+ documentId,
+ documentType,
+ document,
+ formData,
+ branding,
+ productos,
+ totales,
+ isLoading,
+ isSaving,
+ error,
+
+ // Computed
+ documentPayload,
+
+ // Métodos
+ initializeForm,
+ loadDocument,
+ saveDocument,
+ uploadLogo,
+ generatePDF,
+ autoSave
+ };
+}
\ No newline at end of file
diff --git a/src/pages/Templates/Configs/ConfigRemision.js b/src/pages/Templates/Configs/ConfigRemision.js
new file mode 100644
index 0000000..985f022
--- /dev/null
+++ b/src/pages/Templates/Configs/ConfigRemision.js
@@ -0,0 +1,95 @@
+export default{
+ templateId: 'temp-rem-001',
+ nombre: 'Remisión',
+
+ branding: {
+ logo: null,
+ primaryColor: '#2c50dd',
+ slogan: ' ',
+ },
+
+ campos:[
+ {
+ seccion: 'Datos de la Empresa',
+ campos: [
+ {
+ key: 'empresaNombre',
+ label: 'Nombre de la Empresa',
+ tipo: 'text',
+ required: true,
+ placeholder: 'Ej: GOLSYSTEMS',
+ },
+ {
+ key: 'empresaRFC',
+ label: 'RFC',
+ tipo: 'text',
+ required: true,
+ placeholder: 'Ej: ABC123456789',
+ },
+ {
+ key: 'empresaDireccion',
+ label: 'Dirección',
+ tipo: 'textarea',
+ required: true,
+ placeholder: 'Dirección completa',
+ },
+ ],
+ },
+ {
+ seccion : 'Datos del Cliente',
+ campos: [
+ {
+ key: 'clienteNombre',
+ label: 'Nombre del Cliente',
+ tipo: 'text',
+ required: true,
+ placeholder: 'Ej: Juan Pérez',
+ },
+ {
+ key: 'clienteRFC',
+ label: 'RFC',
+ tipo: 'text',
+ required: false,
+ placeholder: 'Ej: ABC123456789',
+ },
+ {
+ key: 'clienteTelefono',
+ label: 'Teléfono',
+ tipo: 'text',
+ required: false,
+ placeholder: 'Ej: 555-123-4567',
+ },
+ {
+ key: 'clienteDomicilio',
+ label: 'Dirección',
+ tipo: 'textarea',
+ required: true,
+ placeholder: 'Dirección completa',
+ },
+ ]
+ },
+ {
+ seccion: 'Detalles del Documento',
+ campos: [
+ {
+ key: 'serie',
+ label: 'Número de Serie',
+ tipo: 'text',
+ required: true,
+ },
+ {
+ key: 'folio',
+ label: 'Número de Folio',
+ tipo: 'text',
+ required: true,
+ },
+ {
+ key: 'fechaRemision',
+ label: 'Fecha',
+ tipo: 'date',
+ required: true,
+ },
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/pages/Templates/Form.vue b/src/pages/Templates/Form.vue
index a319fe7..0c9706c 100644
--- a/src/pages/Templates/Form.vue
+++ b/src/pages/Templates/Form.vue
@@ -1,44 +1,59 @@
+
+
+
+
+
+ Cargando documento...
+
+
+
+
+
+
+
@@ -126,6 +187,12 @@ onMounted(() => {
Genera documento PDF
+
+ · ID: {{ documentId }}
+
+
+ · Guardando...
+
@@ -149,6 +216,17 @@ onMounted(() => {
+
+
+
+ {{ isSaving ? "Guardando..." : "Guardar" }}
+
+
+
{
-
+
\ No newline at end of file
diff --git a/src/services/documentService.js b/src/services/documentService.js
new file mode 100644
index 0000000..e95a1e0
--- /dev/null
+++ b/src/services/documentService.js
@@ -0,0 +1,102 @@
+import { apiURL } from '@/services/Api';
+
+/**
+ * Servicio para gestión de documentos
+ */
+export const documentService = {
+ /**
+ * Guardar documento (crear o actualizar)
+ *
+ */
+ save(payload, documentId = null) {
+ if (!payload || typeof payload !== 'object') {
+ throw new Error('Payload es requerido y debe ser un objeto');
+ }
+
+ if (documentId !== null && (typeof documentId !== 'number' && typeof documentId !== 'string')) {
+ throw new Error('documentId debe ser un número o string');
+ }
+ const method = documentId ? 'put' : 'post';
+ const url = documentId
+ ? apiURL(`documents/${documentId}`)
+ : apiURL('documents');
+
+ return {
+ method,
+ url,
+ data: payload
+ };
+ },
+
+ /**
+ * Obtener documento por ID
+ *
+ */
+ getById(documentId) {
+ return {
+ method: 'get',
+ url: apiURL(`documents/${documentId}`)
+ };
+ },
+
+ /**
+ * Listar documentos con filtros
+ *
+ */
+ list(filters = {}) {
+ return {
+ method: 'get',
+ url: apiURL('documents'),
+ params: filters
+ };
+ },
+
+ /**
+ * Generar PDF del documento
+ *
+ */
+ generatePDF(documentId, options = {}) {
+ if (!documentId) {
+ throw new Error('Id es requerido');
+ }
+
+ const config = {
+ method: 'post',
+ url: apiURL(`documents/${documentId}/generate-pdf`)
+ };
+
+ if (Object.keys(options).length > 0) {
+ config.data = {
+ format: options.format || 'A4',
+ orientation: options.orientation || 'portrait'
+ };
+ }
+
+ return config;
+ },
+
+ /**
+ * Subir logo
+ *
+ */
+ uploadLogo(file) {
+ if (!(file instanceof File)) {
+ throw new Error('El parámetro debe ser un archivo (File)');
+ }
+ return {
+ method: 'post',
+ url: apiURL('documents/upload-logo'),
+ data: { logo: file }
+ };
+ },
+
+ /**
+ * Eliminar documento
+ */
+ delete(documentId) {
+ return {
+ method: 'delete',
+ url: apiURL(`documents/${documentId}`)
+ };
+ }
+};
\ No newline at end of file
diff --git a/src/types/documents.d.ts b/src/types/documents.d.ts
new file mode 100644
index 0000000..52a686d
--- /dev/null
+++ b/src/types/documents.d.ts
@@ -0,0 +1,273 @@
+/**
+ * Tipos y interfaces para el sistema de documentos
+ *
+ * Este archivo define los contratos de datos entre frontend y backend
+ * para el módulo de generación de documentos (Cotizaciones, Facturas, Remisiones)
+ *
+ */
+
+/**
+ * Tipos de documentos soportados
+ */
+export type DocumentType = 'COTIZACION' | 'FACTURA' | 'REMISION';
+
+/**
+ * Estados posibles de un documento
+ */
+export type DocumentStatus = 'BORRADOR' | 'FINALIZADO' | 'ENVIADO' | 'CANCELADO';
+
+/**
+ * Régimen fiscal
+ */
+export type RegimenFiscal =
+ | 'Régimen Simplificado de Confianza'
+ | 'Personas Físicas con Actividades Empresariales y Profesionales';
+
+/**
+ * Tipo de comprobante (para facturas CFDI)
+ */
+export type TipoComprobante = 'Ingreso' | 'Egreso' | 'Traslado';
+
+/**
+ * Configuración de branding/tema del documento
+ */
+export interface DocumentTemplate {
+ documentType?: DocumentType; // Para compatibilidad con código actual
+ primaryColor: string;
+ secondaryColor?: string;
+ logoUrl?: string; // URL del logo almacenado en servidor
+ logo?: string | File; // Temporal para upload (Base64 o File)
+ logoPreview?: string; // Preview en frontend
+ slogan?: string;
+}
+
+/**
+ * Datos de la empresa emisora
+ */
+export interface EmpresaData {
+ nombre: string;
+ rfc: string;
+ email: string;
+ telefono: string;
+ direccion: string;
+ web?: string;
+
+ // Específico para facturas (CFDI)
+ lugar?: string; // Lugar de expedición (código postal)
+ cfdi?: string; // Uso de CFDI (ej: "G03 - Gastos en general")
+ regimen?: RegimenFiscal; // Régimen fiscal
+}
+
+/**
+ * Datos bancarios (opcional, principalmente para cotizaciones)
+ */
+export interface BancosData {
+ banco?: string;
+ tipoCuenta?: string; // Ej: "Cuenta de cheques"
+ cuenta?: string; // Número de cuenta
+}
+
+/**
+ * Datos del cliente receptor
+ */
+export interface ClienteData {
+ nombre: string;
+ rfc?: string;
+ domicilio?: string;
+ telefono?: string;
+
+ // Específico para facturas (CFDI)
+ regimen?: RegimenFiscal; // Régimen fiscal del cliente
+}
+
+/**
+ * Datos del ejecutivo de ventas (solo cotizaciones)
+ */
+export interface EjecutivoData {
+ nombre?: string;
+ correo?: string;
+ celular?: string;
+}
+
+/**
+ * Detalles específicos del documento
+ */
+export interface DocumentoDetalles {
+ // Común para todos
+ folio?: string;
+ observaciones?: string;
+
+ // Específico para cotizaciones
+ fechaRealizacion?: string;
+ vigencia?: string;
+
+ // Específico para facturas
+ serie?: string;
+ fechaEmision?: string;
+ tipoComprobante?: TipoComprobante;
+
+ // Específico para remisiones
+ fechaRemision?: string;
+}
+
+/**
+ * Producto en la tabla de productos
+ */
+export interface Producto {
+ id?: number; // ID
+ clave?: string; // Clave del producto/servicio
+ descripcion: string;
+ cantidad: number;
+ unidad?: string; // Unidad de medida (ej: "Pieza", "Servicio", "Hora")
+ precioUnitario: number;
+ descuento?: number; // Porcentaje de descuento
+ iva?: number; // Porcentaje de IVA
+
+ // Calculados (pueden venir del frontend o backend)
+ subtotal?: number;
+ total?: number;
+}
+
+/**
+ * Totales calculados del documento
+ */
+export interface Totales {
+ // Totales principales
+ subtotal: number;
+ iva: number;
+ total: number;
+
+ // Totales adicionales (opcionales)
+ subtotal1?: number; // Subtotal antes de descuento
+ descuentoTotal?: number; // Total de descuentos aplicados
+ subtotal2?: number; // Subtotal después de descuento
+ impuestosTrasladados?: number; // Para facturas
+}
+
+/**
+ * Estructura completa de datos del documento en la BD
+ *
+ * Este es el formato JSON que se guarda en la base de datos
+ * y el que el backend enviará/recibirá
+ */
+export interface DocumentRecord {
+ // Identificadores
+ id?: string | number;
+ tipo: DocumentType;
+ estado: DocumentStatus;
+ folio: string;
+
+ // Configuración de template/branding
+ templateConfig: DocumentTemplate;
+
+ // Datos agrupados del documento
+ datos: {
+ empresa: EmpresaData;
+ bancos?: BancosData; // Solo para cotizaciones
+ cliente: ClienteData;
+ ejecutivo?: EjecutivoData; // Solo para cotizaciones
+ documento: DocumentoDetalles;
+ productos: Producto[];
+ totales: Totales;
+ };
+
+ // URLs de archivos generados
+ pdfUrl?: string;
+ logoUrl?: string;
+
+ // Metadatos
+ userId?: string | number; // ID del usuario que creó el documento
+ createdAt?: string;
+ updatedAt?: string;
+}
+
+/**
+ * Payload para crear o actualizar un documento
+ *
+ * Este es el objeto que el frontend envía al backend
+ */
+export interface SaveDocumentPayload {
+ tipo: DocumentType;
+ estado?: DocumentStatus;
+ templateConfig: DocumentTemplate;
+ datos: DocumentRecord['datos'];
+}
+
+/**
+ * Respuesta del backend al guardar un documento
+ */
+export interface SaveDocumentResponse {
+ status: 'success' | 'fail' | 'error';
+ data: DocumentRecord;
+ message?: string;
+}
+
+/**
+ * Payload para generar PDF
+ */
+export interface GeneratePDFPayload {
+ documentId: string | number;
+ options?: {
+ format?: 'A4' | 'Letter';
+ orientation?: 'portrait' | 'landscape';
+ };
+}
+
+/**
+ * Respuesta al generar PDF
+ */
+export interface GeneratePDFResponse {
+ status: 'success' | 'fail' | 'error';
+ data: {
+ pdfUrl: string;
+ };
+ message?: string;
+}
+
+/**
+ * Payload para subir logo
+ */
+export interface UploadLogoPayload {
+ logo: File;
+ documentType?: DocumentType;
+}
+
+/**
+ * Respuesta al subir logo
+ */
+export interface UploadLogoResponse {
+ status: 'success' | 'fail' | 'error';
+ data: {
+ logoUrl: string;
+ };
+ message?: string;
+}
+
+/**
+ * Filtros para listar documentos
+ */
+export interface DocumentFilters {
+ tipo?: DocumentType;
+ estado?: DocumentStatus;
+ fechaInicio?: string;
+ fechaFin?: string;
+ search?: string; // Búsqueda por folio, cliente, etc.
+ page?: number;
+ perPage?: number;
+}
+
+/**
+ * Respuesta de listado de documentos
+ */
+export interface DocumentListResponse {
+ status: 'success' | 'fail' | 'error';
+ data: {
+ data: DocumentRecord[];
+ pagination: {
+ total: number;
+ perPage: number;
+ currentPage: number;
+ lastPage: number;
+ };
+ };
+}
\ No newline at end of file