horario_profesor.js 18 KB

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