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 {
|
||||
templateId: 'temp-cot-001',
|
||||
nombre: 'Cotización IP',
|
||||
nombre: 'Cotización',
|
||||
|
||||
branding: {
|
||||
logo: null,
|
||||
primaryColor: '#dc2626',
|
||||
secondaryColor: '#1e40af',
|
||||
logoPartA: 'GOL',
|
||||
logoPartB: 'SYSTEMS'
|
||||
slogan: ' ',
|
||||
},
|
||||
|
||||
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',
|
||||
campos: [
|
||||
{
|
||||
key: 'empresaNombre',
|
||||
label: 'Nombre',
|
||||
label: 'Nombre de la Empresa',
|
||||
tipo: 'text',
|
||||
required: true,
|
||||
defaultValue: 'GOLSYSTEMS'
|
||||
placeholder: 'Ej: GOLSYSTEMS',
|
||||
},
|
||||
{
|
||||
key: 'empresaWeb',
|
||||
label: 'Sitio Web',
|
||||
tipo: 'url',
|
||||
defaultValue: 'www.golsystems.com'
|
||||
required: false,
|
||||
placeholder: 'www.ejemplo.com',
|
||||
},
|
||||
{
|
||||
key: 'empresaEmail',
|
||||
label: 'Email',
|
||||
tipo: 'email',
|
||||
required: true,
|
||||
defaultValue: 'contacto@golsystems.com'
|
||||
placeholder: 'contacto@ejemplo.com',
|
||||
},
|
||||
{
|
||||
key: 'empresaTelefono',
|
||||
label: 'Teléfono',
|
||||
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>
|
||||
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 ConfigCotizacion from '@Pages/Templates/Configs/ConfigCotizacion';
|
||||
|
||||
import TempCot from './Temp-Cot.vue';
|
||||
import ProductTable from '@Holos/ProductTable.vue';
|
||||
import Input from '@Holos/Form/Input.vue';
|
||||
import Textarea from '@Holos/Form/Textarea.vue';
|
||||
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
|
||||
/** Composables */
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { getTemplateById } = useTemplateStorage();
|
||||
const { exportToPDF, isExporting } = usePDFExport();
|
||||
|
||||
/** Usar configuración importada */
|
||||
const templateConfig = ConfigCotizacion;
|
||||
|
||||
/** Estado */
|
||||
const template = ref(null);
|
||||
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 */
|
||||
const cotizacionData = computed(() => ({
|
||||
const cotizacionData = computed(() => {
|
||||
return {
|
||||
...formData.value,
|
||||
template: template.value?.config.branding,
|
||||
// Mock de productos para ejemplo
|
||||
productos: [
|
||||
{
|
||||
lote: 1,
|
||||
cantidad: 10,
|
||||
unidad: 'PZA',
|
||||
codigo: 'SW-001',
|
||||
descripcion: 'Licencia de Software Empresarial',
|
||||
precioUnitario: 1500,
|
||||
descuento: 100
|
||||
template: {
|
||||
documentType: branding.value.documentType,
|
||||
primaryColor: branding.value.primaryColor,
|
||||
secondaryColor: branding.value.secondaryColor,
|
||||
logo: branding.value.logoPreview,
|
||||
slogan: branding.value.slogan
|
||||
},
|
||||
{
|
||||
lote: 2,
|
||||
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
|
||||
}));
|
||||
productos: productos.value,
|
||||
...totales.value
|
||||
};
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const initializeForm = () => {
|
||||
if (!template.value) return;
|
||||
|
||||
const data = {};
|
||||
template.value.config.campos.forEach(seccion => {
|
||||
templateConfig.campos.forEach(seccion => {
|
||||
seccion.campos.forEach(campo => {
|
||||
data[campo.key] = campo.defaultValue || '';
|
||||
});
|
||||
});
|
||||
|
||||
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 = () => {
|
||||
exportToPDF('template-preview', `${template.value.nombre}.pdf`);
|
||||
exportToPDF('template-preview', `${branding.value.documentType}-${formData.value.folio || 'documento'}.pdf`);
|
||||
};
|
||||
|
||||
/** Ciclos */
|
||||
onMounted(() => {
|
||||
template.value = getTemplateById(route.params.id);
|
||||
console.log('Template cargado:', template.value); // DEBUG
|
||||
|
||||
if (template.value) {
|
||||
initializeForm();
|
||||
} else {
|
||||
Notify.error('Plantilla no encontrada');
|
||||
router.push({ name: 'admin.templates.index' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="mb-6">
|
||||
<RouterLink
|
||||
:to="{ name: 'admin.templates.index' }"
|
||||
class="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-blue-600 transition-colors dark:text-primary-dt/70"
|
||||
>
|
||||
<GoogleIcon name="arrow_back" class="text-lg" />
|
||||
Volver a plantillas
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div v-if="template" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- FORMULARIO (Columna Izquierda) -->
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-primary-dt">
|
||||
{{ template.nombre }}
|
||||
{{ templateConfig.nombre }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-primary-dt/70">
|
||||
{{ template.descripcion }}
|
||||
Personaliza y genera documentos profesionales
|
||||
</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 class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- FORMULARIO -->
|
||||
<div class="space-y-6">
|
||||
<!-- SECCIÓN DE BRANDING -->
|
||||
<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">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<GoogleIcon name="palette" class="text-blue-600 dark:text-blue-400" />
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-primary-dt">
|
||||
Personalización del Documento
|
||||
</h3>
|
||||
</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>
|
||||
|
||||
<!-- Secciones del formulario -->
|
||||
<div
|
||||
v-for="seccion in template.config.campos"
|
||||
v-for="seccion in templateConfig.campos"
|
||||
:key="seccion.seccion"
|
||||
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>
|
||||
|
||||
<!-- Tabla de Productos -->
|
||||
<ProductTable
|
||||
v-model="productos"
|
||||
@update:totals="handleTotalsUpdate"
|
||||
/>
|
||||
|
||||
<!-- Botón de exportar (móvil) -->
|
||||
<div class="lg:hidden">
|
||||
<PrimaryButton
|
||||
@ -160,14 +249,13 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VISTA PREVIA (Columna Derecha) -->
|
||||
<!-- VISTA PREVIA -->
|
||||
<div class="sticky top-6 h-fit">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-primary-dt">
|
||||
Vista Previa
|
||||
</h3>
|
||||
|
||||
<!-- Botón de exportar (desktop) -->
|
||||
<div class="hidden lg:block">
|
||||
<PrimaryButton
|
||||
@click="handleExport"
|
||||
@ -180,7 +268,6 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plantilla renderizada -->
|
||||
<div
|
||||
class="border rounded-lg overflow-auto bg-gray-50 dark:bg-gray-900 flex justify-center p-4"
|
||||
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>
|
||||
defineProps({
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
cotizacion: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const template = computed(() => props.cotizacion?.template || {
|
||||
documentType: 'COTIZACION',
|
||||
primaryColor: '#2563eb',
|
||||
logo: null,
|
||||
slogan: 'Optimizando lasTIC\'s en las empresas'
|
||||
});
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat("es-MX", {
|
||||
style: "currency",
|
||||
currency: "MXN",
|
||||
}).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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
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 -->
|
||||
<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>
|
||||
<h1 class="text-2xl font-bold">
|
||||
<span :style="{ color: template?.primaryColor }">{{
|
||||
template?.logoPartA
|
||||
}}</span>
|
||||
<span :style="{ color: template?.secondaryColor }">{{
|
||||
template?.logoPartB
|
||||
}}</span>
|
||||
</h1>
|
||||
<img v-if="template?.logo" :src="template.logo" alt="Logo" class="h-8" />
|
||||
<p class="text-gray-600">
|
||||
Optimizando lasTIC's en las empresas
|
||||
</p>
|
||||
<!-- Logo -->
|
||||
<img
|
||||
v-if="template.logo"
|
||||
:src="template.logo"
|
||||
alt="Logo"
|
||||
class="h-12 mb-1"
|
||||
/>
|
||||
|
||||
<!-- Placeholder si no hay logo -->
|
||||
<div v-else class="h-12 mb-1 flex items-center justify-center bg-gray-100 rounded text-gray-400 text-xs px-3">
|
||||
Sin logo
|
||||
</div>
|
||||
|
||||
<!-- Slogan -->
|
||||
<p class="text-gray-600 mt-1">{{ template.slogan }}</p>
|
||||
|
||||
<div class="mt-2 space-y-0.5">
|
||||
<p class="font-semibold">{{ cotizacion.empresaNombre }}</p>
|
||||
@ -46,18 +71,25 @@ const formatCurrency = (value) => {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<p>
|
||||
<span class="font-semibold">Numero de Folio:</span>
|
||||
<span class="font-semibold">
|
||||
{{ getTipoFolioLabel(template.documentType) }}
|
||||
</span>
|
||||
{{ cotizacion.folio }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Fecha de Realización:</span>
|
||||
{{ cotizacion.fechaRealizacion }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Vigencia de Cotización:</span>
|
||||
<p v-if="template.documentType !== 'FACTURA'">
|
||||
<span class="font-semibold">Vigencia:</span>
|
||||
{{ cotizacion.vigencia }}
|
||||
</p>
|
||||
</div>
|
||||
@ -81,7 +113,10 @@ const formatCurrency = (value) => {
|
||||
|
||||
<!-- Cliente -->
|
||||
<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
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Ejecutivo -->
|
||||
<!-- Ejecutivo y Observaciones -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<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
|
||||
</div>
|
||||
<div class="border border-gray-300 p-1.5">
|
||||
@ -126,7 +164,10 @@ const formatCurrency = (value) => {
|
||||
</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
|
||||
</div>
|
||||
<div class="border border-gray-300 p-1.5 min-h-[50px]">
|
||||
@ -138,7 +179,10 @@ const formatCurrency = (value) => {
|
||||
<!-- Tabla de Productos -->
|
||||
<table class="w-full border-collapse mb-3">
|
||||
<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">CANT</th>
|
||||
<th class="border border-white px-1 py-0.5">UNIDAD</th>
|
||||
@ -156,15 +200,9 @@ const formatCurrency = (value) => {
|
||||
:key="producto.lote"
|
||||
class="odd:bg-blue-100"
|
||||
>
|
||||
<td class="border border-gray-300 px-1 py-0.5 text-center">
|
||||
{{ producto.lote }}
|
||||
</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 text-center">{{ producto.lote }}</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.descripcion }}</td>
|
||||
<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) }}
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -189,40 +223,73 @@ const formatCurrency = (value) => {
|
||||
|
||||
<!-- Totales -->
|
||||
<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="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:
|
||||
</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) }}
|
||||
</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:
|
||||
</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) }}
|
||||
</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:
|
||||
</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) }}
|
||||
</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.
|
||||
</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) }}
|
||||
</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
|
||||
</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) }}
|
||||
</div>
|
||||
</div>
|
||||
@ -235,12 +302,12 @@ const formatCurrency = (value) => {
|
||||
</p>
|
||||
|
||||
<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
|
||||
</div>
|
||||
|
||||
<!-- Logos de Partners (puedes agregar imágenes reales) -->
|
||||
<div class="flex justify-center items-center gap-4 flex-wrap">
|
||||
<div class="text-gray-500">
|
||||
Huawei | Lenovo | Hikvision | Fortinet | Panduit | Honeywell
|
||||
|
||||
@ -374,24 +374,12 @@ const router = createRouter({
|
||||
},
|
||||
{
|
||||
path: 'templates',
|
||||
name: 'admin.template',
|
||||
name: 'admin.templates.index',
|
||||
meta: {
|
||||
title: 'Plantillas',
|
||||
icon: 'templates',
|
||||
},
|
||||
redirect: '/admin/templates',
|
||||
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