ADD: Remisión
This commit is contained in:
parent
47764891d2
commit
fb0e2e7333
@ -20,24 +20,34 @@ const props = defineProps({
|
|||||||
DATOS FISCALES CLIENTE
|
DATOS FISCALES CLIENTE
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 p-1.5 grid grid-cols-2 gap-x-4">
|
<div class="border border-gray-300 p-1.5 grid grid-cols-2 gap-x-4">
|
||||||
|
<!-- Nombre del Cliente (todos los documentos) -->
|
||||||
<div>
|
<div>
|
||||||
<span class="font-semibold">Nombre:</span>
|
<span class="font-semibold">Nombre:</span>
|
||||||
{{ data.clienteNombre }}
|
{{ data.clienteNombre }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Domicilio (todos los documentos usan el mismo campo) -->
|
||||||
<div>
|
<div>
|
||||||
<span class="font-semibold">Domicilio:</span>
|
<span class="font-semibold">Domicilio:</span>
|
||||||
{{ data.clienteDomicilio }}
|
{{ data.clienteDomicilio }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- RFC (todos los documentos) -->
|
||||||
<div>
|
<div>
|
||||||
<span class="font-semibold">RFC:</span> {{ data.clienteRFC }}
|
<span class="font-semibold">RFC:</span>
|
||||||
|
{{ data.clienteRFC }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="template.documentType === 'COTIZACION'">
|
|
||||||
<span class="font-semibold">Telefono:</span>
|
<!-- Teléfono (COTIZACION y REMISION) -->
|
||||||
|
<div v-if="template.documentType === 'COTIZACION' || template.documentType === 'REMISION'">
|
||||||
|
<span class="font-semibold">Teléfono:</span>
|
||||||
{{ data.clienteTelefono }}
|
{{ data.clienteTelefono }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="template.documentType === 'FACTURA'">
|
|
||||||
<span class="font-semibold">Régimen Fiscal:</span> {{ data.clienteRegimen }}
|
|
||||||
|
|
||||||
|
<!-- Régimen Fiscal (solo FACTURA) -->
|
||||||
|
<div v-if="template.documentType === 'FACTURA'">
|
||||||
|
<span class="font-semibold">Régimen Fiscal:</span>
|
||||||
|
{{ data.clienteRegimen }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,9 +36,7 @@ const props = defineProps({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Factura -->
|
<!-- Factura -->
|
||||||
<div
|
<div v-else-if="template.documentType === 'FACTURA'">
|
||||||
v-else-if="template.documentType === 'FACTURA'"
|
|
||||||
>
|
|
||||||
<div class="border border-gray-300 p-1.5 grid grid-cols-2 gap-4">
|
<div class="border border-gray-300 p-1.5 grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold">{{ data.empresaNombre }}</p>
|
<p class="font-semibold">{{ data.empresaNombre }}</p>
|
||||||
@ -53,5 +51,17 @@ const props = defineProps({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="template.documentType === 'REMISION'"
|
||||||
|
class="grid grid-cols-2 gap-1 mb-2"
|
||||||
|
>
|
||||||
|
<!-- Datos Fiscales -->
|
||||||
|
<div class="border border-gray-300 p-1.5">
|
||||||
|
<p class="font-semibold">{{ data.empresaNombre }}</p>
|
||||||
|
<p>RFC: {{ data.empresaRFC }}</p>
|
||||||
|
<p>{{ data.empresaDireccion }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const getTipoFolioLabel = (documentType) => {
|
|||||||
const labels = {
|
const labels = {
|
||||||
COTIZACION: "Número de Folio:",
|
COTIZACION: "Número de Folio:",
|
||||||
FACTURA: "Folio:",
|
FACTURA: "Folio:",
|
||||||
REMISION: "Número de Remisión:",
|
REMISION: "Folio:",
|
||||||
};
|
};
|
||||||
return labels[documentType] || "Número de Folio:";
|
return labels[documentType] || "Número de Folio:";
|
||||||
};
|
};
|
||||||
@ -54,7 +54,7 @@ const getTipoFolioLabel = (documentType) => {
|
|||||||
{{ template.documentType || "COTIZACION" }}
|
{{ template.documentType || "COTIZACION" }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<p v-if="template.documentType === 'FACTURA'">
|
<p v-if="template.documentType === 'FACTURA' || template.documentType === 'REMISION'">
|
||||||
<span class="font-semibold"> Serie: </span>
|
<span class="font-semibold"> Serie: </span>
|
||||||
{{ data.serie }}
|
{{ data.serie }}
|
||||||
</p>
|
</p>
|
||||||
@ -68,13 +68,17 @@ const getTipoFolioLabel = (documentType) => {
|
|||||||
<span class="font-semibold">Fecha de Emisión:</span>
|
<span class="font-semibold">Fecha de Emisión:</span>
|
||||||
{{ data.fechaEmision }}
|
{{ data.fechaEmision }}
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else-if="template.documentType === 'REMISION'">
|
||||||
|
<span class="font-semibold">Fecha de Remisión:</span>
|
||||||
|
{{ data.fechaRemision }}
|
||||||
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
<span class="font-semibold">Fecha de Realización:</span>
|
<span class="font-semibold">Fecha de Realización:</span>
|
||||||
{{ data.fechaRealizacion }}
|
{{ data.fechaRealizacion }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-if="template.documentType !== 'FACTURA'">
|
<p v-if="template.documentType === 'COTIZACION'">
|
||||||
<span class="font-semibold">Vigencia::</span>
|
<span class="font-semibold">Vigencia:</span>
|
||||||
{{ data.vigencia }}
|
{{ data.vigencia }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="template.documentType === 'FACTURA'">
|
<p v-if="template.documentType === 'FACTURA'">
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
template: {
|
template: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -16,10 +18,36 @@ const formatCurrency = (value) => {
|
|||||||
currency: "MXN",
|
currency: "MXN",
|
||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRemision = computed(() => {
|
||||||
|
return props.template.documentType === 'REMISION';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<table class="w-full border-collapse mb-3">
|
<table v-if="isRemision">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-white" :style="{ backgroundColor: template.primaryColor }">
|
||||||
|
<th class="border border-white px-1 py-0.5">CANT</th>
|
||||||
|
<th class="border border-white px-1 py-0.5">UNIDAD</th>
|
||||||
|
<th class="border border-white px-1 py-0.5">CODIGO</th>
|
||||||
|
<th class="border border-white px-1 py-0.5">DESCRIPCION</th>
|
||||||
|
<th class="border border-white px-1 py-0.5">CANTIDAD</th>
|
||||||
|
<th class="border border-white px-1 py-0.5">PRECIO UNIT.</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="producto in productos" :key="producto.lote" class="odd:bg-blue-100">
|
||||||
|
<td class="border border-gray-300 px-1 py-0.5 text-center">{{ producto.cantidad }}</td>
|
||||||
|
<td class="border border-gray-300 px-1 py-0.5 text-center">{{ producto.unidad || producto.unidadSAT }}</td>
|
||||||
|
<td class="border border-gray-300 px-1 py-0.5">{{ producto.codigo }}</td>
|
||||||
|
<td class="border border-gray-300 px-1 py-0.5">{{ producto.descripcion }}</td>
|
||||||
|
<td class="border border-gray-300 px-1 py-0.5">{{ formatCurrency(producto.precioUnitario) }}</td>
|
||||||
|
<td class="border border-gray-300 px-1 py-0.5">{{ formatCurrency(producto.cantidad * producto.precioUnitario) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table v-else class="w-full border-collapse mb-3">
|
||||||
<thead>
|
<thead>
|
||||||
<tr
|
<tr
|
||||||
class="text-white"
|
class="text-white"
|
||||||
|
|||||||
259
src/components/Holos/DocumentSection/RemisionTable.vue
Normal file
259
src/components/Holos/DocumentSection/RemisionTable.vue
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<script setup>
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-4 dark:bg-primary-d dark:border-primary/20">
|
||||||
|
<!-- ENCABEZADO -->
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-primary-dt">
|
||||||
|
Productos / Servicios
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-primary-dt/60">
|
||||||
|
Listado de productos en la remisión
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="agregarProducto"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="add" class="text-lg" />
|
||||||
|
Agregar Producto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PRODUCTOS -->
|
||||||
|
<div v-if="productos.length > 0" class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(producto, index) in productos"
|
||||||
|
:key="index"
|
||||||
|
class="border-2 border-gray-200 rounded-lg p-4 hover:border-blue-300 transition-colors dark:border-primary/20 dark:hover:border-blue-500"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="bg-blue-100 text-blue-700 font-bold px-3 py-1 rounded text-sm dark:bg-blue-900/30 dark:text-blue-300">
|
||||||
|
#{{ index + 1 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="eliminarProducto(index)"
|
||||||
|
type="button"
|
||||||
|
class="text-red-500 hover:text-red-700 p-2 hover:bg-red-50 rounded-lg transition-colors dark:text-red-400 dark:hover:bg-red-900/20"
|
||||||
|
title="Eliminar producto"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid de campos -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<!-- CANTIDAD -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 mb-1 dark:text-primary-dt">
|
||||||
|
Cantidad <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model.number="producto.cantidad"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UNIDAD -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 mb-1 dark:text-primary-dt">
|
||||||
|
Unidad <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
v-model="producto.unidad"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option v-for="unidad in unidades" :key="unidad.codigo" :value="unidad.codigo">
|
||||||
|
{{ unidad.nombre }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CÓDIGO -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 mb-1 dark:text-primary-dt">
|
||||||
|
Código
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="producto.codigo"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="SKU-001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PRECIO UNITARIO -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 mb-1 dark:text-primary-dt">
|
||||||
|
Precio Unit. <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model.number="producto.precioUnitario"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-right dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DESCRIPCIÓN -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 mb-1 dark:text-primary-dt">
|
||||||
|
Descripción <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="producto.descripcion"
|
||||||
|
rows="2"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt focus:ring-2 focus:ring-blue-500 resize-y"
|
||||||
|
placeholder="Descripción detallada del producto..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IMPORTE -->
|
||||||
|
<div class="mt-3 flex justify-end">
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-xs text-gray-500 dark:text-primary-dt/70">Importe:</span>
|
||||||
|
<div class="text-lg font-bold text-blue-700 dark:text-blue-400">
|
||||||
|
{{ formatCurrency(calcularImporte(producto)) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estado vacío -->
|
||||||
|
<div v-else class="text-center py-12 text-gray-500 dark:text-primary-dt/70">
|
||||||
|
<GoogleIcon name="inventory_2" class="text-5xl mb-3 opacity-50" />
|
||||||
|
<p class="text-sm font-semibold">No hay productos agregados</p>
|
||||||
|
<p class="text-xs">Haz clic en "Agregar Producto" para comenzar</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RESUMEN DE TOTALES -->
|
||||||
|
<div v-if="productos.length > 0" class="mt-6 flex justify-end">
|
||||||
|
<div class="bg-blue-50 rounded-lg p-4 min-w-[250px] dark:bg-blue-900/20">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm font-semibold text-gray-700 dark:text-primary-dt">
|
||||||
|
Total:
|
||||||
|
</span>
|
||||||
|
<span class="text-xl font-bold text-blue-700 dark:text-blue-400">
|
||||||
|
{{ formatCurrency(calculos.total) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
464
src/composables/useDocument.js
Normal file
464
src/composables/useDocument.js
Normal file
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
95
src/pages/Templates/Configs/ConfigRemision.js
Normal file
95
src/pages/Templates/Configs/ConfigRemision.js
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,44 +1,59 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from "vue";
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
import { usePDFExport } from "@Pages/Templates/Configs/usePDFExport";
|
import { usePDFExport } from "@Pages/Templates/Configs/usePDFExport";
|
||||||
|
import { useDocument } from "@/composables/useDocument";
|
||||||
|
|
||||||
import ConfigCotizacion from "@Pages/Templates/Configs/ConfigCotizacion.js";
|
import ConfigCotizacion from "@Pages/Templates/Configs/ConfigCotizacion.js";
|
||||||
import ConfigFacturacion from "@Pages/Templates/Configs/ConfigFacturacion.js";
|
import ConfigFacturacion from "@Pages/Templates/Configs/ConfigFacturacion.js";
|
||||||
import Document from "./DocumentTemplate.vue";
|
import ConfigRemision from "@Pages/Templates/Configs/ConfigRemision.js";
|
||||||
import ProductTable from "@Holos/DocumentSection/CotizacionTable.vue";
|
import Document from "./DocumentTemplate.vue";
|
||||||
import FacturaTable from "@Holos/DocumentSection/FacturacionTable.vue";
|
import ProductTable from "@Holos/DocumentSection/CotizacionTable.vue";
|
||||||
import Input from "@Holos/Form/Input.vue";
|
import FacturaTable from "@Holos/DocumentSection/FacturacionTable.vue";
|
||||||
import Textarea from "@Holos/Form/Textarea.vue";
|
import RemisionTable from "@Holos/DocumentSection/RemisionTable.vue";
|
||||||
import PrimaryButton from "@Holos/Button/Primary.vue";
|
import Input from "@Holos/Form/Input.vue";
|
||||||
import GoogleIcon from "@Shared/GoogleIcon.vue";
|
import Textarea from "@Holos/Form/Textarea.vue";
|
||||||
|
import PrimaryButton from "@Holos/Button/Primary.vue";
|
||||||
|
import GoogleIcon from "@Shared/GoogleIcon.vue";
|
||||||
|
|
||||||
|
/** Props (opcional: para modo edición) */
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'COTIZACION'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/** Composables */
|
/** Composables */
|
||||||
const { exportToPDF, isExporting } = usePDFExport();
|
const { exportToPDF, isExporting } = usePDFExport();
|
||||||
|
|
||||||
/** Usar configuración importada */
|
const {
|
||||||
|
documentId,
|
||||||
|
documentType,
|
||||||
|
formData,
|
||||||
|
branding,
|
||||||
|
productos,
|
||||||
|
totales,
|
||||||
|
isLoading,
|
||||||
|
isSaving,
|
||||||
|
error,
|
||||||
|
initializeForm,
|
||||||
|
loadDocument,
|
||||||
|
saveDocument,
|
||||||
|
uploadLogo,
|
||||||
|
generatePDF
|
||||||
|
} = useDocument({
|
||||||
|
documentId: props.id,
|
||||||
|
documentType: props.type
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Estado Local */
|
||||||
const templateConfig = ref(ConfigCotizacion);
|
const templateConfig = ref(ConfigCotizacion);
|
||||||
const showPreview = ref(true);
|
const showPreview = ref(true);
|
||||||
|
|
||||||
/** Estado */
|
|
||||||
const formData = ref({});
|
|
||||||
const branding = ref({
|
|
||||||
documentType: "COTIZACION",
|
|
||||||
...templateConfig.value.branding,
|
|
||||||
});
|
|
||||||
|
|
||||||
const productos = ref([]);
|
|
||||||
const totales = ref({
|
|
||||||
subtotal1: 0,
|
|
||||||
descuentoTotal: 0,
|
|
||||||
subtotal2: 0,
|
|
||||||
iva: 0,
|
|
||||||
|
|
||||||
subtotal: 0,
|
|
||||||
impuestosTrasladados: 0,
|
|
||||||
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Computed */
|
/** Computed */
|
||||||
const documentData = computed(() => {
|
const documentData = computed(() => {
|
||||||
return {
|
return {
|
||||||
@ -56,31 +71,14 @@ const documentData = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
const initializeForm = () => {
|
const handleLogoUpload = async (file) => {
|
||||||
const data = {};
|
|
||||||
templateConfig.value.campos.forEach((seccion) => {
|
|
||||||
seccion.campos.forEach((campo) => {
|
|
||||||
data[campo.key] = campo.defaultValue || "";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
formData.value = data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogoUpload = (file) => {
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
console.warn("No se seleccionó archivo");
|
console.warn("No se seleccionó archivo");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Archivo seleccionado:", file.name, file.type);
|
console.log("Archivo seleccionado:", file.name, file.type);
|
||||||
branding.value.logo = file;
|
await uploadLogo(file);
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
branding.value.logoPreview = e.target.result;
|
|
||||||
};
|
|
||||||
reader.onerror = (error) => {};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTotalsUpdate = (newTotals) => {
|
const handleTotalsUpdate = (newTotals) => {
|
||||||
@ -94,29 +92,92 @@ const handleExport = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
await saveDocument({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
console.log('Documento guardado:', data);
|
||||||
|
},
|
||||||
|
onFail: (err) => {
|
||||||
|
console.error('Error al guardar:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGeneratePDF = async () => {
|
||||||
|
await generatePDF();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Watchers */
|
||||||
watch(
|
watch(
|
||||||
() => branding.value.documentType,
|
() => branding.value.documentType,
|
||||||
(newType) => {
|
(newType) => {
|
||||||
if (newType === "FACTURA") {
|
if (newType === "FACTURA") {
|
||||||
templateConfig.value = ConfigFacturacion;
|
templateConfig.value = ConfigFacturacion;
|
||||||
|
} else if (newType === "REMISION") {
|
||||||
|
templateConfig.value = ConfigRemision;
|
||||||
} else {
|
} else {
|
||||||
templateConfig.value = ConfigCotizacion;
|
templateConfig.value = ConfigCotizacion;
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeForm();
|
initializeForm(templateConfig.value);
|
||||||
|
|
||||||
productos.value = [];
|
productos.value = [];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Ciclos */
|
/** Ciclos de vida */
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
initializeForm();
|
// Si hay ID, cargar documento existente
|
||||||
|
if (props.id) {
|
||||||
|
await loadDocument(props.id);
|
||||||
|
|
||||||
|
// Actualizar templateConfig según el tipo cargado
|
||||||
|
if (documentType.value === "FACTURA") {
|
||||||
|
templateConfig.value = ConfigFacturacion;
|
||||||
|
} else if (documentType.value === "REMISION") {
|
||||||
|
templateConfig.value = ConfigRemision;
|
||||||
|
} else {
|
||||||
|
templateConfig.value = ConfigCotizacion;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Nuevo documento: inicializar formulario vacío
|
||||||
|
initializeForm(templateConfig.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
|
<!-- Loading overlay -->
|
||||||
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div class="bg-white dark:bg-primary-d rounded-lg p-6 flex items-center gap-3">
|
||||||
|
<GoogleIcon name="hourglass_empty" class="animate-spin text-2xl text-blue-600" />
|
||||||
|
<span class="text-gray-900 dark:text-primary-dt font-medium">
|
||||||
|
Cargando documento...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3 dark:bg-red-900/20 dark:border-red-700"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="error" class="text-red-600 dark:text-red-400 text-xl" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-red-800 dark:text-red-300 font-medium">Error</p>
|
||||||
|
<p class="text-red-600 dark:text-red-400 text-sm">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="error = null"
|
||||||
|
class="text-red-600 hover:text-red-800 dark:text-red-400"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-6 flex items-center justify-between gap-4">
|
<div class="mb-6 flex items-center justify-between gap-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
@ -126,6 +187,12 @@ onMounted(() => {
|
|||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-gray-600 dark:text-primary-dt/70">
|
<p class="text-sm text-gray-600 dark:text-primary-dt/70">
|
||||||
Genera documento PDF
|
Genera documento PDF
|
||||||
|
<span v-if="documentId" class="text-blue-600 dark:text-blue-400">
|
||||||
|
· ID: {{ documentId }}
|
||||||
|
</span>
|
||||||
|
<span v-if="isSaving" class="text-orange-600 dark:text-orange-400">
|
||||||
|
· Guardando...
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -149,6 +216,17 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Botón Guardar -->
|
||||||
|
<PrimaryButton
|
||||||
|
@click="handleSave"
|
||||||
|
:disabled="isSaving"
|
||||||
|
class="px-4 py-2 bg-green-600 hover:bg-green-700"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="save" class="mr-2 text-sm" />
|
||||||
|
{{ isSaving ? "Guardando..." : "Guardar" }}
|
||||||
|
</PrimaryButton>
|
||||||
|
|
||||||
|
<!-- Botón Exportar PDF (cliente) -->
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@click="handleExport"
|
@click="handleExport"
|
||||||
:disabled="isExporting"
|
:disabled="isExporting"
|
||||||
@ -169,7 +247,8 @@ onMounted(() => {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
v-model="branding.documentType"
|
v-model="branding.documentType"
|
||||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-medium dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
:disabled="!!documentId"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-medium dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<option value="COTIZACION">Cotización</option>
|
<option value="COTIZACION">Cotización</option>
|
||||||
<option value="FACTURA">Factura</option>
|
<option value="FACTURA">Factura</option>
|
||||||
@ -231,6 +310,7 @@ onMounted(() => {
|
|||||||
@click="
|
@click="
|
||||||
branding.logoPreview = null;
|
branding.logoPreview = null;
|
||||||
branding.logo = null;
|
branding.logo = null;
|
||||||
|
branding.logoUrl = null;
|
||||||
"
|
"
|
||||||
type="button"
|
type="button"
|
||||||
class="ml-auto text-red-500 hover:text-red-700 dark:text-red-400"
|
class="ml-auto text-red-500 hover:text-red-700 dark:text-red-400"
|
||||||
@ -347,6 +427,12 @@ onMounted(() => {
|
|||||||
@update:totals="handleTotalsUpdate"
|
@update:totals="handleTotalsUpdate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<RemisionTable
|
||||||
|
v-if="branding.documentType === 'REMISION'"
|
||||||
|
v-model="productos"
|
||||||
|
@update:totals="handleTotalsUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
<ProductTable
|
<ProductTable
|
||||||
v-else
|
v-else
|
||||||
v-model="productos"
|
v-model="productos"
|
||||||
|
|||||||
102
src/services/documentService.js
Normal file
102
src/services/documentService.js
Normal file
@ -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}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
273
src/types/documents.d.ts
vendored
Normal file
273
src/types/documents.d.ts
vendored
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user