horario_profesor.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. declare function triggerMessage(message: string, title: string, color?: string): void;
  2. declare const write: boolean;
  3. declare const moment: any;
  4. /**
  5. * Funciones auxiliares
  6. */
  7. type Profesor = {
  8. id: number,
  9. grado: string,
  10. profesor: string,
  11. clave: string,
  12. }
  13. type Horario = {
  14. id: number,
  15. carrera_id: number,
  16. materia: string,
  17. salon: string,
  18. profesores: Profesor[],
  19. hora: string,
  20. hora_final: string,
  21. dia: string,
  22. duracion: number,
  23. bloques: number,
  24. grupo: string,
  25. materia_id: number,
  26. }
  27. const compareHours = (hora1: string, hora2: string): number => {
  28. const [h1, m1] = hora1.split(":").map(Number);
  29. const [h2, m2] = hora2.split(":").map(Number);
  30. if (h1 !== h2) {
  31. return h1 > h2 ? 1 : -1;
  32. }
  33. if (m1 !== m2) {
  34. return m1 > m2 ? 1 : -1;
  35. }
  36. return 0;
  37. };
  38. let horarios = [] as Horario[];
  39. const table = document.querySelector("table") as HTMLTableElement;
  40. if (!(table instanceof HTMLTableElement)) {
  41. triggerMessage("No se ha encontrado la tabla", "Error", "error");
  42. throw new Error("No se ha encontrado la tabla");
  43. }
  44. [...Array(16).keys()].map(x => x + 7).forEach(hora => {
  45. // add 7 rows for each hour
  46. [0, 15, 30, 45].map((minute: number) => `${minute}`.padStart(2, '0')).forEach((minute: string) => {
  47. const tr = document.createElement("tr") as HTMLTableRowElement;
  48. tr.id = `hora-${hora}:${minute}`;
  49. tr.classList.add(hora > 13 ? "tarde" : "mañana");
  50. if (minute == "00") {
  51. const th = document.createElement("th") as HTMLTableCellElement;
  52. th.classList.add("text-center");
  53. th.scope = "row";
  54. th.rowSpan = 4;
  55. th.innerText = `${hora}:00`;
  56. th.style.verticalAlign = "middle";
  57. tr.appendChild(th);
  58. }
  59. ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"].forEach(día => {
  60. const td = document.createElement("td") as HTMLTableCellElement;
  61. td.id = `hora-${hora}:${minute}-${día}`;
  62. tr.appendChild(td);
  63. });
  64. const tbody = document.querySelector("tbody#horario") as HTMLTableSectionElement;
  65. if (!(tbody instanceof HTMLTableSectionElement)) {
  66. throw new Error("No se ha encontrado el tbody");
  67. }
  68. tbody.appendChild(tr);
  69. });
  70. });
  71. const empty_table = table.cloneNode(true) as HTMLTableElement;
  72. document.querySelectorAll('.hidden').forEach((element: HTMLElement) => {
  73. element.style.display = "none";
  74. });
  75. // hide the table
  76. table.style.display = "none";
  77. function moveHorario(id: string, día: string, hora: string) {
  78. const formData = new FormData();
  79. formData.append("id", id);
  80. formData.append("hora", hora);
  81. formData.append("día", día);
  82. fetch("action/action_horario_update.php", {
  83. method: "POST",
  84. body: formData
  85. }).then(res => res.json()).then(response => {
  86. if (response.status == "success") {
  87. triggerMessage("Horario movido", "Éxito", "success");
  88. } else {
  89. triggerMessage(response.message, "Error");
  90. }
  91. }).then(() => {
  92. renderHorario();
  93. }).catch(err => {
  94. triggerMessage(err, "Error");
  95. });
  96. }
  97. function renderHorario() {
  98. if (horarios.length == 0) {
  99. triggerMessage("Este profesor hay horarios para mostrar", "Error", "info");
  100. table.style.display = "none";
  101. document.querySelectorAll('.hidden').forEach((element: HTMLElement) => element.style.display = "none");
  102. return;
  103. }
  104. // show the table
  105. table.style.display = "table";
  106. document.querySelectorAll('.hidden').forEach((element: HTMLElement) => element.style.display = "block");
  107. // clear the table
  108. table.innerHTML = empty_table.outerHTML;
  109. function conflicts(horario1: Horario, horario2: Horario): boolean {
  110. const { hora: hora_inicio1, hora_final: hora_final1, dia: dia1 } = horario1;
  111. const { hora: hora_inicio2, hora_final: hora_final2, dia: dia2 } = horario2;
  112. if (dia1 !== dia2) {
  113. return false;
  114. }
  115. const compareInicios = compareHours(hora_inicio1, hora_inicio2);
  116. const compareFinales = compareHours(hora_final1, hora_final2);
  117. if (
  118. compareInicios >= 0 && compareInicios <= compareFinales ||
  119. compareFinales >= 0 && compareFinales <= -compareInicios
  120. ) {
  121. return true;
  122. }
  123. return false;
  124. }
  125. // remove the next 5 cells
  126. function removeNextCells(horas: number, minutos: number, dia: string, cells: number = 5) {
  127. for (let i = 1; i <= cells; i++) {
  128. const minute = minutos + i * 15;
  129. const nextMinute = (minute % 60).toString().padStart(2, "0");
  130. const nextHour = horas + Math.floor(minute / 60);
  131. const cellId = `hora-${nextHour}:${nextMinute}-${dia}`;
  132. const cellElement = document.getElementById(cellId);
  133. if (cellElement) {
  134. cellElement.remove();
  135. }
  136. else {
  137. console.log(`No se ha encontrado la celda ${cellId}`);
  138. break;
  139. }
  140. }
  141. }
  142. function newBlock(horario: Horario, edit = false) {
  143. function move(horario: Horario, cells: number = 5) {
  144. const [horas, minutos] = horario.hora.split(":").map(Number);
  145. const cell = document.getElementById(`hora-${horas}:${minutos.toString().padStart(2, "0")}-${horario.dia}`);
  146. const { top, left } = cell.getBoundingClientRect();
  147. const block = document.getElementById(`block-${horario.id}`);
  148. block.style.top = `${top}px`;
  149. block.style.left = `${left}px`;
  150. removeNextCells(horas, minutos, horario.dia, cells);
  151. }
  152. const [horas, minutos] = horario.hora.split(":").map(x => parseInt(x));
  153. const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
  154. horario.hora = hora;
  155. const cell = document.getElementById(`hora-${horario.hora}-${horario.dia}`) as HTMLTableCellElement;
  156. if (!cell) return;
  157. cell.dataset.ids = `${horario.id}`;
  158. const float_menu = edit ?
  159. `<div class="menu-flotante p-2" style="opacity: .7;">
  160. <a class="mx-2" href="#" data-toggle="modal" data-target="#modal-editar">
  161. <i class="ing-editar ing"></i>
  162. </a>
  163. <a class="mx-2" href="#" data-toggle="modal" data-target="#modal-borrar">
  164. <i class="ing-basura ing"></i>
  165. </a>
  166. </div>`
  167. : '';
  168. cell.innerHTML =
  169. `<div style="overflow-y: auto; overflow-x: hidden; height: 100%;" id="block-${horario.id}" class="position-absolute w-100 h-100">
  170. <small class="text-gray">${horario.hora}</small>
  171. <b class="title">${horario.materia}</b> <br>
  172. <br><span>Salón: </span>${horario.salon} <br>
  173. <small class="my-2">
  174. ${horario.profesores.map((profesor: Profesor) => ` <span class="ing ing-formacion mx-1"></span>${profesor.grado ?? ''} ${profesor.profesor}`).join("<br>")}
  175. </small>
  176. </div>
  177. ${float_menu}`;
  178. cell.classList.add("bloque-clase", "position-relative");
  179. cell.rowSpan = horario.bloques;
  180. // draggable
  181. cell.draggable = write;
  182. if (horario.bloques > 0) {
  183. removeNextCells(horas, minutos, horario.dia, horario.bloques - 1);
  184. }
  185. }
  186. function newConflictBlock(horarios: Horario[], edit = false) {
  187. const first_horario = horarios[0];
  188. const [horas, minutos] = first_horario.hora.split(":").map(x => parseInt(x));
  189. const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
  190. const ids = horarios.map(horario => horario.id);
  191. const cell = document.getElementById(`hora-${hora}-${first_horario.dia}`);
  192. if (cell == null) {
  193. console.error(`Error: No se encontró la celda: hora-${hora}-${first_horario.dia}`);
  194. return;
  195. }
  196. cell.dataset.ids = ids.join(",");
  197. // replace the content of the cell
  198. cell.innerHTML = `
  199. <small class='text-danger'>
  200. ${hora}
  201. </small>
  202. <div class="d-flex justify-content-center align-items-center mt-4">
  203. <div class="d-flex flex-column justify-content-center align-items-center">
  204. <span class="ing ing-importante text-danger" style="font-size: 2rem;"></span>
  205. <b class='text-danger'>
  206. Empalme de ${ids.length} horarios
  207. </b>
  208. <hr>
  209. <i class="text-danger">Ver horarios &#8230;</i>
  210. </div>
  211. </div>
  212. `;
  213. // Add classes and attributes
  214. cell.classList.add("conflict", "bloque-clase");
  215. cell.setAttribute("role", "button");
  216. // Add event listener for the cell
  217. cell.addEventListener("click", () => {
  218. $("#modal-choose").modal("show");
  219. const ids = cell.getAttribute("data-ids").split(",").map(x => parseInt(x));
  220. const tbody = document.querySelector("#modal-choose tbody");
  221. tbody.innerHTML = "";
  222. horarios.filter(horario => ids.includes(horario.id)).sort((a, b) => compareHours(a.hora, b.hora)).forEach(horario => {
  223. tbody.innerHTML += `
  224. <tr data-ids="${horario.id}">
  225. <td><small>${horario.hora.slice(0, -3)}-${horario.hora_final.slice(0, -3)}</small></td>
  226. <td>${horario.materia}</td>
  227. <td>
  228. ${horario.profesores.map(({ grado, profesor }) => `${grado ?? ''} ${profesor}`).join(", ")}
  229. </td>
  230. <td>${horario.salon}</td>
  231. ${edit ? `
  232. <td class="text-center">
  233. <button class="btn btn-sm btn-primary dismiss-editar" data-toggle="modal" data-target="#modal-editar">
  234. <i class="ing-editar ing"></i>
  235. </button>
  236. </td>
  237. <td class="text-center">
  238. <button class="btn btn-sm btn-danger dismiss-editar" data-toggle="modal" data-target="#modal-borrar">
  239. <i class="ing-basura ing"></i>
  240. </button>
  241. </td>
  242. ` : ""}
  243. </tr>`;
  244. });
  245. document.querySelectorAll(".dismiss-editar").forEach(btn => {
  246. btn.addEventListener("click", () => $("#modal-choose").modal("hide"));
  247. });
  248. });
  249. function getDuration(hora_i: string, hora_f: string): number {
  250. const [horas_i, minutos_i] = hora_i.split(":").map(x => parseInt(x));
  251. const [horas_f, minutos_f] = hora_f.split(":").map(x => parseInt(x));
  252. const date_i = new Date(0, 0, 0, horas_i, minutos_i);
  253. const date_f = new Date(0, 0, 0, horas_f, minutos_f);
  254. const diffInMilliseconds = date_f.getTime() - date_i.getTime();
  255. const diffInMinutes = diffInMilliseconds / (1000 * 60);
  256. const diffIn15MinuteIntervals = diffInMinutes / 15;
  257. return Math.floor(diffIn15MinuteIntervals);
  258. }
  259. const maxHoraFinal = horarios.reduce((max: Date, horario: Horario) => {
  260. const [horas, minutos] = horario.hora_final.split(":").map(x => parseInt(x));
  261. const date = new Date(0, 0, 0, horas, minutos);
  262. return date > max ? date : max;
  263. }, new Date(0, 0, 0, 0, 0));
  264. const horaFinalMax = new Date(0, 0, 0, maxHoraFinal.getHours(), maxHoraFinal.getMinutes());
  265. const blocks = getDuration(first_horario.hora, `${horaFinalMax.getHours()}:${horaFinalMax.getMinutes()}`);
  266. cell.setAttribute("rowSpan", blocks.toString());
  267. removeNextCells(horas, minutos, first_horario.dia, blocks - 1);
  268. }
  269. const conflictBlocks = horarios.filter((horario, index, arrayHorario) =>
  270. arrayHorario.filter((_, i) => i != index).some(horario2 =>
  271. conflicts(horario, horario2)))
  272. .sort((a, b) => compareHours(a.hora, b.hora));
  273. const classes = horarios.filter(horario => !conflictBlocks.includes(horario));
  274. const conflictBlocksPacked = []; // array of sets
  275. conflictBlocks.forEach(horario => {
  276. const setIndex = conflictBlocksPacked.findIndex(set => set.some(horario2 => conflicts(horario, horario2)));
  277. if (setIndex === -1) {
  278. conflictBlocksPacked.push([horario]);
  279. } else {
  280. conflictBlocksPacked[setIndex].push(horario);
  281. }
  282. })
  283. classes.forEach(horario =>
  284. newBlock(horario, write)
  285. )
  286. conflictBlocksPacked.forEach(horarios =>
  287. newConflictBlock(horarios, write)
  288. )
  289. // remove the elements that are not in the limits
  290. let max_hour = Math.max(...horarios.map(horario => {
  291. const lastMoment = moment(horario.hora, "HH:mm").add(horario.bloques * 15, "minutes");
  292. const lastHour = moment(`${lastMoment.hours()}:00`, "HH:mm");
  293. const hourInt = parseInt(lastMoment.format("HH"));
  294. return lastMoment.isSame(lastHour) ? hourInt - 1 : hourInt;
  295. }));
  296. let min_hour = Math.min(...horarios.map(horario => parseInt(horario.hora.split(":")[0])));
  297. document.querySelectorAll("tbody#horario tr").forEach(hora => {
  298. const hora_id = parseInt(hora.id.split("-")[1].split(":")[0]);
  299. (hora_id < min_hour || hora_id > max_hour) ? hora.remove() : null;
  300. })
  301. // if there is no sábado, remove the column
  302. if (!horarios.some(horario => horario.dia == "sábado")) {
  303. document.querySelectorAll("tbody#horario td").forEach(td => {
  304. if (td.id.split("-")[2] == "sábado") {
  305. td.remove();
  306. }
  307. });
  308. // remove the header (the last)
  309. document.querySelector("#headers").lastElementChild.remove();
  310. }
  311. // adjust width
  312. const ths = document.querySelectorAll("tr#headers th") as NodeListOf<HTMLTableCellElement>;
  313. ths.forEach((th, key) =>
  314. th.style.width = (key == 0) ? "5%" : `${95 / (ths.length - 1)}%`
  315. );
  316. // search item animation
  317. const menúFlontantes = document.querySelectorAll(".menu-flotante");
  318. menúFlontantes.forEach((element) => {
  319. element.classList.add("d-none");
  320. element.parentElement.addEventListener("mouseover", () =>
  321. element.classList.remove("d-none")
  322. );
  323. element.parentElement.addEventListener("mouseout", (e) =>
  324. element.classList.add("d-none")
  325. );
  326. });
  327. // droppables
  328. // forall the .bloque-elements add the event listeners for drag and drop
  329. document.querySelectorAll(".bloque-clase").forEach(element => {
  330. function dragStart() {
  331. this.classList.add("dragging");
  332. }
  333. function dragEnd() {
  334. this.classList.remove("dragging");
  335. }
  336. element.addEventListener("dragstart", dragStart);
  337. element.addEventListener("dragend", dragEnd);
  338. });
  339. // forall the cells that are not .bloque-clase add the event listeners for drag and drop
  340. document.querySelectorAll("td:not(.bloque-clase)").forEach(element => {
  341. function dragOver(e) {
  342. e.preventDefault();
  343. this.classList.add("dragging-over");
  344. }
  345. function dragLeave() {
  346. this.classList.remove("dragging-over");
  347. }
  348. function drop() {
  349. this.classList.remove("dragging-over");
  350. const dragging = document.querySelector(".dragging");
  351. const id = dragging.getAttribute("data-ids");
  352. const hora = this.id.split("-")[1];
  353. const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
  354. let día = this.id.split("-")[2];
  355. día = días.indexOf(día) + 1;
  356. // rowspan
  357. const bloques = parseInt(dragging.getAttribute("rowspan"));
  358. const horaMoment = moment(hora, "HH:mm");
  359. const horaFin = horaMoment.add(bloques * 15, "minutes");
  360. const limit = moment('22:00', 'HH:mm');
  361. if (horaFin.isAfter(limit)) {
  362. triggerMessage("No se puede mover el bloque a esa hora", "Error");
  363. // scroll to the top
  364. window.scrollTo(0, 0);
  365. return;
  366. }
  367. // get the horario
  368. // remove the horario
  369. const bloque = document.querySelector(`.bloque-clase[data-ids="${id}"]`) as HTMLElement;
  370. // remove all children
  371. while (bloque.firstChild) {
  372. bloque.removeChild(bloque.firstChild);
  373. }
  374. // prepend a loading child
  375. const loading = `<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
  376. <span class="sr-only">Loading...</span>
  377. </div>`;
  378. bloque.insertAdjacentHTML("afterbegin", loading);
  379. // add style vertical-align: middle
  380. bloque.style.verticalAlign = "middle";
  381. bloque.classList.add("text-center");
  382. // remove draggable
  383. bloque.removeAttribute("draggable");
  384. moveHorario(id, día, hora);
  385. }
  386. element.addEventListener("dragover", dragOver);
  387. element.addEventListener("dragleave", dragLeave);
  388. element.addEventListener("drop", drop);
  389. });
  390. }
  391. const form = document.getElementById('form') as HTMLFormElement;
  392. if (!(form instanceof HTMLFormElement)) {
  393. triggerMessage('No se ha encontrado el formulario', 'Error', 'danger');
  394. throw new Error("No se ha encontrado el formulario");
  395. }
  396. form.querySelector('#clave_profesor').addEventListener('input', function (e) {
  397. const input = form.querySelector('#clave_profesor') as HTMLInputElement;
  398. const option = form.querySelector(`option[value="${input.value}"]`) as HTMLOptionElement;
  399. if (input.value == "") {
  400. input.classList.remove("is-invalid", "is-valid");
  401. return;
  402. }
  403. if (!option) {
  404. input.classList.remove("is-valid");
  405. input.classList.add("is-invalid");
  406. }
  407. else {
  408. const profesor_id = form.querySelector('#profesor_id') as HTMLInputElement;
  409. profesor_id.value = option.dataset.id;
  410. input.classList.remove("is-invalid");
  411. input.classList.add("is-valid");
  412. }
  413. });
  414. form.addEventListener('submit', async function (e) {
  415. e.preventDefault();
  416. const input = form.querySelector('#clave_profesor') as HTMLInputElement;
  417. if (input.classList.contains("is-invalid")) {
  418. triggerMessage('El profesor no se encuentra registrado', 'Error', 'danger');
  419. return;
  420. }
  421. const formData = new FormData(form);
  422. try {
  423. const buttons = document.querySelectorAll("button") as NodeListOf<HTMLButtonElement>;
  424. buttons.forEach(button => {
  425. button.disabled = true;
  426. button.classList.add("disabled");
  427. });
  428. const response = await fetch('action/action_horario_profesor.php', {
  429. method: 'POST',
  430. body: formData,
  431. });
  432. const data = await response.json();
  433. buttons.forEach(button => {
  434. button.disabled = false;
  435. button.classList.remove("disabled");
  436. });
  437. if (data.status == 'success') {
  438. horarios = data.data;
  439. renderHorario();
  440. }
  441. else {
  442. triggerMessage(data.message, 'Error en la consulta', 'warning');
  443. }
  444. } catch (error) {
  445. triggerMessage('Fallo al consutar los datos ', 'Error', 'danger');
  446. console.log(error);
  447. }
  448. });
  449. const input = form.querySelector('#clave_profesor') as HTMLInputElement;
  450. const option = form.querySelector(`option[value="${input.value}"]`) as HTMLOptionElement;