ADD: Plantillas WIP
This commit is contained in:
parent
4518be3887
commit
d7887d028c
87
package-lock.json
generated
87
package-lock.json
generated
@ -12,6 +12,10 @@
|
||||
"@tailwindcss/postcss": "^4.0.9",
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@tiptap/extension-color": "^3.5.2",
|
||||
"@tiptap/extension-table": "^3.6.2",
|
||||
"@tiptap/extension-table-cell": "^3.6.2",
|
||||
"@tiptap/extension-table-header": "^3.6.2",
|
||||
"@tiptap/extension-table-row": "^3.6.2",
|
||||
"@tiptap/extension-text-align": "^3.5.2",
|
||||
"@tiptap/extension-text-style": "^3.5.2",
|
||||
"@tiptap/extension-underline": "^3.5.2",
|
||||
@ -21,6 +25,8 @@
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
"apexcharts": "^5.3.5",
|
||||
"axios": "^1.8.1",
|
||||
"html2canvas-pro": "^1.5.11",
|
||||
"html2pdf.js": "^0.12.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"laravel-echo": "^2.0.2",
|
||||
@ -1684,6 +1690,59 @@
|
||||
"@tiptap/core": "^3.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.6.2.tgz",
|
||||
"integrity": "sha512-ozRPpxTXrYABTU/zQq3JlytUUXvQDaEcl19YUR1mL/7Ctf4zRBvSnBHCuP/1Cu+4oHX4zdako/G++Z5qJxa65A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.6.2",
|
||||
"@tiptap/pm": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-cell": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.6.2.tgz",
|
||||
"integrity": "sha512-0mYEVy8YtHVRD781SA6pdQN3ICnRfUaVNwLLP8BJv32qLKUX4akK4Xtd+h3XQ5PY6uqBtiVKLiEfpuLeBVpvZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-header": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.6.2.tgz",
|
||||
"integrity": "sha512-D9J0fzVBgZQQds8BQXb1dm02u9CLG90NGuLWz42kWoddViNmXkzZFJeUmsksGiQmM9s1Uqq87m3KpXcFgy8uvw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-row": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.6.2.tgz",
|
||||
"integrity": "sha512-2PxDZ0DjopWUoxgP9BaMy7v86DMHB158KA/QktBt976zpLUiZ9JPLHezbSqADjt+vaWXesnjZk2iHw2Kgd+zFQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.6.1.tgz",
|
||||
@ -2181,7 +2240,6 @@
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@ -2447,7 +2505,6 @@
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
@ -3169,7 +3226,6 @@
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
@ -3178,6 +3234,29 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas-pro": {
|
||||
"version": "1.5.11",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.5.11.tgz",
|
||||
"integrity": "sha512-W4pEeKLG8+9a54RDOSiEKq7gRXXDzt0ORMaLXX+l6a3urSKbmnkmyzcRDCtgTOzmHLaZTLG2wiTQMJqKLlSh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2pdf.js": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.12.1.tgz",
|
||||
"integrity": "sha512-3rBWQ96H5oOU9jtoz3MnE/epGi27ig9h8aonBk4JTpvUERM3lMRxhIRckhJZEi4wE0YfRINoYOIDY0hLY0CHgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.0.0",
|
||||
"jspdf": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
@ -4470,7 +4549,6 @@
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
@ -4655,7 +4733,6 @@
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
|
||||
@ -14,6 +14,10 @@
|
||||
"@tailwindcss/postcss": "^4.0.9",
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@tiptap/extension-color": "^3.5.2",
|
||||
"@tiptap/extension-table": "^3.6.2",
|
||||
"@tiptap/extension-table-cell": "^3.6.2",
|
||||
"@tiptap/extension-table-header": "^3.6.2",
|
||||
"@tiptap/extension-table-row": "^3.6.2",
|
||||
"@tiptap/extension-text-align": "^3.5.2",
|
||||
"@tiptap/extension-text-style": "^3.5.2",
|
||||
"@tiptap/extension-underline": "^3.5.2",
|
||||
@ -23,6 +27,8 @@
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
"apexcharts": "^5.3.5",
|
||||
"axios": "^1.8.1",
|
||||
"html2canvas-pro": "^1.5.11",
|
||||
"html2pdf.js": "^0.12.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"laravel-echo": "^2.0.2",
|
||||
|
||||
42
src/components/Holos/TemplateCard.vue
Normal file
42
src/components/Holos/TemplateCard.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<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>
|
||||
155
src/components/Holos/TemplateForm.vue
Normal file
155
src/components/Holos/TemplateForm.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useTemplateStorage } from '@Pages/Templates/Composables/useTemplateStorage';
|
||||
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';
|
||||
|
||||
/** Props */
|
||||
const props = defineProps({
|
||||
templateId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
/** Composables */
|
||||
const router = useRouter();
|
||||
const { getTemplateById } = useTemplateStorage();
|
||||
|
||||
/** Estado */
|
||||
const template = ref(null);
|
||||
const formData = ref({});
|
||||
|
||||
/** Computed */
|
||||
const allFieldsFilled = computed(() => {
|
||||
if (!template.value) return false;
|
||||
|
||||
const requiredFields = template.value.config.campos
|
||||
.flatMap(seccion => seccion.campos)
|
||||
.filter(campo => campo.required);
|
||||
|
||||
return requiredFields.every(campo => formData.value[campo.key]);
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const initializeForm = () => {
|
||||
if (!template.value) return;
|
||||
|
||||
const data = {};
|
||||
template.value.config.campos.forEach(seccion => {
|
||||
seccion.campos.forEach(campo => {
|
||||
data[campo.key] = campo.defaultValue || '';
|
||||
});
|
||||
});
|
||||
|
||||
formData.value = data;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!allFieldsFilled.value) {
|
||||
Notify.warning('Por favor completa todos los campos requeridos');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Datos del formulario:', formData.value);
|
||||
Notify.success('Formulario completado');
|
||||
|
||||
router.push({
|
||||
name: 'admin.templates.preview',
|
||||
params: { id: template.value.id },
|
||||
query: { data: JSON.stringify(formData.value) }
|
||||
});
|
||||
};
|
||||
|
||||
/** Ciclos */
|
||||
onMounted(() => {
|
||||
template.value = getTemplateById(props.templateId);
|
||||
if (template.value) {
|
||||
initializeForm();
|
||||
} else {
|
||||
Notify.error('Plantilla no encontrada');
|
||||
router.push({ name: 'admin.templates.index' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="template" class="max-w-4xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<GoogleIcon :name="template.icono" class="text-2xl text-blue-600" />
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-primary-dt">
|
||||
{{ template.nombre }}
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-primary-dt/70">
|
||||
{{ template.descripcion }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Formulario -->
|
||||
<form @submit.prevent="handleSubmit" class="space-y-8">
|
||||
<!-- Secciones dinámicas -->
|
||||
<div
|
||||
v-for="seccion in template.config.campos"
|
||||
:key="seccion.seccion"
|
||||
class="bg-white rounded-lg border border-gray-200 p-6 dark:bg-primary-d dark:border-primary/20"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4 dark:text-primary-dt">
|
||||
{{ seccion.seccion }}
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Campos individuales -->
|
||||
<template v-for="campo in seccion.campos" :key="campo.key">
|
||||
<!-- Input de texto -->
|
||||
<Input
|
||||
v-if="['text', 'email', 'url', 'tel', 'number', 'date'].includes(campo.tipo)"
|
||||
v-model="formData[campo.key]"
|
||||
:id="campo.key"
|
||||
:title="campo.label"
|
||||
:type="campo.tipo"
|
||||
:required="campo.required"
|
||||
:placeholder="campo.placeholder"
|
||||
:class="campo.tipo === 'date' ? 'col-span-1' : ''"
|
||||
/>
|
||||
|
||||
<!-- Textarea -->
|
||||
<Textarea
|
||||
v-else-if="campo.tipo === 'textarea'"
|
||||
v-model="formData[campo.key]"
|
||||
:id="campo.key"
|
||||
:title="campo.label"
|
||||
:required="campo.required"
|
||||
:placeholder="campo.placeholder"
|
||||
class="md:col-span-2"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botones de acción -->
|
||||
<div class="flex gap-4 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@click="router.push({ name: 'admin.templates.index' })"
|
||||
class="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors dark:border-primary/20 dark:text-primary-dt dark:hover:bg-primary/10"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
:disabled="!allFieldsFilled"
|
||||
class="px-6 py-2"
|
||||
>
|
||||
Generar Documento
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,6 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "../../colors.css";
|
||||
@import "./univer.css";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
|
||||
@ -161,6 +161,12 @@ onMounted(() => {
|
||||
name="Maquetador de Documentos"
|
||||
to="admin.maquetador.index"
|
||||
/>
|
||||
<Link
|
||||
v-if="hasPermission('activities.index')"
|
||||
icon="event"
|
||||
name="Plantillas"
|
||||
to="admin.templates.index"
|
||||
/>
|
||||
</Section>
|
||||
</template>
|
||||
<!-- Contenido -->
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
<script setup>
|
||||
import UniverDoc from '@Holos/Editor/UniverDoc.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-primary-dt">
|
||||
Editor de Documentos
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-primary-dt/70">
|
||||
Editor avanzado de documentos con Univer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-primary-d rounded-lg shadow-sm border border-gray-200 dark:border-primary/20">
|
||||
<div class="h-[600px] p-4">
|
||||
<UniverDoc />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
44
src/pages/Templates/Composables/useTemplateStorage.js
Normal file
44
src/pages/Templates/Composables/useTemplateStorage.js
Normal file
@ -0,0 +1,44 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
71
src/pages/Templates/Configs/ConfigCotizacion.js
Normal file
71
src/pages/Templates/Configs/ConfigCotizacion.js
Normal file
@ -0,0 +1,71 @@
|
||||
export default {
|
||||
templateId: 'temp-cot-001',
|
||||
nombre: 'Cotización IP',
|
||||
|
||||
branding: {
|
||||
logo: null,
|
||||
primaryColor: '#dc2626',
|
||||
secondaryColor: '#1e40af',
|
||||
logoPartA: 'GOL',
|
||||
logoPartB: 'SYSTEMS'
|
||||
},
|
||||
|
||||
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',
|
||||
tipo: 'text',
|
||||
required: true,
|
||||
defaultValue: 'GOLSYSTEMS'
|
||||
},
|
||||
{
|
||||
key: 'empresaWeb',
|
||||
label: 'Sitio Web',
|
||||
tipo: 'url',
|
||||
defaultValue: 'www.golsystems.com'
|
||||
},
|
||||
{
|
||||
key: 'empresaEmail',
|
||||
label: 'Email',
|
||||
tipo: 'email',
|
||||
required: true,
|
||||
defaultValue: 'contacto@golsystems.com'
|
||||
},
|
||||
{
|
||||
key: 'empresaTelefono',
|
||||
label: 'Teléfono',
|
||||
tipo: 'tel',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
85
src/pages/Templates/Configs/usePDFExport.js
Normal file
85
src/pages/Templates/Configs/usePDFExport.js
Normal file
@ -0,0 +1,85 @@
|
||||
import { ref } from 'vue';
|
||||
import html2canvas from 'html2canvas-pro';
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
export function usePDFExport() {
|
||||
const isExporting = ref(false);
|
||||
|
||||
const exportToPDF = async (elementId, filename = 'documento.pdf') => {
|
||||
isExporting.value = true;
|
||||
|
||||
try {
|
||||
const element = document.getElementById(elementId);
|
||||
|
||||
if (!element) {
|
||||
throw new Error('Elemento no encontrado');
|
||||
}
|
||||
|
||||
// Capturar el elemento exacto (210mm x 297mm)
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
logging: false,
|
||||
allowTaint: false,
|
||||
imageTimeout: 30000,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
ignoreElements: (element) => {
|
||||
return element.classList?.contains('no-pdf');
|
||||
},
|
||||
onclone: (clonedDoc) => {
|
||||
const allElements = clonedDoc.querySelectorAll('*');
|
||||
allElements.forEach(el => {
|
||||
if (!(el instanceof Element)) return;
|
||||
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
|
||||
// Aplicar colores como inline styles
|
||||
if (computedStyle.color) {
|
||||
el.style.color = computedStyle.color;
|
||||
}
|
||||
if (computedStyle.backgroundColor) {
|
||||
el.style.backgroundColor = computedStyle.backgroundColor;
|
||||
}
|
||||
if (computedStyle.borderColor) {
|
||||
el.style.borderColor = computedStyle.borderColor;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL('image/png', 1.0);
|
||||
|
||||
// Orientación vertical A4
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
compress: true
|
||||
});
|
||||
|
||||
const pdfWidth = 210; // A4 portrait width
|
||||
const pdfHeight = 297; // A4 portrait height
|
||||
|
||||
// Ajustar imagen al tamaño completo de la página (sin márgenes)
|
||||
// para que ocupe toda la hoja A4
|
||||
pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight, '', 'FAST');
|
||||
|
||||
// Guardar PDF
|
||||
pdf.save(filename);
|
||||
|
||||
Notify.success('PDF generado exitosamente');
|
||||
} catch (error) {
|
||||
console.error('Error al generar PDF:', error);
|
||||
Notify.error(`Error al generar el PDF: ${error.message}`);
|
||||
} finally {
|
||||
isExporting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
exportToPDF,
|
||||
isExporting
|
||||
};
|
||||
}
|
||||
199
src/pages/Templates/Form.vue
Normal file
199
src/pages/Templates/Form.vue
Normal file
@ -0,0 +1,199 @@
|
||||
<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 TempCot from './Temp-Cot.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();
|
||||
|
||||
/** Estado */
|
||||
const template = ref(null);
|
||||
const formData = ref({});
|
||||
|
||||
/** Computed */
|
||||
const cotizacionData = computed(() => ({
|
||||
...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
|
||||
},
|
||||
{
|
||||
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
|
||||
}));
|
||||
|
||||
/** Métodos */
|
||||
const initializeForm = () => {
|
||||
if (!template.value) return;
|
||||
|
||||
const data = {};
|
||||
template.value.config.campos.forEach(seccion => {
|
||||
seccion.campos.forEach(campo => {
|
||||
data[campo.key] = campo.defaultValue || '';
|
||||
});
|
||||
});
|
||||
|
||||
formData.value = data;
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
exportToPDF('template-preview', `${template.value.nombre}.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">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-primary-dt">
|
||||
{{ template.nombre }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-primary-dt/70">
|
||||
{{ template.descripcion }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secciones del formulario -->
|
||||
<div
|
||||
v-for="seccion in template.config.campos"
|
||||
:key="seccion.seccion"
|
||||
class="bg-white rounded-lg border border-gray-200 p-4 dark:bg-primary-d dark:border-primary/20"
|
||||
>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-3 dark:text-primary-dt">
|
||||
{{ seccion.seccion }}
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<template v-for="campo in seccion.campos" :key="campo.key">
|
||||
<Input
|
||||
v-if="['text', 'email', 'url', 'tel', 'number', 'date'].includes(campo.tipo)"
|
||||
v-model="formData[campo.key]"
|
||||
:id="campo.key"
|
||||
:title="campo.label"
|
||||
:type="campo.tipo"
|
||||
:required="campo.required"
|
||||
:placeholder="campo.placeholder"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
v-else-if="campo.tipo === 'textarea'"
|
||||
v-model="formData[campo.key]"
|
||||
:id="campo.key"
|
||||
:title="campo.label"
|
||||
:required="campo.required"
|
||||
:placeholder="campo.placeholder"
|
||||
class="md:col-span-2"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botón de exportar (móvil) -->
|
||||
<div class="lg:hidden">
|
||||
<PrimaryButton
|
||||
@click="handleExport"
|
||||
:disabled="isExporting"
|
||||
class="w-full px-6 py-3"
|
||||
>
|
||||
<GoogleIcon name="picture_as_pdf" class="mr-2" />
|
||||
{{ isExporting ? 'Generando PDF...' : 'Exportar a PDF' }}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VISTA PREVIA (Columna Derecha) -->
|
||||
<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"
|
||||
:disabled="isExporting"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
<GoogleIcon name="picture_as_pdf" class="mr-2 text-sm" />
|
||||
{{ isExporting ? 'Generando...' : 'Exportar PDF' }}
|
||||
</PrimaryButton>
|
||||
</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;"
|
||||
>
|
||||
<div
|
||||
id="template-preview"
|
||||
class="shadow-lg"
|
||||
style="width: 210mm; height: 297mm; overflow: hidden;"
|
||||
>
|
||||
<TempCot :cotizacion="cotizacionData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
54
src/pages/Templates/Index.vue
Normal file
54
src/pages/Templates/Index.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<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>
|
||||
250
src/pages/Templates/Temp-Cot.vue
Normal file
250
src/pages/Templates/Temp-Cot.vue
Normal file
@ -0,0 +1,250 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
cotizacion: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat("es-MX", {
|
||||
style: "currency",
|
||||
currency: "MXN",
|
||||
}).format(value);
|
||||
};
|
||||
</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;"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex justify-between items-start border-b-2 border-blue-700 pb-2 mb-3"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div class="mt-2 space-y-0.5">
|
||||
<p class="font-semibold">{{ cotizacion.empresaNombre }}</p>
|
||||
<p>{{ cotizacion.empresaWeb }}</p>
|
||||
<p>{{ cotizacion.empresaEmail }}</p>
|
||||
<p>{{ cotizacion.empresaTelefono }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<h2 class="text-xl font-bold text-blue-600 mb-1">COTIZACION IP</h2>
|
||||
<div class="space-y-0.5">
|
||||
<p>
|
||||
<span class="font-semibold">Numero de Folio:</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>
|
||||
{{ cotizacion.vigencia }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datos Fiscales y Bancarios -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div class="border border-gray-300 p-1.5">
|
||||
<p class="font-semibold">{{ cotizacion.empresaNombre }}</p>
|
||||
<p>RFC: {{ cotizacion.empresaRFC }}</p>
|
||||
<p>{{ cotizacion.empresaDireccion }}</p>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 p-1.5">
|
||||
<p class="font-semibold">{{ cotizacion.bancoBanco }}</p>
|
||||
<p>{{ cotizacion.bancoTipoCuenta }}</p>
|
||||
<p>{{ cotizacion.bancoCuenta }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cliente -->
|
||||
<div class="mb-3">
|
||||
<div class="bg-blue-600 text-white font-semibold px-2 py-0.5">
|
||||
DATOS FISCALES CLIENTE
|
||||
</div>
|
||||
<div class="border border-gray-300 p-1.5 grid grid-cols-2 gap-x-4">
|
||||
<div>
|
||||
<span class="font-semibold">Nombre:</span>
|
||||
{{ cotizacion.clienteNombre }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold">Domicilio:</span>
|
||||
{{ cotizacion.clienteDomicilio }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold">RFC:</span> {{ cotizacion.clienteRFC }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold">Telefono:</span>
|
||||
{{ cotizacion.clienteTelefono }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ejecutivo -->
|
||||
<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">
|
||||
DATOS DEL EJECUTIVO DE CUENTAS
|
||||
</div>
|
||||
<div class="border border-gray-300 p-1.5">
|
||||
<p>
|
||||
<span class="font-semibold">NOMBRE:</span>
|
||||
{{ cotizacion.ejecutivoNombre }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">CORREO:</span>
|
||||
{{ cotizacion.ejecutivoCorreo }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">CELULAR:</span>
|
||||
{{ cotizacion.ejecutivoCelular }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="bg-blue-600 text-white font-semibold px-2 py-0.5">
|
||||
OBSERVACIONES
|
||||
</div>
|
||||
<div class="border border-gray-300 p-1.5 min-h-[50px]">
|
||||
{{ cotizacion.observaciones }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de Productos -->
|
||||
<table class="w-full border-collapse mb-3">
|
||||
<thead>
|
||||
<tr class="bg-blue-600 text-white">
|
||||
<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>
|
||||
<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">P. UNIT.</th>
|
||||
<th class="border border-white px-1 py-0.5">IMPORTE</th>
|
||||
<th class="border border-white px-1 py-0.5">DESC</th>
|
||||
<th class="border border-white px-1 py-0.5">TOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="producto in cotizacion.productos"
|
||||
: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">{{ 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">
|
||||
{{ formatCurrency(producto.precioUnitario) }}
|
||||
</td>
|
||||
<td class="border border-gray-300 px-1 py-0.5 text-right whitespace-nowrap">
|
||||
{{ formatCurrency(producto.cantidad * producto.precioUnitario) }}
|
||||
</td>
|
||||
<td class="border border-gray-300 px-1 py-0.5 text-right whitespace-nowrap">
|
||||
{{ 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
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totales -->
|
||||
<div class="flex justify-end mb-3">
|
||||
<div class="border border-blue-600 w-56">
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
||||
SUBTOTAL 1:
|
||||
</div>
|
||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
||||
{{ formatCurrency(cotizacion.subtotal1) }}
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
||||
DESCUENTO:
|
||||
</div>
|
||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
||||
{{ formatCurrency(cotizacion.descuentoTotal) }}
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
||||
SUBTOTAL 2:
|
||||
</div>
|
||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
||||
{{ formatCurrency(cotizacion.subtotal2) }}
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-600 text-white font-semibold px-2 py-1 text-right">
|
||||
I.V.A.
|
||||
</div>
|
||||
<div class="border-l border-blue-600 px-2 py-1 text-right whitespace-nowrap">
|
||||
{{ formatCurrency(cotizacion.iva) }}
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-600 text-white font-bold px-2 py-1 text-right">
|
||||
TOTAL
|
||||
</div>
|
||||
<div class="border-l border-blue-600 px-2 py-1 text-right font-bold whitespace-nowrap">
|
||||
{{ formatCurrency(cotizacion.total) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<p class="text-center font-bold mb-1">
|
||||
DIECIOCHO MIL NOVENTA Y SEIS PESOS 00/100 M.N.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="bg-blue-600 text-white font-semibold px-2 py-0.5 text-center mb-1"
|
||||
>
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -357,13 +357,13 @@ const router = createRouter({
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'documentation',
|
||||
name: 'admin.documentation',
|
||||
path: 'maquetador',
|
||||
name: 'admin.maquetador',
|
||||
meta: {
|
||||
title: 'Maquetador de Documentos',
|
||||
icon: 'documents',
|
||||
},
|
||||
redirect: '/admin/documentation',
|
||||
redirect: '/admin/maquetador',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@ -372,6 +372,27 @@ const router = createRouter({
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'templates',
|
||||
name: 'admin.template',
|
||||
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