supervisor.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Supervisor</title>
  7. <?php
  8. include 'import/html_css_files.php';
  9. ?>
  10. <style>
  11. [v-cloak] {
  12. display: none;
  13. }
  14. </style>
  15. </head>
  16. <body>
  17. <?
  18. $redirect = $_SERVER['PHP_SELF'];
  19. include "import/html_header.php";
  20. // 200.0.0.1/checador_otros/admin_checdor/[this_page].php => ruta = [this_page].php
  21. global $user;
  22. html_header(
  23. "Registro de asistencia - Vicerrectoría Académica",
  24. "Sistema de gestión de checador",
  25. );
  26. ?>
  27. <main class="container-fluid px-4" id="app" v-cloak @vue:mounted="mounted">
  28. <!-- error messages -->
  29. <div class="container mb-4 mt-2">
  30. <div class="row">
  31. <div class="col-12">
  32. <div class="alert alert-dismissible fade show" role="alert" v-for="message in messages.data"
  33. :class="`alert-${message.color}`" :key="message.hora">
  34. <!-- messages: {error, hora} -->
  35. <div :key="message" class="d-flex justify-content-between">
  36. <span>
  37. <code>[{{message.hora}}]</code>
  38. <strong>{{message.prefix}}</strong>
  39. </span>
  40. {{ message.message }}
  41. <button type="button" class="close"
  42. @click="messages.data.splice(messages.data.indexOf(message), 1)" data-dismiss="alert">
  43. <span aria-hidden="true">&times;</span>
  44. </button>
  45. </div>
  46. </div>
  47. </div>
  48. </div>
  49. </div>
  50. <!-- filtros -->
  51. <div v-if="store.rutas.data.length > 0">
  52. <div class="card mt-4">
  53. <div class="card-header bg-dark d-flex justify-content-between align-items-center flex-wrap text-white">
  54. <h2 class="col-md-10 col-12 text-white font-weight-bold text-uppercase text-center">
  55. {{ JSON.parse(store.rutas.data.find(ruta => ruta.salon_id === store.rutas.selected)?.salon_array
  56. ?? null)?.splice(1)?.join('/') ?? 'No datos' }}
  57. </h2>
  58. <div>
  59. <button type="button" class="btn btn-info btn-sm"
  60. @click="store.rutas.data = []; header = 'Seleccione una ruta'">
  61. <i class="ing-flecha ing-rotate-180"></i>
  62. </button>
  63. <button type="button" class="btn btn-success btn-sm" data-toggle="modal"
  64. data-target="#editar-ubicaciones">
  65. <i class="ing-editar"></i>
  66. </button>
  67. </div>
  68. </div>
  69. <div class="card-body bg-info">
  70. <div class="container-fluid">
  71. <div class="row flex-nowrap mw-100 overflow-auto">
  72. <!-- size big -->
  73. <div class="mx-2 my-2 col-auto" v-for="ruta in store.rutas.data" :key="ruta.salon_id">
  74. <span class="shadow badge badge-pill py-2 px-4" @click="store.selectRuta(ruta.salon_id)"
  75. :class="{ 'badge-primary': store.rutas.selected == ruta.salon_id, 'badge-light text-primary': store.rutas.selected != ruta.salon_id, 'badge-dark text-muted disabled' : ruta.horarios.every(({estado_supervisor_id}) => estado_supervisor_id) && store.rutas.selected != ruta.salon_id }">
  76. {{ JSON.parse(ruta.salon_array).splice(1).join('/') }}
  77. <span class="badge mx-3"
  78. v-if="ruta.horarios.some(({estado_supervisor_id}) => !estado_supervisor_id)"
  79. :class="{ 'badge-success': ruta.horarios.length > 0, 'badge-danger': ruta.horarios.length == 0 }">
  80. {{ ruta.horarios.filter(({estado_supervisor_id}) => estado_supervisor_id).length
  81. }} / {{
  82. ruta.horarios.length }}
  83. </span>
  84. <span v-else class="badge mx-3"
  85. :class="{ 'badge-success': ruta.horarios.length > 0, 'badge-danger': ruta.horarios.length == 0 }">
  86. <i class="ing-aceptar"></i>
  87. </span>
  88. <span class="sr-only">
  89. Faltan {{ ruta.horarios.filter(({estado_supervisor_id}) =>
  90. estado_supervisor_id).length }} horarios
  91. por registrar
  92. </span>
  93. <span class="badge mx-1 badge-warning" @click="location.hash = '#sin-internet'"
  94. v-if="ruta.horarios.some(({pendiente}) => pendiente)">
  95. <i class="ing-importante2"></i>
  96. </span>
  97. </span>
  98. </div>
  99. </div>
  100. </div>
  101. </div>
  102. <div class="card-footer bg-dark d-flex justify-content-between align-items-center flex-wrap text-white">
  103. <button class="btn btn-info" :disabled="store.bloquesHorario.selected == 0"
  104. @click="store.selectBloque(store.bloquesHorario.selected - 1); rutas(current_espacio)">
  105. <i class="ing-caret ing-rotate-90"></i>
  106. <span class="d-none d-md-inline-block">
  107. Bloque horario anterior
  108. </span>
  109. </button>
  110. <h3 class="text-white font-weight-bold text-uppercase text-center">
  111. {{ store.hora_inicio.slice(0, 5) }} - {{ store.hora_fin.slice(0, 5) }}
  112. </h3>
  113. <button class="btn btn-info"
  114. @click="store.selectBloque(store.bloquesHorario.selected + 1); rutas(current_espacio)"
  115. :disabled="store.bloquesHorario.selected == store.bloquesHorario.data.length - 1">
  116. <span class="d-none d-md-inline-block">
  117. Bloque horario siguiente
  118. </span>
  119. <i class="ing-caret ing-rotate-270"></i>
  120. </button>
  121. </div>
  122. </div>
  123. <section id="#warnings" class="mt-4" v-if="clases.some(clase => clase.pendiente)">
  124. <div class="alert alert-warning" role="alert">
  125. <h4 class="alert-heading"><i class="ing-importante2"></i> Sin conexión a internet</h4>
  126. <p>
  127. Hay datos en esta ruta que no pudieron guardarse, por favor, revise su conexión a internet y dé
  128. click en
  129. <button class="btn btn-outline-dark btn-sm mb-4" @click="guardarCambios"><i
  130. class="ing-guardar"></i> Guardar cambios</button>
  131. </p>
  132. <hr>
  133. <p class="mb-0">
  134. Los datos se mantendrán mientras tenga la página abierta, pero si la cierra o la refresca, se
  135. perderán.
  136. </p>
  137. </div>
  138. </section>
  139. <div class="mt-3 d-flex justify-content-center">
  140. <!-- refresh -->
  141. <div class="table-responsive">
  142. <table class="table table-hover table-striped table-bordered table-sm">
  143. <thead class="thead-dark">
  144. <tr>
  145. <th scope="col" class="text-center align-middle text-nowrap px-2">
  146. <button @click="invertir" class="btn btn-info mr-3" v-if="clases.length > 0">
  147. <i class="ing-cambiar ing-rotate-90"></i>
  148. </button>
  149. Salón
  150. </th>
  151. <th scope="col" class="text-center align-middle text-nowrap px-2">Profesor</th>
  152. <th scope="col" class="text-center align-middle text-nowrap px-2">Horario</th>
  153. <th scope="col" class="text-center align-middle text-nowrap px-2">Acciones</th>
  154. </tr>
  155. </thead>
  156. <tbody>
  157. <tr v-if="clases.length == 0">
  158. <td colspan="6" class="text-center">No hay clases en este horario</td>
  159. </tr>
  160. <tr v-for="clase in clases" :key="clase.horario_id">
  161. <td class="text-center align-middle">{{ clase.salon }}</td>
  162. <td class="text-center align-middle">
  163. <div class="col-12">
  164. {{ clase.profesor_nombre }}
  165. </div>
  166. <div class="col-12">
  167. <button type="button" class="btn btn-outline-dark btn-sm"
  168. @click="store.profesor_selected = clase.horario_id" data-toggle="modal"
  169. data-target="#ver-detalle">
  170. Ver detalle <i class="ing-ojo"></i>
  171. </button>
  172. </div>
  173. </td>
  174. <td class="text-center align-middle">
  175. {{ clase.hora_inicio.slice(0, 5) }} - {{ clase.hora_fin.slice(0, 5) }}
  176. </td>
  177. <td class="text-center align-middle text-nowrap">
  178. <!-- data-toggle="button" -->
  179. <button class="btn text-center mx-2" v-for="estado in estados" :key="estado.id"
  180. @click="store.cambiarEstado(clase.horario_id, estado.id === clase.estado_supervisor_id ? null : estado.id)"
  181. :class="[{'active': estado.id === clase.estado_supervisor_id}, `btn-outline-${estado.color}`]"
  182. :aria-pressed="estado.id === clase.estado_supervisor_id">
  183. <i :class="estado.icon"></i>
  184. </button>
  185. <button class="btn btn-outline-primary text-center mx-2" data-toggle="modal"
  186. data-target="#editar-comentario" :class="{ 'active': clase.comentario }"
  187. @click="store.selectEditor(clase.horario_id)">
  188. <i class="ing-editar"></i>
  189. <span class="badge badge-pill badge-primary" v-if="clase.comentario">...</span>
  190. <span class="sr-only">Editar comentario</span>
  191. </button>
  192. </td>
  193. </tr>
  194. </tbody>
  195. </table>
  196. </div>
  197. </div>
  198. <button class="btn btn-primary btn-lg btn-block mb-4" @click="guardarCambios">
  199. <i class="ing-guardar"></i>
  200. Guardar cambios
  201. </button>
  202. </div>
  203. <div v-else-if="store.bloquesHorario.selected === -1">
  204. <div class="list-group my-4 container">
  205. <div class="card text-center">
  206. <div class="card-header bg-dark text-white">
  207. <h2 class="text-center">
  208. {{header}}
  209. </h2>
  210. </div>
  211. <div class="card-body" v-if="!loading">
  212. <a :href="`#horario-${horario.id}`" class="list-group-item list-group-item-action"
  213. v-for="horario in store.bloquesHorario.data" :key="horario.id"
  214. @click="store.bloquesHorario.selected = store.bloquesHorario.data.indexOf(horario)">
  215. <div class="d-flex w-100 justify-content-between">
  216. <h5 class="mb-1">{{ horario.hora_inicio.slice(0, 5) }} - {{horario.hora_fin.slice(0, 5)
  217. }}</h5>
  218. </div>
  219. </a>
  220. </div>
  221. <div class="card-body" v-else>
  222. <div class="d-flex justify-content-center">
  223. <div class="spinner-border text-primary" role="status">
  224. <span class="sr-only">Cargando...</span>
  225. </div>
  226. </div>
  227. </div>
  228. <div class="card-footer text-muted bg-dark text-white">
  229. Lista de bloques horario
  230. </div>
  231. </div>
  232. </div>
  233. </div>
  234. <div v-else>
  235. <div class="list-group my-4 container">
  236. <div class="card text-center">
  237. <div class="card-header bg-dark text-white">
  238. <h2 class="text-center">
  239. {{header}}
  240. </h2>
  241. </div>
  242. <div class="card-body" v-if="!loading">
  243. <a :href="`#ruta-${ruta.id_espacio_sgu}`" class="list-group-item list-group-item-action"
  244. v-for="ruta in catálogo_rutas.data" :key="ruta.salon_id" @click="rutas(ruta.id_espacio_sgu)"
  245. disabled>
  246. <div class="d-flex w-100 justify-content-between">
  247. <h5 class="mb-1">{{ ruta.salon }}</h5>
  248. <small v-if="ruta.subrutas.length > 0">{{ ruta.subrutas.length }} espacios</small>
  249. <small v-else class="text-danger">Sin espacios</small>
  250. </div>
  251. </a>
  252. </div>
  253. <div class="card-body" v-else>
  254. <div class="d-flex justify-content-center">
  255. <div class="spinner-border text-primary" role="status">
  256. <span class="sr-only">Cargando...</span>
  257. </div>
  258. </div>
  259. </div>
  260. <div class="card-footer text-muted bg-dark text-white">
  261. Rutas de la Universidad La Salle
  262. </div>
  263. </div>
  264. </div>
  265. </div>
  266. <!-- MODAL -->
  267. <div class="modal" tabindex="-1" id="editar-ubicaciones">
  268. <div class="modal-dialog">
  269. <div class="modal-content">
  270. <div class="modal-header">
  271. <h5 class="modal-title">Editar rutas</h5>
  272. <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
  273. <span aria-hidden="true">&times;</span>
  274. </button>
  275. </div>
  276. <div class="modal-body">
  277. <div class="container">
  278. <h2>Reordena las rutas</h2>
  279. <ul id="sortable" class="list-group">
  280. <li class="list-group-item" v-for="ruta in store.rutas.data" :key="ruta.salon_id"
  281. :id="'ruta-' + ruta.salon_id"
  282. :class="[ruta.horarios.every(horario => horario.estado_supervisor_id) ? ['disabled', 'bg-light', 'undraggable'] : '']">
  283. {{ JSON.parse(ruta.salon_array).join('/') }}
  284. </li>
  285. </ul>
  286. </div>
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. <div class="modal" tabindex="-1" id="editar-comentario">
  292. <div class="modal-dialog">
  293. <div class="modal-content">
  294. <div class="modal-header">
  295. <h5 class="modal-title">Añadir comentario</h5>
  296. <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
  297. <span aria-hidden="true">&times;</span>
  298. </button>
  299. </div>
  300. <div class="modal-body">
  301. <div class="container">
  302. <h2 class="text-center">Comentarios de la clase</h2>
  303. <br>
  304. <div class="input-group">
  305. <div class="input-group-prepend">
  306. <span class="input-group-text bg-primary text-white">Comentario
  307. <button class="btn btn-light ml-2 text-primary"
  308. @click="store.limpiarComentario">
  309. <i class="ing-borrar"></i>
  310. </button>
  311. </span>
  312. </div>
  313. <textarea class="form-control" aria-label="Comentarios de la clase"
  314. v-model="store.editor.texto"></textarea>
  315. </div>
  316. </div>
  317. </div>
  318. <div class="modal-footer">
  319. <button type="button" class="btn btn-outline-danger" data-dismiss="modal">
  320. <i class="ing-cancelar"></i>
  321. Cancelar
  322. </button>
  323. <button type="button" class="btn btn-primary" data-dismiss="modal"
  324. @click="store.guardarComentario">
  325. Guardar comentario
  326. </button>
  327. </div>
  328. </div>
  329. </div>
  330. </div>
  331. <div class="modal" tabindex="-1" id="ver-detalle">
  332. <div class="modal-dialog modal-dialog-centered modal-xl" v-if="clase_vista">
  333. <div class="modal-content">
  334. <div class="modal-header">
  335. <h2 class="modal-title" :data-id="clase_vista.horario_id">Detalle de la clase</h2>Detalle de la clase</h2>
  336. <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
  337. <span aria-hidden="true">&times;</span>
  338. </button>
  339. </div>
  340. <div class="modal-body">
  341. <div class="container" v-if="store.profesor_selected">
  342. <div class="row">
  343. <section class="col-12 col-md-6">
  344. <h4 class="h4">Profesor</h4>
  345. <div class="row">
  346. <div class="col-12">
  347. <strong>Nombre:</strong>
  348. {{ clase_vista.profesor_nombre }}
  349. </div>
  350. <div class="col-12">
  351. <strong>Correo:</strong>
  352. <a :href="`mailto:${clase_vista.profesor_correo}`"><strong>{{
  353. clase_vista.profesor_correo }}</strong></a>
  354. </div>
  355. <div class="col-12">
  356. <strong>Clave:</strong>
  357. {{ clase_vista.profesor_clave }}
  358. </div>
  359. <div class="col-12">
  360. <strong>Facultad:</strong>
  361. {{ clase_vista.facultad }}
  362. </div>
  363. </div>
  364. </section>
  365. <section class="col-12 col-md-6">
  366. <h4 class="h4">Clase</h4>
  367. <div class="row">
  368. <div class="col-12">
  369. <strong>Materia:</strong>
  370. {{ clase_vista.materia }}
  371. </div>
  372. <div class="col-12">
  373. <strong>Carrera:</strong>
  374. {{ clase_vista.carrera }}
  375. </div>
  376. <div class="col-12">
  377. <strong>Grupo:</strong>
  378. {{ clase_vista.horario_grupo }}
  379. </div>
  380. <div class="col-12">
  381. <strong>Horario:</strong>
  382. <!-- hora hh:mm:ss to hh:mm -->
  383. {{ clase_vista.hora_inicio?.slice(0, 5) }} - {{
  384. clase_vista.hora_fin?.slice(0, 5) }}
  385. </div>
  386. <div class="col-12">
  387. <strong>Salón:</strong>
  388. {{ clase_vista.salon }}
  389. </div>
  390. </div>
  391. </section>
  392. </div>
  393. <div class="row">
  394. <section class="col-12">
  395. <h4 class="h4 mt-4">Registro</h4>
  396. <div class="row">
  397. <div class="col-12 text-center" v-if="!clase_vista.registro_fecha">
  398. <strong><span class="badge badge-danger"><i class="ing-cancelar"></i></span>
  399. El profesor aún no ha registrado su asistencia</strong>
  400. </div>
  401. <div class="col-6 text-center" v-else>
  402. El profesor registró su asistencia a las
  403. <code>{{clase_vista.registro_fecha.slice(11, 16)}}</code>
  404. <hr>
  405. <p v-if="!clase_vista.registro_retardo" class="text-center">
  406. <span class="badge badge-success"><i class="ing-aceptar"></i></span>
  407. A tiempo
  408. </p>
  409. <p v-else class="text-center">
  410. <span class="badge badge-warning"><i class="ing-retardo"></i></span>
  411. Con retardo
  412. </p>
  413. </div>
  414. </div>
  415. </section>
  416. </div>
  417. </div>
  418. </div>
  419. <div class="modal-footer">
  420. <!-- botón aceptar -->
  421. <button type="button" class="btn btn-outline-primary" data-dismiss="modal">
  422. <i class="ing-aceptar"></i>
  423. Aceptar
  424. </button>
  425. </div>
  426. </div>
  427. </div>
  428. </div>
  429. </div>
  430. </main>
  431. <?php
  432. include "import/html_footer.php";
  433. ?>
  434. <!-- filtro modal -->
  435. <script src="js/jquery.min.js"></script>
  436. <script src="js/jquery-ui.js"></script>
  437. <script src="js/jquery-ui.touch-punch.min.js"></script>
  438. <script src="js/bootstrap/bootstrap.min.js"></script>
  439. <?php include_once 'js/messages.php'; ?>
  440. <script src="https://unpkg.com/petite-vue"></script>
  441. <script>
  442. const estados = [
  443. {
  444. color: "success",
  445. icon: "ing-autorizar",
  446. id: 1,
  447. },
  448. {
  449. color: "danger",
  450. icon: "ing-negar",
  451. id: 2,
  452. },
  453. {
  454. color: "warning",
  455. icon: "ing-retardo",
  456. id: 3,
  457. },
  458. {
  459. color: "info",
  460. icon: "ing-justificar",
  461. id: 4,
  462. },
  463. ];
  464. const messages = PetiteVue.reactive({
  465. data: [],
  466. push_message(message, silent = false) {
  467. if (silent) {
  468. console.log(message);
  469. return
  470. }
  471. // go to the top
  472. window.scrollTo({
  473. top: 0,
  474. behavior: 'smooth'
  475. });
  476. this.data.push(message);
  477. setTimeout(() => {
  478. this.data.pop();
  479. }, 5000);
  480. },
  481. });
  482. const store = PetiteVue.reactive({
  483. messages,
  484. bloquesHorario: {
  485. data: [],
  486. selected: 0
  487. },
  488. rutas: {
  489. data: [],
  490. selected: 0
  491. },
  492. editor: {
  493. id: 0,
  494. texto: "",
  495. },
  496. get hora_inicio() {
  497. return this.bloquesHorario.data[this.bloquesHorario.selected]?.hora_inicio ?? "";
  498. },
  499. get hora_fin() {
  500. return this.bloquesHorario.data[this.bloquesHorario.selected]?.hora_fin ?? "";
  501. },
  502. selectRuta(index) {
  503. this.rutas.selected = index;
  504. },
  505. order() {
  506. const finals = this.rutas.data.filter(ruta => ruta.horarios.length > 0 && ruta.horarios.every(horario => horario.estado_supervisor_id));
  507. const lasts = this.rutas.data.filter(ruta => ruta.horarios.length == 0);
  508. const notLasts = this.rutas.data.filter(ruta => ruta.horarios.some(horario => !horario.estado_supervisor_id));
  509. // console.log("finals", finals, "lasts", lasts, "notLasts", notLasts)
  510. this.rutas.data = [...notLasts, ...finals, ...lasts];
  511. },
  512. // clases
  513. selectBloque(bloqueIndex) {
  514. this.bloquesHorario.selected = bloqueIndex;
  515. },
  516. // estado
  517. async cambiarEstado(horario_id, estadoId) {
  518. const ruta = store.rutas.data.find(ruta => ruta.salon_id == this.rutas.selected);
  519. const clase = ruta.horarios.find(clase => clase.horario_id == horario_id);
  520. clase.estado_supervisor_id = estadoId;
  521. try {
  522. if (!navigator.onLine) {
  523. clase.pendiente = true;
  524. throw ("No hay conexión a internet");
  525. }
  526. const cambio = await fetch("action/registro_supervisor.php", {
  527. method: "POST",
  528. headers: {
  529. "Content-Type": "application/json"
  530. },
  531. body: JSON.stringify([{
  532. horario_id: horario_id,
  533. estado: estadoId,
  534. profesor_id: clase.profesor_id,
  535. comentario: clase.comentario,
  536. supervisor_id: <?= $user->user['id'] ?>,
  537. }])
  538. }).then(res => res.json());
  539. if (cambio.error) throw cambio.error;
  540. clase.pendiente = false;
  541. } catch (error) {
  542. messages.push_message({
  543. message: error,
  544. hora: new Date().toLocaleTimeString('es-MX', { timeZone: 'America/Mexico_City' }),
  545. color: "danger",
  546. prefix: "Error",
  547. }, true);
  548. }
  549. // scroll to the top only if this ruta has no clases with estado 0
  550. if (ruta.horarios.every(clase => clase.estado_supervisor_id != null))
  551. window.scrollTo({
  552. top: 0,
  553. behavior: 'smooth'
  554. });
  555. this.order();
  556. },
  557. // editor
  558. selectEditor(horario_id) {
  559. this.editor.id = horario_id;
  560. this.editor.texto = store.rutas.data.find(ruta => ruta.salon_id == this.rutas.selected).horarios.find(clase => clase.horario_id == horario_id).comentario;
  561. },
  562. guardarComentario() {
  563. const ruta = store.rutas.data.find(ruta => ruta.salon_id == this.rutas.selected);
  564. const clase = ruta.horarios.find(clase => clase.horario_id == this.editor.id);
  565. clase.comentario = this.editor.texto;
  566. store.cambiarEstado(clase.horario_id, clase.estado_supervisor_id);
  567. },
  568. limpiarComentario() {
  569. this.editor.texto = "";
  570. },
  571. profesor_selected: null,
  572. });
  573. $(document).ready(function () {
  574. $("#sortable").sortable({
  575. update: function (event, ui) {
  576. // get the new order
  577. var newOrder = $(this).children().map(function () {
  578. // id = ruta-{id}
  579. return parseInt(this.id.split('-')[1]);
  580. }).get();
  581. // store the new order
  582. store.rutas.data = newOrder.map(function (id) {
  583. return store.rutas.data.find(function (ruta) {
  584. return ruta.salon_id === id;
  585. });
  586. });
  587. },
  588. items: "li:not(.undraggable)"
  589. }).disableSelection();
  590. $('#sortable>li:not(.undraggable)').draggable({
  591. axis: 'y',
  592. containment: 'parent',
  593. })
  594. });
  595. PetiteVue.createApp({
  596. store,
  597. messages,
  598. header: "Cargando auditoría",
  599. loading: true,
  600. catálogo_rutas: {
  601. data: [],
  602. selected: 0
  603. },
  604. get clases() {
  605. const clases = store.rutas.data.find(ruta => ruta.salon_id == store.rutas.selected)?.horarios ?? [];
  606. // console.log("All clases", JSON.parse(JSON.stringify(clases)), "Selected: ", store.rutas.selected);
  607. return clases;
  608. },
  609. async guardarCambios() {
  610. try {
  611. if (!navigator.onLine)
  612. throw "No hay conexión a internet";
  613. console.log(store.rutas.data.map(ruta => ruta.horarios).flat(1).filter(clase => clase.pendiente));
  614. const cambio = await fetch("action/registro_supervisor.php", {
  615. method: "POST",
  616. headers: {
  617. "Content-Type": "application/json"
  618. },
  619. body: JSON.stringify(store.rutas.data.map(ruta => ruta.horarios).flat(1).filter(clase => clase.pendiente).map(clase => ({
  620. horario_id: clase.horario_id,
  621. estado: clase.estado_supervisor_id,
  622. profesor_id: clase.profesor_id,
  623. comentario: clase.comentario,
  624. supervisor_id: <?= $user->user['id'] ?>,
  625. }))),
  626. }).then(res => res.json());
  627. if (cambio.error) throw cambio.error;
  628. } catch (error) {
  629. messages.push_message({
  630. message: error,
  631. hora: new Date().toLocaleTimeString('es-MX', { timeZone: 'America/Mexico_City' }),
  632. color: "danger",
  633. prefix: "Error",
  634. });
  635. return
  636. }
  637. store.rutas.data.map(ruta => ruta.horarios).flat(1).filter(clase => clase.pendiente).forEach(clase => clase.pendiente = false);
  638. messages.push_message({
  639. message: "Cambios guardados",
  640. hora: new Date().toLocaleTimeString('es-MX', { timeZone: 'America/Mexico_City' }),
  641. color: "success",
  642. prefix: "Éxito",
  643. });
  644. },
  645. invertir() {
  646. this.clases.reverse();
  647. },
  648. async mounted() {
  649. store.bloquesHorario.data = await fetch('action/action_grupo_horario.php').then(res => res.json());
  650. store.bloquesHorario.selected = store.bloquesHorario.data.findIndex(bloque => bloque.selected);
  651. // console.log(store.bloquesHorario.selected);
  652. if (store.bloquesHorario.selected == -1) {
  653. this.header = "Seleccione un horario";
  654. }
  655. else {
  656. this.header = "Seleccione una ruta";
  657. }
  658. this.catálogo_rutas.data = await fetch('action/rutas.php').then(res => res.json());
  659. this.loading = false;
  660. },
  661. current_espacio: null,
  662. async rutas(id_espacio_sgu) {
  663. store.rutas.data = [];
  664. store.rutas.selected = 0;
  665. this.loading = true;
  666. this.current_espacio = id_espacio_sgu;
  667. this.loading = true;
  668. this.header = `Cargando rutas para ${this.catálogo_rutas.data.find(ruta => ruta.id_espacio_sgu == id_espacio_sgu).salon}`;
  669. this.catálogo_rutas.selected = id_espacio_sgu;
  670. const url = 'action/rutas_salón_horario.php'
  671. const searchParams = new URLSearchParams({
  672. id_espacio_sgu: id_espacio_sgu,
  673. bloque_horario_id: store.bloquesHorario.data[store.bloquesHorario.selected].id
  674. });
  675. const rutas = await fetch(`${url}?${searchParams}`).then(res => res.json());
  676. store.rutas.data = rutas.filter(ruta => ruta.horarios.length > 0);
  677. if (store.rutas.data.length == 0) {
  678. this.header = `No hay clases en este horario`;
  679. this.loading = false;
  680. return
  681. }
  682. store.rutas.selected = store.rutas.data[0].salon_id;
  683. store.order();
  684. // inject horarios
  685. this.loading = false;
  686. },
  687. get clase_vista() {
  688. return this.clases.find(clase => clase.horario_id == store.profesor_selected) ?? false;
  689. },
  690. }).mount('#app')
  691. </script>
  692. </body>
  693. </html>