ソースを参照

Stable 2-ago-2023

Alejandro Rosales 2 年 前
コミット
f0cc3c585d
60 ファイル変更6492 行追加903 行削除
  1. 57 0
      action/action_auditoria.php
  2. 42 0
      action/action_estado_supervisor.php
  3. 45 5
      action/action_facultad.php
  4. 26 0
      action/action_fechas_clase.php
  5. 3 4
      action/action_grupo.php
  6. 28 0
      action/action_grupo_horario.php
  7. 37 3
      action/action_horario_profesor.php
  8. 38 35
      action/action_login.php
  9. 54 0
      action/action_reposiciones.php
  10. 2 2
      action/force_session.php
  11. 60 0
      action/registro_supervisor.php
  12. 28 0
      action/rutas.php
  13. 73 0
      action/rutas_salón_horario.php
  14. 24 0
      action/schemas/registro_supervisor.json
  15. 377 0
      auditoría.php
  16. 0 1
      bypass.php
  17. 89 46
      class/c_login.php
  18. 2 1
      composer.json
  19. 71 1
      composer.lock
  20. 93 89
      consultar_horario.php
  21. 18 0
      css/sgi.css
  22. 92 0
      css/style.css
  23. 235 0
      demo.html
  24. 29 14
      horario_profesor.php
  25. 2 1
      import/html_css_files.php
  26. 50 24
      import/html_header.php
  27. 26 23
      import/periodo.php
  28. 1 0
      index.php
  29. 160 0
      js/auditoría.js
  30. 120 0
      js/client.js
  31. 2 2
      js/datalist.js
  32. 0 0
      js/declaration.js
  33. 414 0
      js/horario_profesor.js
  34. 0 20
      js/horarios_profesor.js
  35. 11 0
      js/jquery-ui.touch-punch.min.js
  36. 178 0
      js/reposiciones.js
  37. 66 6
      main.php
  38. 212 10
      package-lock.json
  39. 9 1
      package.json
  40. 284 0
      reposiciones.php
  41. 0 136
      route.php
  42. 803 0
      sample/rutas.json
  43. 224 0
      selector_rutas.php
  44. 27 0
      service/auto.php
  45. 76 0
      service/backend/carreras.php
  46. 60 0
      service/backend/periodos.php
  47. 155 0
      service/client.html
  48. 81 0
      service/horarios.php
  49. 35 0
      service/periodos.v1.php
  50. 37 0
      service/periodos.v2.php
  51. 734 0
      supervisor.php
  52. 237 0
      ts/auditoría.ts
  53. 149 0
      ts/client.ts
  54. 0 347
      ts/consulta_horarios.ts
  55. 0 107
      ts/consultar_horarios.ts
  56. 0 10
      ts/date_functions.ts
  57. 1 0
      ts/declaration.ts
  58. 532 0
      ts/horario_profesor.ts
  59. 272 0
      ts/reposiciones.ts
  60. 11 15
      tsconfig.json

+ 57 - 0
action/action_auditoria.php

@@ -0,0 +1,57 @@
+<?
+#input $_GET['id_espacio_sgu']
+#output rutas: [ ...ruta, salones: [{...salon}] ]
+header('Content-Type: application/json charset=utf-8');
+$information = [
+    'GET' => [
+        #'periodo_id',
+    ],
+];
+$ruta = "../";
+require_once "../class/c_login.php";
+// check method
+try {
+
+
+    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+        array_walk($information['GET'], function ($value) {
+            if (!array_key_exists($value, $_GET)) {
+                http_response_code(400);
+                echo json_encode(['error' => "$value is required"]);
+                exit;
+            }
+        });
+
+        $data = $db->query("SELECT *, horario_view.facultad_id FROM registro
+                            JOIN horario_view USING (horario_id)
+                            LEFT JOIN estado_supervisor USING (estado_supervisor_id)
+                            LEFT JOIN profesor USING (profesor_id)
+                            LEFT JOIN usuario ON usuario.usuario_id = registro.supervisor_id
+                            ORDER BY registro_fecha_ideal DESC, horario_hora ASC, registro_fecha_supervisor ASC");
+
+        $last_query = [
+            'query' => $db->getLastQuery(),
+        ];
+
+        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    } else {
+        http_response_code(405);
+        echo json_encode(['error' => 'method not allowed']);
+        exit;
+
+    }
+
+} catch (PDOException $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+        'query' => $db->getLastQuery(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+} catch (Exception $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+}

+ 42 - 0
action/action_estado_supervisor.php

@@ -0,0 +1,42 @@
+<?
+#input $_GET['id_espacio_sgu']
+define("INFORMATION", [
+    'GET' => [
+    ],
+]);
+#output rutas: [ ...ruta, salones: [{...salon}] ]
+header('Content-Type: application/json charset=utf-8');
+#return html
+$ruta = "../";
+require_once "../class/c_login.php";
+// check method
+try {
+    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+        // check parameters
+        $raw = file_get_contents('php://input');
+        $post_get = json_decode($raw, true);
+        
+        $data = $db->get('estado_supervisor');
+        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    } else {
+        http_response_code(405);
+        echo json_encode(['error' => 'method not allowed']);
+        exit;
+
+    }
+
+} catch (PDOException $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+        'query' => $db->getLastQuery(),
+        'post_data' => $post_get,
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+} catch (Exception $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+}

+ 45 - 5
action/action_facultad.php

@@ -1,11 +1,51 @@
-<?php
+<?
+$information = [
+    'GET' => [],
+];
+header('Content-Type: application/json charset=utf-8');
 $ruta = "../";
 require_once "../class/c_login.php";
 
 // check if the session is started
-if (!isset($_SESSION['user']))
-    die(json_encode(['error' => 'No se ha iniciado sesión']));
+if (!isset($_SESSION['user'])) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => 'No se ha iniciado sesión'
+    ]);
+    exit;
+}
 
 $user = unserialize($_SESSION['user']);
-$ruta = "../";
-require '../include/bd_pdo.php';
+try {
+    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+        // check parameters
+        array_walk($information['GET'], function ($value) {
+            if (!array_key_exists($value, $_GET)) {
+                http_response_code(400);
+                echo json_encode(['error' => "$value is required"]);
+                exit;
+            }
+        });
+        // step 1: get subrutas
+        $data = $db->get('facultad');
+
+        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    } else {
+        http_response_code(405);
+        echo json_encode(['error' => 'method not allowed']);
+        exit;
+    }
+} catch (PDOException $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+        'query' => $db->getLastQuery(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+} catch (Exception $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+}

+ 26 - 0
action/action_fechas_clase.php

@@ -0,0 +1,26 @@
+<?php
+$ruta = "../";
+require_once "../class/c_login.php";
+
+if (!isset($_SESSION['user']))
+    die(json_encode(['error' => 'No se ha iniciado sesión']));
+
+$user = unserialize($_SESSION['user']);
+$ruta = "../";
+require_once "../include/bd_pdo.php";
+
+// if method is get
+header("Content-Type: application/json");
+if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+    $user->print_to_log("Acceso a reposiciones");
+    if (empty($_GET['horario_id']))
+        die(json_encode(['error' => 'No se ha enviado el id del horario']));
+    // fecha greater than today
+    $reposiciones = $db->query("SELECT fecha, EXTRACT(DOW FROM fecha) as day, EXTRACT(MONTH FROM fecha) as month, EXTRACT(YEAR FROM fecha) as year, EXTRACT(DAY FROM fecha) as dia_mes FROM fechas_clase(:horario_id) WHERE fecha > CURRENT_DATE", [
+        'horario_id' => $_GET['horario_id']
+    ]);
+    echo json_encode([
+        'status' => 'success',
+        'data' => $reposiciones
+    ]);
+}

+ 3 - 4
action/action_grupo.php

@@ -10,13 +10,12 @@ $user = unserialize($_SESSION['user']);
 $ruta = "../";
 require_once("../include/bd_pdo.php");
 extract($_POST);
-$params = ['per' => $periodo, 'fac' => $facultad, 'car' => $carrera];
+$params = ['per' => $_POST['periodo'], 'fac' => $_POST['facultad'], 'car' => $_POST['carrera']];
 
 $user->print_to_log("Acceso a grupos", old: $params);
 $grupos = queryAll("SELECT DISTINCT LENGTH(GRUPO), GRUPO FROM fs_horario_basic WHERE PERIODO_ID = COALESCE(:per, PERIODO_ID) AND FACULTAD_ID = COALESCE(:fac, FACULTAD_ID) AND CARRERA_ID = COALESCE(:car, CARRERA_ID) ORDER BY LENGTH(GRUPO), GRUPO", $params);
-$grupos = array_map(function ($grupo) {
-    return $grupo['grupo'];
-}, $grupos);
+
+$grupos = array_map(fn ($grupo) => $grupo['grupo'], $grupos);
 
 echo json_encode([
     'status' => 'success',

+ 28 - 0
action/action_grupo_horario.php

@@ -0,0 +1,28 @@
+<?php
+header('Content-Type: application/json');
+
+$ruta = "../";
+require_once("../include/bd_pdo.php");
+
+$grupo = isset($_GET['grupo']) ? $_GET['grupo'] : 1;
+$grupo_horarios = $db->querySingle(
+    "WITH bloques AS (
+    SELECT id, hora_inicio, hora_fin
+    FROM public.bloque_horario
+    WHERE grupo = ?
+    ORDER BY hora_inicio ASC
+) 
+
+SELECT json_agg(json_build_object(
+    'id', id,
+    'hora_inicio', hora_inicio,
+    'hora_fin', hora_fin,
+	'selected', current_time between hora_inicio and hora_fin
+)) AS bloque_horario
+FROM bloques
+",
+    [$grupo]
+)['bloque_horario'];
+
+
+echo $grupo_horarios;

+ 37 - 3
action/action_horario_profesor.php

@@ -1,4 +1,38 @@
 <?php
-die(json_encode([
-    'message' => 'ok',
-]));
+header('Content-Type: application/json');
+$ruta = "../";
+require_once("../include/bd_pdo.php");
+
+$dias = array("domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado");
+
+try {
+    if(empty($_POST['profesor_id']))
+        throw new Exception("No se ha especificado un profesor");
+        
+    // RECORD LAST QUERY
+    $horarios = $db->query("SELECT * FROM fs_horario(_periodo_id => ?, _last => true, _profesor_id => ?) ORDER BY MATERIA", [
+        $_POST['periodo_id'],
+        $_POST['profesor_id'],
+    ]);
+    
+    $horarios = array_map(function ($horario) use ($dias, $db) {
+        $horario['profesores'] = array_map(
+            fn ($profesor) =>
+            $db->where("id", $profesor)->getOne("fs_profesor"),
+            explode(",", substr($horario['profesores'], 1, -1))
+        );
+        $horario['dia'] = $dias[$horario['dia']];
+        return $horario;
+    }, $horarios);
+    die(json_encode([
+        "status" => "success",
+        "data" => $horarios,
+        // "data" => [],
+    ]));
+} catch (Exception $e) {
+    die(json_encode([
+        "status" => "error",
+        "message" => $e->getMessage(),
+        "query" => $db->getLastQuery(),
+    ]));
+}

+ 38 - 35
action/action_login.php

@@ -1,40 +1,43 @@
-    <?php
-    /* 
-    * Valida usuario con la BD y devuelve contraseña para validar con PHP
-    * 
-    * Recibe:
-    *  POST: usuario, password
-    * 
-    * Error:
-    *  0 - No se recibieron datos
-    *  1 - Usuario/Contraseña incorrectos
-    *  2 - Usuario no esta en BD
-    *  3 - No existe usuario
-    * 
-    * Success:
-    *  Redirecciona a inicio.php
-    */
-    include_once("../include/nocache.php"); //continue on error
-    $ruta = "../";
-    require_once("../include/bd_pdo.php"); //die on error
-    require_once("../class/c_login.php");
-    require_once("../include/util.php");
-    require_once("../include/nusoap/nusoap.php");
+<?php
+/* 
+ * Valida usuario con la BD y devuelve contraseña para validar con PHP
+ * 
+ * Recibe:
+ *  POST: usuario, password
+ * 
+ * Error:
+ *  0 - No se recibieron datos
+ *  1 - Usuario/Contraseña incorrectos
+ *  2 - Usuario no esta en BD
+ *  3 - No existe usuario
+ * 
+ * Success:
+ *  Redirecciona a inicio.php
+ */
+include_once("../include/nocache.php"); //continue on error
+$ruta = "../";
+require_once("../include/bd_pdo.php"); //die on error
+require_once("../class/c_login.php");
+require_once("../include/util.php");
+require_once("../include/nusoap/nusoap.php");
 
-    if (!isset($_POST["username"]) || !isset($_POST["passwd"]))
-        die(header("Location: ../index.php?error=0"));
+if (!isset($_POST["username"]) || !isset($_POST["passwd"]))
+    die(header("Location: ../index.php?error=0"));
 
-    $usr = trim(filter_input(INPUT_POST, "username")); //limpia texto
-    $pass = $_POST["passwd"];
+$usr = trim(filter_input(INPUT_POST, "username")); //limpia texto
+$pass = $_POST["passwd"];
 
-    $user =  Login::validUser($usr, $pass);
+$user = Login::validUser($usr, $pass);
 
-    if ($user === false) {
-        $_SESSION['error'] = true;
-        header("Location: ../");
-    } else {
-        $_SESSION['user'] = serialize($user);
-        header("Location: ../main.php");
-    }
+if (is_array($user)) {
+    $_SESSION['error'] = true;
+    // build query params
+    $params = http_build_query($user);
+    header("Location: ../index.php?$params");
+} else {
+    $_SESSION['user'] = serialize($user);
 
-    exit;
+    header("Location: " . (isset($_SESSION['ruta']) ? $_SESSION['ruta'] : "../main.php"));
+}
+
+exit;

+ 54 - 0
action/action_reposiciones.php

@@ -0,0 +1,54 @@
+<?php
+$ruta = "../";
+require_once "../class/c_login.php";
+
+if (!isset($_SESSION['user']))
+    die(json_encode(['error' => 'No se ha iniciado sesión']));
+
+$user = unserialize($_SESSION['user']);
+$ruta = "../";
+require_once "../include/bd_pdo.php";
+
+// if method is get
+header("Content-Type: application/json");
+if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+    $user->print_to_log("Acceso a reposiciones");
+    $reposiciones = $db
+        ->where('periodo_id', $_GET['periodo_id'] ?? null)
+        ->where('profesor_id', $_GET['profesor_id'] ?? [])
+        ->get("reposicion");
+    echo json_encode([
+        'status' => 'success',
+        'reposiciones' => $reposiciones
+    ]);
+} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $user->print_to_log("Creación de reposición", new: $params);
+    try {
+        $requiredParams = ['horario_id', 'fecha', 'hora', 'duracion_id', 'descripcion', 'profesor_id', 'salon', 'unidad', 'periodo_id', 'fecha_clase'];
+
+        // Filter params based on requiredParams
+        $params = array_filter($_POST, function ($key) use ($requiredParams) {
+            return in_array($key, $requiredParams);
+        }, ARRAY_FILTER_USE_KEY);
+
+        // Check if all required params are present
+        if (count($params) !== count($requiredParams)) {
+            throw new Exception('Falta uno o más parámetros requeridos');
+        }
+
+        $db->insert("reposicion", $params);
+
+        // Return success response
+        echo json_encode([
+            "status" => "success",
+            "message" => "Reposición creada correctamente",
+        ]);
+    } catch (Exception $e) {
+        // Return error response
+        echo json_encode([
+            "status" => "error",
+            "message" => "No se pudo crear la reposición",
+            "error" => $e->getMessage(),
+        ]);
+    }
+}

+ 2 - 2
action/force_session.php

@@ -29,8 +29,8 @@ $user = [
 ];
 
 $user = new Login($user, $facultad, $rol, $admin, $periodo);
-
-session_start();
+if (isset($_SESSION))
+    session_start();
 $_SESSION['user'] = serialize($user);
 
 header("Location: ../main.php");

+ 60 - 0
action/registro_supervisor.php

@@ -0,0 +1,60 @@
+<?
+#input $_GET['id_espacio_sgu']
+define("INFORMATION", [
+    'POST' => [
+        'profesor_id',
+        'horario_id',
+        'estado',
+        'comentario',
+        'supervisor_id',
+    ],
+]);
+#output rutas: [ ...ruta, salones: [{...salon}] ]
+header('Content-Type: application/json charset=utf-8');
+#return html
+$ruta = "../";
+require_once "../class/c_login.php";
+// check method
+try {
+    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+        // check parameters
+        $raw = file_get_contents('php://input');
+        $post_data = json_decode($raw, true);
+        // if it's a list
+        // step 1: get subrutas
+        if (empty($post_data)) {
+            http_response_code(400);
+            echo json_encode(['error' => 'No hay clases pendientes']);
+            exit;
+        }
+
+        $data = $db->query(
+            'INSERT INTO registro (profesor_id, horario_id, registro_fecha_supervisor, estado_supervisor_id, registro_fecha_ideal, supervisor_id, comentario)
+            VALUES' .
+            implode(',', array_map(fn($x) => "({$x['profesor_id']} , {$x['horario_id']}, NOW()," . (is_null($x['estado']) ? 'null' : $x['estado']) . ", NOW(), {$x['supervisor_id']}," . (empty($x['comentario']) ? 'null' : "'{$x['comentario']}'") . ')', $post_data))
+            . ' ON CONFLICT (profesor_id, horario_id, registro_fecha_ideal) DO UPDATE SET estado_supervisor_id = EXCLUDED.estado_supervisor_id, registro_fecha_supervisor = NOW(), comentario = EXCLUDED.comentario
+            RETURNING *'
+        );
+        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    } else {
+        http_response_code(405);
+        echo json_encode(['error' => 'method not allowed']);
+        exit;
+
+    }
+
+} catch (PDOException $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+        'query' => $db->getLastQuery(),
+        'post_data' => $post_data,
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+} catch (Exception $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+}

+ 28 - 0
action/rutas.php

@@ -0,0 +1,28 @@
+<?
+header('Content-Type: application/json charset=utf-8');
+$ruta = "../";
+require_once "../class/c_login.php";
+
+$universidad_la_salle = $db
+    ->where('salon', 'UNIVERSIDAD LA SALLE', 'ILIKE')
+    ->getOne('salon_view');
+
+$rutas =
+    array_map(
+        function (&$ruta) use ($db) {
+            $ruta['subrutas'] =
+                $db
+                    ->where('id_espacio_padre', $ruta['id_espacio_sgu'])
+                    ->orderBy('salon')
+                    ->get('salon_view');
+            return $ruta;
+
+        },
+        $db
+            ->where('id_espacio_padre', $universidad_la_salle['id_espacio_sgu'])
+            ->orderBy('salon')
+            ->get('salon_view')
+    );
+
+// echo json_encode($universidad_la_salle, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); EXIT;
+echo json_encode($rutas, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

+ 73 - 0
action/rutas_salón_horario.php

@@ -0,0 +1,73 @@
+<?
+#input $_GET['id_espacio_sgu']
+$information = [
+    'GET' => [
+        'id_espacio_sgu',
+        'bloque_horario_id',
+    ],
+];
+#output rutas: [ ...ruta, salones: [{...salon}] ]
+header('Content-Type: application/json charset=utf-8');
+$ruta = "../";
+require_once "../class/c_login.php";
+// check method
+try {
+    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+        // check parameters
+        array_walk($information['GET'], function ($value) {
+            if (!array_key_exists($value, $_GET)) {
+                http_response_code(400);
+                echo json_encode(['error' => "$value is required"]);
+                exit;
+            }
+        });
+        // step 1: get subrutas
+        $data = $db
+            ->where('tiene_salones')
+            ->where("{$_GET['id_espacio_sgu']} = ANY(id_espacio_sgu_array)")
+            ->get('salon_view');
+
+        // step 3: get horarios
+        $data = array_map(
+            fn($ruta) => array_merge(
+                [
+                    'horarios' => $db
+                        ->join('periodo', 'periodo.periodo_id = horario_view.periodo_id')
+                        ->join('bloque_horario', '(bloque_horario.hora_inicio, bloque_horario.hora_fin) OVERLAPS (horario_view.horario_hora, horario_view.horario_hora + horario_view.duracion)')
+                        ->join('salon_view', 'salon_view.salon_id = horario_view.salon_id')
+                        ->join('horario_profesor', 'horario_profesor.horario_id = horario_view.horario_id')
+                        ->join('profesor', 'profesor.profesor_id = horario_profesor.profesor_id')
+                        ->join('registro', '(registro.profesor_id, registro.horario_id, registro.registro_fecha_ideal) = (profesor.profesor_id, horario_view.horario_id, CURRENT_DATE)', 'LEFT')
+                        ->where('CURRENT_DATE BETWEEN periodo.periodo_fecha_inicio AND periodo.periodo_fecha_fin')
+                        ->where('horario_dia = EXTRACT(DOW FROM CURRENT_DATE)')
+                        ->where('bloque_horario.id', $_GET['bloque_horario_id'])
+                        ->where('id_espacio_padre', $ruta['id_espacio_sgu'])
+                        ->get('horario_view', null, '*, horario_view.horario_id, profesor.profesor_id'),
+                ],
+                $ruta
+            ),
+            $data
+        );
+
+        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    } else {
+        http_response_code(405);
+        echo json_encode(['error' => 'method not allowed']);
+        exit;
+
+    }
+
+} catch (PDOException $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+        'query' => $db->getLastQuery(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+} catch (Exception $th) {
+    http_response_code(500);
+    echo json_encode([
+        'error' => $th->getMessage(),
+    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    exit;
+}

+ 24 - 0
action/schemas/registro_supervisor.json

@@ -0,0 +1,24 @@
+{
+    "type": "array",
+    "items": {
+        "type": "object",
+        "properties": {
+            "profesor_id": {
+                "type": "integer"
+            },
+            "horario_id": {
+                "type": "integer"
+            },
+            "estado": {
+                "type": ["integer", "null"]
+            },
+            "comentario": {
+                "type": "string"
+            },
+            "supervisor_id": {
+                "type": "integer"
+            }
+        },
+        "required": ["profesor_id", "horario_id", "comentario", "supervisor_id"]
+    }
+}

+ 377 - 0
auditoría.php

@@ -0,0 +1,377 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Supervisor</title>
+    <?php
+    include 'import/html_css_files.php';
+    ?>
+    <style>
+        [v-cloak] {
+            display: none;
+        }
+    </style>
+</head>
+
+<body>
+    <?
+    $redirect = $_SERVER['PHP_SELF'];
+    include "import/html_header.php";
+    global $user;
+    html_header(
+        "Registro de asistencia - Vicerrectoría Académica",
+        "Sistema de gestión de checador",
+    );
+
+    #include "import/periodo.php";
+    ?>
+
+    <main class="container-fluid px-4 mt-4" id="app" v-cloak @vue:mounted="mounted">
+        <form action="">
+            <div class="form-box">
+                <div class="form-group row">
+                    <label for="periodo" class="col-4 col-form-label">Facultad</label>
+                    <div class="col-6">
+                        <div id="dlPeriodo" class="datalist datalist-select mb-1 w-100">
+                            <div class="datalist-input">Selecciona una facultad</div>
+                            <span class="ing-buscar icono"></span>
+                            <ul style="display:none">
+                                <li class="datalist-option" data-id="0" @click="store.filters.facultad_id = null;">
+                                    Todas las facultades
+                                </li>
+                                <li class="datalist-option" v-for="facultad in store.facultades.data"
+                                    :key="facultad.facultad_id" :data-id="facultad.facultad_id"
+                                    @click="store.filters.facultad_id = facultad.facultad_id">
+                                    (<small> {{facultad.clave_dependencia}} </small>) {{ facultad.facultad_nombre }}
+                                </li>
+                            </ul>
+                            <input type="hidden" id="facultad_id" name="id">
+                        </div>
+                    </div>
+                </div>
+                <div class="form-group row align-items-center">
+                    <label for="switchFecha" class="col-4 col-form-label">
+                        Fecha
+                        <!-- switch -->
+                        <div class="custom-control custom-switch">
+                            <input type="checkbox" class="custom-control-input" id="switchFecha"
+                                v-model="store.filters.switchFecha" @input="store.filters.switchFechas">
+                            <label class="custom-control-label" for="switchFecha"></label>
+                        </div>
+                    </label>
+                    <div class="col-3" v-if="store.filters.switchFecha">
+                        <div class="form-row">
+                            <input id="fecha_inicio" name="fecha_inicio" class="form-control date-picker"
+                                placeholder="Seleccione una fecha de inicio" readonly
+                                v-model="store.filters.fecha_inicio">
+                        </div>
+                    </div>
+                    <div class="col-3" v-if="store.filters.switchFecha">
+                        <div class="form-row">
+                            <input id="fecha_fin" name="fecha_fin" class="form-control date-picker"
+                                placeholder="Seleccione una fecha final" readonly v-model="store.filters.fecha_fin">
+                        </div>
+                    </div>
+
+                    <div class="col-6" v-if="!store.filters.switchFecha">
+                        <div class="form-row">
+                            <input id="fecha" name="fecha" class="form-control date-picker"
+                                placeholder="Seleccione una fecha" readonly v-model="store.filters.fecha">
+                        </div>
+                    </div>
+                </div>
+                <div class="form-group row">
+                    <label for="profesor" class="col-4 col-form-label">Profesor</label>
+                    <div class="col-6">
+                        <div class="form-row justify-content-around align-items-center">
+                            <input id="profesor" name="profesor" class="form-control col-11 mr-1 px-2"
+                                placeholder="Seleccione una profesor" list="dlProfesor"
+                                v-model="store.filters.profesor">
+                            <button type="button" class="btn btn-info btn-sm form-control col ml-auto"
+                                @click="store.filters.profesor = null">
+                                <i class="ing-borrar"></i>
+                            </button>
+                        </div>
+                        <datalist id="dlProfesor">
+                            <option v-for="profesor in profesores" :key="profesor.profesor_id"
+                                :value="`(${profesor.profesor_clave}) ${profesor.profesor_nombre}`">
+                        </datalist>
+                    </div>
+                </div>
+                <div class="form-group row">
+                    <label for="periodo" class="col-4 col-form-label">Asistencia</label>
+                    <div class="col-6">
+                        <div class="form-row justify-content-around align-items-center">
+                            <div id="dlPeriodo" class="datalist datalist-select mb-1 w-100">
+                                <div class="datalist-input" id="estados">Selecciona un estado de asistencia</div>
+                                <span class="ing-buscar icono"></span>
+                                <ul style="display:none">
+                                    <li class="datalist-option" data-id="0" @click="store.filters.estados = [];">
+                                        Todos los registros
+                                    </li>
+                                    <li class="datalist-option" v-for="estado in store.estados.data"
+                                        :key="estado.estado_supervisor_id" :data-id="estado.estado_supervisor_id"
+                                        @click="store.filters.estados = store.toggle(store.filters.estados, estado.estado_supervisor_id); ; setTimeout(store.estados.printEstados, 0);">
+                                        <span class="badge"
+                                            :class="`badge-${store.filters.estados.includes(estado.estado_supervisor_id) ? 'dark' : estado.estado_color}`"><i
+                                                :class="estado.estado_icon"></i> {{estado.nombre}}</span>
+                                    </li>
+                                </ul>
+                                <input type="hidden" id="estado_id" name="estado_id">
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+        </form>
+        <div class="mt-3 d-flex justify-content-center flex-wrap">
+            <!-- refresh -->
+            <div class="table-responsive">
+                <table class="table table-hover table-striped table-bordered table-sm">
+                    <thead class="thead-dark">
+                        <tr>
+                            <th scope="col" class="text-center align-middle px-2">
+                                <button @click="registros.invertir" class="btn btn-info mr-3"
+                                    v-if="registros.relevant.length > 1">
+                                    <i class="ing-cambiar ing-rotate-90"></i>
+                                </button>
+                                Fecha
+                            </th>
+                            <th scope="col" class="text-center align-middle px-2">Salón</th>
+                            <th scope="col" class="text-center align-middle px-2">Profesor</th>
+
+                            <th scope="col" class="text-center align-middle px-2">Horario</th>
+                            <th scope="col" class="text-center align-middle px-2">Registro</th>
+                            <th scope="col" class="text-center align-middle px-2">Supervisor</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="registros.relevant.length == 0">
+                            <td colspan="7" class="text-center">No hay clases en este horario</td>
+                        </tr>
+                        <tr v-for="registro in registros.relevant" :key="registro.registro_id">
+                            <td class="text-center align-middle px-2">{{ registro.registro_fecha_ideal }}
+                            </td>
+                            <td class="text-center align-middle px-2">{{ registro.salon }}</td>
+                            <td class="text-center align-middle px-2">
+                                <div class="col-12">
+                                    <strong>{{ registro.profesor_clave }}</strong>
+                                    {{ registro.profesor_nombre }}
+                                </div>
+                                <div class="col-12">
+                                    <button type="button" class="btn btn-outline-dark btn-sm"
+                                        @click="store.current.clase_vista = registro" data-toggle="modal"
+                                        data-target="#ver-detalle">
+                                        Ver detalle <i class="ing-ojo"></i>
+                                    </button>
+                                </div>
+                            </td>
+
+
+                            <td class="text-center align-middle px-2">{{ registro.horario_hora.slice(0,5) }} - {{
+                                registro.horario_fin.slice(0,5) }}</td>
+                            <!--  -->
+                            <td class="text-center align-middle px-2">
+                                <div v-if="registro.registro_fecha">
+                                    <div class="col-12">
+                                        Registro <small>{{ registro.registro_fecha.slice(11,16) }}</small>
+                                    </div>
+                                </div>
+                                <div v-else>
+                                    <strong>
+                                        <div class="col-12">
+                                            <span class="badge badge-danger"><i class="ing-cancelar"></i></span>
+                                        </div>
+                                        <div class="col-12 mt-2">
+                                            Sin registro
+                                        </div>
+                                    </strong>
+                                </div>
+                            </td>
+
+                            <!-- Sí checó supervisor -->
+                            <td class="text-center align-middle px-2">
+                                <div v-if="registro.registro_fecha_supervisor">
+                                    <div class="row">
+                                        <div class="col-12">
+                                            <strong>{{ registro.usuario_nombre }}</strong>
+                                        </div>
+                                        <div class="col-12">
+                                            Hora
+                                            <small>{{ registro.registro_fecha_supervisor.slice(11,19) }}</small>
+                                        </div>
+                                        <div class="col-12 mt-2">
+                                            <span class="badge" :class="`badge-${registro.estado_color}`">
+                                                <i :class="`${registro.estado_icon}`"></i>
+                                                <strong>{{ registro.nombre }}</strong>
+                                            </span>
+                                        </div>
+                                    </div>
+                                    <!-- comentario -->
+                                    <hr v-if="registro.comentario">
+                                    <div class="col-12 " @click="registros.mostrarComentario(registro.registro_id)"
+                                        v-if="registro.comentario" style="cursor: pointer;">
+                                        <strong class="badge badge-primary">Observaciones:</strong>
+                                        <small class="text-truncate">{{registro.comentario.slice(0,
+                                            25)}}{{registro.comentario.length > 10 ? '...' : ''}}</small>
+                                    </div>
+                                </div>
+
+                                <!-- No checó -->
+                                <div v-else>
+                                    <div class="col-12">
+                                        <span class="badge badge-danger"><i class="ing-cancelar"></i></span>
+                                    </div>
+                                    <div class="col-12 mt-2">
+                                        <strong>Sin registro</strong>
+                                    </div>
+                                </div>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+
+
+        <div class="modal" tabindex="-1" id="ver-comentario">
+            <div class="modal-dialog modal-dialog-centered modal-xl">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">Comentario</h5>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <div class="container">
+                            <div class="input-group">
+                                <textarea class="form-control" aria-label="Comentarios de la clase" rows="5"
+                                    v-model="store.current.comentario" disabled></textarea>
+                            </div>
+                        </div>
+
+                    </div>
+                    <div class="modal-footer">
+                        <button type="button" class="btn btn-outline-primary" data-dismiss="modal">
+                            Aceptar
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="modal" tabindex="-1" id="ver-detalle">
+            <div class="modal-dialog modal-dialog-centered modal-xl" v-if="clase_vista">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h2 class="modal-title" :data-id="clase_vista.horario_id">Detalle de la clase</h2>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <div class="container">
+                            <div class="row">
+                                <section class="col-12 col-md-6">
+                                    <h4 class="h4">Profesor</h4>
+                                    <div class="row">
+                                        <div class="col-12">
+                                            <strong>Nombre:</strong>
+                                            {{ clase_vista.profesor_nombre }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Correo:</strong>
+                                            <a :href="`mailto:${clase_vista.profesor_correo}`"><strong>{{
+                                                    clase_vista.profesor_correo }}</strong></a>
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Clave:</strong>
+                                            {{ clase_vista.profesor_clave }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Facultad:</strong>
+                                            {{ clase_vista.facultad }}
+                                        </div>
+                                    </div>
+                                </section>
+                                <section class="col-12 col-md-6">
+                                    <h4 class="h4">Clase</h4>
+                                    <div class="row">
+                                        <div class="col-12">
+                                            <strong>Materia:</strong>
+                                            {{ clase_vista.materia }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Carrera:</strong>
+                                            {{ clase_vista.carrera }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Nivel:</strong>
+                                            {{ clase_vista.nivel}}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Grupo:</strong>
+                                            {{ clase_vista.horario_grupo }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Horario:</strong>
+                                            <!-- hora hh:mm:ss to hh:mm -->
+                                            {{ clase_vista.horario_hora?.slice(0, 5) }} - {{
+                                            clase_vista.horario_fin?.slice(0, 5) }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Salón:</strong>
+                                            {{ clase_vista.salon }}
+                                        </div>
+                                    </div>
+                                </section>
+                            </div>
+                            <div class="row">
+                                <section class="col-12">
+                                    <h4 class="h4 mt-4">Registro</h4>
+                                    <div class="row">
+                                        <div class="col-12 text-center" v-if="!clase_vista.registro_fecha">
+                                            <strong><span class="badge badge-danger"><i class="ing-cancelar"></i></span>
+                                                El profesor aún no ha registrado su asistencia</strong>
+                                        </div>
+                                        <div class="col-6 text-center" v-else>
+                                            El profesor registró su asistencia a las
+                                            <code>{{clase_vista.registro_fecha.slice(11, 16)}}</code>
+                                            <hr>
+                                            <p v-if="!clase_vista.registro_retardo" class="text-center">
+                                                <span class="badge badge-success"><i class="ing-aceptar"></i></span>
+                                                A tiempo
+                                            </p>
+                                            <p v-else class="text-center">
+                                                <span class="badge badge-warning"><i class="ing-retardo"></i></span>
+                                                Con retardo
+                                            </p>
+                                        </div>
+                                    </div>
+                                </section>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="modal-footer">
+                        <!-- botón aceptar -->
+                        <button type="button" class="btn btn-outline-primary" data-dismiss="modal">
+                            <i class="ing-aceptar"></i>
+                            Aceptar
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </main>
+    <script src="js/jquery.min.js"></script>
+    <script src="js/jquery-ui.js"></script>
+    <script src="js/bootstrap/bootstrap.min.js"></script>
+    <script src="js/datalist.js"></script>
+    <script src="js/datepicker-es.js"></script>
+    <script src="js/auditoría.js" type="module"></script>
+</body>
+
+</html>

+ 0 - 1
bypass.php

@@ -1,5 +1,4 @@
 <?php
-require_once 'class/c_login.php';
 if (isset($_GET["error"]) && is_numeric($_GET["error"])) {
     switch ($_GET["error"]) {
         case 0:

+ 89 - 46
class/c_login.php

@@ -1,20 +1,29 @@
 <?php
-require_once ($ruta ?? '') . "include/bd_pdo.php";
-require_once ($ruta ?? '') . "class/c_logasistencia.php";
-require_once ($ruta ?? '') . "include/nusoap/nusoap.php";
+date_default_timezone_set('America/Mexico_City');
+$currentTime = time();
+$endOfDay = strtotime('tomorrow') - 1;
+$remainingTime = $endOfDay - $currentTime;
+
+session_set_cookie_params($remainingTime, '/', $_SERVER['HTTP_HOST'], false, true);
+
+require_once($ruta ?? '') . "include/bd_pdo.php";
+require_once($ruta ?? '') . "class/c_logasistencia.php";
+require_once($ruta ?? '') . "include/nusoap/nusoap.php";
 
 session_start();
 class Login
 {
     public string $acceso;
-    public function __construct(public array $user, public array $facultad, public array $rol, public bool $admin, public ?int $periodo)
+    public function __construct(public array $user, public array $facultad, public array $rol, public bool $admin, public ?int $periodo, public bool $supervisor, public bool $jefe_carrera, public bool $profesor)
     {
     }
     public function print_to_log(string $desc, array $old = null, array $new = null): void
     {
         $log = new classes\LogAsistencias($_ENV["RUTA_RAIZ"]);
-        if ($old) $desc .= " |#| OLD:" . json_encode($old);
-        if ($new) $desc .= " |#| NEW:" . json_encode($new);
+        if ($old)
+            $desc .= " |#| OLD:" . json_encode($old);
+        if ($new)
+            $desc .= " |#| NEW:" . json_encode($new);
         $log->appendLog($this->user["id"], $this->user["nombre"], $desc);
     }
     public function access(string $pagina = null): void
@@ -25,63 +34,97 @@ class Login
         }
 
         # print_r( $access );
-        $this->acceso =  query('SELECT tipo FROM PERMISO_VIEW WHERE ID = :usr AND PAGINA_RUTA ILIKE :ruta', array(
-            ':usr' => $this->user["id"],
-            ':ruta' => $pagina ?? substr(basename($_SERVER['PHP_SELF']), 0, -4)
-        ))["tipo"] ?? 'n';
+        $this->acceso = query(
+            'SELECT tipo FROM PERMISO_VIEW WHERE ID = :usr AND PAGINA_RUTA ILIKE :ruta',
+            array(
+                ':usr' => $this->user["id"],
+                ':ruta' => $pagina ?? substr(basename($_SERVER['PHP_SELF']), 0, -4)
+            )
+        )["tipo"] ?? 'n';
     }
     public function __toString(): string
     {
-        return "Usuario: {$this->user["nombre"]} ({$this->user["id"]})";
+        return "Usuario: {$this->user["nombre"]} ({$this->user["id"]}), Es admin: {$this->admin}, supervisor: {$this->supervisor}, jefe carrera: {$this->jefe_carrera}, profesor: {$this->profesor}";
     }
     private static function validaUsuario($user, $pass): bool
     {
         file_put_contents('php://stderr', $user);
-        if (in_array($user, ['ad012821']) and $pass == "admin") return true;
-        if (in_array($user, ['ad017045']) and $pass == "admin") return true    ;
+        if (in_array($user, ['ad017045']) and $pass == "admin")
+            return true;
         $client = new nusoap_client('http://200.13.89.2/validacion.php?wsdl', 'wsdl');
-        $error  = $client->getError();
-
-        if ($error) return false;
-
+        $client->getError() and die('Error al crear el cliente: ' . $client->getError());
         $pass = utf8_decode($pass);
         $result = $client->call("valida_user", array($user, $pass));
-
-        if ($client->fault) return false;
-            
+        $client->fault and die('Error al llamar al servicio: ' . $client->getError());
         return $result;
     }
-    public static function validUser(string $user, string $pass): Login | false
+    public static function validUser(string $user, string $pass): Login|array
     {
-        $fs_validaclaveulsa = query(
-            'SELECT * FROM FS_VALIDACLAVEULSA(:usr)', [':usr' => $user]
-        );
-        
-        if (empty($fs_validaclaveulsa["id"])) return false;
-        #die (Login::validaUsuario($user, $pass));
-        if (!Login::validaUsuario($user, $pass)) return false;
- 
-        $user = array(
-            'id' => $fs_validaclaveulsa["id"],
-            'nombre' => $fs_validaclaveulsa["nombre"],
-        );
-        $facultades = query("SELECT FACULTAD_ID id, FACULTAD f FROM FS_PERIODO WHERE ID = :id", [':id' => $fs_validaclaveulsa["periodo_id"]]);
-        $facultad = array(
-            'facultad_id' => $fs_validaclaveulsa["facultad_id"] ?? $facultades["id"],
-            'facultad' => $fs_validaclaveulsa["facultad"] ?? $facultades["f"],
-        );
-        $rol = array(
-            'id' => $fs_validaclaveulsa["rol_id"],
-            'rol' => $fs_validaclaveulsa["rol"]
-        );
+        if (!Login::validaUsuario($user, $pass)) {
+            return [
+                'error' => true,
+                'msg' => 'Error al autenticar usuario'
+            ];
+        }
+        global $db;
+
+        if ($db->has("FS_VALIDACLAVEULSA('$user')")) {
+            #die (Login::validaUsuario($user, $pass));
+            $fs_validaclaveulsa = $db->querySingle(
+                'SELECT * FROM FS_VALIDACLAVEULSA(?)',
+                [$user]
+            );
+
+            $user = array(
+                'id' => $fs_validaclaveulsa["id"],
+                'nombre' => $fs_validaclaveulsa["nombre"],
+            );
+            $facultad = array(
+                'facultad_id' => $fs_validaclaveulsa["facultad_id"],
+                'facultad' => $fs_validaclaveulsa["facultad"],
+            );
+            $rol = array(
+                'id' => $fs_validaclaveulsa["rol_id"],
+                'rol' => $fs_validaclaveulsa["rol"]
+            );
+            $supervisor = $db
+                ->join('rol', 'rol.rol_id = usuario.rol_id')
+                ->where('usuario_id', $user["id"])
+                ->where('rol.rol_titulo', 'Supervisor')
+                ->has('usuario');
+            $jefe_carrera = $db->where('usuario_id', $user["id"])->has('usuario_carrera');
+
+            $admin = $fs_validaclaveulsa["is_admin"];
+            $periodo = $fs_validaclaveulsa["periodo_id"];
+
+            return new Login($user, $facultad, $rol, $admin, $periodo, $supervisor, $jefe_carrera, false);
+        } else if ($db->where('profesor_clave', preg_replace('/^do0*/', '', $user))->has("profesor")) {
+            $profesor = $db->where('profesor_clave', preg_replace('/^do0*/', '', $user))->getOne("profesor");
+            $user = array(
+                'id' => $profesor["profesor_clave"],
+                'nombre' => $profesor["profesor_nombre"],
+            );
+            $facultad = $rol = array(
+                'facultad_id' => null,
+                'facultad' => 'Docente',
+            );
 
-        $admin = $fs_validaclaveulsa["is_admin"];
-        $periodo = $fs_validaclaveulsa["periodo_id"];
-        return new Login($user, $facultad, $rol, $admin, $periodo);
+            $supervisor = false;
+            $jefe_carrera = false;
+            $admin = false;
+            $periodo = null;
+            // CREATE A COOKIE FOR THE REST OF THE day for example: 23:00 then duration will be 1 hour
+            setcookie("profesor", $user["id"], strtotime('today midnight') + 86400, "/");
+            return new Login($user, $facultad, $rol, $admin, $periodo, $supervisor, $jefe_carrera, true);
+        } else
+            return [
+                'error' => true,
+                'msg' => 'Usuario no encontrado'
+            ];
     }
     public static function log_out(): void
     {
         session_start();
         session_destroy();
     }
-}
+}

+ 2 - 1
composer.json

@@ -2,6 +2,7 @@
     "require": {
         "vlucas/phpdotenv": "^5.5",
         "phpoffice/phpspreadsheet": "^1.25",
-        "seinopsys/postgresql-database-class": "^3.1"
+        "seinopsys/postgresql-database-class": "^3.1",
+        "justinrainbow/json-schema": "^5.2"
     }
 }

+ 71 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "5e701c768afe8ce8feabe1b539fa7234",
+    "content-hash": "2b67052b0f31b7059a262343c2640316",
     "packages": [
         {
             "name": "ezyang/htmlpurifier",
@@ -129,6 +129,76 @@
             ],
             "time": "2022-07-30T15:56:11+00:00"
         },
+        {
+            "name": "justinrainbow/json-schema",
+            "version": "5.2.12",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/justinrainbow/json-schema.git",
+                "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/ad87d5a5ca981228e0e205c2bc7dfb8e24559b60",
+                "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+                "json-schema/json-schema-test-suite": "1.2.0",
+                "phpunit/phpunit": "^4.8.35"
+            },
+            "bin": [
+                "bin/validate-json"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "JsonSchema\\": "src/JsonSchema/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bruno Prieto Reis",
+                    "email": "bruno.p.reis@gmail.com"
+                },
+                {
+                    "name": "Justin Rainbow",
+                    "email": "justin.rainbow@gmail.com"
+                },
+                {
+                    "name": "Igor Wiedler",
+                    "email": "igor@wiedler.ch"
+                },
+                {
+                    "name": "Robert Schönthal",
+                    "email": "seroscho@googlemail.com"
+                }
+            ],
+            "description": "A library to validate a json schema.",
+            "homepage": "https://github.com/justinrainbow/json-schema",
+            "keywords": [
+                "json",
+                "schema"
+            ],
+            "support": {
+                "issues": "https://github.com/justinrainbow/json-schema/issues",
+                "source": "https://github.com/justinrainbow/json-schema/tree/5.2.12"
+            },
+            "time": "2022-04-13T08:02:27+00:00"
+        },
         {
             "name": "maennchen/zipstream-php",
             "version": "2.2.1",

+ 93 - 89
consultar_horario.php

@@ -17,7 +17,9 @@ $write = $user->admin || in_array($user->acceso, ['w']);
 <html lang="en">
 
 <head>
-    <title>Consultar horario | <?= $user->facultad['facultad'] ?? 'General' ?></title>
+    <title>Consultar horario |
+        <?= $user->facultad['facultad'] ?? 'General' ?>
+    </title>
     <meta charset="utf-8">
     <meta http-equiv="content-type" content="text/plain; charset=UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@@ -44,7 +46,7 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                     #$carreras = query("SELECT * FROM FS_CARRERA WHERE FACULTAD = :fac AND PERIODO = COALESCE(:per, PERIODO) ORDER BY CARRERA", [":fac" => $user->facultad['facultad_id'], ":per" => $user->periodo], single: false);
                     // repliaction of the query in the database with database class
                     $nivel = $user->periodo ? $db->where('id', $user->periodo)->getOne('fs_periodo') : false;
-                    
+
                     $carreras = $nivel ? $db
                         ->orderBy('carrera')
                         ->where('facultad', $nivel['facultad_id'])
@@ -61,11 +63,11 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                                 <ul style="display:none">
                                     <?php
                                     foreach ($carreras as $carrera) {
-                                    ?>
+                                        ?>
                                         <li data-id="<?= $carrera['id'] ?>">
                                             <?= $carrera['carrera'] ?>
                                         </li>
-                                    <?php
+                                        <?php
                                     }
                                     ?>
                                 </ul>
@@ -91,7 +93,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
 
                     <div class="form-group mt-4 row justify-content-center">
                         <?php if ($write) { ?>
-                            <button type="button" id="nuevo" class="btn btn-outline-primary ml-4 d-none" title="Nuevo horario" data-toggle="modal" data-target="#modal-editar">
+                            <button type="button" id="nuevo" class="btn btn-outline-primary ml-4 d-none"
+                                title="Nuevo horario" data-toggle="modal" data-target="#modal-editar">
                                 <span class="ing-mas ing-fw"></span> Nuevo
                             </button>
                         <?php } ?>
@@ -127,7 +130,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
             </table>
         </div>
 
-        <div class="modal fade" id="modal-editar" tabindex="-1" aria-labelledby="modal-editar" aria-hidden="true" data-backdrop="static" data-keyboard="false">
+        <div class="modal fade" id="modal-editar" tabindex="-1" aria-labelledby="modal-editar" aria-hidden="true"
+            data-backdrop="static" data-keyboard="false">
             <div class="modal-dialog modal-dialog-centered modal-lg">
                 <div class="modal-content">
                     <div class="modal-header">
@@ -158,7 +162,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                             <div class="form-grupo row mb-3">
                                 <div class="col-4"></div>
                                 <div class="col-6">
-                                    <input type="text" id="grupo" name="grupo" value="" class="form-control" placeholder="Grupo" required="required" hidden>
+                                    <input type="text" id="grupo" name="grupo" value="" class="form-control"
+                                        placeholder="Grupo" required="required" hidden>
                                     <div class="invalid-feedback">
                                         Por favor, ingrese un grupo.
                                     </div>
@@ -167,7 +172,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                             <div class="form-group row">
                                 <label for="materia" class="col-4 col-form-label">Materia</label>
                                 <div class="col-6">
-                                    <input list="lista_materias" name="dlMateria" id="dlMateria" class="form-control text-center" placeholder="Materia" required="required">
+                                    <input list="lista_materias" name="dlMateria" id="dlMateria"
+                                        class="form-control text-center" placeholder="Materia" required="required">
                                     <datalist id="lista_materias"></datalist>
                                     <input type="hidden" id="materia" name="materia" value="">
                                     <div class="invalid-feedback">
@@ -196,7 +202,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                                         <span class="ing-buscar icono"></span>
                                         <ul style="display:none">
                                             <?php foreach (range(0, 45, 15) as $minuto) { ?>
-                                                <li data-id='<?= $minuto ?>'><?= str_pad($minuto, 2, "0", STR_PAD_LEFT) ?></li>
+                                                <li data-id='<?= $minuto ?>'><?= str_pad($minuto, 2, "0", STR_PAD_LEFT) ?>
+                                                </li>
                                             <?php } ?>
                                         </ul>
                                         <input type="hidden" id="selector_minutos" name="minutos" value="">
@@ -238,9 +245,10 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                                                 $nombre = $duración['duracion_nombre'];
                                                 $id = $duración['duracion_id'];
                                                 $bloques = $duración['duracion_bloques'];
-                                            ?>
-                                                <li data-id="<?= $id; ?>" data-bloques="<?= $bloques; ?>"><?= $nombre; ?></li>
-                                            <?php
+                                                ?>
+                                                <li data-id="<?= $id; ?>" data-bloques="<?= $bloques; ?>"><?= $nombre; ?>
+                                                </li>
+                                                <?php
                                             }
                                             ?>
                                         </ul>
@@ -256,7 +264,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                             <div class="form-group row">
                                 <label for="editor_profesor" class="col-4 col-form-label">Profesor</label>
                                 <div class="col-6">
-                                    <input list="lista_profesores" name="dlProfesor" id="dlProfesor" class="form-control" placeholder="Profesor" required="required">
+                                    <input list="lista_profesores" name="dlProfesor" id="dlProfesor"
+                                        class="form-control" placeholder="Profesor" required="required">
                                     <div class="valid-feedback">
                                         Profesor encontrado
                                     </div>
@@ -265,11 +274,10 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                                     </div>
                                     <datalist id="lista_profesores">
                                         <?php
-                                        $profesores = $db->where('facultad_id', $user->facultad['facultad_id'])->get("fs_profesor");
+                                        $profesores = $db->get("profesor");
                                         foreach ($profesores as $profesor) {
-                                            extract($profesor);
                                         ?>
-                                            <option data-grado="<?= $grado ?>" data-clave="<?= $clave ?>" data-profesor="<?= $profesor ?>" data-id="<?= $id; ?>" value="<?= "$clave | $grado $profesor" ?>"></option>
+                                            <option data-clave="<?= $profesor['profesor_clave'] ?>" data-profesor="<?= $profesor['profesor_nombre'] ?>" data-id="<?= $id; ?>" value="<?= "{$profesor['profesor_clave']} | {$profesor['profesor_grado']} {$profesor['profesor_nombre']}" ?>"></option>
                                         <?php
                                         }
                                         ?>
@@ -283,7 +291,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                             <div class="form-group row">
                                 <label for="editor_salón" class="col-4 col-form-label">Salón</label>
                                 <div class="col-6">
-                                    <input type="text" class="form-control" id="editor_salón" name="salón" placeholder="Salón" maxlength="100" required="required">
+                                    <input type="text" class="form-control" id="editor_salón" name="salón"
+                                        placeholder="Salón" maxlength="100" required="required">
                                     <div class="invalid-feedback">
                                         El salón no puede estar vacío.
                                     </div>
@@ -292,7 +301,8 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                         </div>
                     </div>
                     <div class="modal-footer">
-                        <button data-id="" type="button" class="btn btn-primary" id="btn-guardar"><i class="ing-guardar ing"></i> Guardar</button>
+                        <button data-id="" type="button" class="btn btn-primary" id="btn-guardar"><i
+                                class="ing-guardar ing"></i> Guardar</button>
                         <button type="button" class="btn btn-outline-primary" data-dismiss="modal">Cancelar</button>
                     </div>
                 </div>
@@ -425,8 +435,8 @@ require_once("import/html_footer.php");
 
     function compareHours(hora1, hora2) {
         // parseInt each hour and minute
-        const [h1, m1, ] = hora1.split(":").map(x => parseInt(x));
-        const [h2, m2, ] = hora2.split(":").map(x => parseInt(x));
+        const [h1, m1,] = hora1.split(":").map(x => parseInt(x));
+        const [h2, m2,] = hora2.split(":").map(x => parseInt(x));
 
         if (h1 > h2)
             return 1;
@@ -588,11 +598,11 @@ require_once("import/html_footer.php");
                     <b class="title">${materia}</b> <br>
                     <br><span>Salón: </span>${salon} <br>
                     <small class="my-2">
-                        ${profesores.map(({grado, profesor}) => /*html*/ ` <span class="ing ing-formacion mx-1"></span>${grado ?? ''} ${profesor}`).join("<br>")}
+                        ${profesores.map(({ grado, profesor }) => /*html*/ ` <span class="ing ing-formacion mx-1"></span>${grado ?? ''} ${profesor}`).join("<br>")}
                     </small>
                 </div>
                 ${edit && float_menu}`
-                    
+
                 cell.classList.add("bloque-clase", "position-relative");
                 cell.rowSpan = bloques;
                 // draggable
@@ -712,8 +722,8 @@ require_once("import/html_footer.php");
             }
 
             const conflictBlocks = horarios.filter((horario, index, arrayHorario) =>
-                    arrayHorario.filter((_, i) => i != index).some(horario2 =>
-                        conflicts(horario, horario2)))
+                arrayHorario.filter((_, i) => i != index).some(horario2 =>
+                    conflicts(horario, horario2)))
                 .sort((a, b) => compareHours(a.hora, b.hora));
 
             const classes = horarios.filter(horario => !conflictBlocks.includes(horario));
@@ -749,7 +759,7 @@ require_once("import/html_footer.php");
 
             document.querySelectorAll("tbody#horario tr").forEach(hora => {
                 const hora_id = parseInt(hora.id.split("-")[1].split(":")[0]);
-                (hora_id < min_hour || hora_id > max_hour) ? hora.remove(): null;
+                (hora_id < min_hour || hora_id > max_hour) ? hora.remove() : null;
             })
 
             // if there is no sábado, remove the column
@@ -788,7 +798,7 @@ require_once("import/html_footer.php");
 
         // droppables
         // forall the .bloque-elements add the event listeners for drag and drop
-        <?php if ($write) : ?>
+        <?php if ($write): ?>
             document.querySelectorAll(".bloque-clase").forEach(element => {
                 function dragStart() {
                     this.classList.add("dragging");
@@ -802,69 +812,69 @@ require_once("import/html_footer.php");
                 element.addEventListener("dragend", dragEnd);
             });
 
-            // forall the cells that are not .bloque-clase add the event listeners for drag and drop
-            document.querySelectorAll("td:not(.bloque-clase)").forEach(element => {
-                function dragOver(e) {
-                    e.preventDefault();
-                    this.classList.add("dragging-over");
-                }
+        // forall the cells that are not .bloque-clase add the event listeners for drag and drop
+        document.querySelectorAll("td:not(.bloque-clase)").forEach(element => {
+            function dragOver(e) {
+                e.preventDefault();
+                this.classList.add("dragging-over");
+            }
 
-                function dragLeave() {
-                    this.classList.remove("dragging-over");
-                }
+            function dragLeave() {
+                this.classList.remove("dragging-over");
+            }
 
-                function drop() {
-                    this.classList.remove("dragging-over");
-                    const dragging = document.querySelector(".dragging");
+            function drop() {
+                this.classList.remove("dragging-over");
+                const dragging = document.querySelector(".dragging");
 
-                    const id = /* for data-ids */ dragging.getAttribute("data-ids");
-                    const hora = /* for data-hora */ this.id.split("-")[1];
-                    const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
-                    let día = /* for data-dia */ this.id.split("-")[2];
-                    día = días.indexOf(día) + 1;
+                const id = /* for data-ids */ dragging.getAttribute("data-ids");
+                const hora = /* for data-hora */ this.id.split("-")[1];
+                const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
+                let día = /* for data-dia */ this.id.split("-")[2];
+                día = días.indexOf(día) + 1;
 
-                    //  rowspan
-                    const bloques = parseInt(dragging.getAttribute("rowspan"));
-                    const horaMoment = moment(hora, "HH:mm");
-                    const horaFin = horaMoment.add(bloques * 15, "minutes");
+                //  rowspan
+                const bloques = parseInt(dragging.getAttribute("rowspan"));
+                const horaMoment = moment(hora, "HH:mm");
+                const horaFin = horaMoment.add(bloques * 15, "minutes");
 
-                    const limit = moment('22:00', 'HH:mm');
+                const limit = moment('22:00', 'HH:mm');
 
-                    if (horaFin.isAfter(limit)) {
-                        triggerMessage("No se puede mover el bloque a esa hora", "Error");
+                if (horaFin.isAfter(limit)) {
+                    triggerMessage("No se puede mover el bloque a esa hora", "Error");
 
-                        // scroll to the top
-                        window.scrollTo(0, 0);
-                        return;
-                    }
+                    // scroll to the top
+                    window.scrollTo(0, 0);
+                    return;
+                }
 
-                    // get the horario
-                    // remove the horario
-                    const bloque = document.querySelector(`.bloque-clase[data-ids="${id}"]`);
+                // get the horario
+                // remove the horario
+                const bloque = document.querySelector(`.bloque-clase[data-ids="${id}"]`);
 
-                    // remove all children
-                    while (bloque.firstChild) {
-                        bloque.removeChild(bloque.firstChild);
-                    }
+                // remove all children
+                while (bloque.firstChild) {
+                    bloque.removeChild(bloque.firstChild);
+                }
 
-                    // prepend a loading child
-                    const loading = `<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
+                // prepend a loading child
+                const loading = `<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
                                         <span class="sr-only">Loading...</span>
                                     </div>`;
-                    bloque.insertAdjacentHTML("afterbegin", loading);
-                    // add style vertical-align: middle
-                    bloque.style.verticalAlign = "middle";
-                    bloque.classList.add("text-center");
-                    // remove draggable
-                    bloque.removeAttribute("draggable");
-
-                    moveHorario(id, día, hora);
-                }
+                bloque.insertAdjacentHTML("afterbegin", loading);
+                // add style vertical-align: middle
+                bloque.style.verticalAlign = "middle";
+                bloque.classList.add("text-center");
+                // remove draggable
+                bloque.removeAttribute("draggable");
+
+                moveHorario(id, día, hora);
+            }
 
-                element.addEventListener("dragover", dragOver);
-                element.addEventListener("dragleave", dragLeave);
-                element.addEventListener("drop", drop);
-            });
+            element.addEventListener("dragover", dragOver);
+            element.addEventListener("dragleave", dragLeave);
+            element.addEventListener("drop", drop);
+        });
         <?php endif; ?>
     }
     async function guardar(id) {
@@ -980,7 +990,6 @@ require_once("import/html_footer.php");
         }
 
     }
-
     function guardarHorario() {
         let goBack = false;
         const data = {
@@ -1111,7 +1120,6 @@ require_once("import/html_footer.php");
 
         // buscarGrupo();
     }
-
     function moveHorario(id, día, hora) {
 
         const formData = new FormData();
@@ -1136,7 +1144,6 @@ require_once("import/html_footer.php");
         });
 
     }
-
     function extractFromModal() {
         // remove all is-valid and is-invalid
         document.querySelectorAll(".is-valid, .is-invalid").forEach(el =>
@@ -1213,7 +1220,6 @@ require_once("import/html_footer.php");
 
         return formData;
     }
-
     function resetFormModal() {
         const modalNuevo = document.querySelector("#modal-editar");
         modalNuevo.querySelectorAll("input").forEach(input => input.value = "");
@@ -1231,7 +1237,6 @@ require_once("import/html_footer.php");
         // remove bg-info
         modalNuevo.querySelectorAll(".bg-info").forEach(el => el.classList.remove("bg-info"));
     }
-
     function insertHorario(horario) {
         const fetchOptions = {
             method: "POST",
@@ -1256,7 +1261,6 @@ require_once("import/html_footer.php");
             triggerMessage(err, "Error");
         });
     }
-
     // initial state
     {
         // fill the table with empty cells
@@ -1289,7 +1293,7 @@ require_once("import/html_footer.php");
         // query selector All tds and ths inside the tbody#horario
         // previous query selector: "tbody#horario td, tbody#horario tr"
         document.querySelectorAll("tbody#horario td, tbody#horario tr").forEach(element => element.style.height = "2.5rem");
-        document.getElementById('dlProfesor').addEventListener('input', function(e) {
+        document.getElementById('dlProfesor').addEventListener('input', function (e) {
             var input = document.getElementById('dlProfesor');
             var value = input.value;
             var option = document.querySelector(`option[value="${value}"]`);
@@ -1318,7 +1322,7 @@ require_once("import/html_footer.php");
                 });
             e.target.value = "";
         });
-        document.getElementById('dlMateria').addEventListener('input', function(e) {
+        document.getElementById('dlMateria').addEventListener('input', function (e) {
             var input = document.getElementById('dlMateria');
             var value = input.value;
             var option = document.querySelector(`option[value="${value}"]`);
@@ -1373,9 +1377,9 @@ require_once("import/html_footer.php");
         }, 0));
 
         fetch("export/horario_excel.php", {
-                method: "POST",
-                body: formData
-            })
+            method: "POST",
+            body: formData
+        })
             .then(response => response.blob())
             .then(blob => {
                 const url = window.URL.createObjectURL(blob);
@@ -1479,7 +1483,7 @@ require_once("import/html_footer.php");
 
     })
     // on modal edit, show the data
-    $("#modal-editar").on("show.bs.modal", async function(event) {
+    $("#modal-editar").on("show.bs.modal", async function (event) {
         document.querySelectorAll("#modal-editar .is-invalid, #modal-editar .is-valid")?.forEach(element => element.classList.remove("is-invalid", "is-valid"));
 
 
@@ -1597,7 +1601,7 @@ require_once("import/html_footer.php");
         backdrop: "static",
         keyboard: false,
     })
-    $("#modal-borrar").on("show.bs.modal", async function(event) {
+    $("#modal-borrar").on("show.bs.modal", async function (event) {
         const button = event.relatedTarget;
         const id = button.parentElement.parentElement.getAttribute("data-ids");
 

+ 18 - 0
css/sgi.css

@@ -1053,4 +1053,22 @@ footer .tab-pane p {
         justify-content: center;
         align-items: center;
     }*/
+}
+
+.movie {
+    transition: all 0.1s;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.movie:hover {
+    transform: scale(1.05);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+    font-size: 1.1em;
+}
+
+.movie:active {
+    transform: scale(1.1);
+    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
+    font-size: 1.2em;
+    font-weight: bold;
 }

+ 92 - 0
css/style.css

@@ -4,4 +4,96 @@
 
 .azul {
     color: #00a6CE;
+}
+
+/* for sm */
+
+.custom-switch.custom-switch-sm .custom-control-label {
+    padding-left: 1rem;
+    padding-bottom: 1rem;
+}
+
+.custom-switch.custom-switch-sm .custom-control-label::before {
+    height: 1rem;
+    width: calc(1rem + 0.75rem);
+    border-radius: 2rem;
+}
+
+.custom-switch.custom-switch-sm .custom-control-label::after {
+    width: calc(1rem - 4px);
+    height: calc(1rem - 4px);
+    border-radius: calc(1rem - (1rem / 2));
+}
+
+.custom-switch.custom-switch-sm .custom-control-input:checked ~ .custom-control-label::after {
+    transform: translateX(calc(1rem - 0.25rem));
+}
+
+/* for md */
+
+.custom-switch.custom-switch-md .custom-control-label {
+    padding-left: 2rem;
+    padding-bottom: 1.5rem;
+}
+
+.custom-switch.custom-switch-md .custom-control-label::before {
+    height: 1.5rem;
+    width: calc(2rem + 0.75rem);
+    border-radius: 3rem;
+}
+
+.custom-switch.custom-switch-md .custom-control-label::after {
+    width: calc(1.5rem - 4px);
+    height: calc(1.5rem - 4px);
+    border-radius: calc(2rem - (1.5rem / 2));
+}
+
+.custom-switch.custom-switch-md .custom-control-input:checked ~ .custom-control-label::after {
+    transform: translateX(calc(1.5rem - 0.25rem));
+}
+
+/* for lg */
+
+.custom-switch.custom-switch-lg .custom-control-label {
+    padding-left: 3rem;
+    padding-bottom: 2rem;
+}
+
+.custom-switch.custom-switch-lg .custom-control-label::before {
+    height: 2rem;
+    width: calc(3rem + 0.75rem);
+    border-radius: 4rem;
+}
+
+.custom-switch.custom-switch-lg .custom-control-label::after {
+    width: calc(2rem - 4px);
+    height: calc(2rem - 4px);
+    border-radius: calc(3rem - (2rem / 2));
+}
+
+.custom-switch.custom-switch-lg .custom-control-input:checked ~ .custom-control-label::after {
+    transform: translateX(calc(2rem - 0.25rem));
+}
+
+/* for xl */
+
+.custom-switch.custom-switch-xl .custom-control-label {
+    padding-left: 4rem;
+    padding-bottom: 2.5rem;
+}
+
+.custom-switch.custom-switch-xl .custom-control-label::before {
+    height: 2.5rem;
+    width: calc(4rem + 0.75rem);
+    border-radius: 5rem;
+}
+
+.custom-switch.custom-switch-xl .custom-control-label::after {
+    width: calc(2.5rem - 4px);
+    height: calc(2.5rem - 4px);
+    border-radius: calc(4rem - (2.5rem / 2));
+}
+
+.custom-switch.custom-switch-xl .custom-control-input:checked ~ .custom-control-label::after {
+    transform: translateX(calc(2.5rem - 0.25rem));
 }

+ 235 - 0
demo.html

@@ -0,0 +1,235 @@
+<link rel="stylesheet" href="css/indivisa.css">
+<p class="container">
+    <i class="ing-fb1"></i> ing-fb1
+</p>
+<p class="container">
+    <i class="ing-fb2"></i> ing-fb2
+</p>
+<p class="container">
+    <i class="ing-tw1"></i> ing-tw1
+</p>
+<p class="container">
+    <i class="ing-tw2"></i> ing-tw2
+</p>
+<p class="container">
+    <i class="ing-in1"></i> ing-in1
+</p>
+<p class="container">
+    <i class="ing-in2"></i> ing-in2
+</p>
+<p class="container">
+    <i class="ing-instra1"></i> ing-instra1
+</p>
+<p class="container">
+    <i class="ing-instra2"></i> ing-instra2
+</p>
+<p class="container">
+    <i class="ing-youtube"></i> ing-youtube
+</p>
+<p class="container">
+    <i class="ing-telefono"></i> ing-telefono
+</p>
+<p class="container">
+    <i class="ing-mail"></i> ing-mail
+</p>
+<p class="container">
+    <i class="ing-link"></i> ing-link
+</p>
+<p class="container">
+    <i class="ing-ubicacion"></i> ing-ubicacion
+</p>
+<p class="container">
+    <i class="ing-puntos"></i> ing-puntos
+</p>
+<p class="container">
+    <i class="ing-usuario"></i> ing-usuario
+</p>
+<p class="container">
+    <i class="ing-pass"></i> ing-pass
+</p>
+<p class="container">
+    <i class="ing-menu"></i> ing-menu
+</p>
+<p class="container">
+    <i class="ing-salir"></i> ing-salir
+</p>
+<p class="container">
+    <i class="ing-flecha"></i> ing-flecha
+</p>
+<p class="container">
+    <i class="ing-cambiar"></i> ing-cambiar
+</p>
+<p class="container">
+    <i class="ing-caret"></i> ing-caret
+</p>
+<p class="container">
+    <i class="ing-aceptar"></i> ing-aceptar
+</p>
+<p class="container">
+    <i class="ing-cancelar"></i> ing-cancelar
+</p>
+<p class="container">
+    <i class="ing-mas"></i> ing-mas
+</p>
+<p class="container">
+    <i class="ing-menos"></i> ing-menos
+</p>
+<p class="container">
+    <i class="ing-editar"></i> ing-editar
+</p>
+<p class="container">
+    <i class="ing-buscar"></i> ing-buscar
+</p>
+<p class="container">
+    <i class="ing-ojo"></i> ing-ojo
+</p>
+<p class="container">
+    <i class="ing-borrar"></i> ing-borrar
+</p>
+<p class="container">
+    <i class="ing-basura"></i> ing-basura
+</p>
+<p class="container">
+    <i class="ing-camara"></i> ing-camara
+</p>
+<p class="container">
+    <i class="ing-importante"></i> ing-importante
+</p>
+<p class="container">
+    <i class="ing-bullet"></i> ing-bullet
+</p>
+<p class="container">
+    <i class="ing-home"></i> ing-home
+</p>
+<p class="container">
+    <i class="ing-formacion"></i> ing-formacion
+</p>
+<p class="container">
+    <i class="ing-empleo"></i> ing-empleo
+</p>
+<p class="container">
+    <i class="ing-insignia1"></i> ing-insignia1
+</p>
+<p class="container">
+    <i class="ing-insignia2"></i> ing-insignia2
+</p>
+<p class="container">
+    <i class="ing-insignia3"></i> ing-insignia3
+</p>
+<p class="container">
+    <i class="ing-insignia4"></i> ing-insignia4
+</p>
+<p class="container">
+    <i class="ing-eventos"></i> ing-eventos
+</p>
+<p class="container">
+    <i class="ing-reporte"></i> ing-reporte
+</p>
+<p class="container">
+    <i class="ing-catalogo"></i> ing-catalogo
+</p>
+<p class="container">
+    <i class="ing-evalua-cartel"></i> ing-evalua-cartel
+</p>
+<p class="container">
+    <i class="ing-revision-cartel"></i> ing-revision-cartel
+</p>
+<p class="container">
+    <i class="ing-reporte-resultados"></i> ing-reporte-resultados
+</p>
+<p class="container">
+    <i class="ing-mi-cartel"></i> ing-mi-cartel
+</p>
+<p class="container">
+    <i class="ing-galeria1"></i> ing-galeria1
+</p>
+<p class="container">
+    <i class="ing-galeria2"></i> ing-galeria2
+</p>
+<p class="container">
+    <i class="ing-iniciar-sesion"></i> ing-iniciar-sesion
+</p>
+<p class="container">
+    <i class="ing-finalistas"></i> ing-finalistas
+</p>
+<p class="container">
+    <i class="ing-comite"></i> ing-comite
+</p>
+<p class="container">
+    <i class="ing-administrador"></i> ing-administrador
+</p>
+<p class="container">
+    <i class="ing-estrella1"></i> ing-estrella1
+</p>
+<p class="container">
+    <i class="ing-estrella2"></i> ing-estrella2
+</p>
+<p class="container">
+    <i class="ing-carga-archivo"></i> ing-carga-archivo
+</p>
+<p class="container">
+    <i class="ing-carga-multiple"></i> ing-carga-multiple
+</p>
+<p class="container">
+    <i class="ing-descarga"></i> ing-descarga
+</p>
+<p class="container">
+    <i class="ing-autorizar"></i> ing-autorizar
+</p>
+<p class="container">
+    <i class="ing-negar"></i> ing-negar
+</p>
+<p class="container">
+    <i class="ing-no-cargado"></i> ing-no-cargado
+</p>
+<p class="container">
+    <i class="ing-alumnos"></i> ing-alumnos
+</p>
+<p class="container">
+    <i class="ing-cardex"></i> ing-cardex
+</p>
+<p class="container">
+    <i class="ing-configuracion"></i> ing-configuracion
+</p>
+<p class="container">
+    <i class="ing-listado-menus"></i> ing-listado-menus
+</p>
+<p class="container">
+    <i class="ing-mi-cuenta"></i> ing-mi-cuenta
+</p>
+<p class="container">
+    <i class="ing-ver"></i> ing-ver
+</p>
+<p class="container">
+    <i class="ing-grafica"></i> ing-grafica
+</p>
+<p class="container">
+    <i class="ing-clic"></i> ing-clic
+</p>
+<p class="container">
+    <i class="ing-guardar"></i> ing-guardar
+</p>
+<p class="container">
+    <i class="ing-regresar"></i> ing-regresar
+</p>
+<p class="container">
+    <i class="ing-cuadrado"></i> ing-cuadrado
+</p>
+<p class="container">
+    <i class="ing-imprimir"></i> ing-imprimir
+</p>
+<p class="container">
+    <i class="ing-importante2"></i> ing-importante2
+</p>
+<p class="container">
+    <i class="ing-copiar"></i> ing-copiar
+</p>
+<p class="container">
+    <i class="ing-reloj"></i> ing-reloj
+</p>
+<p class="container">
+    <i class="ing-retardo"></i> ing-retardo
+</p>
+<p class="container">
+    <i class="ing-justificar"></i> ing-justificar
+</p>

+ 29 - 14
horario_profesor.php

@@ -28,7 +28,11 @@ $write = $user->admin || in_array($user->acceso, ['w']);
     <script src="js/bootstrap/bootstrap.min.js" defer></script>
 
     <script src="js/messages.js" defer></script>
-    <script src="js/horarios_profesor.js" defer></script>
+    <script>
+        const write = <?= $write ? 'true' : 'false' ?>;
+    </script>
+    <script src="js/moment.js" defer></script>
+    <script src="js/horario_profesor.js" defer></script>
 </head>
 <!--  -->
 
@@ -47,20 +51,31 @@ $write = $user->admin || in_array($user->acceso, ['w']);
             <div class="form-group">
                 <div class="form-box">
                     <input type="hidden" name="periodo" value="<?= $user->periodo ?>" />
-                    <div class="form-box">
-                        <div class="form-group row">
-                            <label for="clave" class="col-4 col-form-label">Carrera</label>
-                            <div class="col-6">
-                                <input type="text" class="form-control" id="clave" name="clave" placeholder="Clave del profesor (opcional)" value="<?= $clave ?? '' ?>" pattern="(do)?[0-9]{3,6}" title="La clave debe tener 8 caracteres, los primeros 2 deben ser letras y los últimos 6 números" minlength="3" maxlength="8">
+                    <div class="form-group row">
+                        <label for="clave_profesor" class="col-4 col-form-label">Profesor</label>
+                        <div class="col-6">
+                            <input list="lista_profesores" name="clave_profesor" id="clave_profesor" class="form-control" placeholder="Profesor" required="required">
+                            <div class="valid-feedback">
+                                Profesor encontrado
                             </div>
-                        </div>
-                        <div class="form-group row">
-                            <label for="profesor" class="col-4 col-form-label">Nombre</label>
-                            <div class="col-6 ">
-                                <input type="text" class="form-control" id="profesor" name="nombre" placeholder="Nombre del profesor (opcional)">
+                            <div class="invalid-feedback">
+                                Profesor no encontrado
                             </div>
+                            <datalist id="lista_profesores">
+                                <?php
+                                $profesores = $db->where('facultad_id', $user->facultad['facultad_id'])->get("fs_profesor");
+                                foreach ($profesores as $profesor) {
+                                    extract($profesor);
+                                ?>
+                                    <option data-grado="<?= $grado ?>" data-clave="<?= $clave ?>" data-profesor="<?= $profesor ?>" data-id="<?= $id; ?>" value="<?= "$clave | $grado $profesor" ?>"></option>
+                                <?php
+                                }
+                                ?>
+                            </datalist>
+                            <ul class="list-group" id="profesores"></ul>
+                            <input type="hidden" id="periodo_id" name="periodo_id" value="<?= $user->periodo ?>">
+                            <input type="hidden" id="profesor_id" name="profesor_id" value="">
                         </div>
-
                     </div>
 
                     <!-- ICO-BUSCAR FILTRAR & ICO-BORRAR LIMPIAR -->
@@ -69,7 +84,7 @@ $write = $user->admin || in_array($user->acceso, ['w']);
                             <span class="ing-buscar icono"></span>
                             Buscar horario
                         </button>
-                        <button type="button" class="btn btn-outline-danger" onclick="">
+                        <button type="button" class="btn btn-outline-danger" onclick="location.reload()">
                             <span class="ing-borrar icono"></span>
                             Limpiar
                         </button>
@@ -99,7 +114,7 @@ $write = $user->admin || in_array($user->acceso, ['w']);
         </div>
         <!-- Table responsive -->
         <div class="table-responsive">
-            <table class="table table-bordered table-sm table-responsive-sm" id="table-horario">
+            <table class="table table-bordered table-sm table-responsive-md" id="table-horario">
                 <thead class="thead-dark">
                     <tr id="headers">
                         <th scope="col" class="text-center">Hora</th>

+ 2 - 1
import/html_css_files.php

@@ -2,4 +2,5 @@
 <link rel="stylesheet" href="css/bootstrap-ulsa.min.css" type="text/css">
 <link rel="stylesheet" href="css/indivisa.css" type="text/css">
 <link rel="stylesheet" href="css/sgi.css?rand=<?php echo rand(); ?>" type="text/css">
-<link rel="stylesheet" href="css/style.css">
+<link rel="stylesheet" href="css/style.css">
+<link rel="stylesheet" href="css/jquery-ui.css">

+ 50 - 24
import/html_header.php

@@ -6,8 +6,12 @@ require_once 'class/c_login.php';
 $ruta = "../";
 require_once 'include/bd_pdo.php';
 
-if (!isset($_SESSION['user'])) 
-    die(header('Location: index.php'));
+if (!isset($_SESSION['user'])) {
+    if (isset($redirect))
+        $_SESSION['ruta'] = $redirect;
+
+    header('Location: index.php');
+}
 
 $user = unserialize($_SESSION['user']);
 
@@ -21,10 +25,11 @@ function html_header($title, $header = null)
     else
         $paginas = queryAll("SELECT * FROM PERMISO_VIEW WHERE id = :id ORDER BY pagina_ruta", array(":id" => $user->user['id']));
 
-?>
+    ?>
     <aside id="sidebar" class="bg-light defaultShadow d-flex flex-column p-4">
         <div class="d-flex align-items-center mb-5">
-            <div class="logotipo"><a href="https://lasalle.mx/" target="_blank"><img src="imagenes/logo_lasalle.png"></a></div>
+            <div class="logotipo"><a href="https://lasalle.mx/" target="_blank"><img src="imagenes/logo_lasalle.png"></a>
+            </div>
             <div class="flex-grow-1 d-flex justify-content-end">
                 <nav class="navbar navbar-expand d-none d-flex">
                     <ul class="navbar-nav">
@@ -33,10 +38,12 @@ function html_header($title, $header = null)
                 </nav>
                 <div class="d-flex mainMenu justify-content-center align-items-center">
                     <div class="max-h iconSesion">
-                        <a href="salir.php" class="iconOff max-h pl-3 d-flex justify-content-start align-items-center"><i class="ing-salir"></i></a>
+                        <a href="salir.php" class="iconOff max-h pl-3 d-flex justify-content-start align-items-center"><i
+                                class="ing-salir"></i></a>
                     </div>
                     <div class="max-h">
-                        <div class="bg-primary rounded-circle pointer max-h max-w d-flex justify-content-center align-items-center" id="dismiss">
+                        <div class="bg-primary rounded-circle pointer max-h max-w d-flex justify-content-center align-items-center"
+                            id="dismiss">
                             <span class="text-white iconMenuSidebar ing-cancelar"></span>
                         </div>
                     </div>
@@ -51,7 +58,7 @@ function html_header($title, $header = null)
             </p>
 
             <?php
-            if($user->admin){ ?>
+            if ($user->admin) { ?>
                 <p class="mb-0 mt-3 ml-4 pl-1">
                     <a href="permisos.php" class="d-block side-menu">
                         <span class="ing-pass"></span> Permisos
@@ -60,11 +67,13 @@ function html_header($title, $header = null)
             <?php }
             $cont = 0;
             foreach ($grupos as $grupo) {
-            ?>
+                ?>
                 <p class="mb-0 mt-3">
-                    <a class="d-block side-menu collapsed" data-toggle="collapse" href="#menu_<?= $cont ?>" role="button" aria-expanded="flase">
+                    <a class="d-block side-menu collapsed" data-toggle="collapse" href="#menu_<?= $cont ?>" role="button"
+                        aria-expanded="false">
                         <i class="ing-caret ing-fw mr-2"></i>
-                        <span class="<?= $grupo['grupo_icon'] ?>"></span> <?= ucfirst($grupo['grupo_nombre']) ?>
+                        <span class="<?= $grupo['grupo_icon'] ?>"></span>
+                        <?= ucfirst($grupo['grupo_nombre']) ?>
                     </a>
                 </p>
                 <div id="menu_<?= $cont ?>" class="collapse" data-parent="#accordionMenu" style>
@@ -76,57 +85,74 @@ function html_header($title, $header = null)
                             $user->access($pagina['pagina_ruta'] ?? '');
                             if ($grupo['grupo_id'] == $pagina['grupo_id']) {
                                 if ($user->admin || $user->acceso != 'n') {
-                        ?>
+                                    ?>
                                     <li class="mt-1">
                                         <a href="<?= $pagina['pagina_ruta'] ?>.php">
                                             <?= $page ?>
                                         </a>
                                     </li>
-                        <?php }
+                                <?php }
                             }
                         }
                         ?>
                     </ul>
                 </div>
-            <?php $cont++;
-            } 
+                <?php $cont++;
+            }
             ?>
         </div>
     </aside>
     <div class="overlay"></div>
     <header class="sticky-top bg-white">
         <div class="container marco menu d-flex align-items-center">
-            <div class="logotipo"><a href="https://lasalle.mx/" target="_blank"><img src="imagenes/logo_lasalle.png"></a></div>
+            <div class="logotipo">
+                <a href="https://lasalle.mx/" target="_blank">
+                    <img src="imagenes/logo_lasalle.png">
+                </a>
+            </div>
             <div class="flex-grow-1 d-flex justify-content-end">
-                <nav class="navbar navbar-expand d-none d-flex">
-                    <ul class="navbar-nav">
-
-                    </ul>
+                <nav class="navbar navbar-expand-lg d-flex">
+                    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
+                        aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
+                        <span class="navbar-toggler-icon"></span>
+                    </button>
+                    <div class="collapse navbar-collapse" id="navbarNav">
+                        <ul class="navbar-nav">
+                            <!-- Add your navigation items here -->
+                        </ul>
+                    </div>
                 </nav>
                 <div class="d-flex mainMenu justify-content-center align-items-center">
                     <div class="max-h iconSesion">
-                        <a href="salir.php" class="iconOff max-h pl-3 d-flex justify-content-start align-items-center"><i class="ing-salir"></i></a>
+                        <a href="salir.php" class="iconOff max-h pl-3 d-flex justify-content-start align-items-center">
+                            <i class="ing-salir"></i>
+                        </a>
                     </div>
                     <div class="max-h">
-                        <span id="sidebarCollapse" style="font-size: 44px;" class="ing-menu bg-white rounded-circle pointer max-w d-flex justify-content-center align-items-center"></span>
+                        <span id="sidebarCollapse" style="font-size: 44px;"
+                            class="ing-menu bg-white rounded-circle pointer max-w d-flex justify-content-center align-items-center"></span>
                     </div>
                 </div>
             </div>
         </div>
     </header>
+
     <div class="row bg-info mx-0 barra-gris d-flex flex-column">
         <?php
         if ($header != null) {
-        ?>
+            ?>
             <div class="marco">
                 <div class="col-sm-12">
-                    <h2 class="text-muted"><?= $header; ?>
+                    <h2 class="text-muted">
+                        <?= $header; ?>
                 </div>
             </div>
         <?php } ?>
         <div class="marco">
             <div class="col-sm-12 py-3">
-                <h2 class="text-uppercase"><?= $title; ?></h2>
+                <h2 class="text-uppercase">
+                    <?= $title; ?>
+                </h2>
             </div>
         </div>
     </div>

+ 26 - 23
import/periodo.php

@@ -5,10 +5,14 @@
         <div class="col-12">
             <?php
             $target = $target ?? strtok($_SERVER["REQUEST_URI"], '?');
-            $periodos = queryAll("SELECT * FROM FS_PERIODO WHERE FACULTAD_ID = COALESCE(:fac, FACULTAD_ID) ORDER BY INICIO DESC", [":fac" =>  $user->admin ? null : $user->facultad['facultad_id']]);
+            $niveles = array_map(
+                fn($nivel) => array_merge(
+                    $nivel,
+                    ['periodos' => $db->where('nivel_id', $nivel['nivel_id'])->get('periodo_view')]
+                ), $db->get("nivel")
+            );
+
             // collect facultad_id's with facultad from $periodos
-            if ($user->admin)
-                $facultades = array_unique(array_column($periodos, 'facultad', 'facultad_id'));
             ?>
             <input type="hidden" name="target" value="<?= $target ?>">
             <div class="form-box">
@@ -19,27 +23,26 @@
                             <div class="datalist-input">Selecciona un periodo</div>
                             <span class="ing-buscar icono"></span>
                             <ul style="display:none">
-                                <?php if (!$user->admin) foreach ($periodos as $periodo) { ?>
-
-                                    <li data-id="<?= $periodo['id'] ?>">
-                                        <?= "{$periodo['nivel']} - {$periodo['periodo']} ({$periodo['estado']})" ?>
-                                    </li>
-                                    <?php }
-                                else {
-                                    foreach ($facultades as $facultad_id => $facultad) {
+                                <?php
+                                foreach ($niveles as $nivel) {
                                     ?>
-                                        <li class="facultad not-selectable" data-id="<?= $facultad_id ?>">
-                                            <?= $facultad ?>
-                                        </li>
-                                        <?php
-                                        foreach (array_filter($periodos, fn ($p) => $p['facultad_id'] == $facultad_id) as $periodo) {
+                                    <li data-id="<?= $nivel['nivel_id'] ?>" class="not-selectable disable">
+                                        <?= $nivel['nivel_nombre'] ?>
+                                    </li>
+                                    <?
+                                    $periodos_rs = $db->query(
+                                        'SELECT * FROM fs_periodo(NULL, :nivel,	4)',
+                                        [':nivel' => $nivel['nivel_id']]
+                                    );
+                                    foreach ($periodos_rs as $per) {
                                         ?>
-                                            <li data-id="<?= $periodo['id'] ?>">
-                                                <?= "{$periodo['nivel']} - {$periodo['periodo']} ({$periodo['estado']})" ?>
-                                            </li>
-                                <?php }
-                                    }
-                                } ?>
+                                        <li data-id="<?= $per['periodo_id'] ?>" <?php if ($user->periodo == $per["periodo_id"]) {
+                                              echo 'class="selected"';
+                                          } ?>>
+                                            <?= $per['periodo_nombre'] ?>
+                                        </li>
+                                    <?php } ?>
+                                <?php } ?>
                             </ul>
                             <input type="hidden" id="periodo" name="id" value="">
                         </div>
@@ -55,7 +58,7 @@
     setDatalist('#periodo', <?php echo $user->periodo; ?>)
     makeRequiredDatalist("#periodo", true);
 
-    $(document).on('click', '#dlPeriodo ul li', function() {
+    $(document).on('click', '#dlPeriodo ul li', function () {
         $('#formaPeriodo').submit();
     });
 </script>

+ 1 - 0
index.php

@@ -3,6 +3,7 @@ require_once 'class/c_login.php';
 $error = isset($_SESSION["error"]);
 unset($_SESSION["error"]);
 if ($error) $errorDesc = "El usuario y/o contraseña son incorrectos.";
+
 ?>
 <!DOCTYPE html>
 <html lang="es" prefix="og: http://ogp.me/ns#">

+ 160 - 0
js/auditoría.js

@@ -0,0 +1,160 @@
+import { createApp, reactive } from 'https://unpkg.com/petite-vue?module';
+const store = reactive({
+    loading: false,
+    current: {
+        comentario: '',
+        clase_vista: null,
+        empty: '',
+    },
+    facultades: {
+        data: [],
+        async fetch() {
+            this.data = [];
+            const res = await fetch('action/action_facultad.php');
+            this.data = await res.json();
+        },
+    },
+    filters: {
+        facultad_id: null,
+        fecha: null,
+        fecha_inicio: null,
+        fecha_fin: null,
+        profesor: null,
+        estados: [],
+        switchFecha: false,
+        switchFechas() {
+            $(function () {
+                store.filters.fecha_inicio = store.filters.fecha_fin = store.filters.fecha = null;
+                $("#fecha, #fecha_inicio, #fecha_fin").datepicker({
+                    minDate: -3,
+                    maxDate: new Date(),
+                    dateFormat: "yy-mm-dd",
+                    showAnim: "slide",
+                });
+                const fecha = $("#fecha"), inicio = $("#fecha_inicio"), fin = $("#fecha_fin");
+                inicio.on("change", function () {
+                    store.filters.fecha_inicio = inicio.val();
+                    fin.datepicker("option", "minDate", inicio.val());
+                });
+                fin.on("change", function () {
+                    store.filters.fecha_fin = fin.val();
+                    inicio.datepicker("option", "maxDate", fin.val());
+                });
+                fecha.on("change", function () {
+                    store.filters.fecha = fecha.val();
+                });
+            });
+        }
+    },
+    estados: {
+        data: [],
+        async fetch() {
+            this.data = [];
+            const res = await fetch('action/action_estado_supervisor.php');
+            this.data = await res.json();
+        },
+        getEstado(id) {
+            return this.data.find((estado) => estado.estado_supervisor_id === id);
+        },
+        printEstados() {
+            if (store.filters.estados.length > 0)
+                document.querySelector('#estados').innerHTML = store.filters.estados.map((estado) => `<span class="mx-2 badge badge-${store.estados.getEstado(estado).estado_color}">
+                    <i class="${store.estados.getEstado(estado).estado_icon}"></i> ${store.estados.getEstado(estado).nombre}
+                </span>`).join('');
+            else
+                document.querySelector('#estados').innerHTML = `Todos los registros`;
+        }
+    },
+    toggle(arr, element) {
+        const newArray = arr.includes(element) ? arr.filter((item) => item !== element) : [...arr, element];
+        // if all are selected, then unselect all
+        if (newArray.length === this.estados.data.length)
+            return [];
+        return newArray;
+    },
+});
+createApp({
+    store,
+    get clase_vista() {
+        return store.current.clase_vista;
+    },
+    registros: {
+        data: [],
+        async fetch() {
+            this.loading = true;
+            this.data = [];
+            const res = await fetch('action/action_auditoria.php');
+            this.data = await res.json();
+            this.loading = false;
+        },
+        invertir() {
+            this.data = this.data.reverse();
+        },
+        mostrarComentario(registro_id) {
+            const registro = this.data.find((registro) => registro.registro_id === registro_id);
+            store.current.comentario = registro.comentario;
+            $('#ver-comentario').modal('show');
+        },
+        get relevant() {
+            /*
+                facultad_id: null,
+                fecha: null,
+                fecha_inicio: null,
+                fecha_fin: null,
+                profesor: null,
+                asistencia: null,
+                estado_id: null,
+                if one of the filters is null, then it is not relevant
+    
+            */
+            const filters = Object.keys(store.filters).filter((filtro) => store.filters[filtro] || store.filters[filtro]?.length > 0);
+            return this.data.filter((registro) => {
+                return filters.every((filtro) => {
+                    switch (filtro) {
+                        case 'fecha':
+                            return registro.registro_fecha_ideal === store.filters[filtro];
+                        case 'fecha_inicio':
+                            return registro.registro_fecha_ideal >= store.filters[filtro];
+                        case 'fecha_fin':
+                            return registro.registro_fecha_ideal <= store.filters[filtro];
+                        case 'profesor':
+                            const textoFiltro = store.filters[filtro].toLowerCase();
+                            if (/^\([^)]+\)\s[\s\S]+$/.test(textoFiltro)) {
+                                const clave = registro.profesor_clave.toLowerCase();
+                                const filtroClave = textoFiltro.match(/\((.*?)\)/)?.[1];
+                                console.log(clave, filtroClave);
+                                return clave.includes(filtroClave);
+                            }
+                            else {
+                                const nombre = registro.profesor_nombre.toLowerCase();
+                                return nombre.includes(textoFiltro);
+                            }
+                        case 'facultad_id':
+                            return registro.facultad_id === store.filters[filtro];
+                        case 'estados':
+                            if (store.filters[filtro].length === 0)
+                                return true;
+                            return store.filters[filtro].includes(registro.estado_supervisor_id);
+                        default:
+                            return true;
+                    }
+                });
+            });
+        },
+    },
+    get profesores() {
+        return this.registros.data.map((registro) => ({
+            profesor_id: registro.profesor_id,
+            profesor_nombre: registro.profesor_nombre,
+            profesor_correo: registro.profesor_correo,
+            profesor_clave: registro.profesor_clave,
+            profesor_grado: registro.profesor_grado,
+        })).sort((a, b) => a.profesor_nombre.localeCompare(b.profesor_nombre));
+    },
+    async mounted() {
+        await this.registros.fetch();
+        await store.facultades.fetch();
+        await store.estados.fetch();
+        store.filters.switchFechas();
+    }
+}).mount('#app');

+ 120 - 0
js/client.js

@@ -0,0 +1,120 @@
+// @ts-ignore Import module
+import { createApp, reactive } from 'https://unpkg.com/petite-vue?module';
+const webServices = {
+    getPeriodosV1: async () => {
+        try {
+            const response = await fetch('periodos.v1.php');
+            return await response.json();
+        }
+        catch (error) {
+            console.log(error);
+            return [];
+        }
+    },
+    getPeriodosV2: async () => {
+        try {
+            const response = await fetch('periodos.v2.php');
+            return await response.json();
+        }
+        catch (error) {
+            console.log(error);
+            return [];
+        }
+    }
+};
+const store = reactive({
+    periodosV1: [],
+    periodosV2: [],
+    errors: [],
+    fechas(idPeriodo) {
+        const periodo = this.periodosV2.find((periodo) => periodo.IdPeriodo === idPeriodo);
+        return {
+            inicio: periodo ? periodo.FechaInicio : '',
+            fin: periodo ? periodo.FechaFin : ''
+        };
+    },
+    periodov1(idPeriodo) {
+        return this.periodosV1.find((periodo) => periodo.IdPeriodo === idPeriodo);
+    },
+    periodov2(idPeriodo) {
+        return this.periodosV2.filter((periodo) => periodo.IdPeriodo === idPeriodo);
+    },
+    async addPeriodo(periodo) {
+        try {
+            const result = await fetch('backend/periodos.php', {
+                method: 'POST',
+                body: JSON.stringify({
+                    ...periodo,
+                    ...this.fechas(periodo.IdPeriodo)
+                }),
+                headers: {
+                    'Content-Type': 'application/json'
+                }
+            }).then((response) => response.json());
+            if (result.success) {
+                this.periodosV1 = this.periodosV1.map((periodoV1) => {
+                    if (periodoV1.IdPeriodo === periodo.IdPeriodo) {
+                        periodoV1.in_db = true;
+                    }
+                    return periodoV1;
+                });
+                return result;
+            }
+            else {
+                this.errors.push(result.message);
+            }
+        }
+        catch (error) {
+            this.errors.push(error);
+        }
+    },
+    async addCarreras(idPeriodo) {
+        try {
+            const periodoV1 = this.periodov1(idPeriodo);
+            const periodoV2 = this.periodov2(idPeriodo);
+            const data = periodoV2.map(({ ClaveCarrera, NombreCarrera }) => ({
+                ClaveCarrera: ClaveCarrera,
+                NombreCarrera: NombreCarrera,
+                IdNivel: periodoV1.IdNivel,
+            }));
+            const result = await fetch('backend/carreras.php', {
+                method: 'POST',
+                body: JSON.stringify(data),
+                headers: {
+                    'Content-Type': 'application/json'
+                }
+            }).then((response) => response.json());
+            if (result.success) {
+                await webServices.getPeriodosV1().then((periodosV1) => {
+                    this.periodosV1 = periodosV1;
+                });
+                await webServices.getPeriodosV2().then((periodosV2) => {
+                    this.periodosV2 = periodosV2;
+                });
+            }
+        }
+        catch (error) {
+            this.errors.push(error);
+        }
+    }
+});
+createApp({
+    store,
+    info(IdPeriodo) {
+        const periodo = store.periodosV2.find((periodo) => periodo.IdPeriodo === IdPeriodo &&
+            periodo.FechaInicio != '' && periodo.FechaFin != '');
+        return periodo;
+    },
+    complete(IdPeriodo) {
+        const info = this.info(IdPeriodo);
+        return info !== undefined;
+    },
+    mounted: async () => {
+        await webServices.getPeriodosV1().then((periodosV1) => {
+            store.periodosV1 = periodosV1;
+        });
+        await webServices.getPeriodosV2().then((periodosV2) => {
+            store.periodosV2 = periodosV2;
+        });
+    }
+}).mount();

+ 2 - 2
js/datalist.js

@@ -70,7 +70,7 @@ $(function () {
     $.each($(".datalist").find('ul li:not(.not-selectable)'), function () {
         if ($(this).hasClass("selected")) {
             var elementRoot = $(this).parents('.datalist');
-            elementRoot.find('.datalist-input').text($(this).html().replace(/[\t\n]+/g, ' ').trim());
+            elementRoot.find('.datalist-input').html($(this).html().replace(/[\t\n]+/g, ' ').trim());
             var cid = $(this).data('id');
             elementRoot.find("input[type=hidden]").val(cid);
         }
@@ -82,7 +82,7 @@ $(function () {
 
 $(document).on('click', '.datalist-select > ul li:not(.not-selectable)', function () {
     var elementRoot = $(this).parents('.datalist');
-    elementRoot.find('.datalist-input').text($(this).html().replace(/[\t\n]+/g, ' ').trim());
+    elementRoot.find('.datalist-input').html($(this).html().replace(/[\t\n]+/g, ' ').trim());
     // $(this).parent('ul').siblings('input[type="text"]').blur();
     ocultaList({ "data": { "padre": elementRoot } });
     var cid = $(this).data('id');

+ 0 - 0
js/declaration.js


+ 414 - 0
js/horario_profesor.js

@@ -0,0 +1,414 @@
+const compareHours = (hora1, hora2) => {
+    const [h1, m1] = hora1.split(":").map(Number);
+    const [h2, m2] = hora2.split(":").map(Number);
+    if (h1 !== h2) {
+        return h1 > h2 ? 1 : -1;
+    }
+    if (m1 !== m2) {
+        return m1 > m2 ? 1 : -1;
+    }
+    return 0;
+};
+let horarios = [];
+const table = document.querySelector("table");
+if (!(table instanceof HTMLTableElement)) {
+    triggerMessage("No se ha encontrado la tabla", "Error", "error");
+    throw new Error("No se ha encontrado la tabla");
+}
+[...Array(16).keys()].map(x => x + 7).forEach(hora => {
+    // add 7 rows for each hour
+    [0, 15, 30, 45].map((minute) => `${minute}`.padStart(2, '0')).forEach((minute) => {
+        const tr = document.createElement("tr");
+        tr.id = `hora-${hora}:${minute}`;
+        tr.classList.add(hora > 13 ? "tarde" : "mañana");
+        if (minute == "00") {
+            const th = document.createElement("th");
+            th.classList.add("text-center");
+            th.scope = "row";
+            th.rowSpan = 4;
+            th.innerText = `${hora}:00`;
+            th.style.verticalAlign = "middle";
+            tr.appendChild(th);
+        }
+        ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"].forEach(día => {
+            const td = document.createElement("td");
+            td.id = `hora-${hora}:${minute}-${día}`;
+            tr.appendChild(td);
+        });
+        const tbody = document.querySelector("tbody#horario");
+        if (!(tbody instanceof HTMLTableSectionElement)) {
+            throw new Error("No se ha encontrado el tbody");
+        }
+        tbody.appendChild(tr);
+    });
+});
+const empty_table = table.cloneNode(true);
+document.querySelectorAll('.hidden').forEach((element) => {
+    element.style.display = "none";
+});
+// hide the table
+table.style.display = "none";
+function moveHorario(id, día, hora) {
+    const formData = new FormData();
+    formData.append("id", id);
+    formData.append("hora", hora);
+    formData.append("día", día);
+    fetch("action/action_horario_update.php", {
+        method: "POST",
+        body: formData
+    }).then(res => res.json()).then(response => {
+        if (response.status == "success") {
+            triggerMessage("Horario movido", "Éxito", "success");
+        }
+        else {
+            triggerMessage(response.message, "Error");
+        }
+    }).then(() => {
+        renderHorario();
+    }).catch(err => {
+        triggerMessage(err, "Error");
+    });
+}
+function renderHorario() {
+    if (horarios.length == 0) {
+        triggerMessage("Este profesor hay horarios para mostrar", "Error", "info");
+        table.style.display = "none";
+        document.querySelectorAll('.hidden').forEach((element) => element.style.display = "none");
+        return;
+    }
+    // show the table
+    table.style.display = "table";
+    document.querySelectorAll('.hidden').forEach((element) => element.style.display = "block");
+    // clear the table
+    table.innerHTML = empty_table.outerHTML;
+    function conflicts(horario1, horario2) {
+        const { hora: hora_inicio1, hora_final: hora_final1, dia: dia1 } = horario1;
+        const { hora: hora_inicio2, hora_final: hora_final2, dia: dia2 } = horario2;
+        if (dia1 !== dia2) {
+            return false;
+        }
+        const compareInicios = compareHours(hora_inicio1, hora_inicio2);
+        const compareFinales = compareHours(hora_final1, hora_final2);
+        if (compareInicios >= 0 && compareInicios <= compareFinales ||
+            compareFinales >= 0 && compareFinales <= -compareInicios) {
+            return true;
+        }
+        return false;
+    }
+    // remove the next 5 cells
+    function removeNextCells(horas, minutos, dia, cells = 5) {
+        for (let i = 1; i <= cells; i++) {
+            const minute = minutos + i * 15;
+            const nextMinute = (minute % 60).toString().padStart(2, "0");
+            const nextHour = horas + Math.floor(minute / 60);
+            const cellId = `hora-${nextHour}:${nextMinute}-${dia}`;
+            const cellElement = document.getElementById(cellId);
+            if (cellElement) {
+                cellElement.remove();
+            }
+            else {
+                console.log(`No se ha encontrado la celda ${cellId}`);
+                break;
+            }
+        }
+    }
+    function newBlock(horario, edit = false) {
+        function move(horario, cells = 5) {
+            const [horas, minutos] = horario.hora.split(":").map(Number);
+            const cell = document.getElementById(`hora-${horas}:${minutos.toString().padStart(2, "0")}-${horario.dia}`);
+            const { top, left } = cell.getBoundingClientRect();
+            const block = document.getElementById(`block-${horario.id}`);
+            block.style.top = `${top}px`;
+            block.style.left = `${left}px`;
+            removeNextCells(horas, minutos, horario.dia, cells);
+        }
+        const [horas, minutos] = horario.hora.split(":").map(x => parseInt(x));
+        const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
+        horario.hora = hora;
+        const cell = document.getElementById(`hora-${horario.hora}-${horario.dia}`);
+        if (!cell)
+            return;
+        cell.dataset.ids = `${horario.id}`;
+        const float_menu = edit ?
+            `<div class="menu-flotante p-2" style="opacity: .7;">
+            <a class="mx-2" href="#" data-toggle="modal" data-target="#modal-editar">
+                <i class="ing-editar ing"></i>
+            </a>
+            <a class="mx-2" href="#" data-toggle="modal" data-target="#modal-borrar">
+                <i class="ing-basura ing"></i>
+            </a>
+        </div>`
+            : '';
+        cell.innerHTML =
+            `<div style="overflow-y: auto; overflow-x: hidden; height: 100%;" id="block-${horario.id}" class="position-absolute w-100 h-100">
+            <small class="text-gray">${horario.hora}</small>
+            <b class="title">${horario.materia}</b> <br>
+            <br><span>Salón: </span>${horario.salon} <br>
+            <small class="my-2">
+              ${horario.profesores.map((profesor) => ` <span class="ing ing-formacion mx-1"></span>${profesor.grado ?? ''} ${profesor.profesor}`).join("<br>")}
+            </small>
+        </div>
+          ${float_menu}`;
+        cell.classList.add("bloque-clase", "position-relative");
+        cell.rowSpan = horario.bloques;
+        // draggable
+        cell.draggable = write;
+        if (horario.bloques > 0) {
+            removeNextCells(horas, minutos, horario.dia, horario.bloques - 1);
+        }
+    }
+    function newConflictBlock(horarios, edit = false) {
+        const first_horario = horarios[0];
+        const [horas, minutos] = first_horario.hora.split(":").map(x => parseInt(x));
+        const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
+        const ids = horarios.map(horario => horario.id);
+        const cell = document.getElementById(`hora-${hora}-${first_horario.dia}`);
+        if (cell == null) {
+            console.error(`Error: No se encontró la celda: hora-${hora}-${first_horario.dia}`);
+            return;
+        }
+        cell.dataset.ids = ids.join(",");
+        // replace the content of the cell
+        cell.innerHTML = `
+          <small class='text-danger'>
+            ${hora}
+          </small>
+          <div class="d-flex justify-content-center align-items-center mt-4">
+            <div class="d-flex flex-column justify-content-center align-items-center">
+              <span class="ing ing-importante text-danger" style="font-size: 2rem;"></span>
+              <b class='text-danger'>
+                Empalme de ${ids.length} horarios
+              </b>
+              <hr>
+              <i class="text-danger">Ver horarios &#8230;</i>
+            </div>
+          </div>
+        `;
+        // Add classes and attributes
+        cell.classList.add("conflict", "bloque-clase");
+        cell.setAttribute("role", "button");
+        // Add event listener for the cell
+        cell.addEventListener("click", () => {
+            $("#modal-choose").modal("show");
+            const ids = cell.getAttribute("data-ids").split(",").map(x => parseInt(x));
+            const tbody = document.querySelector("#modal-choose tbody");
+            tbody.innerHTML = "";
+            horarios.filter(horario => ids.includes(horario.id)).sort((a, b) => compareHours(a.hora, b.hora)).forEach(horario => {
+                tbody.innerHTML += `
+              <tr data-ids="${horario.id}">
+                <td><small>${horario.hora.slice(0, -3)}-${horario.hora_final.slice(0, -3)}</small></td>
+                <td>${horario.materia}</td>
+                <td>
+                  ${horario.profesores.map(({ grado, profesor }) => `${grado ?? ''} ${profesor}`).join(", ")}
+                </td>
+                <td>${horario.salon}</td>
+                ${edit ? `
+                  <td class="text-center">
+                    <button class="btn btn-sm btn-primary dismiss-editar" data-toggle="modal" data-target="#modal-editar">
+                      <i class="ing-editar ing"></i>
+                    </button>
+                  </td>
+                  <td class="text-center">
+                    <button class="btn btn-sm btn-danger dismiss-editar" data-toggle="modal" data-target="#modal-borrar">
+                      <i class="ing-basura ing"></i>
+                    </button>
+                  </td>
+                ` : ""}
+              </tr>`;
+            });
+            document.querySelectorAll(".dismiss-editar").forEach(btn => {
+                btn.addEventListener("click", () => $("#modal-choose").modal("hide"));
+            });
+        });
+        function getDuration(hora_i, hora_f) {
+            const [horas_i, minutos_i] = hora_i.split(":").map(x => parseInt(x));
+            const [horas_f, minutos_f] = hora_f.split(":").map(x => parseInt(x));
+            const date_i = new Date(0, 0, 0, horas_i, minutos_i);
+            const date_f = new Date(0, 0, 0, horas_f, minutos_f);
+            const diffInMilliseconds = date_f.getTime() - date_i.getTime();
+            const diffInMinutes = diffInMilliseconds / (1000 * 60);
+            const diffIn15MinuteIntervals = diffInMinutes / 15;
+            return Math.floor(diffIn15MinuteIntervals);
+        }
+        const maxHoraFinal = horarios.reduce((max, horario) => {
+            const [horas, minutos] = horario.hora_final.split(":").map(x => parseInt(x));
+            const date = new Date(0, 0, 0, horas, minutos);
+            return date > max ? date : max;
+        }, new Date(0, 0, 0, 0, 0));
+        const horaFinalMax = new Date(0, 0, 0, maxHoraFinal.getHours(), maxHoraFinal.getMinutes());
+        const blocks = getDuration(first_horario.hora, `${horaFinalMax.getHours()}:${horaFinalMax.getMinutes()}`);
+        cell.setAttribute("rowSpan", blocks.toString());
+        removeNextCells(horas, minutos, first_horario.dia, blocks - 1);
+    }
+    const conflictBlocks = horarios.filter((horario, index, arrayHorario) => arrayHorario.filter((_, i) => i != index).some(horario2 => conflicts(horario, horario2)))
+        .sort((a, b) => compareHours(a.hora, b.hora));
+    const classes = horarios.filter(horario => !conflictBlocks.includes(horario));
+    const conflictBlocksPacked = []; // array of sets
+    conflictBlocks.forEach(horario => {
+        const setIndex = conflictBlocksPacked.findIndex(set => set.some(horario2 => conflicts(horario, horario2)));
+        if (setIndex === -1) {
+            conflictBlocksPacked.push([horario]);
+        }
+        else {
+            conflictBlocksPacked[setIndex].push(horario);
+        }
+    });
+    classes.forEach(horario => newBlock(horario, write));
+    conflictBlocksPacked.forEach(horarios => newConflictBlock(horarios, write));
+    // remove the elements that are not in the limits
+    let max_hour = Math.max(...horarios.map(horario => {
+        const lastMoment = moment(horario.hora, "HH:mm").add(horario.bloques * 15, "minutes");
+        const lastHour = moment(`${lastMoment.hours()}:00`, "HH:mm");
+        const hourInt = parseInt(lastMoment.format("HH"));
+        return lastMoment.isSame(lastHour) ? hourInt - 1 : hourInt;
+    }));
+    let min_hour = Math.min(...horarios.map(horario => parseInt(horario.hora.split(":")[0])));
+    document.querySelectorAll("tbody#horario tr").forEach(hora => {
+        const hora_id = parseInt(hora.id.split("-")[1].split(":")[0]);
+        (hora_id < min_hour || hora_id > max_hour) ? hora.remove() : null;
+    });
+    // if there is no sábado, remove the column
+    if (!horarios.some(horario => horario.dia == "sábado")) {
+        document.querySelectorAll("tbody#horario td").forEach(td => {
+            if (td.id.split("-")[2] == "sábado") {
+                td.remove();
+            }
+        });
+        // remove the header (the last)
+        document.querySelector("#headers").lastElementChild.remove();
+    }
+    // adjust width
+    const ths = document.querySelectorAll("tr#headers th");
+    ths.forEach((th, key) => th.style.width = (key == 0) ? "5%" : `${95 / (ths.length - 1)}%`);
+    // search item animation
+    const menúFlontantes = document.querySelectorAll(".menu-flotante");
+    menúFlontantes.forEach((element) => {
+        element.classList.add("d-none");
+        element.parentElement.addEventListener("mouseover", () => element.classList.remove("d-none"));
+        element.parentElement.addEventListener("mouseout", (e) => element.classList.add("d-none"));
+    });
+    // droppables
+    // forall the .bloque-elements add the event listeners for drag and drop
+    document.querySelectorAll(".bloque-clase").forEach(element => {
+        function dragStart() {
+            this.classList.add("dragging");
+        }
+        function dragEnd() {
+            this.classList.remove("dragging");
+        }
+        element.addEventListener("dragstart", dragStart);
+        element.addEventListener("dragend", dragEnd);
+    });
+    // forall the cells that are not .bloque-clase add the event listeners for drag and drop
+    document.querySelectorAll("td:not(.bloque-clase)").forEach(element => {
+        function dragOver(e) {
+            e.preventDefault();
+            this.classList.add("dragging-over");
+        }
+        function dragLeave() {
+            this.classList.remove("dragging-over");
+        }
+        function drop() {
+            this.classList.remove("dragging-over");
+            const dragging = document.querySelector(".dragging");
+            const id = dragging.getAttribute("data-ids");
+            const hora = this.id.split("-")[1];
+            const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
+            let día = this.id.split("-")[2];
+            día = días.indexOf(día) + 1;
+            //  rowspan
+            const bloques = parseInt(dragging.getAttribute("rowspan"));
+            const horaMoment = moment(hora, "HH:mm");
+            const horaFin = horaMoment.add(bloques * 15, "minutes");
+            const limit = moment('22:00', 'HH:mm');
+            if (horaFin.isAfter(limit)) {
+                triggerMessage("No se puede mover el bloque a esa hora", "Error");
+                // scroll to the top
+                window.scrollTo(0, 0);
+                return;
+            }
+            // get the horario
+            // remove the horario
+            const bloque = document.querySelector(`.bloque-clase[data-ids="${id}"]`);
+            // remove all children
+            while (bloque.firstChild) {
+                bloque.removeChild(bloque.firstChild);
+            }
+            // prepend a loading child
+            const loading = `<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
+                                <span class="sr-only">Loading...</span>
+                            </div>`;
+            bloque.insertAdjacentHTML("afterbegin", loading);
+            // add style vertical-align: middle
+            bloque.style.verticalAlign = "middle";
+            bloque.classList.add("text-center");
+            // remove draggable
+            bloque.removeAttribute("draggable");
+            moveHorario(id, día, hora);
+        }
+        element.addEventListener("dragover", dragOver);
+        element.addEventListener("dragleave", dragLeave);
+        element.addEventListener("drop", drop);
+    });
+}
+const form = document.getElementById('form');
+if (!(form instanceof HTMLFormElement)) {
+    triggerMessage('No se ha encontrado el formulario', 'Error', 'danger');
+    throw new Error("No se ha encontrado el formulario");
+}
+form.querySelector('#clave_profesor').addEventListener('input', function (e) {
+    const input = form.querySelector('#clave_profesor');
+    const option = form.querySelector(`option[value="${input.value}"]`);
+    if (input.value == "") {
+        input.classList.remove("is-invalid", "is-valid");
+        return;
+    }
+    if (!option) {
+        input.classList.remove("is-valid");
+        input.classList.add("is-invalid");
+    }
+    else {
+        const profesor_id = form.querySelector('#profesor_id');
+        profesor_id.value = option.dataset.id;
+        input.classList.remove("is-invalid");
+        input.classList.add("is-valid");
+    }
+});
+form.addEventListener('submit', async function (e) {
+    e.preventDefault();
+    const input = form.querySelector('#clave_profesor');
+    if (input.classList.contains("is-invalid")) {
+        triggerMessage('El profesor no se encuentra registrado', 'Error', 'danger');
+        return;
+    }
+    const formData = new FormData(form);
+    try {
+        const buttons = document.querySelectorAll("button");
+        buttons.forEach(button => {
+            button.disabled = true;
+            button.classList.add("disabled");
+        });
+        const response = await fetch('action/action_horario_profesor.php', {
+            method: 'POST',
+            body: formData,
+        });
+        const data = await response.json();
+        buttons.forEach(button => {
+            button.disabled = false;
+            button.classList.remove("disabled");
+        });
+        if (data.status == 'success') {
+            horarios = data.data;
+            renderHorario();
+        }
+        else {
+            triggerMessage(data.message, 'Error en la consulta', 'warning');
+        }
+    }
+    catch (error) {
+        triggerMessage('Fallo al consutar los datos ', 'Error', 'danger');
+        console.log(error);
+    }
+});
+const input = form.querySelector('#clave_profesor');
+const option = form.querySelector(`option[value="${input.value}"]`);

+ 0 - 20
js/horarios_profesor.js

@@ -1,20 +0,0 @@
-document.getElementById('form').addEventListener('submit', async function (e) {
-    e.preventDefault();
-    const formData = new FormData(this);
-    try {
-        const response = await fetch('action/action_horario_profesor.php', {
-            method: 'POST',
-            body: formData,
-        });
-        const data = await response.json();
-        
-        if (data.status == 'ok') {
-            
-        }
-        else {
-            triggerMessage(data.message, 'Error en la consulta', 'warning');
-        }
-    } catch (error) {
-        triggerMessage('Fallo al consutar los datos ', 'Error', 'danger');
-    }
-});

+ 11 - 0
js/jquery-ui.touch-punch.min.js

@@ -0,0 +1,11 @@
+/*!
+ * jQuery UI Touch Punch 0.2.3
+ *
+ * Copyright 2011–2014, Dave Furfero
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ *
+ * Depends:
+ *  jquery.ui.widget.js
+ *  jquery.ui.mouse.js
+ */
+!function (a) { function f(a, b) { if (!(a.originalEvent.touches.length > 1)) { a.preventDefault(); var c = a.originalEvent.changedTouches[0], d = document.createEvent("MouseEvents"); d.initMouseEvent(b, !0, !0, window, 1, c.screenX, c.screenY, c.clientX, c.clientY, !1, !1, !1, !1, 0, null), a.target.dispatchEvent(d) } } if (a.support.touch = "ontouchend" in document, a.support.touch) { var e, b = a.ui.mouse.prototype, c = b._mouseInit, d = b._mouseDestroy; b._touchStart = function (a) { var b = this; !e && b._mouseCapture(a.originalEvent.changedTouches[0]) && (e = !0, b._touchMoved = !1, f(a, "mouseover"), f(a, "mousemove"), f(a, "mousedown")) }, b._touchMove = function (a) { e && (this._touchMoved = !0, f(a, "mousemove")) }, b._touchEnd = function (a) { e && (f(a, "mouseup"), f(a, "mouseout"), this._touchMoved || f(a, "click"), e = !1) }, b._mouseInit = function () { var b = this; b.element.bind({ touchstart: a.proxy(b, "_touchStart"), touchmove: a.proxy(b, "_touchMove"), touchend: a.proxy(b, "_touchEnd") }), c.call(b) }, b._mouseDestroy = function () { var b = this; b.element.unbind({ touchstart: a.proxy(b, "_touchStart"), touchmove: a.proxy(b, "_touchMove"), touchend: a.proxy(b, "_touchEnd") }), d.call(b) } } }(jQuery);

+ 178 - 0
js/reposiciones.js

@@ -0,0 +1,178 @@
+// Get references to the HTML elements
+const form = document.getElementById('form');
+const steps = Array.from(form.querySelectorAll('.step'));
+const nextButton = document.getElementById('next-button');
+const prevButton = document.getElementById('prev-button');
+let currentStep = 0;
+// #clave_profesor on change => show step 2
+const clave_profesor = document.getElementById('clave_profesor');
+const horario_reponer = document.getElementById('horario_reponer');
+const fechas_clase = document.getElementById('fechas_clase');
+const fecha_reponer = $('#fecha_reponer');
+const hora_reponer = $('#hora_reponer');
+const minutos_reponer = $('#minutos_reponer');
+clave_profesor.addEventListener('change', async () => {
+    const step2 = document.getElementById('step-2');
+    clave_profesor.disabled = true;
+    // get option which value is the same as clave_profesor.value
+    const option = document.querySelector(`option[value="${clave_profesor.value}"]`);
+    // make a form data with #form
+    const profesor_id = document.getElementById('profesor_id');
+    profesor_id.value = option.dataset.id;
+    const formData = new FormData(form);
+    const response = await fetch(`./action/action_horario_profesor.php`, {
+        method: 'POST',
+        body: formData
+    });
+    const data = await response.json();
+    if (data['success'] === false) {
+        const message = "Hubo un error al obtener los horarios del profesor.";
+        const title = 'Error';
+        const color = 'danger';
+        triggerMessage(message, title, color);
+        return;
+    }
+    const horarios = data.data;
+    const initial = document.createElement('option');
+    initial.value = '';
+    initial.textContent = 'Seleccione un horario';
+    initial.selected = true;
+    initial.disabled = true;
+    horario_reponer.innerHTML = '';
+    horario_reponer.appendChild(initial);
+    horarios.forEach((horario) => {
+        const dias = ['Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Sabado', 'Domingo'];
+        const option = document.createElement('option');
+        option.value = `${horario.id}`;
+        // materia máx 25 caracteres, if materia.length > 25 then slice(0, 20)
+        const max = 25;
+        option.textContent = `${horario.materia.slice(0, max) + (horario.materia.length > max ? '...' : '')} - Grupo: ${horario.grupo} - ${horario.hora.slice(0, 5)}-${horario.hora_final.slice(0, 5)} - Salon: ${horario.salon} - ${horario.dia}`;
+        option.dataset.materia = `${horario.materia}`;
+        option.dataset.grupo = `${horario.grupo}`;
+        option.dataset.hora = `${horario.hora.slice(0, 5)}`; // slice(0, 5) => HH:MM
+        option.dataset.hora_final = `${horario.hora_final.slice(0, 5)}`;
+        option.dataset.salon = `${horario.salon}`;
+        option.dataset.dia = `${horario.dia}`;
+        option.dataset.id = `${horario.id}`;
+        horario_reponer.appendChild(option);
+    });
+    currentStep = 1;
+    step2.style.display = 'block';
+    prevButton.disabled = false;
+});
+// disable clave_profesor
+// from second step to first step
+prevButton.addEventListener('click', () => {
+    const inputs = [clave_profesor, horario_reponer, fechas_clase, fecha_reponer, hora_reponer];
+    switch (currentStep) {
+        case 1:
+        case 2:
+        case 3:
+            const step = document.getElementById(`step-${currentStep + 1}`);
+            step.style.display = 'none';
+            inputs[currentStep - 1].disabled = false;
+            inputs[currentStep - 1].value = '';
+            if (--currentStep === 0) {
+                prevButton.disabled = true;
+            }
+            break;
+        case 4:
+            const step5 = document.getElementById('step-5');
+            step5.style.display = 'none';
+            fecha_reponer.prop('disabled', false);
+            fecha_reponer.val('');
+            hora_reponer.parent().removeClass('disabled');
+            hora_reponer.siblings('.datalist-input').text('hh');
+            hora_reponer.val('');
+            minutos_reponer.parent().removeClass('disabled');
+            minutos_reponer.siblings('.datalist-input').text('mm');
+            minutos_reponer.val('');
+            currentStep--;
+            break;
+    }
+    nextButton.disabled = true;
+});
+// #horario_reponer on change => show step 3
+horario_reponer.addEventListener('change', async () => {
+    const selected = horario_reponer.querySelector(`option[value="${horario_reponer.value}"]`);
+    horario_reponer.title = `Materia: ${selected.dataset.materia} - Grupo: ${selected.dataset.grupo} - Horario: ${selected.dataset.hora}-${selected.dataset.hora_final} - Salon: ${selected.dataset.salon} - Día: ${selected.dataset.dia}`;
+    const step3 = document.getElementById('step-3');
+    horario_reponer.disabled = true;
+    // make a form data with #form
+    const response = await fetch(`./action/action_fechas_clase.php?horario_id=${horario_reponer.value}`, {
+        method: 'GET',
+    });
+    const data = await response.json();
+    if (data['success'] === false) {
+        const message = "Hubo un error al obtener las fechas de clase.";
+        const title = 'Error';
+        const color = 'danger';
+        triggerMessage(message, title, color);
+        return;
+    }
+    const meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
+    const fechas = data.data;
+    const initial = document.createElement('option');
+    initial.value = '';
+    initial.textContent = 'Seleccione la fecha de la falta';
+    initial.selected = true;
+    initial.disabled = true;
+    fechas_clase.innerHTML = '';
+    fechas_clase.appendChild(initial);
+    fechas_clase.title = 'Seleccione la fecha de la falta';
+    fechas.forEach((fecha) => {
+        const option = document.createElement('option');
+        option.value = `${fecha}`;
+        option.textContent = `${fecha.dia_mes} de ${meses[fecha.month - 1]} de ${fecha.year}`;
+        fechas_clase.appendChild(option);
+    });
+    step3.style.display = 'block';
+    currentStep = 2;
+});
+// #fechas_clase on change => show step 4
+fechas_clase.addEventListener('change', () => {
+    const step4 = document.getElementById('step-4');
+    step4.style.display = 'block';
+    fechas_clase.disabled = true;
+    currentStep = 3;
+});
+// when both #fecha_reponer and #hora_reponer are selected => show step 5
+const lastStep = () => {
+    // timeout to wait for the value to be set 
+    setTimeout(() => {
+        if (fecha_reponer.val() !== '' && hora_reponer.val() !== '' && minutos_reponer.val() !== '') {
+            const step5 = document.getElementById('step-5');
+            step5.style.display = 'block';
+            // disable both
+            fecha_reponer.prop('disabled', true);
+            hora_reponer.parent().addClass('disabled');
+            minutos_reponer.parent().addClass('disabled');
+            const nextButton = document.getElementById('next-button');
+            // remove property disabled
+            nextButton.removeAttribute('disabled');
+            currentStep = 4;
+        }
+    }, 100);
+};
+fecha_reponer.on('change', lastStep);
+// on click on the sibling ul>li of #hora_reponer and #minutos_reponer
+hora_reponer.siblings('ul').children('li').on('click', lastStep);
+minutos_reponer.siblings('ul').children('li').on('click', lastStep);
+// Initialize the form
+hideSteps();
+showCurrentStep();
+function hideSteps() {
+    steps.forEach((step) => {
+        step.style.display = 'none';
+    });
+}
+function showCurrentStep() {
+    steps[currentStep].style.display = 'block';
+    prevButton.disabled = currentStep === 0;
+}
+function handleSubmit(event) {
+    event.preventDefault();
+    // Handle form submission
+    // You can access the form data using the FormData API or serialize it manually
+}
+export {};

+ 66 - 6
main.php

@@ -1,6 +1,6 @@
 <?php
 require_once 'class/c_login.php';
-if (!isset($_SESSION['user'])) 
+if (!isset($_SESSION['user']))
     die(header('Location: index.php'));
 
 $user = unserialize($_SESSION['user']);
@@ -11,7 +11,12 @@ $user = unserialize($_SESSION['user']);
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Editar Horarios | <?php echo $user->facultad['facultad'] ?? "Administrador"; ?></title>
+    <title>Editar Horarios |
+        <?= $user->facultad['facultad'] ?? "Administrador"; ?>
+    </title>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
+        integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
+        crossorigin="anonymous" referrerpolicy="no-referrer" />
     <?php
     include 'import/html_css_files.php';
     ?>
@@ -28,10 +33,65 @@ $user = unserialize($_SESSION['user']);
     <main class="content marco">
         <section id="message"></section>
         <h3 class="text-center mt-3">
-            <b><?= $user->user['nombre']; ?></b> | <?= $user->facultad['facultad'] ?? "General"; ?> | <?= $user->rol['rol']; ?>
+            <b>
+                <?= $user->user['nombre']; ?>
+            </b>
+            <i>
+                <?= $user->facultad['facultad'] ?? "General"; ?>
+            </i>
+            <small>
+                <?= $user->rol['rol']; ?>
+            </small>
         </h3>
         <hr>
-        
+        <div class="d-flex justify-content-center align-items-center flex-wrap">
+            <?php
+            $has_token = $db->querySingle("SELECT FALSE") or die($db->getLastError());
+            if (array_pop($has_token)) {
+                ?>
+                <div class="movie card col-10 col-md-3 border-primary border-3 m-2">
+                    <div class="card-body text-center bg-light">
+                        <a href="http://200.13.89.27/checador_otros/main"
+                            class="card-link text-decoration-none text-primary d-flex flex-column align-items-center">
+                            <i class="fa fa-clock" aria-hidden="true"></i>
+                            <h5 class="card-title mt-2">Checador</h5>
+                        </a>
+                    </div>
+                </div>
+                <?php
+            } else {
+                ?>
+                <div class="card col-10 col-md-3 border-primary border-3 m-2 disabled bg-dark">
+                    <div class="card-body text-center disabled">
+                        <a href="#"
+                            class="card-link text-decoration-none text-primary d-flex flex-column align-items-center disabled text-danger">
+                            <i class="ing-cancelar" aria-hidden="true"></i>
+                            <h5 class="card-title mt-2">Checador</h5>
+                        </a>
+                    </div>
+                </div>
+                <?php
+            }
+            ?>
+            <div class="movie card col-10 col-md-3 border-primary border-3 m-2">
+                <div class="card-body text-center bg-light">
+                    <a href="#"
+                        class="card-link text-decoration-none text-primary d-flex flex-column align-items-center">
+                        <i class="fa fa-calendar" aria-hidden="true"></i>
+                        <h5 class="card-title mt-2">Mis horarios</h5>
+                    </a>
+                </div>
+            </div>
+            <div class="movie card col-10 col-md-3 border-primary border-3 m-2">
+                <div class="card-body text-center bg-light">
+                    <a href="#"
+                        class="card-link text-decoration-none text-primary d-flex flex-column align-items-center">
+                        <i class="fa fa-table" aria-hidden="true"></i>
+                        <h5 class="card-title mt-2">Mis asistencias</h5>
+                    </a>
+                </div>
+            </div>
+        </div>
     </main>
     <?php
     include "import/html_footer.php";
@@ -40,9 +100,9 @@ $user = unserialize($_SESSION['user']);
     <script src="js/bootstrap/bootstrap.min.js"></script>
     <?php include_once 'js/messages.php'; ?>
     <script>
-        $(document).ready(function() {
+        $(document).ready(function () {
             // constantly update the date and time
-            setInterval(function() {
+            setInterval(function () {
                 $('.fecha_hora').html(new Date().toLocaleString());
             }, 1000);
 

+ 212 - 10
package-lock.json

@@ -1,19 +1,26 @@
 {
-  "name": "admin_checador",
+  "name": "admin_checador (pruebas)",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
+      "dependencies": {
+        "@popperjs/core": "^2.11.7",
+        "axios": "^1.4.0",
+        "es6-promise": "^4.2.8",
+        "moment": "^2.29.4",
+        "petite-vue": "^0.4.1"
+      },
       "devDependencies": {
         "@types/bootstrap": "^5.2.6",
-        "@types/jquery": "^3.5.14"
+        "@types/jquery": "^3.5.14",
+        "@types/node": "^20.2.1"
       }
     },
     "node_modules/@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
-      "dev": true,
+      "version": "2.11.7",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
+      "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/popperjs"
@@ -37,19 +44,132 @@
         "@types/sizzle": "*"
       }
     },
+    "node_modules/@types/node": {
+      "version": "20.2.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.1.tgz",
+      "integrity": "sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==",
+      "dev": true
+    },
     "node_modules/@types/sizzle": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
       "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
       "dev": true
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/axios": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+      "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+      "dependencies": {
+        "follow-redirects": "^1.15.0",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/es6-promise": {
+      "version": "4.2.8",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/moment": {
+      "version": "2.29.4",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+      "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/petite-vue": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/petite-vue/-/petite-vue-0.4.1.tgz",
+      "integrity": "sha512-/gtYKQe9r1OV4IEwn2RsPXAHgFTe1nVq4QhldAP6/l8DSe9I754K6Oe1+Ff6dbnT5P8X2XP7PTUZkGRz5uFnFQ=="
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
     }
   },
   "dependencies": {
     "@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
-      "dev": true
+      "version": "2.11.7",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
+      "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw=="
     },
     "@types/bootstrap": {
       "version": "5.2.6",
@@ -69,11 +189,93 @@
         "@types/sizzle": "*"
       }
     },
+    "@types/node": {
+      "version": "20.2.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.1.tgz",
+      "integrity": "sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==",
+      "dev": true
+    },
     "@types/sizzle": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
       "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
       "dev": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "axios": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+      "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+      "requires": {
+        "follow-redirects": "^1.15.0",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+    },
+    "es6-promise": {
+      "version": "4.2.8",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
+    },
+    "follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+    },
+    "form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+    },
+    "mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
+    "moment": {
+      "version": "2.29.4",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+      "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
+    },
+    "petite-vue": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/petite-vue/-/petite-vue-0.4.1.tgz",
+      "integrity": "sha512-/gtYKQe9r1OV4IEwn2RsPXAHgFTe1nVq4QhldAP6/l8DSe9I754K6Oe1+Ff6dbnT5P8X2XP7PTUZkGRz5uFnFQ=="
+    },
+    "proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
     }
   }
 }

+ 9 - 1
package.json

@@ -1,6 +1,14 @@
 {
   "devDependencies": {
     "@types/bootstrap": "^5.2.6",
-    "@types/jquery": "^3.5.14"
+    "@types/jquery": "^3.5.14",
+    "@types/node": "^20.2.1"
+  },
+  "dependencies": {
+    "@popperjs/core": "^2.11.7",
+    "axios": "^1.4.0",
+    "es6-promise": "^4.2.8",
+    "moment": "^2.29.4",
+    "petite-vue": "^0.4.1"
   }
 }

+ 284 - 0
reposiciones.php

@@ -0,0 +1,284 @@
+<?php
+
+require_once 'class/c_login.php';
+if (!isset($_SESSION['user']))
+    die(header('Location: index.php'));
+
+$user = unserialize($_SESSION['user']);
+$user->access();
+if (!$user->admin && in_array($user->acceso, ['n']))
+    die(header('Location: main.php?error=1'));
+
+$user->print_to_log('Consultar horario');
+
+$write = $user->admin || in_array($user->acceso, ['w']);
+
+$en_fecha = $db->querySingle("SELECT ESTA_EN_PERIODO(NOW()::DATE, :periodo_id)", [':periodo_id' => $user->periodo])['esta_en_periodo'];
+$periodo_fin = $db->querySingle("SELECT periodo_fecha_fin FROM periodo WHERE periodo_id = :periodo_id", [':periodo_id' => $user->periodo])['periodo_fecha_fin'];
+?>
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <title>Consultar horario | <?= $user->facultad['facultad'] ?? 'General' ?></title>
+
+    <meta charset="utf-8">
+    <meta http-equiv="content-type" content="text/plain; charset=UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <?php include_once "import/html_css_files.php"; ?>
+    <link rel="stylesheet" href="css/jquery-ui.css">
+    <link rel="stylesheet" href="css/richtext.css" type="text/css">
+    <link rel="stylesheet" href="css/clockpicker.css">
+    <link rel="stylesheet" href="css/calendar.css">
+    <link rel="stylesheet" href="css/fa_all.css" type="text/css">
+
+    <script src="js/scrollables.js" defer></script>
+    <script>
+        const write = <?= $write ? 'true' : 'false' ?>;
+    </script>
+    <script src="js/moment.js" defer></script>
+
+</head>
+<!--  -->
+
+<body style="display: block;">
+    <?php
+    include('include/constantes.php');
+    include("import/html_header.php");
+    html_header("Consultar horario", "Sistema de gestión de checador");
+    ?>
+    <?= "<!-- $user -->" ?>
+    <main class="container content marco content-margin" id="local-app">
+        <section id="message"></section>
+        <?php require('import/periodo.php') ?>
+
+        <!-- Botón para abrir el modal -->
+        <span class="d-inline-block" tabindex="0" data-toggle="tooltip" <?php if (!$en_fecha) : ?> title="No se puede crear una reposición fuera de la fecha de reposición" <?php endif; ?>>
+            <button type="button" class="btn btn-primary" data-toggle="modal" <?php if ($en_fecha) : ?>data-target="#crearReposición" <?php else : ?>disabled style="pointer-events: none;" <?php endif; ?>>
+                Crear Reposición
+            </button>
+        </span>
+
+
+        <!-- Modal del formulario -->
+        <div class="modal fade" id="crearReposición" tabindex="-1" role="dialog" aria-labelledby="crearReposiciónLabel" aria-hidden="true">
+            <div class="modal-dialog modal-xl" role="document">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title" id="crearReposiciónLabel">Crear Reposición</h5>
+                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <form id="form" class="form-horizontal">
+                            <div class="form-group step" id="step-1">
+                                <div class="form-box">
+                                    <div class="form-group row">
+                                        <label for="clave_profesor" class="col-4 col-form-label">Profesor</label>
+                                        <div class="col-8">
+                                            <input list="lista_profesores" name="clave_profesor" id="clave_profesor" class="form-control" placeholder="Profesor" required="required">
+                                            <div class="valid-feedback">
+                                                Profesor encontrado
+                                            </div>
+                                            <div class="invalid-feedback">
+                                                Profesor no encontrado
+                                            </div>
+                                            <datalist id="lista_profesores">
+
+                                                <?php
+                                                $profesores = $db->query('SELECT * FROM fs_profesor A WHERE facultad_id = :facultad_id AND EXISTS (
+                                                    SELECT * FROM horario join HORARIO_PROFESOR ON horario.HORARIO_ID = HORARIO_PROFESOR.horario_id WHERE HORARIO_PROFESOR.profesor_id = A.id AND HORARIO.periodo_id = :periodo_id
+                                                ) ORDER BY grado, nombre', [':facultad_id' => $user->facultad['facultad_id'], ':periodo_id' => $user->periodo]);
+
+                                                foreach ($profesores as $profesor) {
+                                                    extract($profesor);
+                                                ?>
+                                                    <option data-grado="<?= $grado ?>" data-clave="<?= $clave ?>" data-profesor="<?= $profesor ?>" data-id="<?= $id; ?>" value="<?= "$clave | $grado $profesor" ?>"></option>
+                                                <?php
+                                                }
+                                                ?>
+                                            </datalist>
+                                            <ul class="list-group" id="profesores"></ul>
+                                            <input type="hidden" id="periodo_id" name="periodo_id" value="<?= $user->periodo ?>">
+                                            <input type="hidden" id="profesor_id" name="profesor_id" value="">
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-group step" id="step-2">
+                                <div class="form-box">
+                                    <div class="form-group row">
+                                        <label for="horario_reponer" class="col-4 col-form-label">Horario a reponer</label>
+                                        <div class="col-8">
+                                            <select name="horario_reponer" id="horario_reponer" class="form-control" required="required">
+                                            </select>
+                                        </div>
+                                    </div>
+                                </div>
+                                <input type="hidden" name="horario_id" id="horario_id">
+                            </div>
+                            <div class="form-group step" id="step-3">
+                                <div class="form-box">
+                                    <div class="form-group row">
+                                        <label for="fechas_clase" class="col-4 col-form-label">Fecha de clase</label>
+                                        <div class="col-8">
+                                            <select name="fechas_clase" id="fechas_clase" class="form-control" required="required">
+                                            </select>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-group step" id="step-4">
+                                <div class="form-box">
+                                    <div class="form-group row">
+                                        <label for="fecha_reponer" class="col-4 col-form-label">Fecha de reposición</label>
+                                        <div class="col-6">
+                                            <input type="text" placeholder="dd/mm/aaaa" name="fecha_reponer" id="fecha_reponer" class="form-control date-picker" required="required">
+                                        </div>
+                                    </div>
+                                    <div class="form-group row">
+                                        <label for="hora" class="col-4 col-form-label">Hora</label>
+                                        <div class="col-3">
+                                            <div id="hora" class="datalist datalist-select mb-1">
+                                                <div class="datalist-input text-center">hh</div>
+                                                <span class="ing-buscar icono"></span>
+                                                <ul style="display:none">
+                                                    <?php foreach (range(7, 21) as $hora) { ?>
+                                                        <li data-id='<?= $hora ?>'><?= str_pad($hora, 2, "0", STR_PAD_LEFT) ?></li>
+                                                    <?php } ?>
+                                                </ul>
+                                                <input type="hidden" id="hora_reponer" name="horas" value="">
+                                            </div>
+                                        </div>
+                                        <div class="col-3">
+                                            <div id="minutos" class="datalist datalist-select mb-1">
+                                                <div class="datalist-input text-center">mm</div>
+                                                <span class="ing-buscar icono"></span>
+                                                <ul style="display:none">
+                                                    <?php foreach (range(0, 45, 15) as $minuto) { ?>
+                                                        <li data-id='<?= $minuto ?>'><?= str_pad($minuto, 2, "0", STR_PAD_LEFT) ?></li>
+                                                    <?php } ?>
+                                                </ul>
+                                                <input type="hidden" id="minutos_reponer" name="minutos" value="">
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-group step" id="step-5">
+                                <div class="form-box">
+                                    <div class="form-group row">
+                                        <label for="descripcion_reposicion" class="col-4 col-form-label">Comentarios</label>
+                                        <div class="col-6">
+                                            <textarea name="descripcion_reposicion" id="descripcion_reposicion" rows="4" required="required" placeholder="Se requiere proyector, etc." maxlength="255" class="form-control"></textarea>
+                                        </div>
+                                    </div>
+                                    <div class="form-group row align-items-center">
+                                        <label class="col-4 col-form-label" for="sala">¿En sala de cómputo?</label>
+                                        <div class="col-6">
+                                            <div class="custom-control custom-switch">
+                                                <input type="checkbox" class="custom-control-input" id="sala">
+                                                <label class="custom-control-label" for="sala"></label>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </form>
+                    </div>
+                    <div class="modal-footer justify-content-center">
+                        <button class="btn btn-secondary" type="button" id="prev-button">Anterior</button>
+                        <button class="btn btn-secondary" type="button" id="next-button" disabled data-toggle="modal" data-target="#confirmationModal">Proponer reposición</button>
+                    </div>
+
+                    <!-- Modal confirmación -->
+                    <div class="modal fade" id="confirmationModal" tabindex="-1" role="dialog" aria-labelledby="confirmationModalLabel" aria-hidden="true">
+                        <div class="modal-dialog modal-sm" role="document">
+                            <div class="modal-content">
+                                <div class="modal-header">
+                                    <h5 class="modal-title" id="confirmationModalLabel">Confirmación</h5>
+                                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                                        <span aria-hidden="true">&times;</span>
+                                    </button>
+                                </div>
+                                <div class="modal-body">
+                                    <p>¿Estás seguro de que deseas proponer la reposición?</p>
+                                    <small>Recuerda que la aprobará tu jefe de carrera.</small>
+                                </div>
+                                <div class="modal-footer">
+                                    <button type="button" class="btn btn-info" onclick="$('#confirmationModal').modal('hide');">Cancelar</button>
+                                    <button type="button" class="btn btn-primary" data-dismiss="modal">Aceptar</button>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </main>
+</body>
+<script src="js/jquery.min.js"></script>
+<script src="js/bootstrap/popper.min.js"></script>
+<script src="js/bootstrap/bootstrap.min.js"></script>
+<script src="js/richtext.js"></script>
+<script src="js/clockpicker.js"></script>
+<script src="js/jquery-ui.js"></script>
+<script src="js/datepicker-es.js"></script>
+<script>
+    $(document).ready(function() {
+        $('.richtext').richText({
+            fontList: ['indivisa-text', 'Arial'],
+            imageUpload: true,
+            placeholder: 'Escribe aquí la información de la reposición: necesito un proyector, etc.',
+        });
+    });
+    $(".date-picker").datepicker($.datepicker.regional.es);
+    $(".date-picker").datepicker({
+        dateFormat: "dd/mm/yyyy",
+        changeMonth: true,
+        beforeShowDay: function(date) {
+            // Disable Sundays (0 represents Sunday)
+            return [date.getDay() != 0, ''];
+            // Disable 2020-05-01
+        }
+    });
+    // the minimum is today + 3 laboral days
+    function getNextWorkingDay(date) {
+        const day = date.getDay(); // Get the day of the week (0-6, where 0 is Sunday)
+
+        // Check if it's Saturday (6), if so, add 2 days
+        if (day === 6) {
+            date.setDate(date.getDate() + 2);
+        }
+        // Add 1 day to skip to the next day
+        date.setDate(date.getDate() + 1);
+
+        // Check if it's a Sunday (0), if so, add 1 day
+        if (date.getDay() === 0) {
+            date.setDate(date.getDate() + 1);
+        }
+
+        // Add laboral days
+        let laboralDaysCount = 1; // Start with 1 to account for the current day
+        while (laboralDaysCount < 3) {
+            date.setDate(date.getDate() + 1); // Add a day
+            if (date.getDay() !== 6) { // Skip Saturdays
+                laboralDaysCount++;
+            }
+        }
+
+        return date;
+    }
+    $(".date-picker").datepicker("option", "minDate", getNextWorkingDay(new Date()));
+    // the maximum is periodo_fin
+    $(".date-picker").datepicker("option", "maxDate", new Date("<?= $periodo_fin ?>T03:24:00"));
+
+    $(function() {
+        $('[data-toggle="tooltip"]').tooltip()
+    })
+</script>
+<script src="js/messages.js"></script>
+<script type="module" src="js/reposiciones.js"></script>
+
+</html>

+ 0 - 136
route.php

@@ -1,136 +0,0 @@
-<?php
-//@NabiKAZ
-//https://gist.github.com/NabiKAZ/91f716faa89aab747317fe09db694be8
-//For show advanced list of files and directories with sort, date, size, icon type,...,
-//Save bellow content code as route.php file, and then run this command:
-//   php -S 0.0.0.0:8080 -t . route.php
-//And then open http://localhost:8080/ in the browser.
-//////////////////////////////////////////////////////////////////
-// This block MUST be at the very top of the page!
-@ob_start('ob_gzhandler');
-if(isset($_GET['icon']))
-{
-    $e=$_GET['icon'];
-    $I['file']='R0lGODlhEAAPAOYAAIyMlu7u9PHx9vDw9fT0+PPz97u7vvf3+vb2+d/f4vn5+/39/vv7/Pr6+/b29+3t7pCRnI6PmZOVn5ibpZWYopqeqJ2hq6KnsaClr9fZ3ff4+t/g4qSqtKmwuqeuuM3P0vHz9tze4be6vuzv8+vu8urt8eXo7Kuzva61vquyu9/k6uXp7uTo7cvU3dHZ4dDY4Nfe5d3j6dzi6Nvh5+Po7eLn7OHm69HV2ejs8Ofr7+7x9OHk597h5PT2+PP19/Hz9evt7+Lk5t3f4fr7/Pn6+/b3+PX299Tc49rh5+ru8fDz9ff5+vb4+fP19vz9/f////7+/vv7+/Pz8+/v7+zs7Orq6ubm5uHh4d7e3sDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAFkALAAAAAAQAA8AAAevgFmCJ4SFhDuCiYIdUI1QDQ9NKCGKgh4LjQsOG0smKRmVHAxPUAtGQktLPxxTihcKjU5FQR8iBhdXihgHC05DTEA8NwkYWIoWCENEGj1KJCsIFsaJFQRMPT5KIzk2BBXTghMFIEo6JDk1MQUT4FkUAiVJOCs2MTACFO0SAyw0NiozYLgYIKEdhAAyZiCBceRFiwAQ2kUIQLFixQjtAGjcyBFAuyhSqFgZSXJklSyBAAA7';
-    $I['dir']='R0lGODlhEAAOAOYAAP79uv//4/j43f//5f7+5v7+6f//7f//7//8qf/9vf/9wf/+xP/9yf793P/+4f32hP/6iv/6pP/9zP/4kf/4nP/4n/373//1jf/2nP32tvnodfzuff/xhv/3uP/revftrvz32v/ocf/odP/rhPnz0/z44+DAPOHAPe/gnPXrvv3zyM6hAMicAMeaANStJdu4PNy7RODDWN7EaOLIauLIa//10ffuzcudAMiaAMSWAMOVAMGTAL2PALqMAMilLtayPNe2TNW0TNKxTNm7V9e5V82vU9zEd+7jvurfvPXryfbu072NALiKALaIALOEALKDALGCAK+AAMinRMalRMOiRNCuTM6sTMuqTMmnTLyfUdm8ZtW9et3Kl+XUoa19AKp6AK6EG7GHIr2YNr+cQ7iXQ8akTMSjTb6jXs2waMOqbNjEkNDDpM2xctfQwPz8/Pv7+/r6+vX19fHx8ezs7Ovr6+jo6Ofn5+bm5uHh4eDg4N/f397e3t3d3cDAwAAAAAAAACH5BAEAAH0ALAAAAAAQAA4AAAe4gEkzMkNERl19iYqKMSkGjwZbRVJTVGhqijACEJwQEpCPWXKJOBYPp6ioFjh2bn06IBuys7MgOnpwfTwkGiY/QEFCVVZXWGVmYl9MNic0BwHQ0QMOBUhgT0cuJQoJ3d4LAARja15aKAwI6QgRFRQYGVxke1AvDRMXHB4iIfwjSmFt6iz50KGGwYMGVbBJgyeODxYrbrTIsYNHjyZOopzhM8fVHT17QooUmYdOrj5v4KhcyRKOqz6BAAA7';
-    $I['doc']='R0lGODlhEAAQAPcAAAEyeCg+bQgviwU2ggg8iAZCmwlLsiFMmjpamDJbtipitzhhrjppuE1qp0BmtERquVVtpF11q2d+s0JuxEl0zFJ3ylV7zl99w1h+0XeKnG6Ov3KQv3KRv3aTvXqVu3uVvH6XulWAyFmBxliCxV2ExF6ExGCBzWGIw2KJw2WKwmeLwWmMwWyOwGeK1XeR1XyX2P8A/4KavIWdvoOc2oCe5oigwIuiwoyiwouk3ZGnxpesyZCu1p2xzYml6ZOr5qO20am71K260K+836q+8a/A2LPD2rfI9MnS4tbc6tLi+tTj+tbk+tfl+9zi9Nnm+9vn+9zo+97p++Dq/OHs/OPt/OXu/Obv/Ojw/erx/evy/e3z/e/0/fDy+vD2/vL3/vT4/vX5/vf6/vn7/////wCpEQAAABLs7NS5srGlQNcVPRQCgBQCQBLtDNdNrxQCgGQCeNdN4xQCgBLtFAAAAJEFyCNr8BLt4JEFURQHqJEFbRLuOAAAABLtPAAAAJEFyFiHuBLuCJEFURQHSBLtWAAAAJEFyFiHuBLuJJEFURQHSJEFbRLuaAAABAAAAOaERAAAAgAABAAAMAAAACNr+NSLsf3QAAAAMAAABBQAABLrmJD7bAAAIAAAAFiHwBLuOAAAAAAAIADwqgAAIAAAAAAAAJDnvJDVhhLuCJD7bJD7cZDVhpDnvBQAABLt5JDnyBLujJDuGJD7eAH//wAABBLtaAAAABLujJDuGJEFcP///5EFbZEJvBQAAAAAAFiHwBLuSJEJkliHwAAAABLunN3tDt3tIGKmyAABxGKm1AAAAAAAAAAAAAAAAAAAABLuaBLu7BS3YBS3YBLuoOb8I8OlLsYaoBLu2MLCzQAABMLC4xS04BS3YAAAAxSwbsXS4BSwABLu1BLupP///xLvQMNclMEgcP///8LC40SV1RS3YGMboGMboEUEtRQAABS04IoASAAAAAAAAOqG1OqG1OqG1OqG1AAC8BLvJN1sdBLvLIoASIoASObgowAACeaCsAAABCH5BAEAADAALAAAAAAQABAAAAjhAGEILALkBw8dOWzIAAFCoEMYRMSEAfPFS5ctIMY0hOHDRw8aL1pgqDBBgZaMGjmOWclypYEsKDX2GDLDBBITTSDgMICFoU8aTWZcaPKgSYMMBq5YqUJlCggXY1w8EHIAB4IjBZY2lQKixRgJDyIMSBBgTIEqO3ZIieLBwhgICIwMGBBkDIGtUaB0oDBGwAIuAxysHDAlLZQnGxi0bAlg7WEnLBQYmFygMoEBAKKkdcJEhcMbWqc4fsJ5CQqHNZimXZ12iZISDmXgfczEdRIRDmN8+NCBg4YVKU6QGBEiREAAADs=';
-    $I['xls']='R0lGODlhEAAQAPcAADVJGjRNGTVNGDRSFzRTFzRYFTReEzReFDVeGjNpDzNtDjRtDjRjETRkEjZjGztoHjpvHjNwDTlwHjp3GTx3Hz57HjJoKDtxIjp9Jz99IWB+XEKAJUaELEeFLUKJNUeIO02MNk+OOUiSP1OTQFKXSFiYR1qaSVieUl6YVV+hU1uiWHCbbWGiVGGgWGWnW2eqX2SoYGmtYmqsZW2wZ3SlcG6Ov3KQv3KRv3aTvXqVu3uVvH6XulWAyFmBxliCxV2ExF6ExGGIw2KJw2WKwmeLwWmMwWyOwP8A/4KavIWdvoephZC9i5ywm6Gzn4igwIuiwoyiwpGnxpesyZCu1p2xzaO20am71JPCjpPEjZbEkZrGlq/A2LPD2sDRwMzay8/dz9bi1t/p39Li+tTj+tbk+tfl+9nm+9vn+9zo+97p++Xs5eDq/OHs/OPt/OXu/Obv/Ojw/erx/evy/e3z/e/0/fD2/vL3/vT4/vX5/vf6/vn7/////xLtPAAAAJEFyCLVGBLuCJEFURQHSBLtWAAAAJEFyCLVGBLuJJEFURQHSJEFbRLuaAAABAAAAOaERAAAAgAABAAAMAAAAFeQiNSLsf3QAAAAMAAABBQAABLrmJD7bAAAIAAAACLVIBLuOAAAAAAAIADwqgAAIAAAAAAAAJDnvJDVhhLuCJD7bJD7cZDVhpDnvBQAABLt5JDnyBLujJDuGJD7eAH//wAABBLtaAAAABLujJDuGJEFcP///5EFbZEJvBQAAAAAACLVIBLuSJEJkiLVIAAAABLunN3tDt3tIGKmyAABwGKm1AAAAAAAAAAAAAAAAAAAABLuaBLu7BSuABSuABLuoOb8I8OlLsYaoBLu2MLCzQAABMLC4xSsQBSuAAAAAxSg2MXS4BSgABLu1BLupP///xLvQMNclMEgcP///8LC40SV1RSuAGMboGMboEUEtRQAABSsQKR+UAAAAAAAAOqG1OqG1OqG1OqG1AACBBLvJN1sdBLvLKR+UKR+UObgowAACeaCsAAABCH5BAEAAEcALAAAAAAQABAAAAjhAI8I5GKlChUpUZ4k2bFDoMMjW/TkwXPHTh06O/Y0PDIjhosUJkaA6LChwpyMGjnuWclyZQQ5KDXOuAKDxB4vKFAwURCHoc8XarDI8ECjxQcwCeC8cdOGzQ4We75oUYHBQpc9DJY2XbOjxJ4lJ7KouLAizAE3U6asSZMjxBIKEBwsEfGgSYGtadDg4NBSiQQEGgawSYvmjI0MLVsKWFvYjJEJERYkaGCgAIEAANKkNVOGiEMoWtkwPsOZjBCHTpimXZ2WzBggDpPgbVzGtZgeDpHo0IHjRo0iQ4L88MGDR0AAADs=';
-    $I['jpg']=$I['gif']=$I['png']='R0lGODlhEAAQAPcAAPuBhP0RI9fU1r24vL25vn+CmKSxzLfF4cPL28bO3srO1oOk4WF4opyuzpWlwpurx6i30qm30ae1zqa0zb3M57G/2Kq3z6m2zcTR6aSvw9Dd9crW7NPe8tXg88PM3OLs/tXe7+Lr/Njh8eHp98/W4wBe9Yibup+vyZemv6e2z6e2zsXW8rLC26e1zK+91Kezx8jU6Nvo/eDr/dzm9uLs/OTt/MvT4ODn897l8dvi7uvy/gxn7Keyw7K9zsTQ4uPu/t7p+dzm9c/Y5t7n9dri7svb8djh7ejw++vw9+fs87jI2+Hu/lem/vH3/lSp/uTx/vP5/snj9+33/qGoqPz+/v3+/lXSYAC1AH3GdS6qHnDIW1OmL+Hp173JqoGaKby9srurRv3slf7dbf7cc/7Xb/7QZ/3SdP7FVd6wUP7LaP6/SP68SeS7cv6vMP62QNycN+KuWOCuW/vt1fueGf6qL/6vN/6xOf65V+C/jP2XEv6eGvmeH/6gI/6iJOCSKM+WR9ScUPjEff6ZG8qELbB/ROG0f/2EAPeAAf6IAuR5BP6JCP6QENKTTdmseuC7kbBhEKlfFa5iGMR0I8d+M45aJr6BRM2VW9ikcNuugNG8pvLk1v37+f17AO1zAKlUBrNiFqBdHplZHr97PcB/Q690Pc6KTcWHT7F9T9CYY9Opg9KujN25l6NJAJJFBK5sMpNnQcqcdKaGbL+fhPfw6qpKALeDWtijebGReOC4mdm2nKuQe+XDqfjy7tm0nedXBa+ZjPjz8OfOwauSh/BvO/55QcY0AN7a2d4dA+kwFdsTBv7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAMsALAAAAAAQABAAAAj/AJcJXIbDSI6DREDAQKBs4MAgVZRJVNaEwwYbDgUaozJRGZQON46QcLgplaNLmFZpoiLDh5AKHgTuKmVJlzBVqEbBSlIEQgMTy2wxkhOFiZMFXLoQOiWCwgEHuCQ1KvRlRwkGWqxgERVrBYYHkwAF68VmSgE8Wa5sCRQqgYYTg8LEgWOGTJk0YLz8uQOJQAwDptCMEXNmjZs6dIgBGOZpwJIJs3JVeqPGDp0+c5AFSNZKwQ8JAnnV8tOGj6A8vo4VozTjg4qBwFzt0bNIESJDrH49oZHCoaxHiQ5x6kTr1QggIXoPFJCJVKRPoG4hkVJDRwSHQ5T04JHhBQsXF1pYA0AREAA7';
-    $I['txt']='R0lGODlhEAAQAPcAAB6Kcm6Ov3KQv3KRv3aTvXqVu3uVvH6XulWAyFmBxliCxV2ExF6ExGGIw2KJw2WKwmeLwWmMwWyOwP8A/4KavIWdvoigwIuiwoyiwo+lxJGnxpKoxpWryJesyZmtypCu1p2xzaO20am71K/A2LPD2tLi+tTj+tbk+tfl+9nm+9vn+9zo+97p++Dq/OHs/OPt/OXu/Obv/Ojw/erx/evy/e3z/e/0/fD2/vL3/vT4/vX5/vf6/vn7/////xLuYAAAQAAAAAAAABLuqBLuaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBLurJDuGAAAAAAAAgAAAQACEBLuYAACEJEZcAa6SAa6KAAAAAAAAAHskAABHhLsBJDuGBLswNS5TACpEQCpERLs1NS47q0nqACpEQAAABLs7NS5sq0nqNcVPRQCgBQCQBLtDNdNrxQCgCoFBtdN4xQCgBLtFAAAAJEFyCLqgBLt4JEFURQHqJEFbRLuOAAAABLtPAAAAJEFyAcKIBLuCJEFURQHSBLtWAAAAJEFyAcKIBLuJJEFURQHSJEFbRLuaAAABAAAAOaERAAAAgAABAAAMAAAACLqiNSLsf3wAAAAMAAABBQAABLrmJD7bAAAIAAAAAcKKBLuOAAAAAAAIADwqgAAIAAAAAAAAJDnvJDVhhLuCJD7bJD7cZDVhpDnvBQAABLt5JDnyBLujJDuGJD7eAH//wAABBLtaAAAABLujJDuGJEFcP///5EFbZEJvBQAAAAAAAcKKBLuSJEJkgcKKAAAABLunN3tDt3tIGKmyAABsGKm1AAAAAAAAAAAAAAAAAAAABLuaBLu7BR5EBR5EBLuoOb8I8OlLsYaoBLu2MLCzQAABMLC4xR2sBR5EAAAAxRwicXS4BRwABLu1BLupP///xLvQMNclMEgcP///8LC40SV1RR5EGMboGMboEUEtRQAABR2sIPdOAAAAAAAAOqG1OqG1OqG1OqG1AABsBLvJN1sdBLvLIPdOIPdOObgowAACeaCsAAABCH5BAEAABMALAAAAAAQABAAAAipACcIJCEiBIgOGi5UOHBAoMMJI3js0JEDxw0bB3o0nACgo8ePHXto5CiyBwCTKEuKPBCypcmTABgybEkTJowXLljChPnSJM4WOkGCbNGCRQGHHmrQmCEjxk0XRVcQcMhh6YerWD+sUCHA4QamTn+y2JpCgsMMTbNiTYECgkMMYaGOVcH2hAOHFm6qvXrCBAOHFcSSRdG3RAKHFAwYIDAgQIQHDRYoQIAgIAA7';
-    $I['avi']=$I['mpg']=$I['mpeg']=$I['mp3']='R0lGODlhEAAQAPcAAEhHSHd2d//+/+/q9+7r9KalqPLx9NrZ3HZ1e4mJjx0dHoeHi+Dg4/n5++np6+jo6tLS1NjY2ZqamxYelholkUBHhIWGjHB2lREwshozpCdDujZPuoOEiIWNqBI7tJWWmdTV2B1NwihTuC9iyDZqzlF60X+Vw/z9/xtbyxZczvb5/h1m0EqA0EZ/z1aP3srd9h502hh24VSc6SuD3oqLjNfY2SGN70ik6cHe9ODn6urv8MTGxnG+AYTVDXiuKaPOX6DEZ+nu4XO3AWWgAU56AYypWoWWaNPcw22pAWacAVyKCTpTCm2bFF+BHHehLEVnAWGNBk9zBXWDV1BhKsbKvZm0VajRMHKKKJG5AUlaBnp9b4ulGoqMgYiIhoKCgLOzseDg325qVf/++dqvAdmvAei7As2oBtCoB8qlB+jAF+jAG6eLE5yDGezHK+fFN4RyIN7AOOTEOuPDO4BuIu3MQ+XIR+rLSuXHSOXHSu3VaO3Wc+7YeIuFa/ry0vz343lxVn16cnh2cMHAvpOOiPjx6evq6eHc2v9zTfV6WfnRxuHLxfvx7v9KG/lHG/hIG/lOIvxNI/dOJP5ZLv5aMexYMvVeOPRjPv9xTP92U/BzU/99Xet2WPKijvWvnu+unvGxofnMwNvLx/r19P////v7+/Ly8uzs7Ojo6Obm5uTk5OLi4uDg4N/f39jY2NbW1tLS0s/Pz87Ozs3NzczMzMfHx7Ozs62traenp56enpycnJqamnl5eWtra2RkZE5OTiEhIRwcHBsbGxMTEwgICAQEBP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAMUALAAAAAAQABAAAAj/AIsJXMRpE6UtPopQMSCwYTFQlRpBeoSFxxAlRg44TBRpEiZNiKz0EJKEiBQIAkVZkoTpU6gINY78QALlCRdVxTw5utSJEK8dDggMAMIkypQCpDIxOqSIEABeu2gJCOKkyRIvq5QeMuTU169hvR5UuZJFC6wWM27kcBqMGDBYDPLAMcNnFosYNnAQUiDsFqwTfuy4OQNolokUMGToGCSowahRe+6oWWMhFggRKFa4eKFCTB89deSM+SOBVbEOHkKMIFGCDh08ccq8+WBLYKELGTBo2NCmTRoyczjkStUQTIIKFCagQcMmDA1crRwWK/WlC4JAARboqnVKekNTrmTJBnqFapTDgAA7';
-    $I['pdf']='R0lGODlhEAAQAPcAAFoAAGMAAHMYGG6Ov3KQv3KRv3aTvXqVu3uVvH6XulWAyFmBxliCxV2ExF6ExGGIw2KJw2WKwmeLwWmMwWyOwIwACJQAAJwhIa0ACLUAAL05OZxCQr1KSr1SWsYAAM4ICM4QENYYGM4pMd45OecIEPcQEPcYGO85OfcpKf8xOc5KSt5KStZja+dKSu9CSu9KSudaWu9SUu9SWudaY+dzc+97e/9zc/8A/4KavIWdvoigwIuiwoyiwo+lxJGnxpKoxpWryJesyZmtypywzJ2xzaO20am71Ky+1q/A2LPD2t6EhN61veeMjO+cnO+trdLi+tTj+tbk+tfl+9nm+9vn+9zo+97p++/W1ufv9+Dq/OHs/OPt/OXu/Obv/Ojw/erx/evy/e3z/e/0/fD2/vL3/vT4/vX5/vf6/vn7/////xQCgBQCQBLtDNdNrxQCgBEGqNdN4xQCgBLtFAAAAJEFyCJ8mBLt4JEFURQHqJEFbRLuOAAAABLtPAAAAJEFyFWi2BLuCJEFURQHSBLtWAAAAJEFyFWi2BLuJJEFURQHSJEFbRLuaAAABAAAAOaERAAAAgAABAAAMAAAACJ8oNSLsf3QAAAAMAAABBQAABLrmJD7bAAAIAAAAFWi4BLuOAAAAAAAIADwqgAAIAAAAAAAAJDnvJDVhhLuCJD7bJD7cZDVhpDnvBQAABLt5JDnyBLujJDuGJD7eAH//wAABBLtaAAAABLujJDuGJEFcP///5EFbZEJvBQAAAAAAFWi4BLuSJEJklWi4AAAABLunN3tDt3tIGKmyAACvGKm1AAAAAAAAAAAAAAAAAAAABLuaBLu7BSjUBSjUBLuoOb8I8OlLsYaoBLu2MLCzQAABMLC4xSo8BSjUAAAAxSgLcXS4BSgABLu1BLupP///xLvQMNclMEgcP///8LC40SV1RSjUGMboGMboEUEtRQAABSo8KR+UAAAAAAAAOqG1OqG1OqG1OqG1AACXBLvJN1sdBLvLKR+UKR+UObgowAACeaCsAAABCH5BAEAADcALAAAAAAQABAAAAjcAG8ITGKkCJEgPnbkSJBAoMMbSNCcMVOGzBgxCdI0fHhkYsWLYTJqvNGBgwYVM2DIgOFCBBiRGm28SIHChIkSJkhg+MKw54kQIEB4+OABQ4YKXrpw2aIlgYUAAARsmLrhwgalTLNsvDFETBgwX1Zg1ZLFygGHQr5+uTKiSVYrVQw4BALWSw0sTJyUrUKFgMMfX7xcaeHEyYoWMZRMoeCwR1IYS8jCXcJCigSHPLrMYCKZ7xQpUSA41EGDRmcqn6NAceAwx1vPoKE8WeAQBwIEBgoMmBDhQQMGChQEBAA7';
-    $I['rar']=$I['zip']='R0lGODlhDwAQAOYAAMjY9gRLsBJPqRZQpydpx7TP9iFbrSNfsChltixquzt6xEmH0VqU2G2h4HSk4HGc05jC9ZCz3jmF1zqA0EOK106P2JC235S335a535i531am9FOa5FOa4l2l61uZ13Cv7ne09IKx3ziZ81yr9mGz/2i3/2Go62as7XK7/26x7ni+/3W38Ha38HKx5nu88XWx5IjD84HA8oTD84vK+ZvP9YnJ963b+bzl+8fs/fT///79mf//r///uf//xPr2k//9pP/4hv/6kPr1kPr1kf/7mvfvgvPkbdm/Ktm/LPn25dm8L/vwvfvzzt61AfbNLNm3KfzUMdm3Mdu5M9q6Nd6+O/3ZR/vdZPzkhfvnl/roodq0Kv7TOP3VOf3aUfzZWPzebfvedPziffrjj/rprfrrs/juytKgB9SjCNWlC8+hDtWmEM+hENKmFNWmFtSnG9WqH9SqH9WqIdWrItiuKOjRh7J9DrR8D7mFKaptDbJ7I6ttDax0HfEZAf///wAAAAAAACH5BAEAAH0ALAAAAAAPABAAAAe8gH2CgzIxLispJyaCdGmOdCY5ODc2NDAsC31pfJxpBJ+gEyMeAWtaT0dISlFSU1R2fSEvAXI6PTw7P0RBQEZ1fRYyAW8+SWVjYmBeW02/GDMBcENMZFhhVlVQzX3P0dPV19nbF9BxQktZV19dXE7bGTUBbEVzbm1qaGdm2xEtAXd59ODZAwrUgw8dAgwStKgECRQqVHRQsHCQiQQIDhwwoLCiRxOLPA7i4KABgwoUJAgQuQFAAQggNIgYEAgAOw==';
-    header('Cache-control: max-age=2592000');
-    header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T',time()+2592000));
-    header('Content-type: image/gif');
-    print base64_decode(isset($I[$e])?$I[$e]:$I['file']);
-    exit;
-}
-// End block
-
-// Start configs
-$sitename='My files';
-$date='Y-m-d H:i:s'; // date format
-$ignore=array('.','..','.htaccess','index.php','icon.php','Thumbs.db','web.config'); // ignore these files
-// End configs
-$root=dirname(__FILE__);
-$dir=isset($_GET['dir'])?$_GET['dir']:'';if(strstr($dir,'..'))$dir='';
-$path="$root/$dir/";
-$dirs=$files=array();
-if(!is_dir($path)||false==($h=opendir($path)))exit('Directory does not exist.');
-while(false!==($f=readdir($h)))
-{
-    if(in_array($f,$ignore))continue;
-    if(is_dir($path.$f))$dirs[]=array('name'=>$f,'date'=>filemtime($path.$f),'url'=>'index.php?dir='.rawurlencode(trim("$dir/$f",'/')));
-    else$files[]=array('name'=>$f,'size'=>filesize($path.$f),'date'=>filemtime($path.$f),'url'=>trim("$dir/".rawurlencode($f),'/'));
-}
-closedir($h);
-$current_dir_name = basename($dir);
-$up_dir=dirname($dir);
-$up_url=($up_dir!=''&&$up_dir!='.')?'index.php?dir='.rawurlencode($up_dir):'index.php';
-// END PHP ?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
-<head>
-<meta http-equiv="Content-type" content="text/html; charset=iso-8859-1" />
-<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-<title><?=$current_dir_name==''?'Directory list':$current_dir_name?></title>
-<style type="text/css">
-body { font-family: tahoma, verdana, arial; font-size: 0.7em; color: black; padding-top: 8px; cursor: default; background-color: #fff; }
-#idx { border: 3px solid #fff; width: 500px; }
-#idx td.center { text-align: center; }
-#idx td { border-bottom: 1px solid #f0f0f0; }
-#idx img { margin-bottom: -2px; }
-#idx table { color: #606060; width: 100%; margin-top:3px; }
-#idx span.link { color: #0066DF; cursor: pointer; }
-#idx .rounded { padding: 10px 7px 10px 10px; -moz-border-radius:6px; }
-#idx .gray { background-color:#fafafa;border-bottom: 1px solid #e5e5e5; }
-#idx p { padding: 0px; margin: 0px;line-height:1.4em;}
-#idx p.left { float:left;width:60%;padding:3px;color:#606060;}
-#idx p.right {float:right;width:35%;text-align:right;color:#707070;padding:3px;}
-#idx strong { font-family: "Trebuchet MS", tahoma, arial; font-size: 1.2em; font-weight: bold; color: #202020; padding-bottom: 3px; margin: 0px; }
-#idx a:link    { color: #0066CC; }
-#idx a:visited { color: #003366; }
-#idx a:hover   { text-decoration: none; }
-#idx a:active  { color: #9DCC00; }
-</style>
-
-<script type="text/javascript">
-<!--
-var _c1='#fefefe'; var _c2='#fafafa'; var _ppg=100; var _cpg=1; var _files=[]; var _dirs=[]; var _tpg=null; var _tsize=0; var _sort='date'; var _sdir={'type':0,'name':0,'size':0,'date':1}; var idx=null; var tbl=null;
-function _obj(s){return document.getElementById(s);}
-function _ge(n){n=n.substr(n.lastIndexOf('.')+1);return n.toLowerCase();}
-function _nf(n,p){if(p>=0){var t=Math.pow(10,p);return Math.round(n*t)/t;}}
-function _s(v,u){if(!u)u='B';if(v>1024&&u=='B')return _s(v/1024,'KB');if(v>1024&&u=='KB')return _s(v/1024,'MB');if(v>1024&&u=='MB')return _s(v/1024,'GB');return _nf(v,1)+'&nbsp;'+u;}
-function _f(name,size,date,url,rdate){_files[_files.length]={'dir':0,'name':name,'size':size,'date':date,'type':_ge(name),'url':url,'rdate':rdate,'icon':'index.php?icon='+_ge(name)};_tsize+=size;}
-function _d(name,date,url){_dirs[_dirs.length]={'dir':1,'name':name,'date':date,'url':url,'icon':'index.php?icon=dir'};}
-function _np(){_cpg++;_tbl();}
-function _pp(){_cpg--;_tbl();}
-function _sa(l,r){return(l['size']==r['size'])?0:(l['size']>r['size']?1:-1);}
-function _sb(l,r){return(l['type']==r['type'])?0:(l['type']>r['type']?1:-1);}
-function _sc(l,r){return(l['rdate']==r['rdate'])?0:(l['rdate']>r['rdate']?1:-1);}
-function _sd(l,r){var a=l['name'].toLowerCase();var b=r['name'].toLowerCase();return(a==b)?0:(a>b?1:-1);}
-function _srt(c){switch(c){case'type':_sort='type';_files.sort(_sb);if(_sdir['type'])_files.reverse();break;case'name':_sort='name';_files.sort(_sd);if(_sdir['name'])_files.reverse();break;case'size':_sort='size';_files.sort(_sa);if(_sdir['size'])_files.reverse();break;case'date':_sort='date';_files.sort(_sc);if(_sdir['date'])_files.reverse();break;}_sdir[c]=!_sdir[c];_obj('sort_type').style.fontStyle=(c=='type'?'italic':'normal');_obj('sort_name').style.fontStyle=(c=='name'?'italic':'normal');_obj('sort_size').style.fontStyle=(c=='size'?'italic':'normal');_obj('sort_date').style.fontStyle=(c=='date'?'italic':'normal');_tbl();return false;}
-
-function _head()
-{
-    if(!idx)return;
-    _tpg=Math.ceil((_files.length+_dirs.length)/_ppg);
-    idx.innerHTML='<div class="rounded gray" style="padding:5px 10px 5px 7px;color:#202020">' +
-        '<p class="left">' +
-            '<strong><?=$current_dir_name==''?$sitename:$current_dir_name?></strong><?=$dir!=''?'&nbsp; (<a href="'.$up_url.'">Back</a>)':''?><br />' + (_files.length+_dirs.length) + ' objects in this folder, ' + _s(_tsize) + ' total.' +
-        '</p>' +
-        '<p class="right">' +
-            'Sort: <span class="link" onmousedown="return _srt(\'name\');" id="sort_name">Name</span>, <span class="link" onmousedown="return _srt(\'type\');" id="sort_type">Type</span>, <span class="link" onmousedown="return _srt(\'size\');" id="sort_size">Size</span>, <span class="link" onmousedown="return _srt(\'date\');" id="sort_date">Date</span>' +
-        '</p>' +
-        '<div style="clear:both;"></div>' +
-    '</div><div id="idx_tbl"></div>';
-    tbl=_obj('idx_tbl');
-}
-
-function _tbl()
-{
-    var _cnt=_dirs.concat(_files);if(!tbl)return;if(_cpg>_tpg){_cpg=_tpg;return;}else if(_cpg<1){_cpg=1;return;}var a=(_cpg-1)*_ppg;var b=_cpg*_ppg;var j=0;var html='';
-    if(_tpg>1)html+='<p style="padding:5px 5px 0px 7px;color:#202020;text-align:right;"><span class="link" onmousedown="_pp();return false;">Previous</span> ('+_cpg+'/'+_tpg+') <span class="link" onmousedown="_np();return false;">Next</span></p>';
-    html+='<table cellspacing="0" cellpadding="5" border="0">';
-    for(var i=a;i<b&&i<(_files.length+_dirs.length);++i)
-    {
-        var f=_cnt[i];var rc=j++&1?_c1:_c2;
-        html+='<tr style="background-color:'+rc+'"><td><img src="'+f['icon']+'" alt="" /> &nbsp;<a href="'+f['url']+'">'+f['name']+'</a></td><td class="center" style="width:50px;">'+(f['dir']?'':_s(f['size']))+'</td><td class="center" style="width:110px;">'+f['date']+'</td></tr>';
-    }
-    tbl.innerHTML=html+'</table>';
-}
-<?php foreach($dirs as $d)print sprintf("_d('%s','%s','%s');\n",addslashes($d['name']),date($date,$d['date']),addslashes($d['url'])); ?>
-<?php foreach($files as $f)print sprintf("_f('%s',%d,'%s','%s',%d);\n",addslashes($f['name']),$f['size'],date($date,$f['date']),addslashes($f['url']),$f['date']);?>
-
-window.onload=function()
-{
-    idx=_obj('idx'); _head(); _srt('name');
-};
--->
-</script>
-</head>
-<body>
-    <div id="idx"><!-- do not remove --></div>
-</body>
-</html>

+ 803 - 0
sample/rutas.json

@@ -0,0 +1,803 @@
+[
+  {
+    "id": 0,
+    "ruta": "Edificio 1 / Piso 0",
+    "horarios": [
+      {
+        "id": 0,
+        "salon": "A-1",
+        "materia": "Sistemas",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 1,
+        "salon": "A-2",
+        "materia": "Sistemas",
+        "hora_inicio": "9:00",
+        "hora_fin": "10:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 2,
+        "salon": "A-3",
+        "materia": "Matemáticas",
+        "hora_inicio": "10:00",
+        "hora_fin": "10:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 3,
+        "salon": "A-4",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "10:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 4,
+        "salon": "A-5",
+        "materia": "Base de datos",
+        "hora_inicio": "10:00",
+        "hora_fin": "11:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 5,
+        "salon": "A-6",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 6,
+        "salon": "A-7",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 7,
+        "salon": "A-8",
+        "materia": "Sistemas",
+        "hora_inicio": "10:00",
+        "hora_fin": "11:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 8,
+        "salon": "A-9",
+        "materia": "Matemáticas",
+        "hora_inicio": "10:00",
+        "hora_fin": "9:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 9,
+        "salon": "A-10",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 10,
+        "salon": "A-11",
+        "materia": "Sistemas",
+        "hora_inicio": "11:00",
+        "hora_fin": "11:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 11,
+        "salon": "A-12",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "9:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 12,
+        "salon": "A-13",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "9:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 13,
+        "salon": "A-14",
+        "materia": "Matemáticas",
+        "hora_inicio": "8:00",
+        "hora_fin": "11:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 14,
+        "salon": "A-15",
+        "materia": "Matemáticas",
+        "hora_inicio": "10:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 15,
+        "salon": "A-16",
+        "materia": "Matemáticas",
+        "hora_inicio": "8:00",
+        "hora_fin": "9:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 16,
+        "salon": "A-17",
+        "materia": "Sistemas",
+        "hora_inicio": "9:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 17,
+        "salon": "A-18",
+        "materia": "Matemáticas",
+        "hora_inicio": "8:00",
+        "hora_fin": "11:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 18,
+        "salon": "A-19",
+        "materia": "Sistemas",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 1,
+        "comentario": ""
+      }
+    ]
+  },
+  {
+    "id": 1,
+    "ruta": "Edificio 1 / Piso 1",
+    "horarios": [
+      {
+        "id": 0,
+        "salon": "A-1",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 1,
+        "salon": "A-2",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 2,
+        "salon": "A-3",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 3,
+        "salon": "A-4",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 4,
+        "salon": "A-5",
+        "materia": "Sistemas",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 5,
+        "salon": "A-6",
+        "materia": "Sistemas",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 6,
+        "salon": "A-7",
+        "materia": "Base de datos",
+        "hora_inicio": "8:00",
+        "hora_fin": "10:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 7,
+        "salon": "A-8",
+        "materia": "Sistemas",
+        "hora_inicio": "10:00",
+        "hora_fin": "10:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 8,
+        "salon": "A-9",
+        "materia": "Matemáticas",
+        "hora_inicio": "8:00",
+        "hora_fin": "9:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 9,
+        "salon": "A-10",
+        "materia": "Sistemas",
+        "hora_inicio": "10:00",
+        "hora_fin": "9:00",
+        "estado": 3,
+        "comentario": ""
+      }
+    ]
+  },
+  {
+    "id": 2,
+    "ruta": "Edificio 1 / Piso 2",
+    "horarios": [
+      {
+        "id": 0,
+        "salon": "A-1",
+        "materia": "Sistemas",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 1,
+        "salon": "A-2",
+        "materia": "Sistemas",
+        "hora_inicio": "8:00",
+        "hora_fin": "8:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 2,
+        "salon": "A-3",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "9:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 3,
+        "salon": "A-4",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 4,
+        "salon": "A-5",
+        "materia": "Sistemas",
+        "hora_inicio": "8:00",
+        "hora_fin": "10:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 5,
+        "salon": "A-6",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 6,
+        "salon": "A-7",
+        "materia": "Matemáticas",
+        "hora_inicio": "8:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 7,
+        "salon": "A-8",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "11:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 8,
+        "salon": "A-9",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 9,
+        "salon": "A-10",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "10:00",
+        "estado": 2,
+        "comentario": ""
+      }
+    ]
+  },
+  {
+    "id": 3,
+    "ruta": "Edificio 1 / Piso 3",
+    "horarios": [
+      {
+        "id": 0,
+        "salon": "A-1",
+        "materia": "Base de datos",
+        "hora_inicio": "8:00",
+        "hora_fin": "9:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 1,
+        "salon": "A-2",
+        "materia": "Sistemas",
+        "hora_inicio": "11:00",
+        "hora_fin": "10:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 2,
+        "salon": "A-3",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "9:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 3,
+        "salon": "A-4",
+        "materia": "Sistemas",
+        "hora_inicio": "8:00",
+        "hora_fin": "8:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 4,
+        "salon": "A-5",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 5,
+        "salon": "A-6",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 6,
+        "salon": "A-7",
+        "materia": "Sistemas",
+        "hora_inicio": "8:00",
+        "hora_fin": "8:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 7,
+        "salon": "A-8",
+        "materia": "Sistemas",
+        "hora_inicio": "9:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 8,
+        "salon": "A-9",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 9,
+        "salon": "A-10",
+        "materia": "Base de datos",
+        "hora_inicio": "8:00",
+        "hora_fin": "10:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 10,
+        "salon": "A-11",
+        "materia": "Sistemas",
+        "hora_inicio": "11:00",
+        "hora_fin": "10:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 11,
+        "salon": "A-12",
+        "materia": "Matemáticas",
+        "hora_inicio": "8:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 12,
+        "salon": "A-13",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "10:00",
+        "estado": 0,
+        "comentario": ""
+      }
+    ]
+  },
+  {
+    "id": 4,
+    "ruta": "Edificio 1 / Piso 4",
+    "horarios": [
+      {
+        "id": 0,
+        "salon": "A-1",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 1,
+        "salon": "A-2",
+        "materia": "Sistemas",
+        "hora_inicio": "10:00",
+        "hora_fin": "9:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 2,
+        "salon": "A-3",
+        "materia": "Sistemas",
+        "hora_inicio": "10:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 3,
+        "salon": "A-4",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 4,
+        "salon": "A-5",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "10:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 5,
+        "salon": "A-6",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "10:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 6,
+        "salon": "A-7",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "10:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 7,
+        "salon": "A-8",
+        "materia": "Base de datos",
+        "hora_inicio": "10:00",
+        "hora_fin": "10:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 8,
+        "salon": "A-9",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "8:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 9,
+        "salon": "A-10",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "9:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 10,
+        "salon": "A-11",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "8:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 11,
+        "salon": "A-12",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "8:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 12,
+        "salon": "A-13",
+        "materia": "Sistemas",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 13,
+        "salon": "A-14",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 14,
+        "salon": "A-15",
+        "materia": "Matemáticas",
+        "hora_inicio": "10:00",
+        "hora_fin": "11:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 15,
+        "salon": "A-16",
+        "materia": "Matemáticas",
+        "hora_inicio": "10:00",
+        "hora_fin": "9:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 16,
+        "salon": "A-17",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "11:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 17,
+        "salon": "A-18",
+        "materia": "Base de datos",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 3,
+        "comentario": ""
+      }
+    ]
+  },
+  {
+    "id": 5,
+    "ruta": "Edificio 1 / Piso 5",
+    "horarios": [
+      {
+        "id": 0,
+        "salon": "A-1",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 1,
+        "salon": "A-2",
+        "materia": "Base de datos",
+        "hora_inicio": "10:00",
+        "hora_fin": "10:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 2,
+        "salon": "A-3",
+        "materia": "Matemáticas",
+        "hora_inicio": "9:00",
+        "hora_fin": "11:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 3,
+        "salon": "A-4",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "10:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 4,
+        "salon": "A-5",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "9:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 5,
+        "salon": "A-6",
+        "materia": "Sistemas",
+        "hora_inicio": "10:00",
+        "hora_fin": "9:00",
+        "estado": 2,
+        "comentario": ""
+      },
+      {
+        "id": 6,
+        "salon": "A-7",
+        "materia": "Base de datos",
+        "hora_inicio": "9:00",
+        "hora_fin": "10:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 7,
+        "salon": "A-8",
+        "materia": "Matemáticas",
+        "hora_inicio": "8:00",
+        "hora_fin": "11:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 8,
+        "salon": "A-9",
+        "materia": "Base de datos",
+        "hora_inicio": "10:00",
+        "hora_fin": "8:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 9,
+        "salon": "A-10",
+        "materia": "Matemáticas",
+        "hora_inicio": "10:00",
+        "hora_fin": "10:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 10,
+        "salon": "A-11",
+        "materia": "Sistemas",
+        "hora_inicio": "9:00",
+        "hora_fin": "10:00",
+        "estado": 3,
+        "comentario": ""
+      },
+      {
+        "id": 11,
+        "salon": "A-12",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "8:00",
+        "estado": 1,
+        "comentario": ""
+      },
+      {
+        "id": 12,
+        "salon": "A-13",
+        "materia": "Base de datos",
+        "hora_inicio": "8:00",
+        "hora_fin": "10:00",
+        "estado": 4,
+        "comentario": ""
+      },
+      {
+        "id": 13,
+        "salon": "A-14",
+        "materia": "Base de datos",
+        "hora_inicio": "8:00",
+        "hora_fin": "8:00",
+        "estado": 0,
+        "comentario": ""
+      },
+      {
+        "id": 14,
+        "salon": "A-15",
+        "materia": "Matemáticas",
+        "hora_inicio": "11:00",
+        "hora_fin": "9:00",
+        "estado": 0,
+        "comentario": ""
+      }
+    ]
+  }
+]

+ 224 - 0
selector_rutas.php

@@ -0,0 +1,224 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Supervisor - Seleccionar rutas</title>
+    <?php
+    include 'import/html_css_files.php';
+    ?>
+    <style>
+        [v-cloak] {
+            display: none;
+        }
+    </style>
+</head>
+
+<body>
+    <?php
+    include "import/html_header.php";
+    html_header(    
+        "Selecciona la ruta a supervisar",
+        "Sistema de gestión de checador",
+    );
+    ?>
+    <main class="container-fluid px-4" id="app" v-cloak @vue:mounted="mounted">
+        <!-- filtros -->
+        <div class="card mt-4">
+            <div class="card-header bg-dark d-flex justify-content-between align-items-center flex-wrap text-white">
+                <h2 class="col-md-10 col-12 text-white font-weight-bold text-uppercase text-center">
+                    Facultad de ingeniería: {{ store.rutas.data[store.rutas.selected]?.ruta ?? '' }}
+                </h2>
+                <button type="button" class="btn btn-success btn-sm" data-toggle="modal"
+                    data-target="#editar-ubicaciones">
+                    <i class="ing-editar"></i>
+                </button>
+            </div>
+            <div class="card-body bg-info">
+                <div class="container-fluid">
+                    <div class="row flex-nowrap overflow-auto">
+                        <!-- size big -->
+                        <div class="col-9 col-sm-7 col-md-4 col-lg-3 col-xl-2 my-2"
+                            v-for="(ruta, index) in store.rutas.data" :key="ruta.id">
+                            <span class="shadow badge badge-pill py-2 px-4" @click="store.selectRuta(ruta.id)"
+                                :class="{ 'badge-primary': store.rutas.selected == ruta.id, 'badge-light text-primary': store.rutas.selected != ruta.id, 'badge-dark text-muted disabled' : ruta.horarios.every(({estado}) => estado != 0) && store.rutas.selected != ruta.id }">
+                                {{ ruta.ruta }}
+                                <span class="badge mx-3" v-if="ruta.horarios.some(({estado}) => estado == 0)"
+                                    :class="{ 'badge-success': ruta.horarios.length > 0, 'badge-danger': ruta.horarios.length == 0 }">
+                                    {{ruta.horarios.filter(({estado}) => estado != 0).length}} / {{
+                                    ruta.horarios.length}}
+                                </span>
+                                <span v-else class="badge mx-3"
+                                    :class="{ 'badge-success': ruta.horarios.length > 0, 'badge-danger': ruta.horarios.length == 0 }">
+
+                                    <i class="ing-aceptar"></i>
+                                </span>
+                                <span class="sr-only">Faltan {{ruta.horarios.filter(({estado}) => estado == 0).length}}
+                                    horarios por registrar</span>
+                            </span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="card-footer bg-dark d-flex justify-content-between align-items-center flex-wrap text-white">
+                <button class="btn btn-info" :disabled="store.bloquesHorario.selected == 0"
+                    @click="store.selectBloque(store.bloquesHorario.selected - 1)">
+                    <i class="ing-caret ing-rotate-90"></i>
+                    <span class="d-none d-md-inline-block">
+                        Bloque horario anterior
+                    </span>
+                </button>
+
+                <h3 class="text-white font-weight-bold text-uppercase text-center">
+                    {{ store.hora_inicio.slice(0, 5) }} - {{ store.hora_fin.slice(0, 5) }}
+                </h3>
+
+                <button class="btn btn-info" @click="store.selectBloque(store.bloquesHorario.selected + 1)"
+                    :disabled="store.bloquesHorario.selected == store.bloquesHorario.data.length - 1">
+                    <span class="d-none d-md-inline-block">
+                        Bloque horario siguiente
+                    </span>
+                    <i class="ing-caret ing-rotate-270"></i>
+                </button>
+            </div>
+        </div>
+
+        <div class="mt-3 d-flex justify-content-center flex-wrap">
+            <!-- refresh -->
+            <div class="table-responsive">
+                <table class="table table-hover table-striped table-bordered table-sm">
+                    <thead class="thead-dark">
+                        <tr>
+                            <th scope="col" class="text-center align-middle text-nowrap px-2">
+                                <button @click="store.invertir" class="btn btn-info mr-3" v-if="clases.length > 0">
+                                    <i class="ing-cambiar ing-rotate-90"></i>
+                                </button>
+                                Salón
+                            </th>
+                            <th scope="col" class="text-center align-middle text-nowrap px-2">Profesor</th>
+                            <th scope="col" class="text-center align-middle text-nowrap px-2">Horario</th>
+                            <th scope="col" class="text-center align-middle text-nowrap px-2">Acciones</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="clases.length == 0">
+                            <td colspan="6" class="text-center">No hay clases en este horario</td>
+                        </tr>
+                        <tr v-for="clase in clases" :key="clase.id">
+                            <td class="text-center align-middle">{{ clase.salon }}</td>
+                            <td class="text-center align-middle">{{ clase.profesor }} {{ clase.materia }}
+                            <td class="text-center align-middle">
+                                {{ clase.hora_inicio }} - {{ clase.hora_fin }}
+                            </td>
+                            <td class="text-center align-middle text-nowrap">
+                                <button class="btn btn-outline text-center mx-2" v-for="estado in estados"
+                                    :key="estado.id" @click="store.cambiarEstado(clase.id, estado.id)"
+                                    :class="[{'active': estado.id === clase.estado}, `btn-outline-${estado.color}`]">
+                                    <i :class="estado.icon"></i>
+                                </button>
+
+                                <button class="btn btn-outline-primary text-center mx-2" data-toggle="modal"
+                                    data-target="#editar-comentario" :class="{ 'active': clase.comentario != '' }"
+                                    @click="store.selectEditor(clase.id)">
+                                    <i class="ing-editar"></i>
+                                    <span class="badge badge-pill badge-primary"
+                                        v-if="clase.comentario != ''">...</span>
+                                    <span class="sr-only">Editar comentario</span>
+                                </button>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+
+        <!-- MODAL -->
+        <div class="modal" tabindex="-1" id="editar-ubicaciones">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">Editar rutas</h5>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <div class="container">
+                            <h2>Reordena las rutas</h2>
+                            <ul id="sortable" class="list-group">
+                                <li class="list-group-item" v-for="ruta in store.rutas.data" :key="ruta.id"
+                                    :id="'ruta-' + ruta.id"
+                                    :class="[ruta.horarios.every(horario => horario.estado != 0) ? ['disabled', 'bg-light', 'undraggable'] : '']">
+                                    {{ruta.ruta}}
+                                </li>
+                            </ul>
+                        </div>
+
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <button class="btn btn-primary btn-lg btn-block mb-4" @click="store.guardarCambios">
+            <i class="ing-guardar"></i>
+            Guardar cambios
+        </button>
+
+        <div class="modal" tabindex="-1" id="editar-comentario">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">Añadir comentario</h5>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <div class="container">
+                            <h2 class="text-center">Comentarios de la clase</h2>
+                            <br>
+                            <div class="input-group">
+                                <div class="input-group-prepend">
+                                    <span class="input-group-text bg-primary text-white">Comentario
+                                        <button class="btn btn-light ml-2 text-primary"
+                                            @click="store.limpiarComentario">
+                                            <i class="ing-borrar"></i>
+                                        </button>
+                                    </span>
+                                </div>
+                                <textarea class="form-control" aria-label="Comentarios de la clase"
+                                    v-model="store.editor.texto"></textarea>
+                            </div>
+                        </div>
+
+                    </div>
+                    <div class="modal-footer">
+                        <button type="button" class="btn btn-outline-danger" data-dismiss="modal">
+                            <i class="ing-cancelar"></i>
+                            Cancelar
+                        </button>
+                        <button type="button" class="btn btn-primary" data-dismiss="modal"
+                            @click="store.guardarComentario">
+                            Guardar comentario
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </main>
+    <?php
+    include "import/html_footer.php";
+    ?>
+    <!-- filtro modal -->
+    <script src="js/jquery.min.js"></script>
+    <script src="js/jquery-ui.js"></script>
+    <script src="js/jquery-ui.touch-punch.min.js"></script>
+    <script src="js/bootstrap/bootstrap.min.js"></script>
+    <?php include_once 'js/messages.php'; ?>
+    <script src="https://unpkg.com/petite-vue"></script>
+    <script>
+    </script>
+</body>
+
+</html>

+ 27 - 0
service/auto.php

@@ -0,0 +1,27 @@
+<?
+$ruta = "../";
+require_once "$ruta/include/bd_pdo.php";
+header('Content-Type: application/json');
+
+// json data from service\periodos.v1.php (input)
+
+$urls = array(
+    'periodos.v1',
+    'periodos.v2',
+    'horarios',
+);
+
+$urls = array_map(fn($item) => "../$item.php", $urls);
+
+ob_start();
+include_once 'periodos.v1.php';
+$periodos_v1 = ob_get_contents();
+ob_end_clean();
+
+ob_start();
+include_once 'periodos.v2.php';
+$periodos_v2 = ob_get_contents();
+ob_end_clean();
+
+// echo $periodos_v1;
+echo $periodos_v2;

+ 76 - 0
service/backend/carreras.php

@@ -0,0 +1,76 @@
+<?
+$ruta = "../../";
+require_once "$ruta/include/bd_pdo.php";
+header('Content-Type: application/json');
+global $db;
+// json data from service\periodos.v1.php (input)
+$data = json_decode(file_get_contents('php://input'), true);
+
+// check if the input is empty
+
+if (is_response_empty($data)) {
+    echo json_encode([
+        'status' => 'error',
+        'message' => 'No se recibieron datos',
+        'data' => $data
+    ]);
+    exit;
+}
+
+// check if data is array
+if (!is_array($data)) {
+    echo json_encode([
+        'status' => 'error',
+        'message' => 'La información recibida no es válida',
+        'data' => $data
+    ]);
+    exit;
+}
+
+/**
+ * [{
+ * carrera_nombre
+ * carrera_clave
+ * id_nivel
+ * },]
+ */
+// check for this schema
+array_walk($data, function ($item) {
+    if (!isset($item['ClaveCarrera']) || !isset($item['NombreCarrera']) || !isset($item['IdNivel'])) {
+        echo json_encode([
+            'status' => 'error',
+            'message' => 'Los datos recibidos no son validos',
+            'data' => $item
+        ]);
+        exit;
+    }
+});
+
+
+
+array_walk($data, function ($item) use ($db) {
+
+    if ($db->where('carrera_nombre', "%{$item['NombreCarrera']}", 'ILIKE')->has('carrera'))
+        $db
+            ->where('carrera_nombre', "%{$item['NombreCarrera']}", 'ILIKE')
+            ->update('carrera', [
+                'carrera_nombre' => $item['NombreCarrera'],
+                'id_referencia' => $item['ClaveCarrera'],
+            ]);
+    else {
+        try {
+            $db->insert('carrera', [
+                'carrera_nombre' => $item['NombreCarrera'],
+                'id_referencia' => $item['ClaveCarrera'],
+                'nivel_id' => $item['IdNivel'],
+            ]);
+        } catch (PDOException $th) {
+            echo json_encode([
+                'success' => false,
+                'message' => $th->getMessage(),
+                'last_query' => $db->getLastQuery(),
+            ]);
+            exit;
+        }
+    }
+});

+ 60 - 0
service/backend/periodos.php

@@ -0,0 +1,60 @@
+<?
+$ruta = "../../";
+require_once "$ruta/include/bd_pdo.php";
+header('Content-Type: application/json');
+
+// json data from service\periodos.v1.php (input)
+$data = json_decode(file_get_contents('php://input'), true);
+
+// check if the input is empty
+
+if (is_response_empty($data)) {
+    echo json_encode([
+        'status' => 'error',
+        'message' => 'No se recibieron datos',
+    ]);
+    exit;
+}
+
+/* 
+{
+    "IdNivel": 1,
+    "IdPeriodo": 635,
+    "NombreNivel": "LICENCIATURA",
+    "NombrePeriodo": "241",
+    "in_db": false
+    inicio,
+    fin
+}
+*/
+
+// insert into database
+setlocale(LC_TIME, 'es_MX.UTF-8');
+$formatter = new IntlDateFormatter('es_MX', IntlDateFormatter::FULL, IntlDateFormatter::FULL, 'America/Mexico_City', IntlDateFormatter::GREGORIAN, 'MMMM');
+$inicio = strtotime($data['inicio']);
+$fin = strtotime($data['fin']);
+try {
+
+    $result = $db->insert('periodo', [
+        'id_reference' => $data['IdPeriodo'],
+        'periodo_nombre' => "{$data['NombreNivel']}: {$formatter->format($inicio)} - {$formatter->format($fin)} " . date('Y', $inicio),
+        'nivel_id' => $data['IdNivel'],
+        'periodo_fecha_inicio' => $data['inicio'],
+        'periodo_fecha_fin' => $data['fin'],
+        'estado_id' => 4,
+        'periodo_clave' => $data['NombrePeriodo']
+    ], ['id_reference']);
+} catch (PDOException $th) {
+    echo json_encode([
+        'success' => false,
+        'message' => $th->getMessage()
+    ]);
+    exit;
+}
+echo json_encode($result ? [
+    'success' => true,
+    'message' => 'Periodo agregado correctamente'
+] : [
+    'success' => false,
+    'message' => 'Error al agregar el periodo'
+]);

+ 155 - 0
service/client.html

@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Cliente REST</title>
+    <script type="module" src="../js/client.js" defer></script>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
+        integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
+    <link rel="stylesheet" href="../css/indivisa.css">
+</head>
+
+<body>
+    <header class="container-fluid bg-dark text-white text-center">
+        Página de Cliente REST
+    </header>
+    <main class="container" @vue:mounted="mounted">
+        <div v-for="error in store.errors" class="alert alert-danger alert-dismissible fade show" role="alert">
+            <strong>Error!</strong> {{error}}
+            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+        </div>
+        <div class="row">
+            <div class="col-12">
+                <h1>Periodos activos</h1>
+            </div>
+        </div>
+        <ul class="list-group">
+
+            <li href="#" class="list-group-item" v-for="(periodo, index) in store.periodosV1" :key="periodo.IdPeriodo">
+                <!-- v1
+                IdNivel: number;
+                IdPeriodo: number;
+                NombreNivel: string;
+                NombrePeriodo: string;
+                -->
+                <div class="row">
+                    <span class="badge bg-secondary">{{index}}</span>
+                    <h2 class="text-center">
+                        Información
+                    </h2>
+                    <div class="col-md-3">
+                        <strong>ID Nivel:</strong> {{periodo.IdNivel}}
+                    </div>
+                    <div class="col-md-3">
+                        <strong>ID Periodo:</strong> {{periodo.IdPeriodo}}
+                    </div>
+                    <div class="col-md-3">
+                        <strong>Nombre Nivel:</strong> {{periodo.NombreNivel}}
+                    </div>
+                    <div class="col-md-3">
+                        <strong>Nombre Periodo:</strong> {{periodo.NombrePeriodo}}
+                    </div>
+                </div>
+                <!--
+                    FechaFin: string;
+                    FechaInicio: string;
+                    IdPeriodo: number;
+                    -- info(IdPeriodo) --
+                -->
+                <div class="row mt-2" v-if="complete(periodo.IdPeriodo)">
+                    <h2 class="text-center">
+                        Fechas
+                    </h2>
+                    <div class="col-md-2">
+                        <strong>Fecha Inicio:</strong> {{info(periodo.IdPeriodo).FechaInicio}}
+                    </div>
+                    <div class="col-md-2">
+                        <strong>Fecha Fin:</strong> {{info(periodo.IdPeriodo).FechaFin}}
+                    </div>
+                </div>
+                <div v-if="!periodo.in_db" class="row mt-2">
+                    <!-- 
+                        PeriodoV2
+                                ClaveCarrera: string;
+                                NombreCarrera: string;
+                        PeriodoV1
+                                IdNivel: number;
+                    -->
+                    <div class="col-md-12">
+                        <button class="btn btn-primary float-end" @click="store.addPeriodo(periodo)"
+                            :disabled="!complete(periodo.IdPeriodo)">
+                            Agregar
+                            <i class="ing ing-mas"></i>
+                        </button>
+                    </div>
+                </div>
+                <div v-else class="row mt-2">
+                    <div class="col-md-12">
+                        <button class="btn btn-success float-end disabled">
+                            Agregado
+                            <i class="ing-aceptar"></i>
+                        </button>
+                    </div>
+                </div>
+
+                <div class="row mt-2">
+                    <div class="col-md-12">
+                        <button class="btn btn-secondary float-end" @click="store.addCarreras(periodo.IdPeriodo)">
+                            Sincronizar Carreras
+                            <i class="ing ing-link"></i>
+                        </button>
+                    </div>
+                </div>
+                <div class="accordion mt-2">
+                    <div class="accordion-item">
+                        <h2 class="accordion-header">
+                            <button class="accordion-button collapsed" data-bs-toggle="collapse"
+                                :data-bs-target="`#collapse-${periodo.IdPeriodo}`">
+                                Horarios del periodo
+                            </button>
+                        </h2>
+                        <div :id="`collapse-${periodo.IdPeriodo}`" class="accordion-collapse collapse">
+                            <div class="accordion-body">
+                                <ul class="list-group">
+                                    <li class="list-group-item"
+                                        v-for="periodo in store.periodosV2.filter(periodov2 => periodov2.IdPeriodo === periodo.IdPeriodo)">
+                                        <div class="row">
+                                            <div class="col-md-3">
+                                                <strong>Clave Carrera:</strong> {{periodo.ClaveCarrera}}
+                                            </div>
+                                            <div class="col-md-6">
+                                                <strong>Nombre Carrera:</strong> {{periodo.NombreCarrera}}
+                                            </div>
+
+                                            <div class="col-md-3">
+                                                <span class="badge float-end mx-1"
+                                                    :class="periodo.linked ?'bg-success':'bg-secondary'">
+                                                    <i class="ing-link"></i>
+                                                </span>
+                                                <span class="badge float-end mx-1"
+                                                    :class="periodo.in_db ?'bg-success':'bg-secondary'">
+                                                    <i class="ing-aceptar"></i>
+                                                </span>
+                                            </div>
+                                        </div>
+                                    </li>
+                                </ul>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </li>
+        </ul>
+    </main>
+    <footer>
+
+    </footer>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
+        integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
+        crossorigin="anonymous"></script>
+
+</body>
+
+</html>

+ 81 - 0
service/horarios.php

@@ -0,0 +1,81 @@
+<?
+/*
+•	idPeriodo: identificador del periodo a consultar (obligatorio, número entero)
+•	claveFacultad: clave de la facultad a consultar (opcional, cadena)
+•	claveCarrera: clave de la carrera a consultar (opcional, cadena)
+•	claveProfesor: clave del empleado a consultar (opcional, cadena)
+•	fecha: fecha de la clase (opcional, cadena en formato yyyy-MM-dd)
+ */
+$required_params = [
+    'idPeriodo'
+];
+
+$optional_params = [
+    'claveFacultad',
+    'claveCarrera',
+    'claveProfesor',
+    'fecha'
+];
+
+// Check if all required params are present in $_GET
+$params = array_map('strtolower', $_GET); // Convert keys to lowercase for case-insensitive comparison
+
+// Check for missing required parameters
+$missing_params = array_diff($required_params, array_keys($params));
+if (!empty($missing_params)) {
+    $missing_params_str = implode(', ', $missing_params);
+    die("Missing required parameter(s): $missing_params_str");
+}
+
+// Filter and retain only the required and optional parameters
+$params = array_filter($params, function ($key) use ($required_params, $optional_params) {
+    return in_array($key, $required_params) || in_array($key, $optional_params);
+}, ARRAY_FILTER_USE_KEY);
+
+$curl = curl_init();
+curl_setopt_array($curl, [
+    CURLOPT_URL => "https://portal.ulsa.edu.mx/servicios/AuditoriaAsistencialRest/AuditoriaAsistencialService.svc/auditoriaAsistencial/seleccionar",
+    CURLOPT_RETURNTRANSFER => true,
+    CURLOPT_ENCODING => "",
+    CURLOPT_MAXREDIRS => 10,
+    CURLOPT_TIMEOUT => 0,
+    CURLOPT_CUSTOMREQUEST => "POST",
+    CURLOPT_POSTFIELDS => json_encode($params),
+    CURLOPT_HTTPHEADER => [
+        "token: e12e2dde0e95a32e274328fd274e07d53f127630c211d838efffacd3cafc4f14edf3f3de6a649eb23f98edf6a1863a008f60e78a316d4dec996b79aeea161a0c",
+        "username: SGU_APSA_AUD_ASIST",
+        "Content-Type: application/json"
+    ],
+]);
+
+$response = curl_exec($curl);
+$err = curl_error($curl);
+
+curl_close($curl);
+
+if ($err)
+    die("cURL Error #:$err");
+
+
+$selectedData = json_decode($response, true);
+
+$rawInput = file_get_contents('php://input');
+
+$input = json_decode($rawInput, true);
+// check for {collect: []} in raw input
+if (isset($input['collect']) && is_array($input['collect'])) {
+    $collect = $input['collect'];
+    $selectedData = array_map(function ($item) use ($collect) {
+        return array_intersect_key($item, array_flip($collect));
+    }, $selectedData);
+    // unique and distinct
+    $selectedData = array_unique($selectedData, SORT_REGULAR);
+}
+else {
+    // return invalid request error
+    die($rawInput);
+}
+
+// Output the selected data directly
+header('Content-Type: application/json');
+echo json_encode($selectedData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

+ 35 - 0
service/periodos.v1.php

@@ -0,0 +1,35 @@
+<?
+$ruta = "../";
+require_once '../include/bd_pdo.php';
+$curl = curl_init();
+global $db;
+curl_setopt_array($curl, [
+    CURLOPT_URL => "https://portal.ulsa.edu.mx/servicios/AuditoriaAsistencialRest/AuditoriaAsistencialService.svc/auditoriaAsistencial/catalogos/periodos/v1/seleccionar",
+    CURLOPT_RETURNTRANSFER => true,
+    CURLOPT_POSTFIELDS => "",
+    CURLOPT_HTTPHEADER => [
+        "token: f65f921264e4ab135472adb5a946212dd4b995d929294afec31eef192b8de8d6a076648906f70012c9803e5918d0fc99499d7d1fb7c998cc06c7a10eef61f66a",
+        "username: SGU_APSA_AUD_ASIST"
+    ],
+]);
+
+$response = curl_exec($curl);
+$err = curl_error($curl);
+
+curl_close($curl);
+
+if ($err)
+    die("cURL Error #:$err");
+
+$data = json_decode($response, true);
+
+$in_db = array_map(function ($item) use ($db) {
+    $item['in_db'] = $db->where('id_reference', $item['IdPeriodo'])->has('periodo');
+    return $item;
+}, $data);
+
+$selectedData = array_unique($in_db, SORT_REGULAR);
+
+// Output the selected data directly
+header('Content-Type: application/json');
+echo json_encode($selectedData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

+ 37 - 0
service/periodos.v2.php

@@ -0,0 +1,37 @@
+<?
+$ruta = "../";
+require_once '../include/bd_pdo.php';
+global $db;
+
+$curl = curl_init();
+
+curl_setopt_array($curl, [
+    CURLOPT_URL => "https://portal.ulsa.edu.mx/servicios/AuditoriaAsistencialRest/AuditoriaAsistencialService.svc/auditoriaAsistencial/catalogos/periodos/v2/seleccionar",
+    CURLOPT_RETURNTRANSFER => true,
+    CURLOPT_POSTFIELDS => "",
+    CURLOPT_HTTPHEADER => [
+        "token: f65f921264e4ab135472adb5a946212dd4b995d929294afec31eef192b8de8d6a076648906f70012c9803e5918d0fc99499d7d1fb7c998cc06c7a10eef61f66a",
+        "username: SGU_APSA_AUD_ASIST"
+    ],
+]);
+
+$response = curl_exec($curl);
+$err = curl_error($curl);
+
+curl_close($curl);
+
+if ($err)
+    die("cURL Error #:$err");
+
+
+$json = json_decode($response, true);
+
+$selectedData = array_map(function ($item) use ($db) {
+    $item['in_db'] = $db->where('carrera_nombre', $item['NombreCarrera'], 'ILIKE')->has('carrera');
+    $item['linked'] = $db->where('id_referencia', $item['ClaveCarrera'], 'ILIKE')->has('carrera');
+    return $item;
+}, $json);
+
+// Output the selected data directly
+header('Content-Type: application/json');
+echo json_encode($selectedData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

+ 734 - 0
supervisor.php

@@ -0,0 +1,734 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Supervisor</title>
+    <?php
+    include 'import/html_css_files.php';
+    ?>
+    <style>
+        [v-cloak] {
+            display: none;
+        }
+    </style>
+</head>
+
+<body>
+    <?
+    $redirect = $_SERVER['PHP_SELF'];
+    include "import/html_header.php";
+    // 200.0.0.1/checador_otros/admin_checdor/[this_page].php => ruta = [this_page].php
+    global $user;
+    html_header(
+        "Registro de asistencia - Vicerrectoría Académica",
+        "Sistema de gestión de checador",
+    );
+    ?>
+    <main class="container-fluid px-4" id="app" v-cloak @vue:mounted="mounted">
+        <!-- error messages -->
+        <div class="container mb-4 mt-2">
+            <div class="row">
+                <div class="col-12">
+                    <div class="alert alert-dismissible fade show" role="alert" v-for="message in messages.data"
+                        :class="`alert-${message.color}`" :key="message.hora">
+                        <!-- messages: {error, hora} -->
+                        <div :key="message" class="d-flex justify-content-between">
+                            <span>
+                                <code>[{{message.hora}}]</code>
+                                <strong>{{message.prefix}}</strong>
+                            </span>
+                            {{ message.message }}
+                            <button type="button" class="close"
+                                @click="messages.data.splice(messages.data.indexOf(message), 1)" data-dismiss="alert">
+                                <span aria-hidden="true">&times;</span>
+                            </button>
+
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!-- filtros -->
+        <div v-if="store.rutas.data.length > 0">
+            <div class="card mt-4">
+                <div class="card-header bg-dark d-flex justify-content-between align-items-center flex-wrap text-white">
+                    <h2 class="col-md-10 col-12 text-white font-weight-bold text-uppercase text-center">
+                        {{ JSON.parse(store.rutas.data.find(ruta => ruta.salon_id === store.rutas.selected)?.salon_array
+                        ?? null)?.splice(1)?.join('/') ?? 'No datos' }}
+                    </h2>
+                    <div>
+                        <button type="button" class="btn btn-info btn-sm"
+                            @click="store.rutas.data = []; header = 'Seleccione una ruta'">
+                            <i class="ing-flecha ing-rotate-180"></i>
+                        </button>
+                        <button type="button" class="btn btn-success btn-sm" data-toggle="modal"
+                            data-target="#editar-ubicaciones">
+                            <i class="ing-editar"></i>
+                        </button>
+                    </div>
+                </div>
+                <div class="card-body bg-info">
+                    <div class="container-fluid">
+                        <div class="row flex-nowrap mw-100 overflow-auto">
+                            <!-- size big -->
+                            <div class="mx-2 my-2 col-auto" v-for="ruta in store.rutas.data" :key="ruta.salon_id">
+                                <span class="shadow badge badge-pill py-2 px-4" @click="store.selectRuta(ruta.salon_id)"
+                                    :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 }">
+                                    {{ JSON.parse(ruta.salon_array).splice(1).join('/') }}
+                                    <span class="badge mx-3"
+                                        v-if="ruta.horarios.some(({estado_supervisor_id}) => !estado_supervisor_id)"
+                                        :class="{ 'badge-success': ruta.horarios.length > 0, 'badge-danger': ruta.horarios.length == 0 }">
+                                        {{ ruta.horarios.filter(({estado_supervisor_id}) => estado_supervisor_id).length
+                                        }} / {{
+                                        ruta.horarios.length }}
+                                    </span>
+                                    <span v-else class="badge mx-3"
+                                        :class="{ 'badge-success': ruta.horarios.length > 0, 'badge-danger': ruta.horarios.length == 0 }">
+
+                                        <i class="ing-aceptar"></i>
+                                    </span>
+                                    <span class="sr-only">
+                                        Faltan {{ ruta.horarios.filter(({estado_supervisor_id}) =>
+                                        estado_supervisor_id).length }} horarios
+                                        por registrar
+                                    </span>
+
+                                    <span class="badge mx-1 badge-warning" @click="location.hash = '#sin-internet'"
+                                        v-if="ruta.horarios.some(({pendiente}) => pendiente)">
+                                        <i class="ing-importante2"></i>
+                                    </span>
+                                </span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="card-footer bg-dark d-flex justify-content-between align-items-center flex-wrap text-white">
+                    <button class="btn btn-info" :disabled="store.bloquesHorario.selected == 0"
+                        @click="store.selectBloque(store.bloquesHorario.selected - 1); rutas(current_espacio)">
+                        <i class="ing-caret ing-rotate-90"></i>
+                        <span class="d-none d-md-inline-block">
+                            Bloque horario anterior
+                        </span>
+                    </button>
+
+                    <h3 class="text-white font-weight-bold text-uppercase text-center">
+                        {{ store.hora_inicio.slice(0, 5) }} - {{ store.hora_fin.slice(0, 5) }}
+                    </h3>
+
+                    <button class="btn btn-info"
+                        @click="store.selectBloque(store.bloquesHorario.selected + 1); rutas(current_espacio)"
+                        :disabled="store.bloquesHorario.selected == store.bloquesHorario.data.length - 1">
+                        <span class="d-none d-md-inline-block">
+                            Bloque horario siguiente
+                        </span>
+                        <i class="ing-caret ing-rotate-270"></i>
+                    </button>
+                </div>
+            </div>
+            <section id="#warnings" class="mt-4" v-if="clases.some(clase => clase.pendiente)">
+                <div class="alert alert-warning" role="alert">
+                    <h4 class="alert-heading"><i class="ing-importante2"></i> Sin conexión a internet</h4>
+                    <p>
+                        Hay datos en esta ruta que no pudieron guardarse, por favor, revise su conexión a internet y dé
+                        click en
+                        <button class="btn btn-outline-dark btn-sm mb-4" @click="guardarCambios"><i
+                                class="ing-guardar"></i> Guardar cambios</button>
+                    </p>
+                    <hr>
+                    <p class="mb-0">
+                        Los datos se mantendrán mientras tenga la página abierta, pero si la cierra o la refresca, se
+                        perderán.
+                    </p>
+                </div>
+            </section>
+            <div class="mt-3 d-flex justify-content-center">
+                <!-- refresh -->
+                <div class="table-responsive">
+                    <table class="table table-hover table-striped table-bordered table-sm">
+                        <thead class="thead-dark">
+                            <tr>
+                                <th scope="col" class="text-center align-middle text-nowrap px-2">
+                                    <button @click="invertir" class="btn btn-info mr-3" v-if="clases.length > 0">
+                                        <i class="ing-cambiar ing-rotate-90"></i>
+                                    </button>
+                                    Salón
+                                </th>
+                                <th scope="col" class="text-center align-middle text-nowrap px-2">Profesor</th>
+                                <th scope="col" class="text-center align-middle text-nowrap px-2">Horario</th>
+                                <th scope="col" class="text-center align-middle text-nowrap px-2">Acciones</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr v-if="clases.length == 0">
+                                <td colspan="6" class="text-center">No hay clases en este horario</td>
+                            </tr>
+                            <tr v-for="clase in clases" :key="clase.horario_id">
+                                <td class="text-center align-middle">{{ clase.salon }}</td>
+                                <td class="text-center align-middle">
+                                    <div class="col-12">
+                                        {{ clase.profesor_nombre }}
+                                    </div>
+                                    <div class="col-12">
+                                        <button type="button" class="btn btn-outline-dark btn-sm"
+                                            @click="store.profesor_selected = clase.horario_id" data-toggle="modal"
+                                            data-target="#ver-detalle">
+                                            Ver detalle <i class="ing-ojo"></i>
+                                        </button>
+                                    </div>
+
+
+                                </td>
+                                <td class="text-center align-middle">
+                                    {{ clase.hora_inicio.slice(0, 5) }} - {{ clase.hora_fin.slice(0, 5) }}
+                                </td>
+                                <td class="text-center align-middle text-nowrap">
+                                    <!-- data-toggle="button" -->
+                                    <button class="btn text-center mx-2" v-for="estado in estados" :key="estado.id"
+                                        @click="store.cambiarEstado(clase.horario_id, estado.id === clase.estado_supervisor_id ? null : estado.id)"
+                                        :class="[{'active': estado.id === clase.estado_supervisor_id}, `btn-outline-${estado.color}`]"
+                                        :aria-pressed="estado.id === clase.estado_supervisor_id">
+                                        <i :class="estado.icon"></i>
+                                    </button>
+
+                                    <button class="btn btn-outline-primary text-center mx-2" data-toggle="modal"
+                                        data-target="#editar-comentario" :class="{ 'active': clase.comentario }"
+                                        @click="store.selectEditor(clase.horario_id)">
+                                        <i class="ing-editar"></i>
+                                        <span class="badge badge-pill badge-primary" v-if="clase.comentario">...</span>
+                                        <span class="sr-only">Editar comentario</span>
+                                    </button>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+
+            <button class="btn btn-primary btn-lg btn-block mb-4" @click="guardarCambios">
+                <i class="ing-guardar"></i>
+                Guardar cambios
+            </button>
+
+        </div>
+        <div v-else-if="store.bloquesHorario.selected === -1">
+            <div class="list-group my-4 container">
+                <div class="card text-center">
+                    <div class="card-header bg-dark text-white">
+                        <h2 class="text-center">
+                            {{header}}
+                        </h2>
+                    </div>
+                    <div class="card-body" v-if="!loading">
+                        <a :href="`#horario-${horario.id}`" class="list-group-item list-group-item-action"
+                            v-for="horario in store.bloquesHorario.data" :key="horario.id"
+                            @click="store.bloquesHorario.selected = store.bloquesHorario.data.indexOf(horario)">
+                            <div class="d-flex w-100 justify-content-between">
+                                <h5 class="mb-1">{{ horario.hora_inicio.slice(0, 5) }} - {{horario.hora_fin.slice(0, 5)
+                                    }}</h5>
+                            </div>
+                        </a>
+                    </div>
+                    <div class="card-body" v-else>
+                        <div class="d-flex justify-content-center">
+                            <div class="spinner-border text-primary" role="status">
+                                <span class="sr-only">Cargando...</span>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="card-footer text-muted bg-dark text-white">
+                        Lista de bloques horario
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div v-else>
+            <div class="list-group my-4 container">
+                <div class="card text-center">
+                    <div class="card-header bg-dark text-white">
+                        <h2 class="text-center">
+                            {{header}}
+                        </h2>
+                    </div>
+                    <div class="card-body" v-if="!loading">
+                        <a :href="`#ruta-${ruta.id_espacio_sgu}`" class="list-group-item list-group-item-action"
+                            v-for="ruta in catálogo_rutas.data" :key="ruta.salon_id" @click="rutas(ruta.id_espacio_sgu)"
+                            disabled>
+                            <div class="d-flex w-100 justify-content-between">
+                                <h5 class="mb-1">{{ ruta.salon }}</h5>
+                                <small v-if="ruta.subrutas.length > 0">{{ ruta.subrutas.length }} espacios</small>
+                                <small v-else class="text-danger">Sin espacios</small>
+                            </div>
+                        </a>
+                    </div>
+                    <div class="card-body" v-else>
+                        <div class="d-flex justify-content-center">
+                            <div class="spinner-border text-primary" role="status">
+                                <span class="sr-only">Cargando...</span>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="card-footer text-muted bg-dark text-white">
+                        Rutas de la Universidad La Salle
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!-- MODAL -->
+        <div class="modal" tabindex="-1" id="editar-ubicaciones">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">Editar rutas</h5>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <div class="container">
+                            <h2>Reordena las rutas</h2>
+                            <ul id="sortable" class="list-group">
+                                <li class="list-group-item" v-for="ruta in store.rutas.data" :key="ruta.salon_id"
+                                    :id="'ruta-' + ruta.salon_id"
+                                    :class="[ruta.horarios.every(horario => horario.estado_supervisor_id) ? ['disabled', 'bg-light', 'undraggable'] : '']">
+                                    {{ JSON.parse(ruta.salon_array).join('/') }}
+                                </li>
+                            </ul>
+                        </div>
+
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="modal" tabindex="-1" id="editar-comentario">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">Añadir comentario</h5>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <div class="container">
+                            <h2 class="text-center">Comentarios de la clase</h2>
+                            <br>
+                            <div class="input-group">
+                                <div class="input-group-prepend">
+                                    <span class="input-group-text bg-primary text-white">Comentario
+                                        <button class="btn btn-light ml-2 text-primary"
+                                            @click="store.limpiarComentario">
+                                            <i class="ing-borrar"></i>
+                                        </button>
+                                    </span>
+                                </div>
+                                <textarea class="form-control" aria-label="Comentarios de la clase"
+                                    v-model="store.editor.texto"></textarea>
+                            </div>
+                        </div>
+
+                    </div>
+                    <div class="modal-footer">
+                        <button type="button" class="btn btn-outline-danger" data-dismiss="modal">
+                            <i class="ing-cancelar"></i>
+                            Cancelar
+                        </button>
+                        <button type="button" class="btn btn-primary" data-dismiss="modal"
+                            @click="store.guardarComentario">
+                            Guardar comentario
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="modal" tabindex="-1" id="ver-detalle">
+            <div class="modal-dialog modal-dialog-centered modal-xl" v-if="clase_vista">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h2 class="modal-title" :data-id="clase_vista.horario_id">Detalle de la clase</h2>Detalle de la clase</h2>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <div class="container" v-if="store.profesor_selected">
+                            <div class="row">
+                                <section class="col-12 col-md-6">
+                                    <h4 class="h4">Profesor</h4>
+                                    <div class="row">
+                                        <div class="col-12">
+                                            <strong>Nombre:</strong>
+                                            {{ clase_vista.profesor_nombre }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Correo:</strong>
+                                            <a :href="`mailto:${clase_vista.profesor_correo}`"><strong>{{
+                                                    clase_vista.profesor_correo }}</strong></a>
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Clave:</strong>
+                                            {{ clase_vista.profesor_clave }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Facultad:</strong>
+                                            {{ clase_vista.facultad }}
+                                        </div>
+                                    </div>
+                                </section>
+                                <section class="col-12 col-md-6">
+                                    <h4 class="h4">Clase</h4>
+                                    <div class="row">
+                                        <div class="col-12">
+                                            <strong>Materia:</strong>
+                                            {{ clase_vista.materia }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Carrera:</strong>
+                                            {{ clase_vista.carrera }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Grupo:</strong>
+                                            {{ clase_vista.horario_grupo }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Horario:</strong>
+                                            <!-- hora hh:mm:ss to hh:mm -->
+                                            {{ clase_vista.hora_inicio?.slice(0, 5) }} - {{
+                                            clase_vista.hora_fin?.slice(0, 5) }}
+                                        </div>
+                                        <div class="col-12">
+                                            <strong>Salón:</strong>
+                                            {{ clase_vista.salon }}
+                                        </div>
+                                    </div>
+                                </section>
+                            </div>
+                            <div class="row">
+                                <section class="col-12">
+                                    <h4 class="h4 mt-4">Registro</h4>
+                                    <div class="row">
+                                        <div class="col-12 text-center" v-if="!clase_vista.registro_fecha">
+                                            <strong><span class="badge badge-danger"><i class="ing-cancelar"></i></span>
+                                                El profesor aún no ha registrado su asistencia</strong>
+                                        </div>
+                                        <div class="col-6 text-center" v-else>
+                                            El profesor registró su asistencia a las
+                                            <code>{{clase_vista.registro_fecha.slice(11, 16)}}</code>
+                                            <hr>
+                                            <p v-if="!clase_vista.registro_retardo" class="text-center">
+                                                <span class="badge badge-success"><i class="ing-aceptar"></i></span>
+                                                A tiempo
+                                            </p>
+                                            <p v-else class="text-center">
+                                                <span class="badge badge-warning"><i class="ing-retardo"></i></span>
+                                                Con retardo
+                                            </p>
+                                        </div>
+                                    </div>
+                                </section>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="modal-footer">
+                        <!-- botón aceptar -->
+                        <button type="button" class="btn btn-outline-primary" data-dismiss="modal">
+                            <i class="ing-aceptar"></i>
+                            Aceptar
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        </div>
+    </main>
+    <?php
+    include "import/html_footer.php";
+    ?>
+    <!-- filtro modal -->
+    <script src="js/jquery.min.js"></script>
+    <script src="js/jquery-ui.js"></script>
+    <script src="js/jquery-ui.touch-punch.min.js"></script>
+    <script src="js/bootstrap/bootstrap.min.js"></script>
+    <?php include_once 'js/messages.php'; ?>
+    <script src="https://unpkg.com/petite-vue"></script>
+    <script>
+        const estados = [
+            {
+                color: "success",
+                icon: "ing-autorizar",
+                id: 1,
+            },
+            {
+                color: "danger",
+                icon: "ing-negar",
+                id: 2,
+            },
+            {
+                color: "warning",
+                icon: "ing-retardo",
+                id: 3,
+            },
+            {
+                color: "info",
+                icon: "ing-justificar",
+                id: 4,
+            },
+        ];
+
+        const messages = PetiteVue.reactive({
+            data: [],
+            push_message(message, silent = false) {
+                if (silent) {
+                    console.log(message);
+                    return
+                }
+                // go to the top
+                window.scrollTo({
+                    top: 0,
+                    behavior: 'smooth'
+                });
+
+                this.data.push(message);
+                setTimeout(() => {
+                    this.data.pop();
+                }, 5000);
+            },
+        });
+        const store = PetiteVue.reactive({
+            messages,
+            bloquesHorario: {
+                data: [],
+                selected: 0
+            },
+            rutas: {
+                data: [],
+                selected: 0
+            },
+            editor: {
+                id: 0,
+                texto: "",
+            },
+            get hora_inicio() {
+                return this.bloquesHorario.data[this.bloquesHorario.selected]?.hora_inicio ?? "";
+            },
+            get hora_fin() {
+                return this.bloquesHorario.data[this.bloquesHorario.selected]?.hora_fin ?? "";
+            },
+
+            selectRuta(index) {
+                this.rutas.selected = index;
+            },
+            order() {
+                const finals = this.rutas.data.filter(ruta => ruta.horarios.length > 0 && ruta.horarios.every(horario => horario.estado_supervisor_id));
+                const lasts = this.rutas.data.filter(ruta => ruta.horarios.length == 0);
+                const notLasts = this.rutas.data.filter(ruta => ruta.horarios.some(horario => !horario.estado_supervisor_id));
+                // console.log("finals", finals, "lasts", lasts, "notLasts", notLasts)
+                this.rutas.data = [...notLasts, ...finals, ...lasts];
+            },
+            // clases
+            selectBloque(bloqueIndex) {
+                this.bloquesHorario.selected = bloqueIndex;
+            },
+
+            // estado
+            async cambiarEstado(horario_id, estadoId) {
+                const ruta = store.rutas.data.find(ruta => ruta.salon_id == this.rutas.selected);
+                const clase = ruta.horarios.find(clase => clase.horario_id == horario_id);
+                clase.estado_supervisor_id = estadoId;
+
+                try {
+                    if (!navigator.onLine) {
+                        clase.pendiente = true;
+                        throw ("No hay conexión a internet");
+                    }
+
+                    const cambio = await fetch("action/registro_supervisor.php", {
+                        method: "POST",
+                        headers: {
+                            "Content-Type": "application/json"
+                        },
+                        body: JSON.stringify([{
+                            horario_id: horario_id,
+                            estado: estadoId,
+                            profesor_id: clase.profesor_id,
+                            comentario: clase.comentario,
+                            supervisor_id: <?= $user->user['id'] ?>,
+                        }])
+                    }).then(res => res.json());
+
+                    if (cambio.error) throw cambio.error;
+                    clase.pendiente = false;
+
+                } catch (error) {
+                    messages.push_message({
+                        message: error,
+                        hora: new Date().toLocaleTimeString('es-MX', { timeZone: 'America/Mexico_City' }),
+                        color: "danger",
+                        prefix: "Error",
+                    }, true);
+                }
+                // scroll to the top only if this ruta has no clases with estado 0
+                if (ruta.horarios.every(clase => clase.estado_supervisor_id != null))
+                    window.scrollTo({
+                        top: 0,
+                        behavior: 'smooth'
+                    });
+
+                this.order();
+            },
+
+            // editor
+            selectEditor(horario_id) {
+                this.editor.id = horario_id;
+                this.editor.texto = store.rutas.data.find(ruta => ruta.salon_id == this.rutas.selected).horarios.find(clase => clase.horario_id == horario_id).comentario;
+            },
+            guardarComentario() {
+                const ruta = store.rutas.data.find(ruta => ruta.salon_id == this.rutas.selected);
+                const clase = ruta.horarios.find(clase => clase.horario_id == this.editor.id);
+                clase.comentario = this.editor.texto;
+                store.cambiarEstado(clase.horario_id, clase.estado_supervisor_id);
+            },
+            limpiarComentario() {
+                this.editor.texto = "";
+            },
+            profesor_selected: null,
+        });
+
+        $(document).ready(function () {
+            $("#sortable").sortable({
+                update: function (event, ui) {
+                    // get the new order
+                    var newOrder = $(this).children().map(function () {
+                        // id = ruta-{id}
+                        return parseInt(this.id.split('-')[1]);
+
+                    }).get();
+                    // store the new order
+                    store.rutas.data = newOrder.map(function (id) {
+                        return store.rutas.data.find(function (ruta) {
+                            return ruta.salon_id === id;
+                        });
+                    });
+                },
+                items: "li:not(.undraggable)"
+            }).disableSelection();
+
+            $('#sortable>li:not(.undraggable)').draggable({
+                axis: 'y',
+                containment: 'parent',
+            })
+        });
+
+        PetiteVue.createApp({
+            store,
+            messages,
+            header: "Cargando auditoría",
+            loading: true,
+            catálogo_rutas: {
+                data: [],
+                selected: 0
+            },
+            get clases() {
+                const clases = store.rutas.data.find(ruta => ruta.salon_id == store.rutas.selected)?.horarios ?? [];
+                // console.log("All clases", JSON.parse(JSON.stringify(clases)), "Selected: ", store.rutas.selected);
+                return clases;
+            },
+            async guardarCambios() {
+
+                try {
+                    if (!navigator.onLine)
+                        throw "No hay conexión a internet";
+
+                    console.log(store.rutas.data.map(ruta => ruta.horarios).flat(1).filter(clase => clase.pendiente));
+                    const cambio = await fetch("action/registro_supervisor.php", {
+                        method: "POST",
+                        headers: {
+                            "Content-Type": "application/json"
+                        },
+                        body: JSON.stringify(store.rutas.data.map(ruta => ruta.horarios).flat(1).filter(clase => clase.pendiente).map(clase => ({
+                            horario_id: clase.horario_id,
+                            estado: clase.estado_supervisor_id,
+                            profesor_id: clase.profesor_id,
+                            comentario: clase.comentario,
+                            supervisor_id: <?= $user->user['id'] ?>,
+                        }))),
+                    }).then(res => res.json());
+
+                    if (cambio.error) throw cambio.error;
+                } catch (error) {
+                    messages.push_message({
+                        message: error,
+                        hora: new Date().toLocaleTimeString('es-MX', { timeZone: 'America/Mexico_City' }),
+                        color: "danger",
+                        prefix: "Error",
+                    });
+                    return
+                }
+
+                store.rutas.data.map(ruta => ruta.horarios).flat(1).filter(clase => clase.pendiente).forEach(clase => clase.pendiente = false);
+                messages.push_message({
+                    message: "Cambios guardados",
+                    hora: new Date().toLocaleTimeString('es-MX', { timeZone: 'America/Mexico_City' }),
+                    color: "success",
+                    prefix: "Éxito",
+                });
+            },
+            invertir() {
+                this.clases.reverse();
+            },
+            async mounted() {
+                store.bloquesHorario.data = await fetch('action/action_grupo_horario.php').then(res => res.json());
+                store.bloquesHorario.selected = store.bloquesHorario.data.findIndex(bloque => bloque.selected);
+                // console.log(store.bloquesHorario.selected);
+                if (store.bloquesHorario.selected == -1) {
+                    this.header = "Seleccione un horario";
+                }
+                else {
+                    this.header = "Seleccione una ruta";
+                }
+
+
+                this.catálogo_rutas.data = await fetch('action/rutas.php').then(res => res.json());
+                this.loading = false;
+
+            },
+            current_espacio: null,
+            async rutas(id_espacio_sgu) {
+                store.rutas.data = [];
+                store.rutas.selected = 0;
+                this.loading = true;
+                this.current_espacio = id_espacio_sgu;
+                this.loading = true;
+                this.header = `Cargando rutas para ${this.catálogo_rutas.data.find(ruta => ruta.id_espacio_sgu == id_espacio_sgu).salon}`;
+                this.catálogo_rutas.selected = id_espacio_sgu;
+                const url = 'action/rutas_salón_horario.php'
+                const searchParams = new URLSearchParams({
+                    id_espacio_sgu: id_espacio_sgu,
+                    bloque_horario_id: store.bloquesHorario.data[store.bloquesHorario.selected].id
+                });
+                const rutas = await fetch(`${url}?${searchParams}`).then(res => res.json());
+                store.rutas.data = rutas.filter(ruta => ruta.horarios.length > 0);
+
+                if (store.rutas.data.length == 0) {
+                    this.header = `No hay clases en este horario`;
+                    this.loading = false;
+                    return
+                }
+
+                store.rutas.selected = store.rutas.data[0].salon_id;
+
+                store.order();
+                // inject horarios
+                this.loading = false;
+
+            },
+            get clase_vista() {
+                return this.clases.find(clase => clase.horario_id == store.profesor_selected) ?? false;
+            },
+        }).mount('#app')
+    </script>
+</body>
+
+</html>

+ 237 - 0
ts/auditoría.ts

@@ -0,0 +1,237 @@
+import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'
+import { text } from 'stream/consumers';
+type Registro = {
+    carrera: string;
+    carrera_id: number;
+    comentario: null;
+    dia: string;
+    duracion: string;
+    duracion_id: number;
+    estado_supervisor_id: number;
+    facultad: string;
+    facultad_id: number;
+    horario_dia: number;
+    horario_fin: string;
+    horario_grupo: string;
+    horario_hora: string;
+    horario_id: number;
+    materia: string;
+    materia_id: number;
+    nombre: string;
+    periodo: string;
+    periodo_id: number;
+    profesor_clave: string;
+    profesor_correo: string;
+    profesor_grado: null;
+    profesor_id: number;
+    profesor_nombre: string;
+    registro_fecha: null;
+    registro_fecha_ideal: Date;
+    registro_fecha_supervisor: Date;
+    registro_id: number;
+    registro_justificada: null;
+    registro_retardo: null;
+    salon: string;
+    salon_id: number;
+    supervisor_id: number;
+}
+
+type Estado = {
+    color: string;
+    icon: string;
+    estado_supervisor_id: number;
+}
+
+type Facultad = {
+    clave_dependencia: string;
+    facultad_id: number;
+    facultad_nombre: string;
+}
+
+type Filter = {
+    type: string;
+    value: string;
+    icon: string;
+    field: string;
+    label: string;
+}
+
+const store = reactive({
+    loading: false,
+    current: {
+        comentario: '',
+        clase_vista: null,
+        empty: '',
+    },
+    facultades: {
+        data: [] as Facultad[],
+        async fetch() {
+            this.data = [] as Facultad[]
+            const res = await fetch('action/action_facultad.php')
+            this.data = await res.json()
+        },
+    },
+    filters: {
+        facultad_id: null,
+        fecha: null,
+        fecha_inicio: null,
+        fecha_fin: null,
+        profesor: null,
+        estados: [],
+
+        switchFecha: false,
+        switchFechas() {
+            $(function () {
+                store.filters.fecha_inicio = store.filters.fecha_fin = store.filters.fecha = null
+
+                $("#fecha, #fecha_inicio, #fecha_fin").datepicker({
+                    minDate: -3,
+                    maxDate: new Date(),
+                    dateFormat: "yy-mm-dd",
+                    showAnim: "slide",
+                });
+
+                const fecha = $("#fecha"), inicio = $("#fecha_inicio"), fin = $("#fecha_fin")
+                inicio.on("change", function () {
+                    store.filters.fecha_inicio = inicio.val()
+                    fin.datepicker("option", "minDate", inicio.val());
+                });
+                fin.on("change", function () {
+                    store.filters.fecha_fin = fin.val()
+                    inicio.datepicker("option", "maxDate", fin.val());
+                });
+                fecha.on("change", function () {
+                    store.filters.fecha = fecha.val()
+                });
+            });
+        }
+    },
+    estados: {
+        data: [] as Estado[],
+        async fetch() {
+            this.data = [] as Estado[]
+            const res = await fetch('action/action_estado_supervisor.php')
+            this.data = await res.json()
+        },
+        getEstado(id: number): Estado {
+            return this.data.find((estado: Estado) => estado.estado_supervisor_id === id)
+        },
+        printEstados() {
+            if (store.filters.estados.length > 0)
+                document.querySelector('#estados')!.innerHTML = store.filters.estados.map((estado: number) =>
+                    `<span class="mx-2 badge badge-${store.estados.getEstado(estado).estado_color}">
+                    <i class="${store.estados.getEstado(estado).estado_icon}"></i> ${store.estados.getEstado(estado).nombre}
+                </span>`
+                ).join('')
+            else
+                document.querySelector('#estados')!.innerHTML = `Todos los registros`
+        }
+    },
+    toggle(arr: any, element: any) {
+        const newArray = arr.includes(element) ? arr.filter((item: any) => item !== element) : [...arr, element]
+        // if all are selected, then unselect all
+        if (newArray.length === this.estados.data.length) return []
+        return newArray
+    },
+})
+
+declare var $: any
+
+
+
+type Profesor = {
+    profesor_id: number;
+    profesor_nombre: string;
+    profesor_correo: string;
+    profesor_clave: string;
+    profesor_grado: string;
+}
+
+createApp({
+    store,
+    get clase_vista() {
+        return store.current.clase_vista
+    },
+    registros: {
+        data: [] as Registro[],
+        async fetch() {
+            this.loading = true
+            this.data = [] as Registro[]
+            const res = await fetch('action/action_auditoria.php')
+            this.data = await res.json()
+            this.loading = false
+        },
+        invertir() {
+            this.data = this.data.reverse()
+        },
+        mostrarComentario(registro_id: number) {
+            const registro = this.data.find((registro: Registro) => registro.registro_id === registro_id)
+            store.current.comentario = registro.comentario
+            $('#ver-comentario').modal('show')
+        },
+
+        get relevant() {
+            /* 
+                facultad_id: null,
+                fecha: null,
+                fecha_inicio: null,
+                fecha_fin: null,
+                profesor: null,
+                asistencia: null,
+                estado_id: null,
+                if one of the filters is null, then it is not relevant
+    
+            */
+            const filters = Object.keys(store.filters).filter((filtro) => store.filters[filtro] || store.filters[filtro]?.length > 0)
+            return this.data.filter((registro: Registro) => {
+                return filters.every((filtro) => {
+                    switch (filtro) {
+                        case 'fecha':
+                            return registro.registro_fecha_ideal === store.filters[filtro];
+                        case 'fecha_inicio':
+                            return registro.registro_fecha_ideal >= store.filters[filtro];
+                        case 'fecha_fin':
+                            return registro.registro_fecha_ideal <= store.filters[filtro];
+                        case 'profesor':
+                            const textoFiltro = store.filters[filtro].toLowerCase();
+                            if (/^\([^)]+\)\s[\s\S]+$/.test(textoFiltro)) {
+                                const clave = registro.profesor_clave.toLowerCase();
+                                const filtroClave = textoFiltro.match(/\((.*?)\)/)?.[1];
+                                console.log(clave, filtroClave);
+                                return clave.includes(filtroClave);
+                            } else {
+                                const nombre = registro.profesor_nombre.toLowerCase();
+                                return nombre.includes(textoFiltro);
+                            }
+                        case 'facultad_id':
+                            return registro.facultad_id === store.filters[filtro];
+                        case 'estados':
+                            if (store.filters[filtro].length === 0) return true;
+                            return store.filters[filtro].includes(registro.estado_supervisor_id);
+                        default:
+                            return true;
+                    }
+                })
+            })
+        },
+    },
+    get profesores() {
+        return this.registros.data.map((registro: Registro) => (
+            {
+                profesor_id: registro.profesor_id,
+                profesor_nombre: registro.profesor_nombre,
+                profesor_correo: registro.profesor_correo,
+                profesor_clave: registro.profesor_clave,
+                profesor_grado: registro.profesor_grado,
+            }
+        )).sort((a: Profesor, b: Profesor) =>
+            a.profesor_nombre.localeCompare(b.profesor_nombre)
+        )
+    },
+    async mounted() {
+        await this.registros.fetch()
+        await store.facultades.fetch()
+        await store.estados.fetch()
+        store.filters.switchFechas()
+    }
+}).mount('#app')

+ 149 - 0
ts/client.ts

@@ -0,0 +1,149 @@
+// @ts-ignore Import module
+import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'
+
+export interface PeridoV1 {
+    IdNivel: number;
+    IdPeriodo: number;
+    NombreNivel: string;
+    NombrePeriodo: string;
+    in_db: boolean;
+    linked: boolean;
+}
+
+
+export interface PeridoV2 {
+    ClaveCarrera: string;
+    FechaFin: string;
+    FechaInicio: string;
+    IdPeriodo: number;
+    NombreCarrera: string;
+}
+
+const webServices = {
+    getPeriodosV1: async (): Promise<PeridoV1[]> => {
+        try {
+            const response = await fetch('periodos.v1.php');
+            return await response.json();
+        } catch (error) {
+            console.log(error);
+            return [];
+        }
+    },
+    getPeriodosV2: async (): Promise<PeridoV2[]> => {
+        try {
+            const response = await fetch('periodos.v2.php');
+            return await response.json();
+        } catch (error) {
+            console.log(error);
+            return [];
+        }
+    }
+}
+
+const store = reactive({
+    periodosV1: [] as PeridoV1[],
+    periodosV2: [] as PeridoV2[],
+
+    errors: [] as string[],
+    fechas(idPeriodo: number): { inicio: string, fin: string } {
+        const periodo = this.periodosV2.find((periodo: PeridoV2) => periodo.IdPeriodo === idPeriodo);
+        return {
+            inicio: periodo ? periodo.FechaInicio : '',
+            fin: periodo ? periodo.FechaFin : ''
+        }
+    },
+    periodov1(idPeriodo: number): PeridoV1 | undefined {
+        return this.periodosV1.find((periodo: PeridoV1) => periodo.IdPeriodo === idPeriodo);
+    },
+    periodov2(idPeriodo: number): PeridoV2[] {
+        return this.periodosV2.filter((periodo: PeridoV2) => periodo.IdPeriodo === idPeriodo);
+    },
+    async addPeriodo(periodo: PeridoV1 | PeridoV2) {
+        try {
+            const result = await fetch('backend/periodos.php', {
+                method: 'POST',
+                body: JSON.stringify({
+                    ...periodo,
+                    ...this.fechas(periodo.IdPeriodo)
+                }),
+                headers: {
+                    'Content-Type': 'application/json'
+                }
+            }).then((response) => response.json());
+            if (result.success) {
+                this.periodosV1 = this.periodosV1.map((periodoV1: PeridoV1) => {
+                    if (periodoV1.IdPeriodo === periodo.IdPeriodo) {
+                        periodoV1.in_db = true;
+                    }
+                    return periodoV1;
+                });
+                return result;
+            }
+            else {
+                this.errors.push(result.message);
+            }
+        } catch (error) {
+            this.errors.push(error);
+        }
+    },
+
+    async addCarreras(idPeriodo: number) {
+        try {
+            const periodoV1 = this.periodov1(idPeriodo) as PeridoV1;
+            const periodoV2 = this.periodov2(idPeriodo) as PeridoV2[];
+
+            const data = periodoV2.map(({ ClaveCarrera, NombreCarrera }: PeridoV2) =>
+            ({
+                ClaveCarrera: ClaveCarrera,
+                NombreCarrera: NombreCarrera,
+                IdNivel: periodoV1.IdNivel,
+            })
+            );
+
+            const result = await fetch('backend/carreras.php', {
+                method: 'POST',
+                body: JSON.stringify(data),
+                headers: {
+                    'Content-Type': 'application/json'
+                }
+            }).then((response) => response.json());
+            if (result.success) {
+                await webServices.getPeriodosV1().then((periodosV1) => {
+                    this.periodosV1 = periodosV1;
+                });
+
+                await webServices.getPeriodosV2().then((periodosV2) => {
+                    this.periodosV2 = periodosV2;
+                });
+            }
+
+        } catch (error) {
+            this.errors.push(error);
+        }
+    }
+})
+
+
+createApp({
+    store,
+    info(IdPeriodo: number): PeridoV2 {
+        const periodo = store.periodosV2.find((periodo: PeridoV2) => periodo.IdPeriodo === IdPeriodo &&
+            periodo.FechaInicio != '' && periodo.FechaFin != '');
+        return periodo
+    },
+    complete(IdPeriodo: number): boolean {
+        const info = this.info(IdPeriodo);
+        return info !== undefined;
+    },
+    mounted: async () => {
+
+        await webServices.getPeriodosV1().then((periodosV1) => {
+            store.periodosV1 = periodosV1;
+        });
+
+        await webServices.getPeriodosV2().then((periodosV2) => {
+            store.periodosV2 = periodosV2;
+        });
+
+    }
+}).mount()

+ 0 - 347
ts/consulta_horarios.ts

@@ -1,347 +0,0 @@
-// initial state
-const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
-const horas_estándar = /* range from 7 to 22 */ Array.from(Array(22 - 7 + 1).keys()).map(x => x + 7);
-// fill the table with empty cells
-for (let i = 0; i < horas_estándar.length - 1; i++) {
-    const hora = horas_estándar[i];
-    const tr = document.createElement("tr");
-    tr.id = `hora-${hora}-00`;
-    tr.classList.add(hora > 13 ? "tarde" : "mañana");
-    const th = document.createElement("th");
-    th.classList.add("text-center");
-    th.scope = "row";
-    th.rowSpan = 4;
-    th.innerText = `${hora}:00`;
-    th.style.verticalAlign = "middle";
-    tr.appendChild(th);
-    for (let j = 0; j < días.length; j++) {
-        const día = días[j];
-        const td = document.createElement("td");
-        td.id = `hora-${hora}-00-${día}`;
-        tr.appendChild(td);
-    }
-    document.querySelector("tbody#horario")?.appendChild(tr);
-
-    // add 7 rows for each hour
-    const hours = [15, 30, 45];
-    for (let j = 1; j < 4; j++) {
-        const tr = document.createElement("tr");
-        tr.id = `hora-${hora}-${hours[j - 1]}`;
-        tr.classList.add(hora > 13 ? "tarde" : "mañana");
-        for (let k = 0; k < días.length; k++) {
-            const día = días[k];
-            const td = document.createElement("td");
-            td.id = `hora-${hora}-${hours[j - 1]}-${día}`;
-            // td.innerText = `hora-${hora}-${hours[j - 1]}-${día}`;
-            tr.appendChild(td);
-        }
-        document.querySelector("tbody#horario")?.appendChild(tr);
-    }
-}
-
-// add an inital height to the table cells
-const tds = document.querySelectorAll<HTMLTableRowElement>("tbody#horario td");
-tds.forEach(td => td.style.height = "2rem");
-
-var table = document.querySelector("table") as HTMLTableElement;
-var empty_table = table?.innerHTML || "";
-
-// hide the table
-table.style.display = "none";
-
-
-document.getElementById('dlProfesor')?.addEventListener('input', function () {
-    var input = document.getElementById('dlProfesor') as HTMLInputElement;
-    var value = input.value;
-    var option = document.querySelector(`option[value="${value}"]`);
-    if (option) {
-        var id: string = option.getAttribute('data-id')!;
-        const input_profesor: HTMLInputElement = document.getElementById('editor_profesor') as HTMLInputElement;
-        input_profesor.value = id;
-
-        // remove is invalid class
-        input.classList.remove("is-invalid");
-
-        // add is valid class
-        input.classList.add("is-valid");
-    } else {
-        const input_profesor: HTMLInputElement = document.getElementById('editor_profesor') as HTMLInputElement;
-        input_profesor.value = "";
-        // remove is valid class
-        input.classList.remove("is-valid");
-
-        // add is invalid class
-        input.classList.add("is-invalid");
-    }
-});
-
-
-/**
- * Functions and Methods
- **/
-
-const buscarGrupo = async () => {
-
-    // Add loading animation in the button
-    const btn = document.querySelector("#btn-buscar") as HTMLButtonElement;
-    btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Cargando...';
-    btn.disabled = true;
-
-
-    const carrera = document.querySelector<HTMLInputElement>("#filter_carrera")?.value as string;
-    const grupo = document.querySelector<HTMLInputElement>("#filter_grupo")?.value as string;
-    console.log(`Carrera: ${carrera}, Grupo: ${grupo}`);
-
-    if (carrera == "" || grupo == "") {
-        triggerMessage("El nombre del grupo y la carrera son requeridos", "Faltan campos");
-
-        // Remove loading animation in the button
-        btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
-        btn.disabled = false;
-
-        return;
-    }
-    const formData = new FormData();
-
-    formData.append("carrera", carrera);
-    formData.append("grupo", grupo);
-    formData.append("periodo", document.querySelector<HTMLInputElement>("#periodo")?.value!);
-    const thisScript = document.currentScript as HTMLScriptElement;
-    const facultad = thisScript.getAttribute("data-facultad") as string;
-    formData.append('facultad', facultad);
-
-    try {
-        const response = await fetch("action/action_horario.php", {
-            method: "POST",
-            body: formData
-        }).then(res => res.json());
-        if (response.status == "success") {
-            let limits = {
-                min: 22,
-                max: 7
-            };
-            let sábado = false;
-            const horario = response.horario;
-            // show the table
-            table.style.display = "table";
-
-            // clear the table
-            table.innerHTML = empty_table;
-
-            // fill the table
-            for (let i = 0; i < horario.length; i++) {
-                const dia = horario[i].dia;
-                if (dia == "sábado") sábado = true;
-                const {
-                    hora,
-                    minutos
-                } = {
-                    hora: parseInt(horario[i].hora.split(":")[0]),
-                    minutos: horario[i].hora.split(":")[1]
-                }
-
-                // update the limits
-                if (hora < limits.min) {
-                    limits.min = hora;
-                }
-                if (hora > limits.max) {
-                    limits.max = hora;
-                }
-
-                const materia = horario[i].materia;
-                const profesor = horario[i].profesor;
-                const salon = horario[i].salon;
-                const id = horario[i].horario_id;
-                const prof_id = horario[i].profesor_id;
-
-                const cell = document.querySelector(`#hora-${hora}-${minutos}-${dia}`) as HTMLTableCellElement;
-                cell.innerHTML =
-                    `
-                    <div>
-                        <small class="text-gray">${hora}:${minutos}</small> 
-                        <b class="title">${materia}</b> <br>
-                        <br><span>Salón: </span>${salon} <br>
-                        <span class="ing ing-formacion mx-1"></span>${profesor}
-                    </div>
-                    `;
-
-
-                const html = `<div class="menu-flotante p-2">
-                            <a
-                                class="mx-2"
-                                href="#"
-                                data-toggle="modal"
-                                data-target="#modal-editar"
-                                data-dia="${dia}"
-                                data-hora="${hora}:${minutos}"
-                                data-materia="${materia}"
-                                data-profesor="${prof_id}"
-                                data-salon="${salon}"
-                                data-id="${id}"
-                            >
-                                <i class="ing-editar ing"></i>
-                            </a>
-                            <a
-                                class="mx-2"
-                                href="#"
-                                data-toggle="modal"
-                                data-target="#modal-borrar"
-                                data-hoario_id="${id}"
-                            >
-                                <i class="ing-basura ing"></i>
-                            </a>
-                        </div>`;
-
-                const td = cell.closest("td") as HTMLTableCellElement;
-                td.innerHTML += html;
-                td.classList.add("position-relative");
-
-                // this cell spans 4 rows
-                cell.rowSpan = 6;
-                cell.classList.add("bloque-clase", "overflow");
-
-                for (let j = 1; j < 6; j++) {
-                    const minute = (parseInt(minutos) + j * 15)
-                    const next_minute = (minute % 60).toString().padStart(2, "0");
-                    const next_hour = (hora + Math.floor(minute / 60))
-                    const next_cell = document.querySelector(`#hora-${next_hour}-${next_minute}-${dia}`) as HTMLTableCellElement;
-                    next_cell.remove();
-                }
-            }
-
-            // remove the elements that are not in the limits
-            const horas = document.querySelectorAll("tbody#horario tr") as NodeListOf<HTMLTableRowElement>;
-            for (let i = 0; i < horas.length; i++) {
-                const hora = horas[i];
-                const hora_id = parseInt(hora.id.split("-")[1]);
-                if (hora_id < limits.min || hora_id > limits.max) {
-                    hora.remove();
-                }
-            }
-
-            // if there is no sábado, remove the column
-            if (!sábado) {
-                document.querySelectorAll("tbody#horario td").forEach(td => {
-                    if (td.id.split("-")[3] == "sábado") {
-                        td.remove();
-                    }
-                });
-
-                // remove the header (the last)
-                const headers = document.querySelector("#headers") as HTMLTableRowElement;
-                headers.lastElementChild?.remove();
-            }
-
-            // adjust width
-            const ths = document.querySelectorAll("tr#headers th") as NodeListOf<HTMLTableHeaderCellElement>;
-            const width = 95 / (ths.length - 1);
-
-            ths.forEach((th, key) =>
-                th.style.width = (key == 0) ? "5%" : `${width}%`
-            );
-
-            // search item animation
-            const menúFlontantes = document.querySelectorAll(".menu-flotante");
-            menúFlontantes.forEach((element) => {
-                element.classList.add("d-none");
-                element.parentElement?.addEventListener("mouseover", () => {
-                    element.classList.remove("d-none");
-                });
-                element.parentElement?.addEventListener("mouseout", () => {
-                    element.classList.add("d-none");
-                });
-            });
-
-        } else {
-            triggerMessage(response.message, "Error");
-            // Remove loading animation in the button
-            btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
-            btn.disabled = false;
-        }
-
-    } catch (error) {
-        triggerMessage("Error al cargar el horario", "Error");
-        triggerMessage(error, "Error");
-    }
-
-    // Remove loading animation in the button
-    btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
-    btn.disabled = false;
-}
-
-async function guardar(id: string) {
-    interface Data {
-        hora: HTMLInputElement;
-        dia: HTMLInputElement;
-        profesor: HTMLInputElement;
-        salon: HTMLInputElement;
-    }
-    const btn: HTMLButtonElement = document.querySelector("#btn-guardar") as HTMLButtonElement;
-    const clone: HTMLButtonElement = btn.cloneNode(true) as HTMLButtonElement;
-    btn.innerHTML = '<i class="ing-cargando ing"></i> Guardando...';
-    btn.disabled = true;
-
-    const data: Data = {
-        hora: document.querySelector("#editor_hora") as HTMLInputElement,
-        dia: document.querySelector("#editor_dia") as HTMLInputElement,
-        salon: document.querySelector("#editor_salón") as HTMLInputElement,
-        profesor: document.querySelector("#editor_profesor") as HTMLInputElement,
-    };
-
-    const hora = data.hora.value; // h:mm
-    const { compareHours } = await import('./date_functions');
-    const hora_antes = compareHours(hora, "07:15") < 0;
-    const hora_después = compareHours(hora, "21:30") > 0;
-
-    if (hora_antes || hora_después) {
-        alert(`La hora ${hora} no es válida`);
-        triggerMessage("Selecciona una hora", "Error");
-        btn.innerHTML = clone.innerHTML;
-        btn.disabled = false;
-        data.hora.focus();
-        data.hora.classList.add("is-invalid");
-        return;
-    }
-
-    const dia = data.dia.value;
-    const salon = data.salon.value;
-    const profesor = data.profesor.value;
-
-
-    const formData = new FormData();
-
-    formData.append("id", id);
-    formData.append("hora", hora);
-    formData.append("dia", dia);
-    formData.append("salon", salon);
-    formData.append("profesor", profesor);
-
-    const response = await fetch("action/action_horario_update.php", {
-        method: "POST",
-        body: formData
-    }).then(res => res.json());
-
-    if (response.status == "success") {
-        triggerMessage(response.message, "Éxito", "success");
-        btn.innerHTML = '<i class="ing-aceptar ing"></i> Guardado';
-        btn.classList.add("btn-success");
-        btn.classList.remove("btn-primary");
-
-        // return to the initial state
-        setTimeout(() => {
-            buscarGrupo();
-            btn.replaceWith(clone);
-            $("#modal-editar").modal("hide");
-        }, 1000);
-    } else {
-        triggerMessage(response.message, "Error");
-        btn.replaceWith(clone);
-
-        $("#modal-editar").modal("hide");
-    }
-
-}
-
-function triggerMessage(message: string, header: string, colour: string = "danger") {
-    throw new Error('Function not implemented.');
-}

+ 0 - 107
ts/consultar_horarios.ts

@@ -1,107 +0,0 @@
-// get this script tag
-const script = document.currentScript as HTMLScriptElement;
-// get data-facultad attribute from script tag
-const dataFacultad = script.getAttribute("data-facultad") as string;
-
-
-const table = document.querySelector("table") as HTMLTableElement;
-// hide the table
-table.style.display = "none";
-
-disableDatalist("#filter_grupo");
-
-const buscarGrupo = async () => {
-
-    // Add loading animation in the button
-    const btn = document.querySelector("#btn-buscar") as HTMLButtonElement;
-    btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Cargando...';
-    btn.disabled = true;
-
-
-    const carrera = document.querySelector("#filter_carrera") as HTMLInputElement;
-    const grupo = document.querySelector("#filter_grupo") as HTMLInputElement;
-    const periodo = document.querySelector("#periodo") as HTMLInputElement;
-    console.log(`Carrera: ${carrera}, Grupo: ${grupo}`);
-
-    if (carrera.value == "" || grupo.value == "") {
-        triggerMessage("El nombre del grupo y la carrera son requeridos", "Faltan campos");
-
-        // Remove loading animation in the button
-        btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
-        btn.disabled = false;
-
-        return;
-    }
-    const formData = new FormData();
-
-    formData.append("carrera", carrera.value);
-    formData.append("grupo", grupo.value);
-    formData.append("periodo", periodo.value);
-    formData.append('facultad', dataFacultad);
-
-    try {
-        const response = await fetch("api/horario.php", {
-            method: "POST",
-            body: formData
-        }).then(res => res.json());
-
-    } catch (error) {
-        triggerMessage("Error al cargar el horario", "Error");
-    }
-
-    // Remove loading animation in the button
-    btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
-    btn.disabled = false;
-}
-
-// on click the li element, inside datalist #dlcarera
-const dlcarreras = document.querySelectorAll("#dlcarrera li");
-
-dlcarreras.forEach(li => {
-    li.addEventListener("click", async () => {
-        // get the data-id from the li element
-        const carrera= li.getAttribute("data-id") as string;
-        const facultad = dataFacultad;
-        const periodo = document.querySelector("#periodo") as HTMLSelectElement;
-        
-
-        const formData = new FormData();
-        formData.append("carrera", carrera);
-        formData.append("facultad", facultad);
-        formData.append("periodo", periodo.value);
-
-        try {
-            const {
-                status,
-                grupos
-            } = await fetch("action/action_grupo.php", {
-                method: "POST",
-                body: formData
-            }).then(res => res.json());
-
-            if (status != "success") {
-                throw new Error("Error al cargar los grupos");
-            }
-
-            const dlgrupo = document.querySelector("#dlgrupo ul") as HTMLUListElement;
-            const prompt = document.querySelector("#dlgrupo .datalist-input") as HTMLInputElement;
-            dlgrupo.innerHTML = "";
-
-            grupos.forEach(grupo => {
-                const li = document.createElement("li");
-                // data-id is the id of the group
-                li.setAttribute("data-id", grupo);
-                li.textContent = grupo;
-                dlgrupo.appendChild(li);
-            });
-
-            // write Seleccionar grupo
-            prompt.textContent = "Seleccionar grupo";
-
-            disableDatalist("#filter_grupo", false);
-        } catch (error) {
-            triggerMessage("Error al cargar los grupos", "Error");
-            console.log(error);
-        }
-    });
-});

+ 0 - 10
ts/date_functions.ts

@@ -1,10 +0,0 @@
-// function that receives two hours in hh:mm format and compares as a spaceship operator
-export function compareHours(h1: string, h2: string): number {
-    const [h1h, h1m] = h1.split(":").map(Number);
-    const [h2h, h2m] = h2.split(":").map(Number);
-    if (h1h > h2h) return 1;
-    if (h1h < h2h) return -1;
-    if (h1m > h2m) return 1;
-    if (h1m < h2m) return -1;
-    return 0;
-}

+ 1 - 0
ts/declaration.ts

@@ -0,0 +1 @@
+declare module 'https://*'

+ 532 - 0
ts/horario_profesor.ts

@@ -0,0 +1,532 @@
+declare function triggerMessage(message: string, title: string, color?: string): void;
+declare const write: boolean;
+declare const moment: any;
+
+/**
+ * Funciones auxiliares
+ */
+type Profesor = {
+    id: number,
+    grado: string,
+    profesor: string,
+    clave: string,
+}
+
+type Horario = {
+    id: number,
+    carrera_id: number,
+    materia: string,
+    salon: string,
+    profesores: Profesor[],
+    hora: string,
+    hora_final: string,
+    dia: string,
+    duracion: number,
+    bloques: number,
+    grupo: string,
+    materia_id: number,
+}
+
+const compareHours = (hora1: string, hora2: string): number => {
+    const [h1, m1] = hora1.split(":").map(Number);
+    const [h2, m2] = hora2.split(":").map(Number);
+
+    if (h1 !== h2) {
+        return h1 > h2 ? 1 : -1;
+    }
+
+    if (m1 !== m2) {
+        return m1 > m2 ? 1 : -1;
+    }
+
+    return 0;
+};
+
+let horarios = [] as Horario[];
+const table = document.querySelector("table") as HTMLTableElement;
+if (!(table instanceof HTMLTableElement)) {
+    triggerMessage("No se ha encontrado la tabla", "Error", "error");
+    throw new Error("No se ha encontrado la tabla");
+}
+
+[...Array(16).keys()].map(x => x + 7).forEach(hora => {
+    // add 7 rows for each hour
+    [0, 15, 30, 45].map((minute: number) => `${minute}`.padStart(2, '0')).forEach((minute: string) => {
+        const tr = document.createElement("tr") as HTMLTableRowElement;
+        tr.id = `hora-${hora}:${minute}`;
+        tr.classList.add(hora > 13 ? "tarde" : "mañana");
+        if (minute == "00") {
+            const th = document.createElement("th") as HTMLTableCellElement;
+            th.classList.add("text-center");
+            th.scope = "row";
+            th.rowSpan = 4;
+            th.innerText = `${hora}:00`;
+            th.style.verticalAlign = "middle";
+            tr.appendChild(th);
+        }
+
+        ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"].forEach(día => {
+            const td = document.createElement("td") as HTMLTableCellElement;
+            td.id = `hora-${hora}:${minute}-${día}`;
+            tr.appendChild(td);
+        });
+        const tbody = document.querySelector("tbody#horario") as HTMLTableSectionElement;
+        if (!(tbody instanceof HTMLTableSectionElement)) {
+            throw new Error("No se ha encontrado el tbody");
+        }
+
+        tbody.appendChild(tr);
+    });
+});
+
+const empty_table = table.cloneNode(true) as HTMLTableElement;
+document.querySelectorAll('.hidden').forEach((element: HTMLElement) => {
+    element.style.display = "none";
+});
+// hide the table
+table.style.display = "none";
+
+function moveHorario(id: string, día: string, hora: string) {
+
+    const formData = new FormData();
+
+    formData.append("id", id);
+    formData.append("hora", hora);
+    formData.append("día", día);
+
+    fetch("action/action_horario_update.php", {
+        method: "POST",
+        body: formData
+    }).then(res => res.json()).then(response => {
+        if (response.status == "success") {
+            triggerMessage("Horario movido", "Éxito", "success");
+        } else {
+            triggerMessage(response.message, "Error");
+        }
+    }).then(() => {
+        renderHorario();
+    }).catch(err => {
+        triggerMessage(err, "Error");
+    });
+
+}
+
+function renderHorario() {
+    if (horarios.length == 0) {
+        triggerMessage("Este profesor hay horarios para mostrar", "Error", "info");
+        table.style.display = "none";
+        document.querySelectorAll('.hidden').forEach((element: HTMLElement) => element.style.display = "none");
+        return;
+    }
+    // show the table
+    table.style.display = "table";
+    document.querySelectorAll('.hidden').forEach((element: HTMLElement) => element.style.display = "block");
+    // clear the table
+    table.innerHTML = empty_table.outerHTML;
+
+    function conflicts(horario1: Horario, horario2: Horario): boolean {
+        const { hora: hora_inicio1, hora_final: hora_final1, dia: dia1 } = horario1;
+        const { hora: hora_inicio2, hora_final: hora_final2, dia: dia2 } = horario2;
+
+        if (dia1 !== dia2) {
+            return false;
+        }
+
+        const compareInicios = compareHours(hora_inicio1, hora_inicio2);
+        const compareFinales = compareHours(hora_final1, hora_final2);
+
+        if (
+            compareInicios >= 0 && compareInicios <= compareFinales ||
+            compareFinales >= 0 && compareFinales <= -compareInicios
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+    // remove the next 5 cells
+    function removeNextCells(horas: number, minutos: number, dia: string, cells: number = 5) {
+        for (let i = 1; i <= cells; i++) {
+            const minute = minutos + i * 15;
+            const nextMinute = (minute % 60).toString().padStart(2, "0");
+            const nextHour = horas + Math.floor(minute / 60);
+
+            const cellId = `hora-${nextHour}:${nextMinute}-${dia}`;
+            const cellElement = document.getElementById(cellId);
+            if (cellElement) {
+                cellElement.remove();
+            }
+            else {
+                console.log(`No se ha encontrado la celda ${cellId}`);
+                break;
+            }
+        }
+    }
+    function newBlock(horario: Horario, edit = false) {
+        function move(horario: Horario, cells: number = 5) {
+            const [horas, minutos] = horario.hora.split(":").map(Number);
+
+            const cell = document.getElementById(`hora-${horas}:${minutos.toString().padStart(2, "0")}-${horario.dia}`);
+            const { top, left } = cell.getBoundingClientRect();
+
+            const block = document.getElementById(`block-${horario.id}`);
+            block.style.top = `${top}px`;
+            block.style.left = `${left}px`;
+
+            removeNextCells(horas, minutos, horario.dia, cells);
+        }
+
+        const [horas, minutos] = horario.hora.split(":").map(x => parseInt(x));
+        const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
+        horario.hora = hora;
+
+        const cell = document.getElementById(`hora-${horario.hora}-${horario.dia}`) as HTMLTableCellElement;
+        if (!cell) return;
+
+        cell.dataset.ids = `${horario.id}`;
+
+        const float_menu = edit ?
+            `<div class="menu-flotante p-2" style="opacity: .7;">
+            <a class="mx-2" href="#" data-toggle="modal" data-target="#modal-editar">
+                <i class="ing-editar ing"></i>
+            </a>
+            <a class="mx-2" href="#" data-toggle="modal" data-target="#modal-borrar">
+                <i class="ing-basura ing"></i>
+            </a>
+        </div>`
+            : '';
+
+        cell.innerHTML =
+            `<div style="overflow-y: auto; overflow-x: hidden; height: 100%;" id="block-${horario.id}" class="position-absolute w-100 h-100">
+            <small class="text-gray">${horario.hora}</small>
+            <b class="title">${horario.materia}</b> <br>
+            <br><span>Salón: </span>${horario.salon} <br>
+            <small class="my-2">
+              ${horario.profesores.map((profesor: Profesor) => ` <span class="ing ing-formacion mx-1"></span>${profesor.grado ?? ''} ${profesor.profesor}`).join("<br>")}
+            </small>
+        </div>
+          ${float_menu}`;
+
+        cell.classList.add("bloque-clase", "position-relative");
+        cell.rowSpan = horario.bloques;
+        // draggable
+        cell.draggable = write;
+
+        if (horario.bloques > 0) {
+            removeNextCells(horas, minutos, horario.dia, horario.bloques - 1);
+        }
+    }
+
+    function newConflictBlock(horarios: Horario[], edit = false) {
+        const first_horario = horarios[0];
+        const [horas, minutos] = first_horario.hora.split(":").map(x => parseInt(x));
+        const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
+        const ids = horarios.map(horario => horario.id);
+        const cell = document.getElementById(`hora-${hora}-${first_horario.dia}`);
+        if (cell == null) {
+            console.error(`Error: No se encontró la celda: hora-${hora}-${first_horario.dia}`);
+            return;
+        }
+        cell.dataset.ids = ids.join(",");
+
+        // replace the content of the cell
+        cell.innerHTML = `
+          <small class='text-danger'>
+            ${hora}
+          </small>
+          <div class="d-flex justify-content-center align-items-center mt-4">
+            <div class="d-flex flex-column justify-content-center align-items-center">
+              <span class="ing ing-importante text-danger" style="font-size: 2rem;"></span>
+              <b class='text-danger'>
+                Empalme de ${ids.length} horarios
+              </b>
+              <hr>
+              <i class="text-danger">Ver horarios &#8230;</i>
+            </div>
+          </div>
+        `;
+
+        // Add classes and attributes
+        cell.classList.add("conflict", "bloque-clase");
+        cell.setAttribute("role", "button");
+
+        // Add event listener for the cell
+        cell.addEventListener("click", () => {
+            $("#modal-choose").modal("show");
+            const ids = cell.getAttribute("data-ids").split(",").map(x => parseInt(x));
+            const tbody = document.querySelector("#modal-choose tbody");
+            tbody.innerHTML = "";
+            horarios.filter(horario => ids.includes(horario.id)).sort((a, b) => compareHours(a.hora, b.hora)).forEach(horario => {
+                tbody.innerHTML += `
+              <tr data-ids="${horario.id}">
+                <td><small>${horario.hora.slice(0, -3)}-${horario.hora_final.slice(0, -3)}</small></td>
+                <td>${horario.materia}</td>
+                <td>
+                  ${horario.profesores.map(({ grado, profesor }) => `${grado ?? ''} ${profesor}`).join(", ")}
+                </td>
+                <td>${horario.salon}</td>
+                ${edit ? `
+                  <td class="text-center">
+                    <button class="btn btn-sm btn-primary dismiss-editar" data-toggle="modal" data-target="#modal-editar">
+                      <i class="ing-editar ing"></i>
+                    </button>
+                  </td>
+                  <td class="text-center">
+                    <button class="btn btn-sm btn-danger dismiss-editar" data-toggle="modal" data-target="#modal-borrar">
+                      <i class="ing-basura ing"></i>
+                    </button>
+                  </td>
+                ` : ""}
+              </tr>`;
+            });
+
+            document.querySelectorAll(".dismiss-editar").forEach(btn => {
+                btn.addEventListener("click", () => $("#modal-choose").modal("hide"));
+            });
+        });
+
+        function getDuration(hora_i: string, hora_f: string): number {
+            const [horas_i, minutos_i] = hora_i.split(":").map(x => parseInt(x));
+            const [horas_f, minutos_f] = hora_f.split(":").map(x => parseInt(x));
+            const date_i = new Date(0, 0, 0, horas_i, minutos_i);
+            const date_f = new Date(0, 0, 0, horas_f, minutos_f);
+            const diffInMilliseconds = date_f.getTime() - date_i.getTime();
+            const diffInMinutes = diffInMilliseconds / (1000 * 60);
+            const diffIn15MinuteIntervals = diffInMinutes / 15;
+            return Math.floor(diffIn15MinuteIntervals);
+        }
+
+        const maxHoraFinal = horarios.reduce((max: Date, horario: Horario) => {
+            const [horas, minutos] = horario.hora_final.split(":").map(x => parseInt(x));
+            const date = new Date(0, 0, 0, horas, minutos);
+            return date > max ? date : max;
+        }, new Date(0, 0, 0, 0, 0));
+
+
+        const horaFinalMax = new Date(0, 0, 0, maxHoraFinal.getHours(), maxHoraFinal.getMinutes());
+        const blocks = getDuration(first_horario.hora, `${horaFinalMax.getHours()}:${horaFinalMax.getMinutes()}`);
+        cell.setAttribute("rowSpan", blocks.toString());
+        removeNextCells(horas, minutos, first_horario.dia, blocks - 1);
+    }
+
+
+    const conflictBlocks = horarios.filter((horario, index, arrayHorario) =>
+        arrayHorario.filter((_, i) => i != index).some(horario2 =>
+            conflicts(horario, horario2)))
+        .sort((a, b) => compareHours(a.hora, b.hora));
+
+    const classes = horarios.filter(horario => !conflictBlocks.includes(horario));
+
+    const conflictBlocksPacked = []; // array of sets
+    conflictBlocks.forEach(horario => {
+        const setIndex = conflictBlocksPacked.findIndex(set => set.some(horario2 => conflicts(horario, horario2)));
+        if (setIndex === -1) {
+            conflictBlocksPacked.push([horario]);
+        } else {
+            conflictBlocksPacked[setIndex].push(horario);
+        }
+    })
+
+    classes.forEach(horario =>
+        newBlock(horario, write)
+    )
+
+    conflictBlocksPacked.forEach(horarios =>
+        newConflictBlock(horarios, write)
+    )
+
+    // remove the elements that are not in the limits
+    let max_hour = Math.max(...horarios.map(horario => {
+        const lastMoment = moment(horario.hora, "HH:mm").add(horario.bloques * 15, "minutes");
+        const lastHour = moment(`${lastMoment.hours()}:00`, "HH:mm");
+        const hourInt = parseInt(lastMoment.format("HH"));
+
+        return lastMoment.isSame(lastHour) ? hourInt - 1 : hourInt;
+    }));
+
+    let min_hour = Math.min(...horarios.map(horario => parseInt(horario.hora.split(":")[0])));
+
+    document.querySelectorAll("tbody#horario tr").forEach(hora => {
+        const hora_id = parseInt(hora.id.split("-")[1].split(":")[0]);
+        (hora_id < min_hour || hora_id > max_hour) ? hora.remove() : null;
+    })
+
+    // if there is no sábado, remove the column
+    if (!horarios.some(horario => horario.dia == "sábado")) {
+        document.querySelectorAll("tbody#horario td").forEach(td => {
+            if (td.id.split("-")[2] == "sábado") {
+                td.remove();
+            }
+        });
+
+        // remove the header (the last)
+        document.querySelector("#headers").lastElementChild.remove();
+    }
+    // adjust width
+    const ths = document.querySelectorAll("tr#headers th") as NodeListOf<HTMLTableCellElement>;
+    ths.forEach((th, key) =>
+        th.style.width = (key == 0) ? "5%" : `${95 / (ths.length - 1)}%`
+    );
+
+    // search item animation
+    const menúFlontantes = document.querySelectorAll(".menu-flotante");
+    menúFlontantes.forEach((element) => {
+        element.classList.add("d-none");
+        element.parentElement.addEventListener("mouseover", () =>
+            element.classList.remove("d-none")
+        );
+        element.parentElement.addEventListener("mouseout", (e) =>
+            element.classList.add("d-none")
+        );
+    });
+
+    // droppables
+    // forall the .bloque-elements add the event listeners for drag and drop
+
+    document.querySelectorAll(".bloque-clase").forEach(element => {
+        function dragStart() {
+            this.classList.add("dragging");
+        }
+
+        function dragEnd() {
+            this.classList.remove("dragging");
+        }
+
+        element.addEventListener("dragstart", dragStart);
+        element.addEventListener("dragend", dragEnd);
+    });
+
+    // forall the cells that are not .bloque-clase add the event listeners for drag and drop
+    document.querySelectorAll("td:not(.bloque-clase)").forEach(element => {
+        function dragOver(e) {
+            e.preventDefault();
+            this.classList.add("dragging-over");
+        }
+
+        function dragLeave() {
+            this.classList.remove("dragging-over");
+        }
+
+        function drop() {
+            this.classList.remove("dragging-over");
+            const dragging = document.querySelector(".dragging");
+
+            const id = dragging.getAttribute("data-ids");
+            const hora = this.id.split("-")[1];
+            const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
+            let día = this.id.split("-")[2];
+            día = días.indexOf(día) + 1;
+
+            //  rowspan
+            const bloques = parseInt(dragging.getAttribute("rowspan"));
+            const horaMoment = moment(hora, "HH:mm");
+            const horaFin = horaMoment.add(bloques * 15, "minutes");
+
+            const limit = moment('22:00', 'HH:mm');
+
+            if (horaFin.isAfter(limit)) {
+                triggerMessage("No se puede mover el bloque a esa hora", "Error");
+
+                // scroll to the top
+                window.scrollTo(0, 0);
+                return;
+            }
+
+            // get the horario
+            // remove the horario
+            const bloque = document.querySelector(`.bloque-clase[data-ids="${id}"]`) as HTMLElement;
+
+            // remove all children
+            while (bloque.firstChild) {
+                bloque.removeChild(bloque.firstChild);
+            }
+
+            // prepend a loading child
+            const loading = `<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
+                                <span class="sr-only">Loading...</span>
+                            </div>`;
+            bloque.insertAdjacentHTML("afterbegin", loading);
+            // add style vertical-align: middle
+            bloque.style.verticalAlign = "middle";
+            bloque.classList.add("text-center");
+            // remove draggable
+            bloque.removeAttribute("draggable");
+
+            moveHorario(id, día, hora);
+        }
+
+        element.addEventListener("dragover", dragOver);
+        element.addEventListener("dragleave", dragLeave);
+        element.addEventListener("drop", drop);
+    });
+}
+const form = document.getElementById('form') as HTMLFormElement;
+
+if (!(form instanceof HTMLFormElement)) {
+    triggerMessage('No se ha encontrado el formulario', 'Error', 'danger');
+    throw new Error("No se ha encontrado el formulario");
+}
+
+form.querySelector('#clave_profesor').addEventListener('input', function (e) {
+    const input = form.querySelector('#clave_profesor') as HTMLInputElement;
+    const option = form.querySelector(`option[value="${input.value}"]`) as HTMLOptionElement;
+
+    if (input.value == "") {
+        input.classList.remove("is-invalid", "is-valid");
+        return;
+    }
+
+    if (!option) {
+        input.classList.remove("is-valid");
+        input.classList.add("is-invalid");
+    }
+    else {
+        const profesor_id = form.querySelector('#profesor_id') as HTMLInputElement;
+        profesor_id.value = option.dataset.id;
+        input.classList.remove("is-invalid");
+        input.classList.add("is-valid");
+    }
+});
+
+
+form.addEventListener('submit', async function (e) {
+    e.preventDefault();
+    const input = form.querySelector('#clave_profesor') as HTMLInputElement;
+    if (input.classList.contains("is-invalid")) {
+        triggerMessage('El profesor no se encuentra registrado', 'Error', 'danger');
+        return;
+    }
+    const formData = new FormData(form);
+    try {
+        const buttons = document.querySelectorAll("button") as NodeListOf<HTMLButtonElement>;
+        buttons.forEach(button => {
+            button.disabled = true;
+            button.classList.add("disabled");
+        });
+        const response = await fetch('action/action_horario_profesor.php', {
+            method: 'POST',
+            body: formData,
+        });
+        const data = await response.json();
+
+        buttons.forEach(button => {
+            button.disabled = false;
+            button.classList.remove("disabled");
+        });
+
+        if (data.status == 'success') {
+            horarios = data.data;
+            renderHorario();
+        }
+        else {
+            triggerMessage(data.message, 'Error en la consulta', 'warning');
+        }
+    } catch (error) {
+        triggerMessage('Fallo al consutar los datos ', 'Error', 'danger');
+        console.log(error);
+    }
+});
+
+const input = form.querySelector('#clave_profesor') as HTMLInputElement;
+const option = form.querySelector(`option[value="${input.value}"]`) as HTMLOptionElement;
+

+ 272 - 0
ts/reposiciones.ts

@@ -0,0 +1,272 @@
+import { type } from "os";
+
+declare function triggerMessage(message: string, title: string, type?: string): void;
+declare const write: boolean;
+declare const moment: any;
+// from this 'horario_id', 'fecha', 'hora', 'duracion_id', 'descripcion', 'profesor_id', 'salon', 'unidad', 'periodo_id', 'fecha_clase' make a type of ReposicionParams
+export interface ReposicionParams {
+    horario_id: number;
+    fecha: string;
+    hora: string;
+    duracion_id: number;
+    descripcion: string;
+    profesor_id: number;
+    salon: string;
+    unidad: number;
+    periodo_id: number;
+    fecha_clase: string;
+}
+
+type Horario = {
+    id: number;
+    carrera_id: number;
+    materia_id: number;
+    grupo: string;
+    profesores: Profesor[];
+    dia: string;
+    hora: string;
+    hora_final: string;
+    salon: string;
+    fecha_inicio: string;
+    fecha_final: string;
+    fecha_carga: string;
+    nivel_id: number;
+    periodo_id: number;
+    facultad_id: number;
+    materia: string;
+    horas: number;
+    minutos: number;
+    duracion: number;
+    retardo: boolean;
+    original_id: number;
+    last: boolean;
+    bloques: number;
+};
+
+type Profesor = {
+    id: number;
+    clave: string;
+    grado: string;
+    profesor: string;
+    nombre: string;
+    facultad_id: number;
+};
+// Get references to the HTML elements
+const form = document.getElementById('form') as HTMLFormElement;
+const steps = Array.from(form.querySelectorAll('.step')) as HTMLElement[];
+
+const nextButton = document.getElementById('next-button') as HTMLButtonElement;
+const prevButton = document.getElementById('prev-button') as HTMLButtonElement;
+
+let currentStep = 0;
+
+// #clave_profesor on change => show step 2
+const clave_profesor = document.getElementById('clave_profesor') as HTMLInputElement;
+const horario_reponer = document.getElementById('horario_reponer') as HTMLInputElement;
+const fechas_clase = document.getElementById('fechas_clase') as HTMLInputElement;
+
+const fecha_reponer = $('#fecha_reponer') as JQuery<HTMLElement>;
+const hora_reponer = $('#hora_reponer') as JQuery<HTMLElement>;
+const minutos_reponer = $('#minutos_reponer') as JQuery<HTMLElement>;
+
+clave_profesor.addEventListener('change', async () => {
+    const step2 = document.getElementById('step-2') as HTMLElement;
+    clave_profesor.disabled = true;
+    // get option which value is the same as clave_profesor.value
+    const option = document.querySelector(`option[value="${clave_profesor.value}"]`) as HTMLOptionElement;
+
+    // make a form data with #form
+    const profesor_id = document.getElementById('profesor_id') as HTMLInputElement;
+    profesor_id.value = option.dataset.id;
+
+    const formData = new FormData(form);
+
+    const response = await fetch(`./action/action_horario_profesor.php`, {
+        method: 'POST',
+        body: formData
+    });
+    const data = await response.json();
+    if (data['success'] === false) {
+        const message = "Hubo un error al obtener los horarios del profesor."
+        const title = 'Error';
+        const color = 'danger';
+        triggerMessage(message, title, color);
+        return;
+    }
+    const horarios = data.data as Horario[];
+    const initial = document.createElement('option');
+    initial.value = '';
+    initial.textContent = 'Seleccione un horario';
+    initial.selected = true;
+    initial.disabled = true;
+    horario_reponer.innerHTML = '';
+    horario_reponer.appendChild(initial);
+
+    horarios.forEach((horario) => {
+        const dias = ['Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Sabado', 'Domingo'];
+        const option = document.createElement('option');
+        option.value = `${horario.id}`;
+        // materia máx 25 caracteres, if materia.length > 25 then slice(0, 20)
+        const max = 25;
+        option.textContent = `${horario.materia.slice(0, max) + (horario.materia.length > max ? '...' : '')} - Grupo: ${horario.grupo} - ${horario.hora.slice(0, 5)}-${horario.hora_final.slice(0, 5)} - Salon: ${horario.salon} - ${horario.dia}`;
+
+        option.dataset.materia = `${horario.materia}`;
+        option.dataset.grupo = `${horario.grupo}`;
+        option.dataset.hora = `${horario.hora.slice(0, 5)}`; // slice(0, 5) => HH:MM
+        option.dataset.hora_final = `${horario.hora_final.slice(0, 5)}`;
+        option.dataset.salon = `${horario.salon}`;
+        option.dataset.dia = `${horario.dia}`;
+
+        option.dataset.id = `${horario.id}`;
+        horario_reponer.appendChild(option);
+    });
+    currentStep = 1;
+    step2.style.display = 'block';
+    prevButton.disabled = false;
+});
+// disable clave_profesor
+
+// from second step to first step
+prevButton.addEventListener('click', () => {
+    const inputs = [clave_profesor, horario_reponer, fechas_clase, fecha_reponer, hora_reponer] as HTMLInputElement[];
+    switch (currentStep) {
+        case 1:
+        case 2:
+        case 3:
+            const step = document.getElementById(`step-${currentStep + 1}`) as HTMLElement;
+            step.style.display = 'none';
+            inputs[currentStep - 1].disabled = false;
+            inputs[currentStep - 1].value = '';
+            if (--currentStep === 0) {
+                prevButton.disabled = true;
+            }
+            break;
+        case 4:
+            const step5 = document.getElementById('step-5') as HTMLElement;
+            step5.style.display = 'none';
+            fecha_reponer.prop('disabled', false);
+            fecha_reponer.val('');
+
+            hora_reponer.parent().removeClass('disabled');
+            hora_reponer.siblings('.datalist-input').text('hh');
+            hora_reponer.val('');
+
+            minutos_reponer.parent().removeClass('disabled');
+            minutos_reponer.siblings('.datalist-input').text('mm');
+            minutos_reponer.val('');
+
+            currentStep--;
+            break;
+    }
+
+    nextButton.disabled = true;
+
+});
+
+// #horario_reponer on change => show step 3
+horario_reponer.addEventListener('change', async () => {
+    const selected = horario_reponer.querySelector(`option[value="${horario_reponer.value}"]`) as HTMLOptionElement;
+    horario_reponer.title = `Materia: ${selected.dataset.materia} - Grupo: ${selected.dataset.grupo} - Horario: ${selected.dataset.hora}-${selected.dataset.hora_final} - Salon: ${selected.dataset.salon} - Día: ${selected.dataset.dia}`;
+    const step3 = document.getElementById('step-3') as HTMLElement;
+    horario_reponer.disabled = true;
+    // make a form data with #form
+    const response = await fetch(`./action/action_fechas_clase.php?horario_id=${horario_reponer.value}`, {
+        method: 'GET',
+    });
+
+    const data = await response.json();
+    if (data['success'] === false) {
+        const message = "Hubo un error al obtener las fechas de clase."
+        const title = 'Error';
+        const color = 'danger';
+        triggerMessage(message, title, color);
+        return;
+    }
+    type Fecha = {
+        fecha: string;
+        dia_mes: number;
+        day: number;
+        month: number;
+        year: number;
+    }
+
+    const meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
+
+    const fechas = data.data as Fecha[];
+    const initial = document.createElement('option');
+    initial.value = '';
+    initial.textContent = 'Seleccione la fecha de la falta';
+    initial.selected = true;
+    initial.disabled = true;
+    fechas_clase.innerHTML = '';
+    fechas_clase.appendChild(initial);
+    fechas_clase.title = 'Seleccione la fecha de la falta';
+
+    fechas.forEach((fecha) => {
+        const option = document.createElement('option');
+        option.value = `${fecha}`;
+        option.textContent = `${fecha.dia_mes} de ${meses[fecha.month - 1]} de ${fecha.year}`;
+        fechas_clase.appendChild(option);
+    });
+
+
+    step3.style.display = 'block';
+    currentStep = 2;
+});
+
+// #fechas_clase on change => show step 4
+fechas_clase.addEventListener('change', () => {
+    const step4 = document.getElementById('step-4') as HTMLElement;
+    step4.style.display = 'block';
+    fechas_clase.disabled = true;
+    currentStep = 3;
+});
+
+// when both #fecha_reponer and #hora_reponer are selected => show step 5
+
+const lastStep = () => {
+    // timeout to wait for the value to be set 
+    setTimeout(() => {
+        if (fecha_reponer.val() !== '' && hora_reponer.val() !== '' && minutos_reponer.val() !== '') {
+            const step5 = document.getElementById('step-5') as HTMLElement;
+            step5.style.display = 'block';
+            // disable both
+            fecha_reponer.prop('disabled', true);
+            hora_reponer.parent().addClass('disabled');
+            minutos_reponer.parent().addClass('disabled');
+
+            const nextButton = document.getElementById('next-button') as HTMLButtonElement;
+            // remove property disabled
+            nextButton.removeAttribute('disabled');
+            currentStep = 4;
+        }
+    }, 100);
+}
+fecha_reponer.on('change', lastStep);
+// on click on the sibling ul>li of #hora_reponer and #minutos_reponer
+
+hora_reponer.siblings('ul').children('li').on('click', lastStep);
+minutos_reponer.siblings('ul').children('li').on('click', lastStep);
+
+// Initialize the form
+hideSteps();
+showCurrentStep();
+
+
+function hideSteps() {
+    steps.forEach((step) => {
+        step.style.display = 'none';
+    });
+}
+
+function showCurrentStep() {
+    steps[currentStep].style.display = 'block';
+    prevButton.disabled = currentStep === 0;
+}
+
+function handleSubmit(event: Event) {
+    event.preventDefault();
+
+    // Handle form submission
+    // You can access the form data using the FormData API or serialize it manually
+}

+ 11 - 15
tsconfig.json

@@ -1,18 +1,14 @@
 {
     "compilerOptions": {
-        "module": "ESNext",
-        "moduleResolution": "Node",
-        "target": "ES2020",
-        "jsx": "react",
-        "strictNullChecks": true,
-        "strictFunctionTypes": true,
-        "lib": ["ES2020", "DOM"],
-        "rootDir": "./ts",
-        "outDir": "./js",
-        "watch": true,
-    },
-    "exclude": [
-        "node_modules",
-        "**/node_modules/*"
-    ]
+        "lib": [
+            "ESNext",
+            "dom"
+        ],
+        "outDir": "js",
+        "rootDir": "ts",
+        "target": "ES2022",
+        "moduleResolution": "node",
+        "module": "ESNext"
+        // ts/auditoría.ts:1:37 - error TS2307: Cannot find module 'https://unpkg.com/petite-vue?module' or its corresponding type declarations.
+    }
 }