menu.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <main class="container mt-5">
  2. <div class="modal" tabindex="-1" :class="{ show: option.modal }" style="display: block;" aria-modal="true"
  3. role="dialog" v-if="option.modal" @vue:mounted="store.showModal($el); " @vue:unmounted="store.hideModal($el)"
  4. data-bs-backdrop="static" data-bs-keyboard="false">
  5. <div class="modal-dialog">
  6. <div class="modal-content">
  7. <div class="modal-header">
  8. <h5 class="modal-title">{{ option.modal }}</h5>
  9. <button type="button" class="btn-close" aria-label="Close" @click="closeModal"></button>
  10. </div>
  11. <div class="modal-body">
  12. <form>
  13. <div v-for="item in currentOptions" class="mb-3">
  14. <label :for="item.value" :class="item.label_class">
  15. {{ item.label }}</label>
  16. <input :type="item.type" :name="item.name" :value="item.value" :list="item.name"
  17. :id="item.value" v-model="item.value" :class="item.input_class"
  18. :class="{ 'form-control': item.type !== 'radio' }">
  19. <datalist v-if="item.datalist?.length" :id="item.name">
  20. <option v-for="option in item.datalist" :value="option.id">{{ option.nombre }}</option>
  21. </datalist>
  22. </div>
  23. <div class="d-grid gap-2 d-md-flex justify-content-md-end">
  24. <button type="reset" class="btn btn-danger btn-sm">
  25. <i class="fas fa-eraser"></i>
  26. Limpiar
  27. </button>
  28. <button type="submit" class="btn btn-primary btn-sm" @click="submitModal"
  29. :disabled="currentOptions?.some(item => !item.value)">
  30. <i class="fas fa-check"></i>
  31. Exportar
  32. </button>
  33. </div>
  34. </form>
  35. </div>
  36. </div>
  37. </div>
  38. </div>
  39. <!-- STORE ALERT -->
  40. <div class="alert alert-dismissible fade show" :class="`alert-${store.alert.type}`" v-if="store.alert">
  41. <button type="button" class="btn-close" @click="store.alert = null"></button>
  42. <strong>{{ store.alert.type === 'danger' ? 'Error' : 'Éxito' }}:</strong>
  43. {{ store.alert.message }}
  44. </div>
  45. <!-- Buttons outside the modal/dialog -->
  46. <div class="container" @vue:mounted="lastSnapshot">
  47. <div class="d-block text-center mb-3" v-if="last_snapshot">
  48. Último snapshot:
  49. <code>{{ last_snapshot }}</code>
  50. </div>
  51. <div v-else class="d-block text-center mb-3">
  52. No hay snapshots
  53. </div>
  54. <div class="gap-2 d-flex justify-content-md-center flex-wrap">
  55. <button type="button" class="btn col-5" v-for="item in menu" @click="item.click"
  56. :class="`btn-${item.color ?? 'outline-secondary'}`" :aria-label="`Activate ${item.name}`">
  57. <i :class="item.icon" aria-hidden="true"></i>
  58. <span class="ms-2">{{ item.name }}</span>
  59. </button>
  60. </div>
  61. </div>
  62. </main>
  63. <script>
  64. const option = PetiteVue.reactive({ modal: null });
  65. function createDownloadLink({ url, filename, postData }) {
  66. return async () => {
  67. store.loading = true;
  68. const response = await fetch(url, { method: 'POST', body: JSON.stringify(postData) });
  69. const blob = await response.blob();
  70. const downloadUrl = window.URL.createObjectURL(blob);
  71. const anchor = document.createElement('a');
  72. anchor.href = downloadUrl;
  73. anchor.download = filename;
  74. anchor.charset = "windows-1252"; // Set the charset to ANSI for compatibility
  75. anchor.click();
  76. anchor.remove();
  77. store.loading = false;
  78. };
  79. }
  80. async function fetchOptions(url, optionName) {
  81. const response = await fetch(url);
  82. const data = await response.json();
  83. return data.map(item => ({ id: item[optionName.id], nombre: item[optionName.nombre] }));
  84. }
  85. PetiteVue.createApp({
  86. option,
  87. modal: false,
  88. last_snapshot: null,
  89. menu: [
  90. {
  91. name: 'Construcción de Calificación',
  92. icon: 'fas fa-sliders',
  93. color: 'warning',
  94. url: '/export/excel.php',
  95. filename: 'calificacion.csv',
  96. click: createDownloadLink({ url: '/export/excel.php', filename: 'calificacion.csv', postData: { query: 'c-calif' } })
  97. },
  98. {
  99. name: 'Calificaciones Brutas',
  100. icon: 'fas fa-xmark',
  101. url: '/export/excel.php',
  102. filename: 'calificacion.csv',
  103. click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones.csv', postData: { query: 'n-c-brutas' } })
  104. },
  105. {
  106. name: 'Calificaciones Netas',
  107. icon: 'fas fa-tarp',
  108. url: '/export/excel.php',
  109. filename: 'calificacion.csv',
  110. click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_netas.csv', postData: { query: 'c-net' } })
  111. },
  112. {
  113. name: 'Calificaciones Netas por Curso',
  114. icon: 'fas fa-tarp-droplet',
  115. url: '/export/excel.php',
  116. filename: 'calificacion.csv',
  117. click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_netas_curso.csv', postData: { query: 'c-net-cur' } })
  118. },
  119. {
  120. name: 'Calificaciones Finales',
  121. icon: 'fas fa-chart-simple',
  122. color: 'info',
  123. url: '/export/excel.php',
  124. filename: 'calificacion.csv',
  125. click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_finales.csv', postData: { query: 'c-fin' } })
  126. },
  127. {
  128. name: 'Cuenta de calificaciones',
  129. icon: 'fa-solid fa-arrow-up-9-1',
  130. url: '/export/excel.php',
  131. filename: 'calificacion.csv',
  132. click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_finales.csv', postData: { query: 'usr-cuenta' } })
  133. },
  134. {
  135. name: 'Gráfica de Alumnos',
  136. icon: 'fas fa-chart-bar',
  137. color: 'dark',
  138. url: '/',
  139. click: () => {
  140. // Redirect but with a post (form data page=graph)
  141. const form = document.createElement('form');
  142. form.method = 'POST';
  143. form.action = '/';
  144. const input = document.createElement('input');
  145. input.type = 'hidden';
  146. input.name = 'page';
  147. input.value = 'graph';
  148. form.appendChild(input);
  149. document.body.appendChild(form);
  150. form.submit();
  151. }
  152. },
  153. {
  154. name: 'Guardar Snapshot',
  155. icon: 'fas fa-save',
  156. color: 'success',
  157. url: '/action/snapshot.php',
  158. click: async () => {
  159. store.loading = true;
  160. const response = await fetch('/action/snapshot.php', { method: 'POST' });
  161. const data = await response.json();
  162. store.loading = false;
  163. if (data.success) {
  164. store.alert = { type: 'success', message: data.message };
  165. } else {
  166. store.alert = { type: 'danger', message: data.message };
  167. }
  168. }
  169. },
  170. {
  171. name: 'Usuarios Registrados',
  172. icon: 'fas fa-users',
  173. url: '/export/excel.php',
  174. filename: 'usuarios.csv',
  175. opciones: [
  176. // radio button
  177. { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Alumno', value: 'al' },
  178. { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Usuarios temporales', value: 'usr-temp' },
  179. { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Todos los usuarios', value: 'usr' },
  180. ],
  181. click: async function () {
  182. option.modal = this.name;
  183. try {
  184. await new Promise((resolve, reject) => {
  185. option.closeModal = () => {
  186. option.modal = false;
  187. reject(new Error("Modal closed by user"));
  188. };
  189. option.submitModal = resolve;
  190. });
  191. store.loading = true;
  192. option.modal = false;
  193. let formData = { query: 'usuarios' };
  194. document.querySelectorAll('input[name="query"]').forEach(input => {
  195. if (input.checked) formData.query = input.value;
  196. });
  197. await createDownloadLink({ url: this.url, filename: this.filename, postData: formData })();
  198. } catch (error) {
  199. console.error(error);
  200. } finally {
  201. store.loading = false;
  202. }
  203. }
  204. },
  205. {
  206. name: 'Reporte Syllabus Plan de Cátedra',
  207. icon: 'fas fa-file-invoice',
  208. url: '/export/gema.php',
  209. filename: 'syllabus_plan_catedra.csv',
  210. opciones: [
  211. { type: 'list', name: 'periodo_id', label: 'Periodo de GEMA', value: null, datalist: [] },
  212. ],
  213. mounted: async function () {
  214. this.opciones.find(item => item.name === 'periodo_id').datalist = await fetchOptions('/fetch/periodos.php', { nombre: 'Periodo_desc', id: 'Periodo_id' });
  215. },
  216. click: async function () {
  217. option.modal = this.name;
  218. await this.mounted();
  219. try {
  220. await new Promise((resolve, reject) => {
  221. option.closeModal = () => {
  222. option.modal = false;
  223. reject(new Error("Modal closed by user"));
  224. };
  225. option.submitModal = resolve;
  226. });
  227. store.loading = true;
  228. option.modal = false;
  229. let formData = { query: 'cursos' };
  230. this.opciones.forEach(opt => formData[opt.name] = opt.value);
  231. await createDownloadLink({ url: this.url, filename: this.filename, postData: formData })();
  232. } catch (error) {
  233. console.error(error);
  234. } finally {
  235. store.loading = false;
  236. }
  237. }
  238. },
  239. ],
  240. get currentOptions() {
  241. const currentItem = this.menu.find(item => item.name === this.option.modal);
  242. return currentItem ? currentItem.opciones : [];
  243. },
  244. closeModal: function () {
  245. if (typeof option.closeModal === 'function') option.closeModal();
  246. },
  247. submitModal: function () {
  248. if (typeof option.submitModal === 'function') option.submitModal();
  249. },
  250. async lastSnapshot() {
  251. try {
  252. const response = await fetch('/postgrest/snapshot_calificaciones?limit=1&order=created_at.desc');
  253. const data = await response.json();
  254. this.last_snapshot = data[0]?.created_at;
  255. } catch (error) {
  256. console.error(error);
  257. }
  258. }
  259. }).mount();
  260. </script>