supervisor.php 43 KB

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