123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- <main class="container mt-5">
- <div class="modal" tabindex="-1" :class="{ show: option.modal }" style="display: block;" aria-modal="true"
- role="dialog" v-if="option.modal" @vue:mounted="store.showModal($el); " @vue:unmounted="store.hideModal($el)"
- data-bs-backdrop="static" data-bs-keyboard="false">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">{{ option.modal }}</h5>
- <button type="button" class="btn-close" aria-label="Close" @click="closeModal"></button>
- </div>
- <div class="modal-body">
- <form>
- <div v-for="item in currentOptions" class="mb-3">
- <label :for="item.value" :class="item.label_class">
- {{ item.label }}</label>
- <input :type="item.type" :name="item.name" :value="item.value" :list="item.name"
- :id="item.value" v-model="item.value" :class="item.input_class"
- :class="{ 'form-control': item.type !== 'radio' }">
- <datalist v-if="item.datalist?.length" :id="item.name">
- <option v-for="option in item.datalist" :value="option.id">{{ option.nombre }}</option>
- </datalist>
- </div>
- <div class="d-grid gap-2 d-md-flex justify-content-md-end">
- <button type="reset" class="btn btn-danger btn-sm">
- <i class="fas fa-eraser"></i>
- Limpiar
- </button>
- <button type="submit" class="btn btn-primary btn-sm" @click="submitModal"
- :disabled="currentOptions?.some(item => !item.value)">
- <i class="fas fa-check"></i>
- Exportar
- </button>
- </div>
- </form>
- </div>
- </div>
- </div>
- </div>
- <!-- STORE ALERT -->
- <div class="alert alert-dismissible fade show" :class="`alert-${store.alert.type}`" v-if="store.alert">
- <button type="button" class="btn-close" @click="store.alert = null"></button>
- <strong>{{ store.alert.type === 'danger' ? 'Error' : 'Éxito' }}:</strong>
- {{ store.alert.message }}
- </div>
- <!-- Buttons outside the modal/dialog -->
- <div class="container" @vue:mounted="lastSnapshot">
- <div class="d-block text-center mb-3" v-if="last_snapshot">
- Último snapshot:
- <code>{{ last_snapshot }}</code>
- </div>
- <div v-else class="d-block text-center mb-3">
- No hay snapshots
- </div>
- <div class="gap-2 d-flex justify-content-md-center flex-wrap">
- <button type="button" class="btn col-5" v-for="item in menu" @click="item.click"
- :class="`btn-${item.color ?? 'outline-secondary'}`" :aria-label="`Activate ${item.name}`">
- <i :class="item.icon" aria-hidden="true"></i>
- <span class="ms-2">{{ item.name }}</span>
- </button>
- </div>
- </div>
- </main>
- <script>
- const option = PetiteVue.reactive({ modal: null });
- function createDownloadLink({ url, filename, postData }) {
- return async () => {
- store.loading = true;
- const response = await fetch(url, { method: 'POST', body: JSON.stringify(postData) });
- const blob = await response.blob();
- const downloadUrl = window.URL.createObjectURL(blob);
- const anchor = document.createElement('a');
- anchor.href = downloadUrl;
- anchor.download = filename;
- anchor.charset = "windows-1252"; // Set the charset to ANSI for compatibility
- anchor.click();
- anchor.remove();
- store.loading = false;
- };
- }
- async function fetchOptions(url, optionName) {
- const response = await fetch(url);
- const data = await response.json();
- return data.map(item => ({ id: item[optionName.id], nombre: item[optionName.nombre] }));
- }
- PetiteVue.createApp({
- option,
- modal: false,
- menu: [
- {
- name: 'Construcción de Calificación',
- icon: 'fas fa-sliders',
- color: 'warning',
- url: '/export/excel.php',
- filename: 'calificacion.csv',
- click: createDownloadLink({ url: '/export/excel.php', filename: 'calificacion.csv', postData: { query: 'c-calif' } })
- },
- {
- name: 'Calificaciones Brutas',
- icon: 'fas fa-xmark',
- url: '/export/excel.php',
- filename: 'calificacion.csv',
- click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones.csv', postData: { query: 'n-c-brutas' } })
- },
- {
- name: 'Calificaciones Netas',
- icon: 'fas fa-tarp',
- url: '/export/excel.php',
- filename: 'calificacion.csv',
- click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_netas.csv', postData: { query: 'c-net' } })
- },
- {
- name: 'Calificaciones Netas por Curso',
- icon: 'fas fa-tarp-droplet',
- url: '/export/excel.php',
- filename: 'calificacion.csv',
- click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_netas_curso.csv', postData: { query: 'c-net-cur' } })
- },
- {
- name: 'Calificaciones Finales',
- icon: 'fas fa-chart-simple',
- color: 'info',
- url: '/export/excel.php',
- filename: 'calificacion.csv',
- click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_finales.csv', postData: { query: 'c-fin' } })
- },
- {
- name: 'Gráfica de Alumnos',
- icon: 'fas fa-chart-bar',
- color: 'dark',
- url: '/',
- click: () => {
- // Redirect but with a post (form data page=graph)
- const form = document.createElement('form');
- form.method = 'POST';
- form.action = '/';
- const input = document.createElement('input');
- input.type = 'hidden';
- input.name = 'page';
- input.value = 'graph';
- form.appendChild(input);
- document.body.appendChild(form);
- form.submit();
- }
- },
- {
- name: 'Guardar Snapshot',
- icon: 'fas fa-save',
- color: 'success',
- url: '/action/snapshot.php',
- click: async () => {
- store.loading = true;
- const response = await fetch('/action/snapshot.php', { method: 'POST' });
- const data = await response.json();
- store.loading = false;
- if (data.success) {
- store.alert = { type: 'success', message: data.message };
- } else {
- store.alert = { type: 'danger', message: data.message };
- }
- }
- },
- {
- name: 'Usuarios Registrados',
- icon: 'fas fa-users',
- url: '/export/excel.php',
- filename: 'usuarios.csv',
- opciones: [
- // radio button
- { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Alumno', value: 'al' },
- { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Usuarios temporales', value: 'usr-temp' },
- { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Todos los usuarios', value: 'usr' },
- ],
- click: async function () {
- option.modal = this.name;
- try {
- await new Promise((resolve, reject) => {
- option.closeModal = () => {
- option.modal = false;
- reject(new Error("Modal closed by user"));
- };
- option.submitModal = resolve;
- });
- store.loading = true;
- option.modal = false;
- let formData = { query: 'usuarios' };
- document.querySelectorAll('input[name="query"]').forEach(input => {
- if (input.checked) formData.query = input.value;
- });
- await createDownloadLink({ url: this.url, filename: this.filename, postData: formData })();
- } catch (error) {
- console.error(error);
- } finally {
- store.loading = false;
- }
- }
- },
- {
- name: 'Reporte Syllabus Plan de Cátedra',
- icon: 'fas fa-file-invoice',
- url: '/export/gema.php',
- filename: 'syllabus_plan_catedra.csv',
- opciones: [
- { type: 'list', name: 'periodo_id', label: 'Periodo de GEMA', value: null, datalist: [] },
- ],
- mounted: async function () {
- this.opciones.find(item => item.name === 'periodo_id').datalist = await fetchOptions('/fetch/periodos.php', { nombre: 'Periodo_desc', id: 'Periodo_id' });
- },
- click: async function () {
- option.modal = this.name;
- await this.mounted();
- try {
- await new Promise((resolve, reject) => {
- option.closeModal = () => {
- option.modal = false;
- reject(new Error("Modal closed by user"));
- };
- option.submitModal = resolve;
- });
- store.loading = true;
- option.modal = false;
- let formData = { query: 'cursos' };
- this.opciones.forEach(opt => formData[opt.name] = opt.value);
- await createDownloadLink({ url: this.url, filename: this.filename, postData: formData })();
- } catch (error) {
- console.error(error);
- } finally {
- store.loading = false;
- }
- }
- },
- ],
- get currentOptions() {
- const currentItem = this.menu.find(item => item.name === this.option.modal);
- return currentItem ? currentItem.opciones : [];
- },
- closeModal: function () {
- if (typeof option.closeModal === 'function') option.closeModal();
- },
- submitModal: function () {
- if (typeof option.submitModal === 'function') option.submitModal();
- },
- async lastSnapshot() {
- try {
- const response = await fetch('/postgrest/snapshot_calificaciones?limit=1&order=created_at.desc');
- const data = await response.json();
- this.last_snapshot = data[0]?.created_at;
- } catch (error) {
- console.error(error);
- }
- }
- }).mount();
- </script>
|