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/postcss": "^4.0.9",
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
"@tiptap/extension-color": "^3.5.2",
|
"@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-align": "^3.5.2",
|
||||||
"@tiptap/extension-text-style": "^3.5.2",
|
"@tiptap/extension-text-style": "^3.5.2",
|
||||||
"@tiptap/extension-underline": "^3.5.2",
|
"@tiptap/extension-underline": "^3.5.2",
|
||||||
@ -21,6 +25,8 @@
|
|||||||
"@vuepic/vue-datepicker": "^11.0.2",
|
"@vuepic/vue-datepicker": "^11.0.2",
|
||||||
"apexcharts": "^5.3.5",
|
"apexcharts": "^5.3.5",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
|
"html2canvas-pro": "^1.5.11",
|
||||||
|
"html2pdf.js": "^0.12.1",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"laravel-echo": "^2.0.2",
|
"laravel-echo": "^2.0.2",
|
||||||
@ -1684,6 +1690,59 @@
|
|||||||
"@tiptap/core": "^3.6.1"
|
"@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": {
|
"node_modules/@tiptap/extension-text": {
|
||||||
"version": "3.6.1",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.6.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
}
|
}
|
||||||
@ -2447,7 +2505,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
@ -3169,7 +3226,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-line-break": "^2.1.0",
|
"css-line-break": "^2.1.0",
|
||||||
"text-segmentation": "^1.0.3"
|
"text-segmentation": "^1.0.3"
|
||||||
@ -3178,6 +3234,29 @@
|
|||||||
"node": ">=8.0.0"
|
"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": {
|
"node_modules/iobuffer": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
@ -4655,7 +4733,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-arraybuffer": "^1.0.2"
|
"base64-arraybuffer": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,10 @@
|
|||||||
"@tailwindcss/postcss": "^4.0.9",
|
"@tailwindcss/postcss": "^4.0.9",
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
"@tiptap/extension-color": "^3.5.2",
|
"@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-align": "^3.5.2",
|
||||||
"@tiptap/extension-text-style": "^3.5.2",
|
"@tiptap/extension-text-style": "^3.5.2",
|
||||||
"@tiptap/extension-underline": "^3.5.2",
|
"@tiptap/extension-underline": "^3.5.2",
|
||||||
@ -23,6 +27,8 @@
|
|||||||
"@vuepic/vue-datepicker": "^11.0.2",
|
"@vuepic/vue-datepicker": "^11.0.2",
|
||||||
"apexcharts": "^5.3.5",
|
"apexcharts": "^5.3.5",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
|
"html2canvas-pro": "^1.5.11",
|
||||||
|
"html2pdf.js": "^0.12.1",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"laravel-echo": "^2.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 "tailwindcss";
|
||||||
@import "../../colors.css";
|
@import "../../colors.css";
|
||||||
@import "./univer.css";
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
|
|||||||
@ -161,6 +161,12 @@ onMounted(() => {
|
|||||||
name="Maquetador de Documentos"
|
name="Maquetador de Documentos"
|
||||||
to="admin.maquetador.index"
|
to="admin.maquetador.index"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="hasPermission('activities.index')"
|
||||||
|
icon="event"
|
||||||
|
name="Plantillas"
|
||||||
|
to="admin.templates.index"
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
<!-- Contenido -->
|
<!-- 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',
|
path: 'maquetador',
|
||||||
name: 'admin.documentation',
|
name: 'admin.maquetador',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Maquetador de Documentos',
|
title: 'Maquetador de Documentos',
|
||||||
icon: 'documents',
|
icon: 'documents',
|
||||||
},
|
},
|
||||||
redirect: '/admin/documentation',
|
redirect: '/admin/maquetador',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
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