ADD: Plantilla Doc WIP
This commit is contained in:
parent
d7887d028c
commit
f277c3677a
244
src/components/Holos/ProductTable.vue
Normal file
244
src/components/Holos/ProductTable.vue
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
<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]);
|
||||||
|
|
||||||
|
// Agregar producto vacío
|
||||||
|
const agregarProducto = () => {
|
||||||
|
const nuevoLote = productos.value.length + 1;
|
||||||
|
productos.value.push({
|
||||||
|
lote: nuevoLote,
|
||||||
|
cantidad: 0,
|
||||||
|
unidad: 'PZA',
|
||||||
|
codigo: '',
|
||||||
|
descripcion: '',
|
||||||
|
precioUnitario: 0,
|
||||||
|
descuento: 0
|
||||||
|
});
|
||||||
|
actualizarProductos();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Eliminar producto
|
||||||
|
const eliminarProducto = (index) => {
|
||||||
|
productos.value.splice(index, 1);
|
||||||
|
// Reordenar lotes
|
||||||
|
productos.value.forEach((producto, i) => {
|
||||||
|
producto.lote = i + 1;
|
||||||
|
});
|
||||||
|
actualizarProductos();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calcular importe de un producto
|
||||||
|
const calcularImporte = (producto) => {
|
||||||
|
return producto.cantidad * producto.precioUnitario;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calcular total de un producto (importe - descuento)
|
||||||
|
const calcularTotal = (producto) => {
|
||||||
|
return calcularImporte(producto) - producto.descuento;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cálculos generales
|
||||||
|
const calculos = computed(() => {
|
||||||
|
const subtotal1 = productos.value.reduce((sum, p) => sum + calcularImporte(p), 0);
|
||||||
|
const descuentoTotal = productos.value.reduce((sum, p) => sum + p.descuento, 0);
|
||||||
|
const subtotal2 = subtotal1 - descuentoTotal;
|
||||||
|
const iva = subtotal2 * 0.16; // 16% IVA
|
||||||
|
const total = subtotal2 + iva;
|
||||||
|
|
||||||
|
return {
|
||||||
|
subtotal1,
|
||||||
|
descuentoTotal,
|
||||||
|
subtotal2,
|
||||||
|
iva,
|
||||||
|
total
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
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">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-primary-dt">
|
||||||
|
Productos / Servicios
|
||||||
|
</h3>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Tabla de productos -->
|
||||||
|
<div class="overflow-x-auto -mx-4 px-4">
|
||||||
|
<table class="w-full text-sm" v-if="productos.length > 0">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-primary/20">
|
||||||
|
<th class="text-left py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Lote</th>
|
||||||
|
<th class="text-left py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Cant.</th>
|
||||||
|
<th class="text-left py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Unidad</th>
|
||||||
|
<th class="text-left py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Código</th>
|
||||||
|
<th class="text-left py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Descripción</th>
|
||||||
|
<th class="text-right py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">P. Unit.</th>
|
||||||
|
<th class="text-right py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Importe</th>
|
||||||
|
<th class="text-right py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Desc.</th>
|
||||||
|
<th class="text-right py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Total</th>
|
||||||
|
<th class="text-center py-2 px-2 font-semibold text-gray-700 dark:text-primary-dt">Acción</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(producto, index) in productos"
|
||||||
|
:key="index"
|
||||||
|
class="border-b border-gray-100 dark:border-primary/10 hover:bg-gray-50 dark:hover:bg-primary/5"
|
||||||
|
>
|
||||||
|
<td class="py-2 px-2">
|
||||||
|
<span class="text-gray-600 dark:text-primary-dt/70">{{ producto.lote }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2">
|
||||||
|
<input
|
||||||
|
v-model.number="producto.cantidad"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="w-16 px-2 py-1 border border-gray-300 rounded text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2">
|
||||||
|
<select
|
||||||
|
v-model="producto.unidad"
|
||||||
|
class="w-20 px-2 py-1 border border-gray-300 rounded text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
>
|
||||||
|
<option value="PZA">PZA</option>
|
||||||
|
<option value="KG">KG</option>
|
||||||
|
<option value="LT">LT</option>
|
||||||
|
<option value="M">M</option>
|
||||||
|
<option value="SRV">SRV</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2">
|
||||||
|
<input
|
||||||
|
v-model="producto.codigo"
|
||||||
|
type="text"
|
||||||
|
class="w-24 px-2 py-1 border border-gray-300 rounded text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
placeholder="Código"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2">
|
||||||
|
<input
|
||||||
|
v-model="producto.descripcion"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-2 py-1 border border-gray-300 rounded text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
placeholder="Descripción del producto"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2">
|
||||||
|
<input
|
||||||
|
v-model.number="producto.precioUnitario"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
class="w-24 px-2 py-1 border border-gray-300 rounded text-sm text-right dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2 text-right text-gray-600 dark:text-primary-dt/70 whitespace-nowrap">
|
||||||
|
{{ formatCurrency(calcularImporte(producto)) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2">
|
||||||
|
<input
|
||||||
|
v-model.number="producto.descuento"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
class="w-24 px-2 py-1 border border-gray-300 rounded text-sm text-right dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2 text-right font-semibold text-gray-900 dark:text-primary-dt whitespace-nowrap">
|
||||||
|
{{ formatCurrency(calcularTotal(producto)) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-2 text-center">
|
||||||
|
<button
|
||||||
|
@click="eliminarProducto(index)"
|
||||||
|
type="button"
|
||||||
|
class="text-red-500 hover:text-red-700 dark:text-red-400"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Estado vacío -->
|
||||||
|
<div v-else class="text-center py-8 text-gray-500 dark:text-primary-dt/70">
|
||||||
|
<GoogleIcon name="inventory_2" class="text-4xl mb-2 opacity-50" />
|
||||||
|
<p class="text-sm">No hay productos agregados</p>
|
||||||
|
<p class="text-xs">Haz clic en "Agregar Producto" para comenzar</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resumen de totales -->
|
||||||
|
<div v-if="productos.length > 0" class="mt-4 flex justify-end">
|
||||||
|
<div class="w-64 space-y-2 border-t border-gray-200 dark:border-primary/20 pt-3">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-600 dark:text-primary-dt/70">Subtotal 1:</span>
|
||||||
|
<span class="font-semibold dark:text-primary-dt">{{ formatCurrency(calculos.subtotal1) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-600 dark:text-primary-dt/70">Descuento:</span>
|
||||||
|
<span class="font-semibold dark:text-primary-dt">{{ formatCurrency(calculos.descuentoTotal) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-600 dark:text-primary-dt/70">Subtotal 2:</span>
|
||||||
|
<span class="font-semibold dark:text-primary-dt">{{ formatCurrency(calculos.subtotal2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-600 dark:text-primary-dt/70">IVA (16%):</span>
|
||||||
|
<span class="font-semibold dark:text-primary-dt">{{ formatCurrency(calculos.iva) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-base border-t border-gray-200 dark:border-primary/20 pt-2">
|
||||||
|
<span class="font-bold text-gray-900 dark:text-primary-dt">Total:</span>
|
||||||
|
<span class="font-bold text-blue-600 dark:text-blue-400">{{ formatCurrency(calculos.total) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { RouterLink } from 'vue-router';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
template: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow dark:bg-primary-d dark:border-primary/20">
|
|
||||||
<!-- Icono -->
|
|
||||||
<div class="flex items-center justify-center w-16 h-16 rounded-lg mb-4 bg-blue-100 dark:bg-blue-900/30">
|
|
||||||
<GoogleIcon
|
|
||||||
:name="template.icono"
|
|
||||||
class="text-3xl text-blue-600 dark:text-blue-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Información -->
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-2 dark:text-primary-dt">
|
|
||||||
{{ template.nombre }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p class="text-sm text-gray-600 mb-4 dark:text-primary-dt/70">
|
|
||||||
{{ template.descripcion }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Botón CON router -->
|
|
||||||
<RouterLink
|
|
||||||
:to="{ name: 'admin.templates.form', params: { id: template.id } }"
|
|
||||||
class="block"
|
|
||||||
>
|
|
||||||
<button class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
|
|
||||||
Usar Plantilla
|
|
||||||
</button>
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { ref } from 'vue';
|
|
||||||
import ConfigCotizacion from '../Configs/ConfigCotizacion'
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'holos_templates';
|
|
||||||
|
|
||||||
export function useTemplateStorage() {
|
|
||||||
const templates = ref([]);
|
|
||||||
|
|
||||||
const loadTemplates = () => {
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
|
||||||
templates.value = stored ? JSON.parse(stored) : getDefaultTemplates();
|
|
||||||
console.log('Templates cargados:', templates.value); // DEBUG
|
|
||||||
return templates.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDefaultTemplates = () => {
|
|
||||||
const defaults = [
|
|
||||||
{
|
|
||||||
id: 'temp-cot-001',
|
|
||||||
nombre: 'Cotización IP',
|
|
||||||
descripcion: 'Plantilla de cotización para productos y servicios tecnológicos',
|
|
||||||
componente: 'TempCot',
|
|
||||||
config: ConfigCotizacion,
|
|
||||||
icono: 'description',
|
|
||||||
color: 'blue',
|
|
||||||
activa: true,
|
|
||||||
fechaCreacion: new Date().toISOString()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
console.log('Default templates:', defaults);
|
|
||||||
return defaults;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTemplateById = (id) => {
|
|
||||||
if (templates.value.length === 0) loadTemplates();
|
|
||||||
return templates.value.find(t => t.id === id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
templates,
|
|
||||||
loadTemplates,
|
|
||||||
getTemplateById
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,69 +1,174 @@
|
|||||||
export default {
|
export default {
|
||||||
templateId: 'temp-cot-001',
|
templateId: 'temp-cot-001',
|
||||||
nombre: 'Cotización IP',
|
nombre: 'Cotización',
|
||||||
|
|
||||||
branding: {
|
branding: {
|
||||||
logo: null,
|
logo: null,
|
||||||
primaryColor: '#dc2626',
|
primaryColor: '#dc2626',
|
||||||
secondaryColor: '#1e40af',
|
slogan: ' ',
|
||||||
logoPartA: 'GOL',
|
|
||||||
logoPartB: 'SYSTEMS'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
campos: [
|
campos: [
|
||||||
{
|
|
||||||
seccion: 'Información del Documento',
|
|
||||||
campos: [
|
|
||||||
{
|
|
||||||
key: 'folio',
|
|
||||||
label: 'Número de Folio',
|
|
||||||
tipo: 'text',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'COT-2025-001'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'fechaRealizacion',
|
|
||||||
label: 'Fecha de Realización',
|
|
||||||
tipo: 'date',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'vigencia',
|
|
||||||
label: 'Vigencia',
|
|
||||||
tipo: 'text',
|
|
||||||
required: true,
|
|
||||||
placeholder: '30 días'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
seccion: 'Datos de la Empresa',
|
seccion: 'Datos de la Empresa',
|
||||||
campos: [
|
campos: [
|
||||||
{
|
{
|
||||||
key: 'empresaNombre',
|
key: 'empresaNombre',
|
||||||
label: 'Nombre',
|
label: 'Nombre de la Empresa',
|
||||||
tipo: 'text',
|
tipo: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: 'GOLSYSTEMS'
|
placeholder: 'Ej: GOLSYSTEMS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'empresaWeb',
|
key: 'empresaWeb',
|
||||||
label: 'Sitio Web',
|
label: 'Sitio Web',
|
||||||
tipo: 'url',
|
tipo: 'url',
|
||||||
defaultValue: 'www.golsystems.com'
|
required: false,
|
||||||
|
placeholder: 'www.ejemplo.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'empresaEmail',
|
key: 'empresaEmail',
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
tipo: 'email',
|
tipo: 'email',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: 'contacto@golsystems.com'
|
placeholder: 'contacto@ejemplo.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'empresaTelefono',
|
key: 'empresaTelefono',
|
||||||
label: 'Teléfono',
|
label: 'Teléfono',
|
||||||
tipo: 'tel',
|
tipo: 'tel',
|
||||||
required: true
|
required: true,
|
||||||
|
placeholder: '+52 999 123 4567',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'empresaRFC',
|
||||||
|
label: 'RFC',
|
||||||
|
tipo: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'GME111116GJA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'empresaDireccion',
|
||||||
|
label: 'Dirección',
|
||||||
|
tipo: 'textarea',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Dirección completa',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seccion: 'Datos Bancarios',
|
||||||
|
campos: [
|
||||||
|
{
|
||||||
|
key: 'bancoBanco',
|
||||||
|
label: 'Banco',
|
||||||
|
tipo: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'Banco Nacional',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bancoTipoCuenta',
|
||||||
|
label: 'Tipo de Cuenta',
|
||||||
|
tipo: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'Cuenta de cheques',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bancoCuenta',
|
||||||
|
label: 'Número de Cuenta',
|
||||||
|
tipo: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: '1234567890',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seccion: 'Datos del Cliente',
|
||||||
|
campos: [
|
||||||
|
{
|
||||||
|
key: 'clienteNombre',
|
||||||
|
label: 'Nombre del Cliente',
|
||||||
|
tipo: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Nombre completo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clienteRFC',
|
||||||
|
label: 'RFC',
|
||||||
|
tipo: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'RFC del cliente',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clienteDomicilio',
|
||||||
|
label: 'Domicilio',
|
||||||
|
tipo: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'Dirección',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clienteTelefono',
|
||||||
|
label: 'Teléfono',
|
||||||
|
tipo: 'tel',
|
||||||
|
required: false,
|
||||||
|
placeholder: '+52 999 123 4567',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seccion: 'Datos del Ejecutivo',
|
||||||
|
campos: [
|
||||||
|
{
|
||||||
|
key: 'ejecutivoNombre',
|
||||||
|
label: 'Nombre del Ejecutivo',
|
||||||
|
tipo: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Nombre completo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ejecutivoCorreo',
|
||||||
|
label: 'Correo',
|
||||||
|
tipo: 'email',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'ejecutivo@ejemplo.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ejecutivoCelular',
|
||||||
|
label: 'Celular',
|
||||||
|
tipo: 'tel',
|
||||||
|
required: true,
|
||||||
|
placeholder: '+52 999 123 4567',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seccion: 'Detalles del Documento',
|
||||||
|
campos: [
|
||||||
|
{
|
||||||
|
key: 'folio',
|
||||||
|
label: 'Número de Folio',
|
||||||
|
tipo: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: '17016',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fechaRealizacion',
|
||||||
|
label: 'Fecha de Realización',
|
||||||
|
tipo: 'date',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vigencia',
|
||||||
|
label: 'Vigencia',
|
||||||
|
tipo: 'date',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'observaciones',
|
||||||
|
label: 'Observaciones',
|
||||||
|
tipo: 'textarea',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'Observaciones adicionales',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,120 +1,203 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import { useTemplateStorage } from '@Pages/Templates/Composables/useTemplateStorage';
|
|
||||||
import { usePDFExport } from '@Pages/Templates/Configs/usePDFExport';
|
import { usePDFExport } from '@Pages/Templates/Configs/usePDFExport';
|
||||||
|
import ConfigCotizacion from '@Pages/Templates/Configs/ConfigCotizacion';
|
||||||
|
|
||||||
import TempCot from './Temp-Cot.vue';
|
import TempCot from './Temp-Cot.vue';
|
||||||
|
import ProductTable from '@Holos/ProductTable.vue';
|
||||||
import Input from '@Holos/Form/Input.vue';
|
import Input from '@Holos/Form/Input.vue';
|
||||||
import Textarea from '@Holos/Form/Textarea.vue';
|
import Textarea from '@Holos/Form/Textarea.vue';
|
||||||
import PrimaryButton from '@Holos/Button/Primary.vue';
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
/** Composables */
|
/** Composables */
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const { getTemplateById } = useTemplateStorage();
|
|
||||||
const { exportToPDF, isExporting } = usePDFExport();
|
const { exportToPDF, isExporting } = usePDFExport();
|
||||||
|
|
||||||
|
/** Usar configuración importada */
|
||||||
|
const templateConfig = ConfigCotizacion;
|
||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const template = ref(null);
|
|
||||||
const formData = ref({});
|
const formData = ref({});
|
||||||
|
const branding = ref({
|
||||||
|
documentType: 'COTIZACION',
|
||||||
|
...templateConfig.branding
|
||||||
|
});
|
||||||
|
|
||||||
|
const productos = ref([]);
|
||||||
|
const totales = ref({
|
||||||
|
subtotal1: 0,
|
||||||
|
descuentoTotal: 0,
|
||||||
|
subtotal2: 0,
|
||||||
|
iva: 0,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
/** Computed */
|
/** Computed */
|
||||||
const cotizacionData = computed(() => ({
|
const cotizacionData = computed(() => {
|
||||||
...formData.value,
|
return {
|
||||||
template: template.value?.config.branding,
|
...formData.value,
|
||||||
// Mock de productos para ejemplo
|
template: {
|
||||||
productos: [
|
documentType: branding.value.documentType,
|
||||||
{
|
primaryColor: branding.value.primaryColor,
|
||||||
lote: 1,
|
secondaryColor: branding.value.secondaryColor,
|
||||||
cantidad: 10,
|
logo: branding.value.logoPreview,
|
||||||
unidad: 'PZA',
|
slogan: branding.value.slogan
|
||||||
codigo: 'SW-001',
|
|
||||||
descripcion: 'Licencia de Software Empresarial',
|
|
||||||
precioUnitario: 1500,
|
|
||||||
descuento: 100
|
|
||||||
},
|
},
|
||||||
{
|
productos: productos.value,
|
||||||
lote: 2,
|
...totales.value
|
||||||
cantidad: 5,
|
};
|
||||||
unidad: 'PZA',
|
});
|
||||||
codigo: 'HW-002',
|
|
||||||
descripcion: 'Servidor Dell PowerEdge',
|
|
||||||
precioUnitario: 2500,
|
|
||||||
descuento: 200
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Cálculos automáticos
|
|
||||||
subtotal1: 27500,
|
|
||||||
descuentoTotal: 300,
|
|
||||||
subtotal2: 27200,
|
|
||||||
iva: 4352,
|
|
||||||
total: 31552
|
|
||||||
}));
|
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
const initializeForm = () => {
|
const initializeForm = () => {
|
||||||
if (!template.value) return;
|
|
||||||
|
|
||||||
const data = {};
|
const data = {};
|
||||||
template.value.config.campos.forEach(seccion => {
|
templateConfig.campos.forEach(seccion => {
|
||||||
seccion.campos.forEach(campo => {
|
seccion.campos.forEach(campo => {
|
||||||
data[campo.key] = campo.defaultValue || '';
|
data[campo.key] = campo.defaultValue || '';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
formData.value = data;
|
formData.value = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogoUpload = (file) => {
|
||||||
|
if (!file) {
|
||||||
|
console.warn('No se seleccionó archivo');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Archivo seleccionado:', file.name, file.type);
|
||||||
|
branding.value.logo = file;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
branding.value.logoPreview = e.target.result;
|
||||||
|
};
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTotalsUpdate = (newTotals) => {
|
||||||
|
totales.value = { ...newTotals };
|
||||||
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
exportToPDF('template-preview', `${template.value.nombre}.pdf`);
|
exportToPDF('template-preview', `${branding.value.documentType}-${formData.value.folio || 'documento'}.pdf`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Ciclos */
|
/** Ciclos */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
template.value = getTemplateById(route.params.id);
|
initializeForm();
|
||||||
console.log('Template cargado:', template.value); // DEBUG
|
|
||||||
|
|
||||||
if (template.value) {
|
|
||||||
initializeForm();
|
|
||||||
} else {
|
|
||||||
Notify.error('Plantilla no encontrada');
|
|
||||||
router.push({ name: 'admin.templates.index' });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- Breadcrumb -->
|
<!-- Header -->
|
||||||
<div class="mb-6">
|
<div class="mb-6 flex items-center justify-between gap-4">
|
||||||
<RouterLink
|
<div>
|
||||||
:to="{ name: 'admin.templates.index' }"
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-primary-dt">
|
||||||
class="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-blue-600 transition-colors dark:text-primary-dt/70"
|
{{ templateConfig.nombre }}
|
||||||
>
|
</h2>
|
||||||
<GoogleIcon name="arrow_back" class="text-lg" />
|
<p class="text-sm text-gray-600 dark:text-primary-dt/70">
|
||||||
Volver a plantillas
|
Personaliza y genera documentos profesionales
|
||||||
</RouterLink>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selector de Tipo de Documento -->
|
||||||
|
<div class="min-w-[250px]">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-1">
|
||||||
|
Tipo de Documento
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="COTIZACION">Cotización</option>
|
||||||
|
<option value="FACTURA">Factura</option>
|
||||||
|
<option value="REMISION">Remisión</option>
|
||||||
|
<option value="NOTA DE VENTA">Nota de Venta</option>
|
||||||
|
<option value="PRESUPUESTO">Presupuesto</option>
|
||||||
|
<option value="ORDEN DE COMPRA">Orden de Compra</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="template" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<!-- FORMULARIO (Columna Izquierda) -->
|
<!-- FORMULARIO -->
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-between">
|
<!-- SECCIÓN DE BRANDING -->
|
||||||
<div>
|
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border-2 border-blue-200 p-4 dark:from-blue-900/20 dark:to-indigo-900/20 dark:border-blue-700">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-primary-dt">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
{{ template.nombre }}
|
<GoogleIcon name="palette" class="text-blue-600 dark:text-blue-400" />
|
||||||
</h2>
|
<h3 class="text-base font-semibold text-gray-900 dark:text-primary-dt">
|
||||||
<p class="text-sm text-gray-600 dark:text-primary-dt/70">
|
Personalización del Documento
|
||||||
{{ template.descripcion }}
|
</h3>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<!-- Slogan -->
|
||||||
|
<Input
|
||||||
|
v-model="branding.slogan"
|
||||||
|
title="Slogan de la Empresa"
|
||||||
|
type="text"
|
||||||
|
placeholder="Optimizando lasTIC's en las empresas"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Logo de Imagen -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
||||||
|
Logo de la Empresa
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Preview del logo -->
|
||||||
|
<div v-if="branding.logoPreview" class="mb-3 flex items-center gap-3 p-3 bg-white rounded border dark:bg-primary-d dark:border-primary/20">
|
||||||
|
<img :src="branding.logoPreview" alt="Logo" class="h-16 object-contain" />
|
||||||
|
<button
|
||||||
|
@click="branding.logoPreview = null; branding.logo = null"
|
||||||
|
type="button"
|
||||||
|
class="ml-auto text-red-500 hover:text-red-700 dark:text-red-400"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input para subir logo -->
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg, image/jpg"
|
||||||
|
@change="(e) => handleLogoUpload(e.target.files[0])"
|
||||||
|
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 dark:file:bg-blue-900/30 dark:file:text-blue-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colores -->
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-1">
|
||||||
|
Color Primario
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="branding.primaryColor"
|
||||||
|
type="color"
|
||||||
|
class="h-10 w-16 rounded border border-gray-300 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="branding.primaryColor"
|
||||||
|
type="text"
|
||||||
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
placeholder="#2563eb"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Secciones del formulario -->
|
<!-- Secciones del formulario -->
|
||||||
<div
|
<div
|
||||||
v-for="seccion in template.config.campos"
|
v-for="seccion in templateConfig.campos"
|
||||||
:key="seccion.seccion"
|
:key="seccion.seccion"
|
||||||
class="bg-white rounded-lg border border-gray-200 p-4 dark:bg-primary-d dark:border-primary/20"
|
class="bg-white rounded-lg border border-gray-200 p-4 dark:bg-primary-d dark:border-primary/20"
|
||||||
>
|
>
|
||||||
@ -147,6 +230,12 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabla de Productos -->
|
||||||
|
<ProductTable
|
||||||
|
v-model="productos"
|
||||||
|
@update:totals="handleTotalsUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Botón de exportar (móvil) -->
|
<!-- Botón de exportar (móvil) -->
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@ -160,14 +249,13 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VISTA PREVIA (Columna Derecha) -->
|
<!-- VISTA PREVIA -->
|
||||||
<div class="sticky top-6 h-fit">
|
<div class="sticky top-6 h-fit">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<h3 class="font-semibold text-gray-900 dark:text-primary-dt">
|
<h3 class="font-semibold text-gray-900 dark:text-primary-dt">
|
||||||
Vista Previa
|
Vista Previa
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Botón de exportar (desktop) -->
|
|
||||||
<div class="hidden lg:block">
|
<div class="hidden lg:block">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@click="handleExport"
|
@click="handleExport"
|
||||||
@ -180,7 +268,6 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plantilla renderizada -->
|
|
||||||
<div
|
<div
|
||||||
class="border rounded-lg overflow-auto bg-gray-50 dark:bg-gray-900 flex justify-center p-4"
|
class="border rounded-lg overflow-auto bg-gray-50 dark:bg-gray-900 flex justify-center p-4"
|
||||||
style="max-height: 80vh;"
|
style="max-height: 80vh;"
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { useTemplateStorage } from './Composables/useTemplateStorage';
|
|
||||||
|
|
||||||
import TemplateCard from '@Holos/TemplateCard.vue';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
|
|
||||||
/** Composables */
|
|
||||||
const { templates, loadTemplates } = useTemplateStorage();
|
|
||||||
|
|
||||||
/** Ciclos */
|
|
||||||
onMounted(() => {
|
|
||||||
loadTemplates();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-6 max-w-7xl mx-auto">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<GoogleIcon name="description" class="text-3xl text-blue-600" />
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-primary-dt">
|
|
||||||
Plantillas de Documentos
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600 dark:text-primary-dt/70">
|
|
||||||
Selecciona una plantilla para generar tu documento
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Grid de plantillas -->
|
|
||||||
<div v-if="templates.length > 0"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<TemplateCard
|
|
||||||
v-for="template in templates"
|
|
||||||
:key="template.id"
|
|
||||||
:template="template"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Estado vacío -->
|
|
||||||
<div v-else
|
|
||||||
class="flex flex-col items-center justify-center py-16 text-center">
|
|
||||||
<GoogleIcon name="insert_drive_file" class="text-6xl text-gray-300 mb-4" />
|
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2 dark:text-primary-dt">
|
|
||||||
No hay plantillas disponibles
|
|
||||||
</h3>
|
|
||||||
<p class="text-gray-600 dark:text-primary-dt/70">
|
|
||||||
Las plantillas se cargarán automáticamente
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,41 +1,66 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
cotizacion: {
|
cotizacion: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const template = computed(() => props.cotizacion?.template || {
|
||||||
|
documentType: 'COTIZACION',
|
||||||
|
primaryColor: '#2563eb',
|
||||||
|
logo: null,
|
||||||
|
slogan: 'Optimizando lasTIC\'s en las empresas'
|
||||||
|
});
|
||||||
|
|
||||||
const formatCurrency = (value) => {
|
const formatCurrency = (value) => {
|
||||||
return new Intl.NumberFormat("es-MX", {
|
return new Intl.NumberFormat("es-MX", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "MXN",
|
currency: "MXN",
|
||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTipoFolioLabel = (documentType) => {
|
||||||
|
const labels = {
|
||||||
|
'FACTURA': 'Número de Factura:',
|
||||||
|
'REMISION': 'Número de Remisión:',
|
||||||
|
'NOTA DE VENTA': 'Número de Nota:',
|
||||||
|
'ORDEN DE COMPRA': 'Número de Orden:',
|
||||||
|
'PRESUPUESTO': 'Número de Presupuesto:',
|
||||||
|
'COTIZACION': 'Número de Folio:'
|
||||||
|
};
|
||||||
|
return labels[documentType] || 'Número de Folio:';
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bg-white p-4 text-[9px] leading-tight h-full"
|
class="bg-white p-4 text-[9px] leading-tight h-full"
|
||||||
style="width: 100%; font-family: Arial, sans-serif; box-sizing: border-box;"
|
style="width: 100%; font-family: Arial, sans-serif; box-sizing: border-box"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-start border-b-2 border-blue-700 pb-2 mb-3"
|
class="flex justify-between items-start pb-2 mb-3 border-b-2"
|
||||||
|
:style="{ borderColor: template.primaryColor }"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold">
|
<!-- Logo -->
|
||||||
<span :style="{ color: template?.primaryColor }">{{
|
<img
|
||||||
template?.logoPartA
|
v-if="template.logo"
|
||||||
}}</span>
|
:src="template.logo"
|
||||||
<span :style="{ color: template?.secondaryColor }">{{
|
alt="Logo"
|
||||||
template?.logoPartB
|
class="h-12 mb-1"
|
||||||
}}</span>
|
/>
|
||||||
</h1>
|
|
||||||
<img v-if="template?.logo" :src="template.logo" alt="Logo" class="h-8" />
|
<!-- Placeholder si no hay logo -->
|
||||||
<p class="text-gray-600">
|
<div v-else class="h-12 mb-1 flex items-center justify-center bg-gray-100 rounded text-gray-400 text-xs px-3">
|
||||||
Optimizando lasTIC's en las empresas
|
Sin logo
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
|
<!-- Slogan -->
|
||||||
|
<p class="text-gray-600 mt-1">{{ template.slogan }}</p>
|
||||||
|
|
||||||
<div class="mt-2 space-y-0.5">
|
<div class="mt-2 space-y-0.5">
|
||||||
<p class="font-semibold">{{ cotizacion.empresaNombre }}</p>
|
<p class="font-semibold">{{ cotizacion.empresaNombre }}</p>
|
||||||
@ -46,18 +71,25 @@ const formatCurrency = (value) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<h2 class="text-xl font-bold text-blue-600 mb-1">COTIZACION IP</h2>
|
<h2
|
||||||
|
class="text-xl font-bold mb-1"
|
||||||
|
:style="{ color: template.primaryColor }"
|
||||||
|
>
|
||||||
|
{{ template.documentType || "COTIZACION" }} IP
|
||||||
|
</h2>
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<p>
|
<p>
|
||||||
<span class="font-semibold">Numero de Folio:</span>
|
<span class="font-semibold">
|
||||||
|
{{ getTipoFolioLabel(template.documentType) }}
|
||||||
|
</span>
|
||||||
{{ cotizacion.folio }}
|
{{ cotizacion.folio }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="font-semibold">Fecha de Realización:</span>
|
<span class="font-semibold">Fecha de Realización:</span>
|
||||||
{{ cotizacion.fechaRealizacion }}
|
{{ cotizacion.fechaRealizacion }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p v-if="template.documentType !== 'FACTURA'">
|
||||||
<span class="font-semibold">Vigencia de Cotización:</span>
|
<span class="font-semibold">Vigencia:</span>
|
||||||
{{ cotizacion.vigencia }}
|
{{ cotizacion.vigencia }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -81,7 +113,10 @@ const formatCurrency = (value) => {
|
|||||||
|
|
||||||
<!-- Cliente -->
|
<!-- Cliente -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="bg-blue-600 text-white font-semibold px-2 py-0.5">
|
<div
|
||||||
|
class="text-white font-semibold px-2 py-0.5"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
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">
|
||||||
@ -103,10 +138,13 @@ const formatCurrency = (value) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ejecutivo -->
|
<!-- Ejecutivo y Observaciones -->
|
||||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-blue-600 text-white font-semibold px-2 py-0.5">
|
<div
|
||||||
|
class="text-white font-semibold px-2 py-0.5"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
DATOS DEL EJECUTIVO DE CUENTAS
|
DATOS DEL EJECUTIVO DE CUENTAS
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 p-1.5">
|
<div class="border border-gray-300 p-1.5">
|
||||||
@ -126,7 +164,10 @@ const formatCurrency = (value) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-blue-600 text-white font-semibold px-2 py-0.5">
|
<div
|
||||||
|
class="text-white font-semibold px-2 py-0.5"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
OBSERVACIONES
|
OBSERVACIONES
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 p-1.5 min-h-[50px]">
|
<div class="border border-gray-300 p-1.5 min-h-[50px]">
|
||||||
@ -138,7 +179,10 @@ const formatCurrency = (value) => {
|
|||||||
<!-- Tabla de Productos -->
|
<!-- Tabla de Productos -->
|
||||||
<table class="w-full border-collapse mb-3">
|
<table class="w-full border-collapse mb-3">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-blue-600 text-white">
|
<tr
|
||||||
|
class="text-white"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
<th class="border border-white px-1 py-0.5">LOTE</th>
|
<th class="border border-white px-1 py-0.5">LOTE</th>
|
||||||
<th class="border border-white px-1 py-0.5">CANT</th>
|
<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">UNIDAD</th>
|
||||||
@ -156,15 +200,9 @@ const formatCurrency = (value) => {
|
|||||||
:key="producto.lote"
|
:key="producto.lote"
|
||||||
class="odd:bg-blue-100"
|
class="odd:bg-blue-100"
|
||||||
>
|
>
|
||||||
<td class="border border-gray-300 px-1 py-0.5 text-center">
|
<td class="border border-gray-300 px-1 py-0.5 text-center">{{ producto.lote }}</td>
|
||||||
{{ producto.lote }}
|
<td class="border border-gray-300 px-1 py-0.5 text-center">{{ producto.cantidad }}</td>
|
||||||
</td>
|
<td class="border border-gray-300 px-1 py-0.5 text-center">{{ producto.unidad }}</td>
|
||||||
<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 }}
|
|
||||||
</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.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">{{ producto.descripcion }}</td>
|
||||||
<td class="border border-gray-300 px-1 py-0.5 text-right whitespace-nowrap">
|
<td class="border border-gray-300 px-1 py-0.5 text-right whitespace-nowrap">
|
||||||
@ -177,11 +215,7 @@ const formatCurrency = (value) => {
|
|||||||
{{ formatCurrency(producto.descuento) }}
|
{{ formatCurrency(producto.descuento) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="border border-gray-300 px-1 py-0.5 text-right font-semibold whitespace-nowrap">
|
<td class="border border-gray-300 px-1 py-0.5 text-right font-semibold whitespace-nowrap">
|
||||||
{{
|
{{ formatCurrency(producto.cantidad * producto.precioUnitario - producto.descuento) }}
|
||||||
formatCurrency(
|
|
||||||
producto.cantidad * producto.precioUnitario - producto.descuento
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -189,40 +223,73 @@ const formatCurrency = (value) => {
|
|||||||
|
|
||||||
<!-- Totales -->
|
<!-- Totales -->
|
||||||
<div class="flex justify-end mb-3">
|
<div class="flex justify-end mb-3">
|
||||||
<div class="border border-blue-600 w-56">
|
<div
|
||||||
|
class="border w-56"
|
||||||
|
:style="{ borderColor: template.primaryColor }"
|
||||||
|
>
|
||||||
<div class="grid grid-cols-2">
|
<div class="grid grid-cols-2">
|
||||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
<div
|
||||||
|
class="text-white font-semibold px-2 py-1 text-right"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
SUBTOTAL 1:
|
SUBTOTAL 1:
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
<div
|
||||||
|
class="px-2 py-1 text-right whitespace-nowrap border-l"
|
||||||
|
:style="{ borderColor: template.primaryColor }"
|
||||||
|
>
|
||||||
{{ formatCurrency(cotizacion.subtotal1) }}
|
{{ formatCurrency(cotizacion.subtotal1) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
<div
|
||||||
|
class="text-white font-semibold px-2 py-1 text-right"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
DESCUENTO:
|
DESCUENTO:
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
<div
|
||||||
|
class="px-2 py-1 text-right whitespace-nowrap border-l"
|
||||||
|
:style="{ borderColor: template.primaryColor }"
|
||||||
|
>
|
||||||
{{ formatCurrency(cotizacion.descuentoTotal) }}
|
{{ formatCurrency(cotizacion.descuentoTotal) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
<div
|
||||||
|
class="text-white font-semibold px-2 py-1 text-right"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
SUBTOTAL 2:
|
SUBTOTAL 2:
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
<div
|
||||||
|
class="px-2 py-1 text-right whitespace-nowrap border-l"
|
||||||
|
:style="{ borderColor: template.primaryColor }"
|
||||||
|
>
|
||||||
{{ formatCurrency(cotizacion.subtotal2) }}
|
{{ formatCurrency(cotizacion.subtotal2) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
<div
|
||||||
|
class="text-white font-semibold px-2 py-1 text-right"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
I.V.A.
|
I.V.A.
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
<div
|
||||||
|
class="px-2 py-1 text-right whitespace-nowrap border-l"
|
||||||
|
:style="{ borderColor: template.primaryColor }"
|
||||||
|
>
|
||||||
{{ formatCurrency(cotizacion.iva) }}
|
{{ formatCurrency(cotizacion.iva) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-blue-600 text-white font-bold px-2 py-1 text-right">
|
<div
|
||||||
|
class="text-white font-bold px-2 py-1 text-right"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
|
>
|
||||||
TOTAL
|
TOTAL
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l border-blue-600 px-2 py-1 text-right font-bold whitespace-nowrap">
|
<div
|
||||||
|
class="px-2 py-1 text-right font-bold whitespace-nowrap border-l"
|
||||||
|
:style="{ borderColor: template.primaryColor }"
|
||||||
|
>
|
||||||
{{ formatCurrency(cotizacion.total) }}
|
{{ formatCurrency(cotizacion.total) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -235,12 +302,12 @@ const formatCurrency = (value) => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-blue-600 text-white font-semibold px-2 py-0.5 text-center mb-1"
|
class="text-white font-semibold px-2 py-0.5 text-center mb-1"
|
||||||
|
:style="{ backgroundColor: template.primaryColor }"
|
||||||
>
|
>
|
||||||
CERTIFICACIONES Y PARTNERS
|
CERTIFICACIONES Y PARTNERS
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logos de Partners (puedes agregar imágenes reales) -->
|
|
||||||
<div class="flex justify-center items-center gap-4 flex-wrap">
|
<div class="flex justify-center items-center gap-4 flex-wrap">
|
||||||
<div class="text-gray-500">
|
<div class="text-gray-500">
|
||||||
Huawei | Lenovo | Hikvision | Fortinet | Panduit | Honeywell
|
Huawei | Lenovo | Hikvision | Fortinet | Panduit | Honeywell
|
||||||
|
|||||||
@ -374,24 +374,12 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'templates',
|
path: 'templates',
|
||||||
name: 'admin.template',
|
name: 'admin.templates.index',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Plantillas',
|
title: 'Plantillas',
|
||||||
icon: 'templates',
|
icon: 'templates',
|
||||||
},
|
},
|
||||||
redirect: '/admin/templates',
|
component: () => import('@Pages/Templates/Form.vue'),
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'admin.templates.index',
|
|
||||||
component: () => import('@Pages/Templates/Index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/fill',
|
|
||||||
name: 'admin.templates.form',
|
|
||||||
component: () => import('@Pages/Templates/Form.vue'),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user