Pārlūkot izejas kodu

Desorganización pura

Alejandro Rosales 1 gadu atpakaļ
vecāks
revīzija
0d4bd79529
18 mainītis faili ar 2463 papildinājumiem un 320 dzēšanām
  1. 3 1
      .gitignore
  2. 3 8
      action/login.php
  3. 27 0
      action/snapshot.php
  4. 4 0
      css/bootstrap.css
  5. 492 0
      css/indivisa.css
  6. 104 0
      css/lasalle.css
  7. 1106 0
      css/sgi.css
  8. 1 1
      dependencies.php
  9. 41 133
      export/excel.php
  10. 31 0
      fetch/calificaciones.php
  11. 25 0
      graph.php
  12. 150 50
      index.php
  13. 95 0
      pages/graph.html
  14. 77 85
      pages/host.html
  15. 12 3
      pages/login.html
  16. 141 39
      pages/menu.html
  17. 145 0
      template/footer.html
  18. 6 0
      template/header.html

+ 3 - 1
.gitignore

@@ -1,4 +1,6 @@
 /vendor/
 /.vscode/
 /.env
-/composer.lock
+/composer.lock
+/imagenes
+/fonts

+ 3 - 8
action/login.php

@@ -1,27 +1,22 @@
 <?php
 require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
 
-// Define your username and password
-
-
 if (
     (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) or (
         $db->querySingle("SELECT auth.login(:username, :password)", [
             'username' => $_SERVER['PHP_AUTH_USER'],
             'password' => $_SERVER['PHP_AUTH_PW']
-        ])['login'] === false AND $db->where('username', $_SERVER['PHP_AUTH_USER'])->has('auth.usuario')
+        ])['login'] === false AND $db->where('clave', $_SERVER['PHP_AUTH_USER'])->has('auth.usuario')
     )
 ) {
     header('WWW-Authenticate: Basic realm="Moodle"');
+
     header('HTTP/1.0 401 Unauthorized');
     echo 'Acceso no autorizado';
     exit;
 } else {
     $token = $db->querySingle("SELECT sign(('{\"exp\":' || EXTRACT(EPOCH FROM NOW() + INTERVAL'1 day') || ', \"role\": \"app_user\"}')::JSON, '{$_ENV['KEY_ENCRYPT']}') as token");
-    $_SESSION['user'] = [
-        'id' => 1,
-        'name' => 'Ángel Alfonso',
-    ];
+    $_SESSION['user'] = $_SERVER['PHP_AUTH_USER'];
     header('Content-Type: application/json');
     echo json_encode($token);
 }

+ 27 - 0
action/snapshot.php

@@ -0,0 +1,27 @@
+<?php
+header('Content-Type: application/json');
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+if (!isset($_SESSION['user'])) {
+    serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión');
+    exit();
+}
+
+try {
+    // $db->query("INSERT INTO moodle_host (etiqueta, host, puerto, postgres_user, postgres_dbname, postgres_password, periodos_gema) VALUES (:etiqueta, :host, :puerto, :postgres_user, :postgres_dbname, PGP_SYM_ENCRYPT(:postgres_password, '{$_ENV['KEY_ENCRYPT']}'), :periodos_gema)", $params);
+    $db->querySingle(
+        "INSERT INTO SNAPSHOT_CALIFICACIONES (calificaciones, moodle_host_id) VALUES (
+            public.consulta_moodle(
+                (SELECT replace(consulta_sql, ';', '') from consulta whERE clave = 'c-fin')::TEXT, :moodle_id),
+                :moodle_id
+        )",
+        ['moodle_id' => $_SESSION['moodle_id']]
+    );
+    // return json
+    echo json_encode(['message' => 'Snapshot realizado con éxito', 'success' => true]);
+
+} catch (\PDOException $th) {
+    echo json_encode(['message' => 'Error al realizar el snapshot', 'success' => false]);
+    exit();
+}
+

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 4 - 0
css/bootstrap.css


+ 492 - 0
css/indivisa.css

@@ -0,0 +1,492 @@
+/* 
+    Created on : 5/12/2018, 01:25:27 PM
+    Author     : Alejandro
+    Indivisa Fonts
+*/
+
+@font-face {
+  font-family: 'indivisa-title';
+  src: url('../fonts/indivisaFont/eot/IndivisaTextSans-BoldItalic.eot');
+  src:
+    url('../fonts/indivisaFont/woff/IndivisaTextSans-BoldItalic.woff'),
+    url('../fonts/indivisaFont/ttf/IndivisaTextSans-BoldItalic.ttf'),
+    url('../fonts/indivisaFont/eot/IndivisaTextSans-BoldItalic.IndivisaTextSans-BoldItalic');
+  font-weight: normal;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'indivisa-text';
+  src: url('../fonts/indivisaFont/eot/IndivisaTextSans-Regular.eot');
+  src:
+    url('../fonts/indivisaFont/woff/IndivisaTextSans-Regular.woff'),
+    url('../fonts/indivisaFont/ttf/IndivisaTextSans-Regular.ttf'),
+    url('../fonts/indivisaFont/eot/IndivisaTextSans-Regular.svg#IndivisaTextSans-Regular');
+  font-weight: normal;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'indivisa-text-black';
+  src: url('../fonts/indivisaFont/eot/IndivisaTextSans-Black.eot');
+  src:
+    url('../fonts/indivisaFont/woff/IndivisaTextSans-Black.woff'),
+    url('../fonts/indivisaFont/ttf/IndivisaTextSans-Black.ttf'),
+    url('../fonts/indivisaFont/eot/IndivisaTextSans-Black.svg#IndivisaTextSans-Black');
+  font-weight: normal;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'indivisa-text-bold';
+  src: url('../fonts/indivisaFont/eot/IndivisaTextSans-Bold.eot');
+  src:
+    url('../fonts/indivisaFont/woff/IndivisaTextSans-Bold.woff'),
+    url('../fonts/indivisaFont/ttf/IndivisaTextSans-Bold.ttf'),
+    url('../fonts/indivisaFont/eot/IndivisaTextSans-Bold.svg#IndivisaTextSans-Bold');
+  font-weight: normal;
+  font-style: normal;
+}
+
+.indivisa-display {
+  font-family: 'indivisa-display' !important;
+}
+
+.indivisa-title {
+  font-family: 'indivisa-title' !important;
+}
+
+/* INGENIERIA FONT */
+@font-face {
+  font-family: 'ingfont';
+  src: url('../fonts/ingenieria/ingfont.eot?1fng03');
+  src: url('../fonts/ingenieria/ingfont.eot?1fng03#iefix') format('embedded-opentype'),
+    url('../fonts/ingenieria/ingfont.ttf?1fng03') format('truetype'),
+    url('../fonts/ingenieria/ingfont.woff?1fng03') format('woff'),
+    url('../fonts/ingenieria/ingfont.svg?1fng03#ingfont') format('svg');
+  font-weight: normal;
+  font-style: normal;
+  font-display: block;
+}
+
+.ing-lg {
+  font-size: 1.33333em;
+  line-height: .75em;
+  vertical-align: -.0667em
+}
+
+.ing-2x {
+  font-size: 2em
+}
+
+.ing-3x {
+  font-size: 3em
+}
+
+.ing-8x {
+  font-size: 8em
+}
+
+.ing-fw {
+  text-align: center;
+  width: 1.4em
+}
+
+/*1.25*/
+.ing-ul {
+  list-style-type: none;
+  margin-left: 2.5em;
+  padding-left: 0
+}
+
+.ing-ul>li {
+  position: relative
+}
+
+.ing-li {
+  left: -2em;
+  position: absolute;
+  text-align: center;
+  width: 2em;
+  line-height: inherit
+}
+
+.ing-rotate-90 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
+  -webkit-transform: rotate(90deg);
+  transform: rotate(90deg)
+}
+
+.ing-rotate-180 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
+  -webkit-transform: rotate(180deg);
+  transform: rotate(180deg)
+}
+
+.ing-rotate-270 {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
+  -webkit-transform: rotate(270deg);
+  transform: rotate(270deg)
+}
+
+.ing-flip-horizontal {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
+  -webkit-transform: scaleX(-1);
+  transform: scaleX(-1)
+}
+
+.ing-flip-vertical {
+  -webkit-transform: scaleY(-1);
+  transform: scaleY(-1)
+}
+
+.ing-flip-both,
+.ing-flip-horizontal.ing-flip-vertical,
+.ing-flip-vertical {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"
+}
+
+.ing-flip-both,
+.ing-flip-horizontal.ing-flip-vertical {
+  -webkit-transform: scale(-1);
+  transform: scale(-1)
+}
+
+:root .ing-flip-both,
+:root .ing-flip-horizontal,
+:root .ing-flip-vertical,
+:root .ing-rotate-90,
+:root .ing-rotate-180,
+:root .ing-rotate-270 {
+  -webkit-filter: none;
+  filter: none
+}
+
+[class^="ing-"],
+[class*=" ing-"] {
+  /* use !important to prevent issues with browser extensions that change fonts */
+  font-family: 'ingfont' !important;
+  speak: never;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+  display: inline-block;
+
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.ing-fb1:before {
+  content: "\e932";
+}
+
+.ing-fb2:before {
+  content: "\e933";
+}
+
+.ing-tw1:before {
+  content: "\e912";
+}
+
+.ing-tw2:before {
+  content: "\e900";
+}
+
+.ing-in1:before {
+  content: "\e91a";
+}
+
+.ing-in2:before {
+  content: "\e902";
+}
+
+.ing-instra1:before {
+  content: "\e924";
+}
+
+.ing-instra2:before {
+  content: "\e923";
+}
+
+.ing-youtube:before {
+  content: "\e90e";
+}
+
+.ing-telefono:before {
+  content: "\e911";
+}
+
+.ing-mail:before {
+  content: "\e907";
+}
+
+.ing-link:before {
+  content: "\e919";
+}
+
+.ing-ubicacion:before {
+  content: "\e908";
+}
+
+.ing-puntos:before {
+  content: "\e917";
+}
+
+.ing-usuario:before {
+  content: "\e90d";
+}
+
+.ing-pass:before {
+  content: "\e906";
+}
+
+.ing-menu:before {
+  content: "\e901";
+}
+
+.ing-salir:before {
+  content: "\e90f";
+}
+
+.ing-flecha:before {
+  content: "\e905";
+}
+
+.ing-cambiar:before {
+  content: "\e93c";
+}
+
+.ing-caret:before {
+  content: "\e90b";
+}
+
+.ing-aceptar:before {
+  content: "\e916";
+}
+
+.ing-cancelar:before {
+  content: "\e910";
+}
+
+.ing-mas:before {
+  content: "\e91d";
+}
+
+.ing-menos:before {
+  content: "\e91e";
+}
+
+.ing-editar:before {
+  content: "\e938";
+}
+
+.ing-buscar:before {
+  content: "\e939";
+}
+
+.ing-ojo:before {
+  content: "\e92a";
+}
+
+.ing-borrar:before {
+  content: "\e942";
+}
+
+.ing-basura:before {
+  content: "\e941";
+}
+
+.ing-camara:before {
+  content: "\e909";
+}
+
+.ing-importante:before {
+  content: "\e935";
+}
+
+.ing-bullet:before {
+  content: "\e943";
+}
+
+.ing-home:before {
+  content: "\e934";
+}
+
+.ing-formacion:before {
+  content: "\e914";
+}
+
+.ing-empleo:before {
+  content: "\e915";
+}
+
+.ing-insignia1:before {
+  content: "\e920";
+}
+
+.ing-insignia2:before {
+  content: "\e91f";
+}
+
+.ing-insignia3:before {
+  content: "\e921";
+}
+
+.ing-insignia4:before {
+  content: "\e922";
+}
+
+.ing-eventos:before {
+  content: "\e90a";
+}
+
+.ing-reporte:before {
+  content: "\e918";
+}
+
+.ing-catalogo:before {
+  content: "\e936";
+}
+
+.ing-evalua-cartel:before {
+  content: "\e913";
+}
+
+.ing-revision-cartel:before {
+  content: "\e90c";
+}
+
+.ing-reporte-resultados:before {
+  content: "\e929";
+}
+
+.ing-mi-cartel:before {
+  content: "\e91b";
+}
+
+.ing-galeria1:before {
+  content: "\e91c";
+}
+
+.ing-galeria2:before {
+  content: "\e925";
+}
+
+.ing-iniciar-sesion:before {
+  content: "\e926";
+}
+
+.ing-finalistas:before {
+  content: "\e927";
+}
+
+.ing-comite:before {
+  content: "\e92b";
+}
+
+.ing-administrador:before {
+  content: "\e92c";
+}
+
+.ing-estrella1:before {
+  content: "\e903";
+}
+
+.ing-estrella2:before {
+  content: "\e904";
+}
+
+.ing-carga-archivo:before {
+  content: "\e93d";
+}
+
+.ing-carga-multiple:before {
+  content: "\e93e";
+}
+
+.ing-descarga:before {
+  content: "\e928";
+}
+
+.ing-autorizar:before {
+  content: "\e92d";
+}
+
+.ing-negar:before {
+  content: "\e92e";
+}
+
+.ing-no-cargado:before {
+  content: "\e92f";
+}
+
+.ing-alumnos:before {
+  content: "\e91c";
+}
+
+.ing-cardex:before {
+  content: "\e93f";
+}
+
+.ing-configuracion:before {
+  content: "\e940";
+}
+
+.ing-listado-menus:before {
+  content: "\e944";
+}
+
+.ing-mi-cuenta:before {
+  content: "\e945";
+}
+
+.ing-ver:before {
+  content: "\e946";
+}
+
+.ing-grafica:before {
+  content: "\e930";
+}
+
+.ing-clic:before {
+  content: "\e931";
+}
+
+.ing-guardar:before {
+  content: "\e937";
+}
+
+.ing-regresar:before {
+  content: "\e93a";
+}
+
+.ing-cuadrado:before {
+  content: "\e93b";
+}
+
+.ing-imprimir:before {
+  content: "\e947";
+}
+
+.ing-importante2:before {
+  content: "\e948";
+}
+
+.ing-copiar:before {
+  content: "\e949";
+}
+
+.ing-reloj:before {
+  content: "\e94a";
+}
+
+.ing-retardo:before {
+  content: "\e94b";
+}
+
+.ing-justificar:before {
+  content: "\e94c";
+}

+ 104 - 0
css/lasalle.css

@@ -0,0 +1,104 @@
+/*
+Iconografía de La Salle
+*/
+@font-face {
+  font-family: 'lasalle';
+  src: url("../fonts/lasalle/lasalle.eot");
+  src: url("../fonts/lasalle/lasalle.eot?#iefix") format('embedded-opentype'), url("../fonts/lasalle/lasalle.woff") format('woff'), url("../fonts/lasalle/lasalle.ttf") format('truetype'), url("../fonts/lasalle/lasalle.svg#lasalle") format('svg');
+  font-weight: normal;
+  font-style: normal;
+}
+.icon {
+  font-family: 'lasalle' !important;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  /*line-height: 1;*/
+/* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.icon-fe:before {content: "\e952";}
+.icon-compras:before {content: "\e94d";}
+.icon-arte:before {content: "\e94e";}
+.icon-restaurante:before {content: "\e94f";}
+.icon-cafe:before {content: "\e950";}
+.icon-biblioteca:before {content: "\e951";}
+.icon-markFull:before {content: "\e94c";}
+.icon-comment:before {content: "\e947";}
+.icon-faith:before {content: "\e948";}
+.icon-justice:before {content: "\e949";}
+.icon-compromise:before {content: "\e94a";}
+.icon-fraternity:before {content: "\e94b";}
+.icon-telephone:before {content: "\e943";}
+.icon-onSpeaking:before {content: "\e944";}
+.icon-offSpeaking:before { content: "\e945";}
+.icon-audio:before {content: "\e946";}
+.icon-play:before {content: "\e91c";}
+.icon-link:before {content: "\e936";}
+.icon-ym:before { content: "\e937";}
+.icon-wp:before {content: "\e938";}
+.icon-read:before { content: "\e939";}
+.icon-certificate:before {content: "\e93a";}
+.icon-school:before {content: "\e93b";}
+.icon-speaker:before {content: "\e93c";}
+.icon-atom:before {content: "\e93d";}
+.icon-bag:before {content: "\e93e";}
+.icon-carbuy:before {content: "\e93f";}
+.icon-idea:before {content: "\e940";}
+.icon-hands:before {content: "\e941";}
+.icon-arrowprev:before {content: "\e942";}
+.icon-mouse:before {content: "\e900";}
+.icon-mail:before {content: "\e901";}
+.icon-down:before {content: "\e902";}
+.icon-up:before {content: "\e903";}
+.icon-right:before {content: "\e904";}
+.icon-left:before {content: "\e905";}
+.icon-headphones:before {content: "\e906";}
+.icon-download:before {content: "\e907";}
+.icon-chat:before {content: "\e908";}
+.icon-books:before {content: "\e909";}
+.icon-calculator:before {content: "\e90a";}
+.icon-wrong:before {content: "\e90b";}
+.icon-conversation:before { content: "\e90c";}
+.icon-correct:before {content: "\e90d";}
+.icon-error:before {content: "\e90e";}
+.icon-interchange:before {content: "\e90f";}
+.icon-conectivity:before {content: "\e910";}
+.icon-video:before {content: "\e911";}
+.icon-desktop:before {content: "\e912";}
+.icon-document:before {content: "\e913";}
+.icon-stethoscope:before { content: "\e914";}
+.icon-student:before {content: "\e915";}
+.icon-smartphone:before {content: "\e916";}
+.icon-pencil:before {content: "\e917";}
+.icon-sitemap:before {content: "\e918";}
+.icon-medal:before {content: "\e919";}
+.icon-microphone:before {content: "\e91a";}
+.icon-wireless:before {content: "\e91b";}
+.icon-fountain:before {content: "\e91d";}
+.icon-feather:before {content: "\e91e";}
+.icon-pen:before {content: "\e91f";}
+.icon-pentwo:before {content: "\e920";}
+.icon-watercolor:before {content: "\e921";}
+.icon-search:before {content: "\e922";}
+.icon-security:before {content: "\e923";}
+.icon-consult:before {content: "\e924";}
+.icon-sound:before {content: "\e925";}
+.icon-files:before {content: "\e926";}
+.icon-upload:before {content: "\e927";}
+.icon-close:before {content: "\e928";}
+.icon-arrow:before {content: "\e929";}
+.icon-mark:before {content: "\e92a";}
+.icon-time:before {content: "\e92b";}
+.icon-phone:before {content: "\e92c";}
+.icon-share:before {content: "\e92d";}
+.icon-seeker:before {content: "\e92e";}
+.icon-fb:before {content: "\e92f";}
+.icon-tw:before {content: "\e930";}
+.icon-yt:before {content: "\e931";}
+.icon-ig:before {content: "\e932";}
+.icon-in:before {content: "\e933";}
+.icon-sc:before {content: "\e934";}
+.icon-chk:before {content: "\e935";}

+ 1106 - 0
css/sgi.css

@@ -0,0 +1,1106 @@
+/* 
+    Created on : 5/12/2018, 01:34:49 PM
+    Author     : Alejandro
+*/
+
+/* General */
+:root {
+    --font-primary: 'indivisa-text', Arial;
+    --color-primary-text: #001D68;
+    --color-background: white;
+    --color-info-background: #F0F0F0;
+    --color-bloque-background: #dee2e6;
+    --color-bloque-border: white;
+    --color-conflict-background: #f6cfd6;
+    --color-conflict-border: var(--danger);
+    --bloque-hover-background: hsl(207, 12%, 85%);
+    --conflict-hover-background: hsl(0, 100%, 85%);
+    --border-width: 0.2rem;
+
+    --max-width-marco: 60rem;
+    --max-width-marco-wide: 85.375rem;
+    --content-min-height: 30rem;
+    --menu-padding: 0.9375rem;
+    --system-link-spacing: 0.625rem;
+
+    --subtitle-indicator-width: 5rem;
+    --main-title-margin-bottom: 3.75rem;
+    --heading-letter-spacing: 0.0625rem;
+    --subtitle-indicator-height: 0.1875rem;
+    --border-mid-color: #ccc;
+    --icon-spacing: 0.1875rem;
+}
+
+*,
+*::before,
+*::after {
+    box-sizing: border-box;
+}
+
+body {
+    font-family: var(--font-primary);
+    font-size: 1rem;
+    /* 1rem = 16px if the user has not changed their browser default */
+    color: var(--color-primary-text);
+    background-color: var(--color-background);
+    margin: 0;
+    /* Reset default margin */
+}
+
+#logo {
+    max-height: 4rem;
+    /* Adjusted to rem */
+}
+
+.bg-head,
+.bg-info {
+    background-color: var(--color-background);
+}
+
+.bg-info {
+    background-color: var(--color-info-background);
+}
+
+.bloque-clase {
+    background-color: var(--color-bloque-background);
+    padding: 0.625rem;
+    /* 10px equivalent */
+    margin-bottom: 0.625rem;
+    /* 10px equivalent */
+    min-height: 100%;
+    border: var(--border-width) solid var(--color-bloque-border);
+}
+
+.bloque-clase:hover {
+    background-color: var(--bloque-hover-background);
+}
+
+.bloque-clase.conflict {
+    border-color: var(--color-conflict-border);
+    background-color: var(--color-conflict-background);
+}
+
+.bloque-clase.conflict:hover {
+    background-color: var(--conflict-hover-background);
+}
+
+
+/* Make cursor move if draggable */
+.bloque-clase[draggable="true"] {
+    cursor: move;
+}
+
+.bloque-clase.dragging {
+    opacity: 0.5;
+    border: var(--border-width) solid var(--primary);
+    /* Removed !important for best practices */
+}
+
+.dragging-over {
+    border: var(--border-width) solid var(--primary);
+    /* Removed !important for best practices */
+    background-color: var(--color-bloque-background);
+}
+
+.menu-flotante {
+    z-index: 500;
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    background-color: var(--color-menu-background);
+    border-radius: var(--border-radius-menu) 0 0 0;
+    padding-top: 0.125rem;
+    /* 2px equivalent */
+}
+
+/* SOBREESCRIBE BOOTSTRAP */
+.marco,
+.menu {
+    max-width: var(--max-width-marco);
+    width: 100%;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.marco-wide {
+    max-width: var(--max-width-marco-wide);
+    width: 100%;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.content {
+    min-height: var(--content-min-height);
+    padding: 1em;
+    /* 1em remains relative to the font size of the element or its parent */
+}
+
+.menu {
+    display: flex;
+    padding: 0 var(--menu-padding);
+}
+
+.menu-list .sistema:not(:last-child)::after {
+    content: "|";
+    margin: 0 var(--system-link-spacing);
+}
+
+.sistema-active {
+    color: var(--danger-color);
+    font-weight: bold;
+    /* Avoid using !important by ensuring this rule is more specific or declared last */
+}
+
+/*.font-small{font-size:14px;}*/
+
+/* Contenidos */
+h1,
+h2,
+h3 {
+    letter-spacing: var(--heading-letter-spacing);
+    position: relative;
+}
+
+h1 {
+    margin-bottom: 2.5rem;
+    /* Adjusted from 40px to a more scalable unit */
+}
+
+.subtitle::before {
+    content: '';
+    position: absolute;
+    bottom: -0.5rem;
+    /* Adjusted from -8px to a relative unit */
+    left: 0;
+    width: var(--subtitle-indicator-width);
+    display: block;
+    background: var(--danger-color);
+    height: var(--subtitle-indicator-height);
+}
+
+.main-title {
+    font-size: 3.2rem;
+    /* Remains as is, consider using em if scalability is needed */
+    margin-bottom: var(--main-title-margin-bottom);
+}
+
+/* Otros */
+.alert-heading .ing-caret,
+.card-header .ing-caret,
+.side-menu .ing-caret,
+.alert-heading .fa,
+.card-header .fa {
+    transition: transform 0.3s ease-in-out;
+}
+
+.alert-heading .collapsed .ing-caret,
+.card-header .collapsed .ing-caret,
+#accordionMenu .collapsed .ing-caret,
+.alert-heading .collapsed .fa,
+.card-header .collapsed .fa {
+    transform: rotate(90deg);
+}
+
+#accordionMenu .collapsed .ing-caret,
+#accordionMenu .collapsed .fa {
+    transform: rotate(-90deg);
+}
+
+.border-mid:not(:last-child) {
+    border-bottom: 1px dotted var(--border-mid-color);
+}
+
+.pointer {
+    cursor: pointer;
+}
+
+/* TABLAS */
+.table-white .thead-dark th {
+    text-align: center;
+    border-color: #fff;
+    text-transform: uppercase;
+}
+
+.table-white tr td,
+.table-white tr th {
+    border-left: 1px solid #fff !important;
+    /* Adjusted for clarity */
+}
+
+.table-white tr td:first-child,
+.table-white tr th:first-child {
+    border-left: 0;
+}
+
+.table-nostriped tbody tr:nth-of-type(odd) {
+    background-color: transparent;
+}
+
+.rotate-text {
+    writing-mode: vertical-lr;
+    transform: rotate(180deg);
+    height: max-content;
+}
+
+.icono-acciones span,
+.icono-acciones i {
+    margin: 0 var(--icon-spacing);
+    text-decoration: none !important;
+}
+
+.icono-acciones a:focus,
+.icono-acciones a:hover,
+.icono-acciones a:active {
+    text-decoration: none !important;
+}
+
+/* FORMAS  */
+.form-box {
+    margin-bottom: 28px;
+}
+
+.form-box>.form-group {
+    margin-bottom: 10px
+}
+
+.form-box>.form-group>label {
+    font-weight: bold;
+    text-align: right;
+    color: var(--primary-color);
+    padding-left: 0
+}
+
+.form-box>.form-group>label.disabled {
+    color: #969696;
+}
+
+.form-box>.form-group>label:before {
+    content: '';
+    position: absolute;
+    top: -8px;
+    right: 0px;
+    width: 2px;
+    height: calc(100% + 16px);
+    display: block;
+    background: #d21034;
+}
+
+.form-box>.form-group>label.disabled:before {
+    background: #969696 !important;
+}
+
+.form-box-info>.form-group>div {
+    background-color: #f7f7f7;
+    padding-bottom: 10px;
+    padding-left: 8px;
+}
+
+.form-box-info>.form-group>div:first-child {
+    margin-left: 7px;
+}
+
+.form-box-info>.form-group:first-child>label {
+    padding-top: 27px;
+}
+
+.form-box-info>.form-group:first-child>div {
+    padding-top: 20px;
+}
+
+.form-box-info>.form-group:last-child>div {
+    padding-bottom: 20px;
+}
+
+.form-box-info>.form-group.row {
+    margin-bottom: 0 !important;
+}
+
+.modal .form-box-info>.form-group>div {
+    margin-left: 0px;
+}
+
+.radio-md {
+    width: 1em;
+    height: 1em;
+}
+
+.radio-lg {
+    width: 1.5em;
+    height: 1.5em;
+}
+
+.radio-xl {
+    width: 2em;
+    height: 2em;
+}
+
+select:disabled {
+    color: #969696 !important;
+}
+
+.input-info {
+    color: #969696;
+}
+
+.barra-right:before {
+    content: '';
+    position: absolute;
+    top: -8px;
+    right: 0px;
+    width: 2px;
+    height: calc(100% + 16px);
+    display: block;
+    background: #d21034;
+}
+
+/* Uso independiente */
+.barra-right.disabled:before {
+    background: #969696 !important;
+}
+
+/* Uso independiente */
+
+textarea {
+    resize: none;
+    overflow-x: hidden;
+    overflow-wrap: break-word;
+    overflow-y: auto;
+}
+
+.clock[readonly] {
+    background-color: #fff !important;
+}
+
+.hasDatepicker[readonly] {
+    background-color: #fff !important;
+}
+
+.badge {
+    padding: 0.5em 1.4em;
+}
+
+.ui-autocomplete {
+    max-height: 160px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    font-size: 90%
+}
+
+/* Data list*/
+.datalist {
+    position: relative;
+    border: 1px solid #969696;
+    border-radius: .25rem;
+}
+
+.datalist-input {
+    padding: 6px 30px 6px 12px !important;
+    background: #FFFFFF !important;
+    border-radius: .25rem;
+    cursor: pointer;
+    border: 0;
+}
+
+.datalist.disabled .datalist-input {
+    background: #f7f7f7 !important;
+    cursor: default;
+    color: #969696;
+}
+
+.datalist.disabled .icono {
+    opacity: 0;
+}
+
+.datalist .icono {
+    position: absolute;
+    font-size: 20px;
+    right: 10px;
+    top: 9px;
+    color: #969696;
+    transition: transform 0.2s ease;
+}
+
+/*.iconoAzul{color: #001D68 !important;} Usar text-primary*/
+.datalist>ul {
+    position: absolute;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+    max-height: 204px;
+    top: 100%;
+    left: 0;
+    list-style: none;
+    border-radius: 2px;
+    background: #FFFFFF;
+    border: 1px solid #001D68;
+    overflow: hidden;
+    overflow-y: auto;
+    z-index: 100;
+}
+
+.datalist>ul li {
+    display: flex;
+    align-items: center;
+    justify-content: start;
+    padding: 0.3em 1em
+        /*0.8em 1em 0.8em 1em*/
+    ;
+    color: #969696;
+}
+
+.datalist>ul li:not(.not-selectable):hover {
+    background: #D21034;
+    color: #FFFFFF;
+    cursor: pointer;
+}
+
+.datalist .selected {
+    background: #D21034;
+    color: #FFFFFF;
+}
+
+.datalist .not-selectable {
+    text-align: center;
+    font-weight: bold;
+    cursor: default;
+    color: #001d68;
+    padding-left: 10px !important;
+}
+
+.datalist-invalid {
+    border-color: #d21034;
+}
+
+.datalist-invalid .icono {
+    color: #d21034 !important;
+}
+
+/* Icono alerta */
+.alerta {
+    color: #ffb700 !important;
+    text-shadow: -1px -1px 0 #3f2f06, 1px -1px 0 #3f2f06, -1px 1px 0 #3f2f06, 1px 1px 0 #3f2f06;
+}
+
+/* Modal */
+.modal-content {
+    border: 4px solid #001d68;
+}
+
+.modal-header {
+    background-color: #001D68 !important;
+    color: #fff !important;
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+    padding: 4px 8px 8px 8px;
+}
+
+.modal-title {
+    font-weight: normal;
+    text-align: center;
+    padding: 0 30px;
+}
+
+.modal-header .close {
+    position: absolute;
+    top: 0;
+    right: 0;
+}
+
+.modal-header .close:focus {
+    border: none;
+    outline: none;
+}
+
+/* The side navigation menu */
+#sidebar {
+    width: 400px;
+    position: fixed;
+    top: 0;
+    right: -400px;
+    height: 100vh;
+    z-index: 1023;
+    transition: all 0.3s;
+    overflow-y: auto;
+}
+
+#sidebar.active {
+    right: 0;
+}
+
+.overlay {
+    display: none;
+    position: fixed;
+    width: 100vw;
+    height: 100vh;
+    background: rgba(0, 0, 0, 0.6);
+    z-index: 1022;
+    opacity: 0;
+    transition: all 0.5s ease-in-out;
+}
+
+.overlay.active {
+    display: block;
+    opacity: 1;
+}
+
+#sidebar a:hover {
+    text-decoration: none;
+    color: #d12034;
+}
+
+#sidebar a {
+    transition: color 0.6s ease;
+}
+
+/* ICONOS MENU */
+header {
+    padding: 20px 0;
+    height: 110px;
+}
+
+header .logotipo {
+    float: left;
+    clear: none;
+    text-align: inherit;
+    width: 20%;
+    margin-left: 0;
+    margin-right: 0;
+}
+
+header .logotipo:before {
+    content: '';
+    display: table;
+}
+
+header .logotipo img,
+aside .logotipo img {
+    max-width: 200px;
+}
+
+.mainMenu {
+    min-width: 85px;
+}
+
+.menu .nav-item {
+    border-right: 1px solid #969696;
+}
+
+.menu .nav-item:last-child {
+    border-right: 0;
+}
+
+.menu .nav-item>a,
+.menu .nav-item>span {
+    color: #969696;
+    padding: 1px 10px;
+}
+
+.menu .nav-item>a:hover {
+    color: #D21034;
+    text-decoration: none;
+}
+
+.menu .nav-item>a {
+    transition: color 0.6s ease;
+}
+
+.max-h {
+    height: 45px !important;
+    max-height: 45px;
+}
+
+.max-w {
+    width: 45px !important;
+    max-width: 45px;
+}
+
+.iconSesion {
+    margin-right: -20px;
+}
+
+.iconLogin,
+.iconOff {
+    font-size: 16px;
+    display: block;
+    width: 60px;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+    border-radius: 30px 0 0 30px;
+}
+
+.iconOff:hover,
+.iconOff:active {
+    text-decoration: none;
+    background: #D21034;
+    color: #FFFFFF !important;
+    height: 40px;
+}
+
+.iconOff {
+    color: #D21034 !important;
+}
+
+.iconLogin:hover,
+.iconLogin:active,
+.iconOff:hover,
+.iconOff:active {
+    text-decoration: none;
+    color: #FFFFFF !important;
+    height: 40px;
+}
+
+.iconLogin {
+    color: #339933;
+}
+
+.iconLogin:hover,
+.iconLogin:active {
+    background: #339933;
+}
+
+.iconMenu {
+    font-size: 32px;
+}
+
+.menuicon:hover {
+    color: #101097 !important;
+}
+
+.cerraricon {
+    height: 40px !important;
+    max-height: 40px;
+    width: 40px !important;
+    max-width: 40px;
+    cursor: pointer;
+}
+
+.cerraricon:hover {
+    background: #101097 !important;
+}
+
+.fa-ul {
+    list-style-type: none;
+    margin-left: 2.5em;
+    padding-left: 0;
+}
+
+/* BUTTONS */
+/*.btn .fa-circle{color:rgba(255,255,255,0.15);}
+.btn .icon{font-weight: bold;}
+.btn-round{border-radius:30px; padding-left: 0.5rem!important; padding-right: 0.5rem!important;}*/
+
+.btn-ing {
+    position: relative;
+    padding-right: 35px;
+    padding-left: 20px;
+}
+
+.btn.arrow:after {
+    content: "\e905";
+    font-size: 14px;
+    position: absolute;
+    font-family: "ingfont";
+    right: 14px;
+    margin-top: 3px;
+    font-weight: bold;
+    -webkit-transition: 0.6s all ease;
+    -moz-transition: 0.6s all ease;
+    -o-transition: 0.6s all ease;
+    -ms-transition: 0.6s all ease;
+    transition: 0.6s all ease;
+    vertical-align: middle;
+}
+
+/* SOBREESCRIBE BOOTSTRAP */
+.btn-outline-primary.arrow:after {
+    color: #D21034;
+}
+
+.btn-outline-danger.arrow:after {
+    color: #001D68;
+}
+
+.btn-outline-secondary:hover.arrow:after {
+    color: #D21034;
+}
+
+.btn-outline-info:hover.arrow:after {
+    color: #001D68;
+}
+
+/***** SCROLLBAR *****/
+div ::-webkit-scrollbar {
+    width: 8px;
+}
+
+/*Ancho*/
+div ::-webkit-scrollbar-track {
+    background: #f7f7f7;
+}
+
+/*Riel*/
+div ::-webkit-scrollbar-thumb {
+    background: #969696;
+}
+
+/* Handle */
+div ::-webkit-scrollbar-thumb:hover {
+    background: #001d68;
+}
+
+/* Effects */
+/* Vars for primary, secondary, success, info, warning, danger, light, dark */
+
+:root {
+    --primary-color: #001d68;
+    --secondary-color: #001d68;
+    --success-color: #339933;
+    --danger-color: #d21034;
+    --warning-color: #ffc107;
+    --info-color: #969696;
+    --light-color: #f7f7f7;
+    --dark-color: #343a40;
+}
+
+.glow-primary {
+    background: var(--primary-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--primary-color);
+    transition: all 0.5s ease;
+}
+
+.glow-secondary {
+    background: var(--secondary-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--secondary-color);
+    transition: all 0.5s ease;
+}
+
+.glow-success {
+    background: var(--success-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--success-color);
+    transition: all 0.5s ease;
+}
+
+.glow-danger {
+    background: var(--danger-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--danger-color);
+    transition: all 0.5s ease;
+}
+
+.glow-warning {
+    background: var(--warning-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--warning-color);
+    transition: all 0.5s ease;
+}
+
+.glow-info {
+    background: var(--info-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--info-color);
+    transition: all 0.5s ease;
+}
+
+.glow-light {
+    background: var(--light-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--light-color);
+    transition: all 0.5s ease;
+}
+
+.glow-dark {
+    background: var(--dark-color);
+    color: #fff;
+    border-radius: 5px;
+    padding: 5px;
+    box-shadow: 0 0 5px var(--dark-color);
+    transition: all 0.5s ease;
+}
+
+/*Hover Handle */
+
+/***** FOOTER *****/
+footer {
+    font-size: 14px;
+    color: #fff;
+}
+
+footer .footerTop {
+    background: #001d68;
+    padding: 15px 0;
+}
+
+footer .footerTop .logotipo {
+    overflow: hidden;
+}
+
+footer .footerTop .logotipo h3 {
+    display: inline-block;
+    vertical-align: top;
+    color: #fff;
+    margin: 0;
+    float: right;
+    text-align: right;
+    font-size: 25px;
+    font-family: 'indivisa-text'
+}
+
+footer .footerTop .logotipo h3 span {
+    display: block;
+}
+
+footer .footerTop .menuFooter h3 {
+    font-size: 12px;
+    font-family: 'indivisa-text';
+    color: #fff !important;
+}
+
+footer .footerTop .menuFooter ul {
+    overflow: hidden;
+}
+
+footer ul {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+footer .footerTop .menuFooter ul>li {
+    zoom: 1;
+    float: left;
+    clear: none;
+    text-align: inherit;
+    width: 16%;
+    margin-left: 0;
+    margin-right: 3%;
+}
+
+footer .footerTop .menuFooter ul>li ul li a {
+    font-size: 10px;
+}
+
+footer ul>li {
+    display: inline-block;
+    vertical-align: top;
+}
+
+.footerMore {
+    position: relative;
+    display: none;
+    padding: 5px 0;
+}
+
+footer a {
+    color: #fff;
+    -webkit-transition: color 0.5s;
+    transition: color 0.5s;
+}
+
+footer a:hover {
+    color: #ce0e2d !important;
+    text-decoration: none !important;
+}
+
+footer .footerTop .menuFooter ul>li ul li {
+    display: block;
+    width: 100%;
+    margin-bottom: 0px;
+}
+
+footer .ubicacion {
+    margin-top: 20px;
+    overflow: hidden;
+}
+
+footer .ubicacion .address {
+    display: inline-block;
+    /*width: 65%;*/
+    vertical-align: bottom;
+}
+
+footer .ubicacion .address h4,
+footer .ubicacion .address h4 a {
+    color: #0fb7f1;
+    font-size: 14px;
+    margin: 0 0 0 -5px;
+    position: relative;
+}
+
+footer .ubicacion .address h4 a {
+    display: inline-block;
+}
+
+footer .ubicacion .redes {
+    display: inline-block;
+    vertical-align: bottom;
+    /*width:32%;text-align:right*/
+}
+
+footer .ubicacion .redes h4 {
+    display: inline-block;
+    vertical-align: middle;
+    margin: 0;
+    font-size: 16px !important;
+    font-weight: bold;
+}
+
+footer .ubicacion .redes ul {
+    display: inline-block;
+    vertical-align: middle
+}
+
+footer .ubicacion .redes ul li {
+    margin-left: 2px
+}
+
+footer .footerMiddle {
+    background: #071e58;
+    overflow: hidden;
+}
+
+footer .footerMiddle nav ul {
+    text-align: center;
+}
+
+footer .footerMiddle nav ul li {
+    border-right: 1px solid #fff;
+    padding: 1px 10px;
+    display: inline-block;
+    margin-bottom: 10px;
+}
+
+footer ul>li {
+    display: inline-block;
+    vertical-align: top;
+}
+
+footer .footerBottom {
+    background: #091941;
+    overflow: hidden;
+    padding: 15px 0;
+}
+
+.footerBottom .logotipos {
+    display: inline-block;
+    vertical-align: middle;
+    width: 20%
+}
+
+footer .footerBottom .logotipos a {
+    display: inline-block;
+    width: 80px;
+    margin-right: 6px
+}
+
+footer .footerBottom .logotipos a.internacional {
+    width: 80px
+}
+
+footer .footerBottom .logotipos a.red {
+    width: 75px
+}
+
+footer .footerBottom .legales {
+    text-align: right;
+    float: right;
+    width: 60%;
+    margin-right: 0;
+    margin-left: auto;
+    padding-top: 10px;
+    padding-bottom: 0
+}
+
+footer .footerBottom .legales ul li {
+    border-right: 1px solid #fff;
+    padding: 1px 10px
+}
+
+footer .footerBottom .legales ul li:last-child {
+    border: 0
+}
+
+footer .tab-pane p {
+    font-size: 12px;
+    line-height: 18px;
+}
+
+@media (max-width: 800px) {
+
+    .menu,
+    .subMenu {
+        position: relative;
+    }
+
+    .iconoMenu {
+        position: absolute;
+        display: block;
+        right: 20px;
+        top: 10px;
+    }
+
+    .menuOculto {
+        display: none;
+    }
+
+    .form-box-info>.form-group>div {
+        margin-left: 0px;
+    }
+
+    /*
+    .responsive{
+        float: none;
+        text-align: center;
+        display: flex;
+        flex-direction: column;
+        flex-wrap: wrap;
+        justify-content: center;
+        align-items: center;
+    }*/
+}
+
+.movie {
+    transition: all 0.1s;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+    height: 8rem;
+    /* align all inside content to the middle */
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    background-color: #f7f7f7;
+}
+
+.movie:hover {
+    transform: scale(1.05);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+    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;
+}
+
+.icono.ing-buscar {
+    cursor: pointer;
+}

+ 1 - 1
dependencies.php

@@ -88,7 +88,7 @@ use \SeinopSys\PostgresDb;
 
 function makeConnection($hostOrConnectionString, $port = null, $dbname = null, $user = null, $password = null): PostgresDb
 {
-    $connectionString = is_null($port) ? $hostOrConnectionString : "pgsql:host=$hostOrConnectionString;port=$port;dbname=$dbname;user=$user;password=$password";
+    $connectionString = is_null($port) ? $hostOrConnectionString : "pgsql:host=$hostOrConnectionString;port=$port;dbname=$dbname;user=$user;password=$password;sslmode=disable";
 
     try {
         $pdo = new PDO($connectionString);

+ 41 - 133
export/excel.php

@@ -6,139 +6,26 @@ require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
 use Respect\Validation\Validator as v;
 
 $query = json_decode(file_get_contents('php://input'), true);
-$queries = array(
-    'usuarios' => <<<SQL
-    SELECT 
-        u.id AS user_id,
-        u.username AS username,
-        CONCAT(u.firstname, ' ', u.lastname) AS full_name,
-        u.email AS email,
-        u.lastaccess AS last_access,
-        c.fullname AS course_name,
-        cc.name AS category_name
-    FROM 
-        mdl_user u
-    LEFT JOIN 
-        mdl_user_enrolments ue ON u.id = ue.userid
-    LEFT JOIN 
-        mdl_enrol e ON ue.enrolid = e.id
-    LEFT JOIN 
-        mdl_course c ON e.courseid = c.id
-    LEFT JOIN 
-        mdl_course_categories cc ON c.category = cc.id
-    WHERE 
-        u.deleted = 0
-    ORDER BY 
-        u.id
-    SQL,
-
-    'alumnos' => <<<SQL
-    SELECT u.id, u.username, u.firstname, u.lastname, u.email
-    FROM mdl_user u
-    JOIN mdl_role_assignments ra ON ra.userid = u.id
-    WHERE username LIKE 'al%';
-    SQL,
-
-    'usuarios_temporales' => <<<SQL
-    SELECT id, username, firstname, lastname, email, deleted
-    FROM mdl_user
-    WHERE deleted = 1 or confirmed = 0;
-    SQL,
-
-    'calificaciones' => <<<SQL
-    SELECT  
-    c.id AS courseid,
-    c.shortname,
-    COALESCE(cc2.name, cc.name) AS AREA,
-    CASE 
-        WHEN cc2.name IS NULL THEN NULL 
-        ELSE cc.name 
-    END AS GRUPO,
-    prof.username AS profesor_clave,
-    CONCAT(prof.firstname, ' ', prof.lastname) AS profesor_nombre,
-    c.fullname AS course_fullname,
-    MAX(mgi.calculation) AS fórmula,
-    CASE 
-        WHEN COALESCE(MIN(mgi.calculation) <> '', false) THEN 'Sí' 
-        ELSE 'No' 
-    END AS formula,
-    CASE 
-        WHEN MAX(mgi.AGGREGATIONCOEF) > 0  THEN 'Sí' 
-        ELSE 'No' 
-    END AS ponderacion,
-    CASE 
-        WHEN MAX(mgi.calculation) IS NOT NULL THEN (
-        SELECT SUM(val::numeric) 
-        FROM (
-            SELECT 
-            unnest(regexp_matches(MAX(mgi.calculation), '\d?\.\d+', 'g')) AS val
-        ) AS subquery
-        )
-        ELSE NULL 
-    END AS suma_numeros,
-JSONB_AGG(JSONB_BUILD_OBJECT(COALESCE(gc.fullname, '-'), mgi.aggregationcoef)) FILTER (WHERE mgi.aggregationcoef >0 ) ,
-CASE WHEN MAX(MGI.GRADEMAX) > 0 THEN
-sum(mgi.aggregationcoef) / max(mgi.grademax) FILTER (WHERE mgi.itemtype = 'course')
-ELSE 
-0 END AS "Suma categorías",
-MAX(MGI.GRADEMAX)
-    FROM 
-    mdl_course c 
-    JOIN mdl_grade_items mgi ON c.id = mgi.courseid 
-	LEFT JOIN mdl_grade_categories gc ON mgi.iteminstance = gc.id AND mgi.itemtype = 'category' -- Asegúrate de que el itemtype sea 'category'
-
-    JOIN mdl_course_categories cc ON cc.id = c.category 
-    LEFT JOIN mdl_course_categories cc2 ON cc.parent = cc2.id 
-    JOIN mdl_context ctx ON ctx.instanceid = c.id 
-    JOIN mdl_role_assignments ra ON ra.contextid = ctx.id AND ra.roleid = 3 
-    JOIN mdl_user prof ON ra.userid = prof.id 
-    WHERE 
-    mgi.itemtype IN ('course', 'category') 
-    GROUP BY 
-    c.id, c.shortname, cc.name, cc2.name, c.fullname, prof.firstname, prof.lastname, prof.username 
-    ORDER BY 
-    AREA, GRUPO, profesor_nombre;
-SQL,
-
-'calificaciones_brutas' => <<<SQL
-SELECT
-    gi.itemname AS item_name,
-    c.id AS course_id,
-    c.fullname AS course_name,
-    u.id AS user_id,
-    u.username,
-    u.firstname,
-    u.lastname,
-    gg.finalgrade
-FROM
-    mdl_grade_grades gg
-JOIN
-    mdl_grade_items gi ON gg.itemid = gi.id
-JOIN
-    mdl_course c ON gi.courseid = c.id
-JOIN
-    mdl_user u ON gg.userid = u.id
-WHERE
-    gi.itemtype = 'mod' and gi.itemmodule <> 'attendance' and username like 'al%'
-	
-order by username, item_name;
-SQL,
-);
-
-methods(['POST' => v::keySet(
-    v::key('query', v::in(array_keys($queries))),
-)]);
+$consulta = $db
+    ->where('clave', $query['query'])
+    ->getOne('consulta', 'consulta_sql, archivo');
+
+
+methods([
+    'POST' => v::keySet(
+        v::key('query', v::in($query)),
+    )
+]);
 
 // method must be POST
 if (!isset($_SESSION['user'], $moodle_db)) {
     serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión o no se ha establecido una conexión con la base de datos de Moodle');
     exit();
 }
-$data = $moodle_db->query($queries[$query['query']]);
-$filename = 'test.csv';
+$data = $moodle_db->query($consulta['consulta_sql']);
 
 header('Content-Type: text/csv; charset=UTF-8');
-header('Content-Disposition: attachment; filename="' . $filename . '"');
+header('Content-Disposition: attachment; filename="' . $consulta['archivo'] . '"');
 echo "\xEF\xBB\xBF"; // Añade el BOM de UTF-8 al inicio del archivo para indicar su codificación
 
 $fp = fopen('php://output', 'w'); // 'wb' también es válido en este contexto
@@ -146,10 +33,20 @@ $fp = fopen('php://output', 'w'); // 'wb' también es válido en este contexto
 $headers = array_keys($data[0]);
 
 switch ($query['query']) {
-    case 'calificaciones':
-        $headers[] = 'Tiene calificación'; // Añade el nombre de la nueva columna al final del encabezado
-        $headers[] = 'Calificación Máxima';
-        $headers[] = 'grupo_numerico'; // Añade el nombre de la nueva columna al final del encabezado
+    case 'c-calif':
+        $headers[] = 'TIENE CALIFICACIÓN';
+        $headers[] = 'CALIFICACIÓN MÁXIMA';
+        $headers[] = 'GRUPO NUMÉRICO';
+        break;
+    case 'c-fin':
+        # Añade 'TIENE CALIFICACIÓN'
+        $headers[] = 'TIENE CALIFICACIÓN';
+        # Ejecuta el query de 'c-calif' para obtener la calificación
+        $calificacion = $db
+            ->where('clave', 'c-calif')
+            ->getOne('consulta');
+
+        $result = $moodle_db->query($calificacion['consulta_sql']);
         break;
 }
 // todos los headers en Mayúsculas incluyendo los acentos
@@ -162,7 +59,7 @@ fputcsv($fp, $headers);
 // insert data
 foreach ($data as $line) {
     switch ($query['query']) {
-        case 'calificaciones':
+        case 'c-calif':
             $penultimaColumna = $line['formula']; // Asume que 'formula' es la penúltima columna
             $ultimaColumna = $line['ponderacion']; // Asume que 'ponderacion' es la última columna
 
@@ -170,8 +67,8 @@ foreach ($data as $line) {
             $nuevoValor = ($penultimaColumna === 'Sí' || $ultimaColumna === 'Sí') ? 'Sí' : 'No';
 
             // Añade el nuevo valor al final de la línea
-            $line['Tiene calificación'] = $nuevoValor;
-            $line['Calificación Máxima'] = $line['SUMA_NUMEROS'] +  $line['Suma categorías'];
+            $line['TIENE CALIFICACIÓN'] = $nuevoValor;
+            $line['CALIFICACIÓN MÁXIMA'] = floatval($line['suma_numeros']) + floatval($line['Suma categorías']);
 
 
             $grupoNumerico = '';
@@ -180,7 +77,18 @@ foreach ($data as $line) {
             }
 
             // Añade el valor numérico extraído al final de la línea
-            $line['grupo_numerico'] = $grupoNumerico;
+            $line['GRUPO NUMÉRICO'] = $grupoNumerico;
+            break;
+        case 'c-fin':
+            // encuentra el result que tenga el mismo courseid
+            $courseid = $line['course_id'];
+            $calificacion = array_filter($result, function ($row) use ($courseid) {
+                return $row['courseid'] === $courseid && ($row['ponderacion'] === 'Sí' || $row['formula'] === 'Sí');
+            });
+
+            // si encuentra el result, añade 'Sí' a la última columna
+            $line['TIENE CALIFICACIÓN'] = count($calificacion) > 0 ? 'Sí' : 'No';
+
             break;
     }
 

+ 31 - 0
fetch/calificaciones.php

@@ -0,0 +1,31 @@
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+if (!isset($_SESSION['user'])) {
+    returnResponse(status: 401, error: true, message: 'No se ha iniciado sesión');
+    exit();
+}
+
+header('Content-Type: application/json');
+$params = [
+    'moodle_id' => $_SESSION['moodle_id'],
+    'username' => $_POST['username']
+];
+$calificaciones = $db->query("SELECT * from jsonb_array_elements(public.consulta_moodle(
+    consulta =>	(SELECT replace(consulta_sql, ';', '') from consulta whERE clave = 'c-fin')::TEXT,
+    moodle_id => :moodle_id
+)) AS calificaciones WHERE calificaciones->>'username' = :username;",
+    $params
+) ?? [];
+
+$timeline = $db->query('SELECT * from public.promedio_snapshot(
+    moodle_host_id_param => :moodle_id,
+    username_param =>:username)',
+    $params
+) ?? [];
+
+echo json_encode(
+    array(
+        'calificaciones' => $calificaciones,
+        'timeline' => $timeline
+    )
+);

+ 25 - 0
graph.php

@@ -0,0 +1,25 @@
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+$calificaciones = $db
+    ->where('moodle_host_id', $_SESSION['moodle_id'])
+    ->get('snapshot_calificaciones');
+?>
+
+<table>
+    <thead>
+        <tr>
+            <th>Nombre</th>
+            <th>Calificación</th>
+            <th>Fecha</th>
+        </tr>
+    </thead>
+    <tbody>
+        <?php foreach ($calificaciones as $calificacion): ?>
+            <tr>
+                <td><?= $calificacion['moodle_host_id'] ?></td>
+                <td><?= strlen($calificacion['calificaciones']) > 100 ? substr($calificacion['calificaciones'], 0, 100) . '...' : $calificacion['calificaciones'] ?></td>
+                <td><?= $calificacion['created_at'] ?></td>
+            </tr>
+        <?php endforeach; ?>
+    </tbody>
+</table>

+ 150 - 50
index.php

@@ -1,13 +1,22 @@
 <?php
 require_once "dependencies.php";
-
-if (isset($_SESSION['moodle_db'], $_SESSION['user'])) {
+if(isset($_POST['page'])) {
+    $page = $_POST['page'];
+} else if (isset($_SESSION['moodle_db'], $_SESSION['user'])) {
     $page = 'menu';
 } else if (isset($_SESSION['user'])) {
     $page = 'host';
 } else {
     $page = 'login';
 }
+
+try {
+    $moodle = $db->where('moodle_host_id', $_SESSION['moodle_id'])->getOne('moodle_host', 'etiqueta, host');
+    $usuario = $db->where('clave', $_SESSION['user'])->getOne('auth.usuario', 'alias, estado');
+} catch (Exception $e) {
+    serverError("Error al obtener la información del usuario y el moodle", $e);
+}
+$title = "Administración de Calificaciones";
 ?>
 <!DOCTYPE html>
 <html lang="en">
@@ -15,11 +24,33 @@ if (isset($_SESSION['moodle_db'], $_SESSION['user'])) {
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Administración de calificaciones</title>
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
+    <title>
+        <?= $title ?>
+    </title>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
+        integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
+        crossorigin="anonymous" referrerpolicy="no-referrer" />
+    <link rel="stylesheet" href="css/bootstrap.css">
+    <link rel="stylesheet" href="css/indivisa.css">
+    <link rel="stylesheet" href="css/lasalle.css">
+    <link rel="stylesheet" href="css/sgi.css">
+    <link rel="shortcut icon" href="imagenes/favicon.png" type="image/x-icon">
+
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
+        integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
+        crossorigin="anonymous"></script>
     <script src="https://unpkg.com/petite-vue"></script>
+    <style>
+
+    </style>
     <script>
+        // on document ready
+        document.addEventListener('DOMContentLoaded', () => {
+            const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
+            const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
+        })
+
+
         const store = PetiteVue.reactive({
             error: null,
             loading: false,
@@ -54,69 +85,138 @@ if (isset($_SESSION['moodle_db'], $_SESSION['user'])) {
 
                 return response.json()
             },
+            showModal($el) {
+                const modal = new bootstrap.Modal($el)
+                modal.show()
+            },
+            hideModal($el) {
+                const modal = new bootstrap.Modal($el)
+                modal.hide()
+
+                // delete all backdrop elements
+                document.querySelectorAll('.modal-backdrop').forEach(backdrop => backdrop.remove())
+            }
         });
     </script>
 </head>
 
 <body>
-    <nav>
-        <div class="container">
-            <div class="grid">
-                <h1>Administración de calificaciones</h1>
-                <?php if (isset($_SESSION['user']) || isset($_SESSION['moodle_db'])) : ?>
-                    <div class="grid">
-                        <?php if (isset($_SESSION['user'])) : ?>
-                            <form action="/action/desconectar.php">
-                                <input type="hidden" name="action" value="sign-out">
-                                <button type="submit" @click="sessionStorage.removeItem('token')">Cerrar sesión <i class="fas fa-sign-out-alt"></i></button>
-                            </form>
-                        <?php endif; ?>
-                        <?php if (isset($_SESSION['moodle_db'])) : ?>
-                            <form action="/action/desconectar.php">
-                                <input type="hidden" name="action" value="desconectar">
-                                <button type="submit">Desconectar <i class="fas fa-times-circle"></i></button>
-                            </form>
+    <?php
+    # include an html file from an url
+    require 'template/header.html';
+    ?>
+    <div class="container-fluid">
+        <nav class="navbar navbar-expand-lg navbar-light bg-light">
+            <div class="container marco">
+                <span class="navbar-brand text-primary text-uppercase fs-4" href="/">
+                    <i class="fas fa-graduation-cap"></i>
+                    <?= $title ?>
+                    (<small data-bs-toggle="tooltip" data-bs-title="<?= $moodle['host'] ?>">
+                        <?= $moodle['etiqueta'] ?>
+                    </small>)
+                </span>
+                <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
+                    data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false"
+                    aria-label="Toggle navigation">
+                    <span class="navbar-toggler-icon"></span>
+                </button>
+                <div class="collapse navbar-collapse" id="navbarNavDropdown">
+                    <ul class="navbar-nav ms-auto align-items-lg-center">
+                        <?php if (isset($_SESSION['user']) || isset($_SESSION['moodle_db'])): ?>
+                            <li class="nav-item dropdown">
+                                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button"
+                                    data-bs-toggle="dropdown" aria-expanded="false">
+                                    <i class="fas fa-user"></i>
+                                    Cuenta (<small class="text-primary text-uppercase" data-bs-toggle="tooltip"
+                                        data-bs-title="<?= $usuario['alias'] ?>">
+                                        <?= $_SESSION['user'] ?>
+                                    </small>)
+                                </a>
+                                <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownMenuLink">
+                                    <?php if (isset($_SESSION['user'])): ?>
+                                        <li>
+                                            <form action="/action/desconectar.php" method="get" class="d-grid gap-2 m-2">
+                                                <input type="hidden" name="action" value="sign-out">
+                                                <button type="submit" class="btn btn-danger w-100"
+                                                    onclick="sessionStorage.removeItem('token')">Cerrar sesión <i
+                                                        class="fas fa-sign-out-alt"></i></button>
+                                            </form>
+                                        </li>
+                                    <?php endif; ?>
+                                    <?php if (isset($_SESSION['moodle_db'])): ?>
+                                        <li>
+                                            <form action="/action/desconectar.php" method="get" class="d-grid gap-2 m-2">
+                                                <input type="hidden" name="action" value="desconectar">
+                                                <button type="submit" class="btn btn-secondary w-100">Desconectar <i
+                                                        class="fas fa-times-circle"></i></button>
+                                            </form>
+                                        </li>
+                                    <?php endif; ?>
+                                </ul>
+                            </li>
                         <?php endif; ?>
+                    </ul>
+                </div>
+            </div>
+        </nav>
+    </div>
+
+    <div class="container my-4 marco content" v-scope>
+        <div class="modal" id="loadingModal" tabindex="-1" aria-hidden="true" v-if="store.loading"
+            @vue:mounted="store.showModal($el)" @vue:unmounted="store.hideModal($el)" data-bs-backdrop="static"
+            data-bs-keyboard="false">
+            <div class="modal-dialog modal-dialog-centered">
+                <div class="modal-content">
+                    <div class="modal-body">
+                        <div class="d-flex justify-content-center">
+                            <div class="spinner-border text-primary" role="status">
+                                <span class="visually-hidden">Loading...</span>
+                            </div>
+                        </div>
                     </div>
-                <?php endif; ?>
+                </div>
             </div>
         </div>
-    </nav>
-    <div class="container" v-scope>
-        <dialog :open="store.loading">
-            <div class="grid">
-                <button aria-busy="true" class="secondary"></button>
-            </div>
-        </dialog>
-
-        <dialog :open="store.error !== null" v-if="store.error">
-            <article>
-                <header>
-                    <a href="#close" aria-label="Close" class="close" @click="store.error = null" v-if="store.error?.avoidable"></a>
-                    <strong>
-                        {{ store.error?.title }}
-                    </strong>
-                </header>
-                <p>
-                    {{ store.error?.message }}
-
-                </p>
-                <br>
-                <div class="grid">
-                    <button v-for="action in store.error?.actions ?? []" @click="action.handler" :class="action.class">
-                        {{ action.label }}
-                    </button>
+
+        <div class="modal" id="errorModal" tabindex="-1" aria-hidden="true" v-if="store.error !== null && store.error"
+            @vue:mounted="store.showModal($el)" @vue:unmounted="store.hideModal($el)" data-bs-backdrop="static"
+            data-bs-keyboard="false">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">
+                            {{ store.error?.title }}
+                        </h5>
+                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
+                            @click="store.error = null"></button>
+                    </div>
+                    <div class="modal-body">
+                        <p>{{ store.error?.message }}</p>
+                    </div>
+                    <div class="modal-footer">
+                        <button v-for="action in store.error?.actions ?? []" @click="action.handler"
+                            :class="['btn', action.class]">
+                            {{ action.label }}
+                        </button>
+                    </div>
                 </div>
-            </article>
-        </dialog>
+            </div>
+        </div>
+
+
         <?php
         if (!isset($page)) {
             throw new Exception('No se ha definido la variable $page');
         }
         require "{$_SERVER['DOCUMENT_ROOT']}/pages/$page.html";
         ?>
+
     </div>
+    <?php
+    # include an html file from an url
+    require 'template/footer.html';
 
+    ?>
 </body>
 
 </html>

+ 95 - 0
pages/graph.html

@@ -0,0 +1,95 @@
+<form action="/" method="post">
+    <input type="hidden" name="page" value="menu">
+    <button class="btn btn-primary mb-4">
+        <i class="fas fa-arrow-left"></i> Regresar
+    </button>
+</form>
+<main class="container">
+    <form class="mb-4">
+        <div class="row mb-3 form-box">
+            <label for="item.value" class="col-sm-1 col-form-label form-group barra">Alumno</label>
+            <div class="col-sm-10">
+                <input type="item.type" name="item.name" list="item.name" id="item.value" class="form-control col-form-label">
+                <datalist id="item.name">
+                    <option value="option.id">Alumno_nombre</option>
+                </datalist>
+            </div>
+            <button type="reset" class="btn btn-danger btn-sm col-sm-1">
+                <i class="fas fa-eraser"></i>
+            </button>
+        </div>
+    </form>
+
+    <section>
+        <h2>Gráficos</h2>
+        <div class="row">
+            <div class="col-md-6">
+                <canvas id="chart1" width="400" height="400"></canvas>
+            </div>
+            <div class="col-md-6">
+                <canvas id="chart2" width="400" height="400"></canvas>
+            </div>
+        </div>
+    </section>
+</main>
+
+<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+    <script>
+        const ctx1 = document.getElementById('chart1').getContext('2d');
+        const chart1 = new Chart(ctx1, {
+            type: 'bar',
+            data: {
+                labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
+                datasets: [{
+                    label: '# of Votes',
+                    data: [12, 19, 3, 5, 2, 3],
+                    backgroundColor: [
+                        'rgba(255, 99, 132, 0.2)',
+                        'rgba(54, 162, 235, 0.2)',
+                        'rgba(255, 206, 86, 0.2)',
+                        'rgba(75, 192, 192, 0.2)',
+                        'rgba(153, 102, 255, 0.2)',
+                        'rgba(255, 159, 64, 0.2)'
+                    ],
+                    borderColor: [
+                        'rgba(255, 99, 132, 1)',
+                        'rgba(54, 162, 235, 1)',
+                        'rgba(255, 206, 86, 1)',
+                        'rgba(75, 192, 192, 1)',
+                        'rgba(153, 102, 255, 1)',
+                        'rgba(255, 159, 64, 1)'
+                    ],
+                    borderWidth: 1
+                }]
+            },
+            options: {
+                scales: {
+                    y: {
+                        beginAtZero: true
+                    }
+                }
+            }
+        });
+
+        const ctx2 = document.getElementById('chart2').getContext('2d');
+        const chart2 = new Chart(ctx2, {
+            type: 'line',
+            data: {
+                labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
+                datasets: [{
+                    label: '# of Votes',
+                    data: [12, 19, 3, 5, 2, 3],
+                    backgroundColor: 'rgba(255, 99, 132, 0.2)',
+                    borderColor: 'rgba(255, 99, 132, 1)',
+                    borderWidth: 1
+                }]
+            },
+            options: {
+                scales: {
+                    y: {
+                        beginAtZero: true
+                    }
+                }
+            }
+        });
+    </script>

+ 77 - 85
pages/host.html

@@ -1,106 +1,97 @@
-<main class="container" v-if="page === 'host'" v-scope @vue:mounted="mounted">
-    <h1>Conectar un HOST</h1>
-    <form action="action/conectar_moodle.php" method="post" v-scope="{host: null}">
-        <div class="grid">
-            <div v-if="hosts.length > 0">
-                <label for="moodle-host">
-                    Moodle host
-                    <input list="moodle-hosts" name="moodle-host" placeholder="Moodle host" required
-                        v-model="selectedHost">
-                </label>
+<main class="container mt-5" v-if="page === 'host'" v-scope @vue:mounted="mounted">
+    <h1 class="mb-4">Conectar un HOST</h1>
+    <form action="action/conectar_moodle.php" method="post" v-scope="{host: null}" autocomplete="off">
+        <div v-if="hosts.length > 0">
+            <div class="input-group mb-3">
+                <div class="input-group-prepend">
+                    <label for="moodle-host" class="input-group-text">Moodle host</label>
+                </div>
+                <input class="form-control" list="moodle-hosts" id="moodle-host" name="moodle-host"
+                    placeholder="Seleccione o ingrese un Moodle host" required v-model="selectedHost">
                 <datalist id="moodle-hosts">
-                    <option v-for="host in hosts" :value="host.host">
-                        {{ host.etiqueta }}
-                    </option>
+                    <option v-for="host in hosts" :value="host.host">{{ host.etiqueta }}</option>
                 </datalist>
-            </div>
-            <div v-else>
-                <p>No hay hosts registrados</p>
-            </div>
-            <label for="agregar-host">
-                Agregar host
-                <button id="agregar-host" type="button" @click="page = 'current_host'; selectedHost = null">
+                <button id="limpiar-host" type="reset" class="btn btn-secondary" @click="selectedHost = null">
+                    <i class="fas fa-eraser"></i>
+                </button>
+                <button id="agregar-host" type="button" class="btn btn-success"
+                    @click="page = 'current_host'; selectedHost = null">
                     <i class="fas fa-plus"></i>
                 </button>
-            </label>
+                <button type="button" class="btn btn-info"
+                    :disabled="hosts.filter(host => host.host === selectedHost).length === 0"
+                    @click="page = 'current_host'">
+                    <i class="fas fa-pencil-alt"></i>
+                </button>
+            </div>
         </div>
-        <div class="grid">
-            <button type="button" :disabled="hosts.filter(host => host.host === selectedHost).length === 0"
-                @click="page = 'current_host'">Editar <i class="fas fa-edit"></i>
-            </button>
-            <button type="submit" :disabled="hosts.filter(host => host.host === selectedHost).length === 0">
-                Conectar <i class="fas fa-database"></i>
+        <div v-else class="mb-3">
+            <p>No hay hosts registrados</p>
+        </div>
+
+        <div class="d-grid gap-2">
+            <button type="submit" class="btn btn-primary"
+                :disabled="hosts.filter(host => host.host === selectedHost).length === 0">
+                <i class="fas fa-plug"></i> Conectar
             </button>
         </div>
     </form>
 </main>
 
+
 <div v-else @vue:mounted="selectHost">
-    <main class="container">
+    <main class="container mt-5">
         <h1>Registrar un nuevo HOST de Moodle</h1>
-        <button class="btn btn-primary" @click="page = 'host'">
-            Regresar
-            <i class="fas fa-arrow-left"></i>
+        <button class="btn btn-primary mb-4" @click="page = 'host'">
+            <i class="fas fa-arrow-left"></i> Regresar
         </button>
         <form action="/action/new_host.php" method="post" @submit.prevent="newHost" id="new_host">
-            <div class="grid">
-                <label for="etiqueta">
-                    Etiqueta
-                    <input type="text" name="etiqueta" placeholder="Etiqueta para identificar el host" required
-                        :value="current_host.etiqueta" v-model="current_host.etiqueta" autocomplete="off">
-                    <small>Etiqueta para identificar: <code>Moodle2023A</code></small>
-                </label>
-                <label for="base_datos">
-                    Base de datos
-                    <input type="text" name="base_datos" placeholder="Nombre de la base de datos" required
-                        :value="current_host.postgres_dbname" v-model="current_host.postgres_dbname">
-                    <small>Ejemplo: <code>moodle42licdb</code></small>
-                </label>
+            <div class="mb-3">
+                <label for="etiqueta" class="form-label">Etiqueta</label>
+                <input type="text" name="etiqueta" class="form-control" placeholder="Etiqueta para identificar el host"
+                    required v-model="current_host.etiqueta" autocomplete="off">
+                <div class="form-text">Etiqueta para identificar: <code>Moodle2023A</code></div>
+            </div>
+            <div class="mb-3">
+                <label for="base_datos" class="form-label">Base de datos</label>
+                <input type="text" name="base_datos" class="form-control" placeholder="Nombre de la base de datos"
+                    required v-model="current_host.postgres_dbname">
+                <div class="form-text">Ejemplo: <code>moodle42licdb</code></div>
+            </div>
+            <div class="mb-3">
+                <label for="host" class="form-label">Host de Moodle</label>
+                <input type="text" name="host" class="form-control" placeholder="200.13.89.000" required
+                    v-model="current_host.host">
+                <div class="form-text">localhost, moodleXYZ.lci.ulsa.mx, 200.13.89.000</div>
             </div>
-            <div class="grid">
-                <label for="host">
-                    Host de Moodle
-                    <input type="text" name="host" placeholder="200.13.89.000" required :value="current_host.host"
-                        v-model="current_host.host">
-                    <small>localhost, moodleXYZ.lci.ulsa.mx, 200.13.89.000</small>
-                </label>
-
-
-                <label for="puerto">
-                    Puerto de la base de datos
-                    <!-- validate only numbers -->
-                    <input type="text" name="puerto" placeholder="5432" required value="5432" pattern="[0-9]+"
-                        :value="current_host.puerto" v-model="current_host.puerto">
-                </label>
+            <div class="mb-3">
+                <label for="puerto" class="form-label">Puerto de la base de datos</label>
+                <input type="text" name="puerto" class="form-control" placeholder="5432" required pattern="[0-9]+"
+                    v-model="current_host.puerto">
             </div>
-            <div class="grid">
-                <label for="usuario">
-                    Usuario de Postgres
-                    <input type="text" name="usuario" placeholder="postgres" required value="postgres"
-                        :value="current_host.postgres_user" v-model="current_host.postgres_user">
-                </label>
-                <label for="password">
-                    Contraseña de Postgres
-                    <input type="password" name="password" placeholder="Contraseña del usuario postgres" required
-                        v-model="current_host.postgres_password">
-                </label>
+            <div class="mb-3">
+                <label for="usuario" class="form-label">Usuario de Postgres</label>
+                <input type="text" name="usuario" class="form-control" placeholder="postgres" required
+                    v-model="current_host.postgres_user">
             </div>
-            <div class="grid">
-                <label for="periodos">
-                    Periodos de GEMA
-                    <select id="periodos" name="periodos" multiple required v-model="current_host.periodos_gema">
-
-                        <option v-for="periodo in periodos" :value="periodo.Periodo_id"
-                            :selected="current_host.periodos_gema.includes(periodo.Periodo_id)">
-                            {{ periodo.Periodo_desc }} de {{ periodo.Nivel_desc }}
-                        </option>
-                    </select>
-                </label>
+            <div class="mb-3">
+                <label for="password" class="form-label">Contraseña de Postgres</label>
+                <input type="password" name="password" class="form-control"
+                    placeholder="Contraseña del usuario postgres" required v-model="current_host.postgres_password">
             </div>
-            <div class="grid">
-                <button type="submit">
-                    Registrar
-                    <i class="fas fa-database"></i>
+            <div class="mb-3">
+                <label for="periodos" class="form-label">Periodos de GEMA</label>
+                <select id="periodos" name="periodos" class="form-select" multiple required
+                    v-model="current_host.periodos_gema">
+                    <option v-for="periodo in periodos" :value="periodo.Periodo_id"
+                        :selected="current_host.periodos_gema.includes(periodo.Periodo_id)">
+                        {{ periodo.Periodo_desc }} de {{ periodo.Nivel_desc }}
+                    </option>
+                </select>
+            </div>
+            <div class="d-grid gap-2">
+                <button type="submit" class="btn btn-primary">
+                    <i class="fas fa-database"></i> Registrar
                 </button>
             </div>
         </form>
@@ -109,6 +100,7 @@
 
 
 
+
 <script>
     PetiteVue.createApp({
         store,

+ 12 - 3
pages/login.html

@@ -1,9 +1,18 @@
-<main class="container" v-scope>
-    <form action="/action/login.php" method="post" @submit.prevent="login">
-        <button type="submit">Iniciar Sesión</button>
+<main class="container mt-5" v-scope>
+    <form action="/action/login.php" method="post" @submit.prevent="login" class="row g-3">
+        <div class="row">
+            <div class="col-12">
+                <h1 class="mb-4">Iniciar Sesión</h1>
+                <hr class="mb-4">
+                <div class="d-grid gap-2">
+                    <button type="submit" class="btn btn-primary">Iniciar Sesión</button>
+                </div>
+            </div>
+        </div>
     </form>
 </main>
 
+
 <script>
     PetiteVue.createApp({
         store,

+ 141 - 39
pages/menu.html

@@ -1,36 +1,67 @@
-<main class="container">
-    <dialog :open="option.modal" v-if="option.modal">
-        <article>
-            <header>
-                <a href="#close" aria-label="Close" class="close" @click="closeModal"></a>
-                <h2>{{ option.modal }}</h2>
-            </header>
-            <form>
-                <div v-for="item in currentOptions">
-                    <label :for="item.name">{{ item.label }}</label>
-                    <input :type="item.type" :name="item.name" :value="item.value" :list="item.name"
-                        v-model="item.value">
-                    <datalist v-if="item.datalist.length" :id="item.name">
-                        <option v-for="option in item.datalist" :value="option.id">{{ option.nombre }}</option>
-                    </datalist>
+<main class="container mt-5">
+    <div class="modal" tabindex="-1" :class="{ show: option.modal }" style="display: block;" aria-modal="true"
+        role="dialog" v-if="option.modal" @vue:mounted="store.showModal($el); " @vue:unmounted="store.hideModal($el)"
+        data-bs-backdrop="static" data-bs-keyboard="false">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">{{ option.modal }}</h5>
+                    <button type="button" class="btn-close" aria-label="Close" @click="closeModal"></button>
                 </div>
-                <div class="grid">
-                    <button type="reset" class="danger secondary">
-                        <i class="fas fa-eraser"></i>
-                        Limpiar
-                    </button>
-                    <button type="submit" @click="submitModal" :disabled="currentOptions?.some(item => !item.value)">
-                        <i class="fas fa-check"></i>
-                        Exportar</button>
+                <div class="modal-body">
+                    <form>
+                        <div v-for="item in currentOptions" class="mb-3">
+                            <label :for="item.value" :class="item.label_class">
+                                {{ item.label }}</label>
+                            <input :type="item.type" :name="item.name" :value="item.value" :list="item.name"
+                                :id="item.value" v-model="item.value" :class="item.input_class"
+                                :class="{ 'form-control': item.type !== 'radio' }">
+                            <datalist v-if="item.datalist?.length" :id="item.name">
+                                <option v-for="option in item.datalist" :value="option.id">{{ option.nombre }}</option>
+                            </datalist>
+                        </div>
+                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
+                            <button type="reset" class="btn btn-danger btn-sm">
+                                <i class="fas fa-eraser"></i>
+                                Limpiar
+                            </button>
+                            <button type="submit" class="btn btn-primary btn-sm" @click="submitModal"
+                                :disabled="currentOptions?.some(item => !item.value)">
+                                <i class="fas fa-check"></i>
+                                Exportar
+                            </button>
+                        </div>
+                    </form>
                 </div>
-            </form>
-        </article>
-    </dialog>
-    <button type="button" class="secondary" v-for="item in menu" @click="item.click">
-        <i :class="item.icon"></i>
-        {{ item.name }}
-    </button>
+            </div>
+        </div>
+    </div>
+    <!-- STORE ALERT -->
+    <div class="alert alert-dismissible fade show" :class="`alert-${store.alert.type}`" v-if="store.alert">
+        <button type="button" class="btn-close" @click="store.alert = null"></button>
+        <strong>{{ store.alert.type === 'danger' ? 'Error' : 'Éxito' }}:</strong>
+        {{ store.alert.message }}
+    </div>
+    <!-- Buttons outside the modal/dialog -->
+    <div class="container" @vue:mounted="lastSnapshot">
+        <div class="d-block text-center mb-3" v-if="last_snapshot">
+            Último snapshot:
+            <code>{{ last_snapshot }}</code>
+        </div>
+
+        <div v-else class="d-block text-center mb-3">
+            No hay snapshots
+        </div>
+        <div class="gap-2 d-flex justify-content-md-center flex-wrap">
+            <button type="button" class="btn col-5" v-for="item in menu" @click="item.click"
+                :class="`btn-${item.color ?? 'outline-secondary'}`" :aria-label="`Activate ${item.name}`">
+                <i :class="item.icon" aria-hidden="true"></i>
+                <span class="ms-2">{{ item.name }}</span>
+            </button>
+        </div>
+    </div>
 </main>
+
 <script>
     const option = PetiteVue.reactive({ modal: null });
 
@@ -62,28 +93,89 @@
         menu: [
             {
                 name: 'Construcción de Calificación',
-                icon: 'fas fa-plus',
+                icon: 'fas fa-sliders',
+                color: 'warning',
                 url: '/export/excel.php',
                 filename: 'calificacion.csv',
-                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificacion.csv', postData: { query: 'calificaciones' } })
+                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificacion.csv', postData: { query: 'c-calif' } })
             },
             {
                 name: 'Calificaciones Brutas',
-                icon: 'fas fa-plus',
+                icon: 'fas fa-xmark',
+                url: '/export/excel.php',
+                filename: 'calificacion.csv',
+                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones.csv', postData: { query: 'n-c-brutas' } })
+            },
+            {
+                name: 'Calificaciones Netas',
+                icon: 'fas fa-tarp',
+                url: '/export/excel.php',
+                filename: 'calificacion.csv',
+                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_netas.csv', postData: { query: 'c-net' } })
+            },
+            {
+                name: 'Calificaciones Netas por Curso',
+                icon: 'fas fa-tarp-droplet',
+                url: '/export/excel.php',
+                filename: 'calificacion.csv',
+                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_netas_curso.csv', postData: { query: 'c-net-cur' } })
+            },
+            {
+                name: 'Calificaciones Finales',
+                icon: 'fas fa-chart-simple',
+                color: 'info',
                 url: '/export/excel.php',
                 filename: 'calificacion.csv',
-                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones.csv', postData: { query: 'calificaciones_brutas' } })
+                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones_finales.csv', postData: { query: 'c-fin' } })
+            },
+            {
+                name: 'Gráfica de Alumnos',
+                icon: 'fas fa-chart-bar',
+                color: 'dark',
+                url: '/',
+                click: () => {
+                    // Redirect but with a post (form data page=graph)
+                    const form = document.createElement('form');
+                    form.method = 'POST';
+                    form.action = '/';
+                    const input = document.createElement('input');
+                    input.type = 'hidden';
+                    input.name = 'page';
+                    input.value = 'graph';
+                    form.appendChild(input);
+                    document.body.appendChild(form);
+                    form.submit();
+                }
+            },
+            {
+                name: 'Guardar Snapshot',
+                icon: 'fas fa-save',
+                color: 'success',
+                url: '/action/snapshot.php',
+                click: async () => {
+                    store.loading = true;
+                    const response = await fetch('/action/snapshot.php', { method: 'POST' });
+                    const data = await response.json();
+                    store.loading = false;
+
+                    if (data.success) {
+                        store.alert = { type: 'success', message: data.message };
+                    } else {
+                        store.alert = { type: 'danger', message: data.message };
+                    }
+
+                }
             },
             {
                 name: 'Usuarios Registrados',
-                icon: 'fas fa-plus',
+                icon: 'fas fa-users',
                 url: '/export/excel.php',
                 filename: 'usuarios.csv',
                 opciones: [
                     // radio button
-                    { type: 'radio', name: 'query', label: 'Alumno', value: 'alumnos' },
-                    { type: 'radio', name: 'query', label: 'Usuarios temporales', value: 'usuarios_temporales' },
-                    { type: 'radio', name: 'query', label: 'Todos los usuarios', value: 'usuarios' },
+                    { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Alumno', value: 'al' },
+                    { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Usuarios temporales', value: 'usr-temp' },
+                    { type: 'radio', input_class: "form-check-input", label_class: "form-check-label", name: 'query', label: 'Todos los usuarios', value: 'usr' },
 
 
                 ],
@@ -116,7 +208,7 @@
             },
             {
                 name: 'Reporte Syllabus Plan de Cátedra',
-                icon: 'fas fa-plus',
+                icon: 'fas fa-file-invoice',
                 url: '/export/gema.php',
                 filename: 'syllabus_plan_catedra.csv',
                 opciones: [
@@ -161,5 +253,15 @@
         submitModal: function () {
             if (typeof option.submitModal === 'function') option.submitModal();
         },
+        async lastSnapshot() {
+            try {
+                const response = await fetch('/postgrest/snapshot_calificaciones?limit=1&order=created_at.desc');
+                const data = await response.json();
+
+                this.last_snapshot = data[0]?.created_at;
+            } catch (error) {
+                console.error(error);
+            }
+        }
     }).mount();
 </script>

+ 145 - 0
template/footer.html

@@ -0,0 +1,145 @@
+<div class="container-fluid">
+    <footer class="footer mt-4">
+        <div class="footerTop">
+            <div class="container marco">
+                <div class="logotipo"><img src="imagenes/lasalle-logo-blanco.png" alt="Universidad La Salle - "
+                        class="img-responsive" width="200">
+                    <h3> <span>Profesionales</span>con <strong>Valor</strong></h3>
+                </div>
+                <div class="menuFooter">
+                </div>
+                <div class="ubicacion row">
+                    <div class="address col-12 col-sm-8">
+                        <div class="tabs">
+                            <ul class="nav list-inline" id="tabsFooter" role="tablist">
+                                <li class="list-inline-item">
+                                    <a class="nav-link px-0 pt-0 mr-4 active" id="unidad1-tab" data-toggle="tab"
+                                        href="#unidad1" role="tab" aria-controls="calendario"
+                                        aria-selected="true">Unidad Condesa</a>
+                                </li>
+                                <li class="list-inline-item">
+                                    <a class="nav-link px-0 pt-0 mr-4" id="unidad2-tab" data-toggle="tab"
+                                        href="#unidad2" role="tab" aria-controls="lista" aria-selected="false">Unidad
+                                        Santa Teresa</a>
+                                </li>
+                                <li class="list-inline-item">
+                                    <a class="nav-link px-0 pt-0 mr-4" id="unidad3-tab" data-toggle="tab"
+                                        href="#unidad3" role="tab" aria-controls="lista" aria-selected="false">Unidad
+                                        San Fernando</a>
+                                </li>
+                                <li class="list-inline-item">
+                                    <a class="nav-link px-0 pt-0 mr-4" id="unidad4-tab" data-toggle="tab"
+                                        href="#unidad4" role="tab" aria-controls="lista" aria-selected="false">Unidad
+                                        Santa Lucía</a>
+                                </li>
+                            </ul>
+                            <div class="tab-content" id="tabsCont">
+                                <div class="tab-pane fade show active" id="unidad1" role="tabpanel"
+                                    aria-labelledby="unidad1-tab">
+                                    <p>Benjamín Franklin No 45, Col. Condesa, Alc. Cuauhtémoc, CDMX, CP 06140 <span
+                                            class="tel">Tel. <a href="tel:+525552789500">55 5278-9500</a> / <a
+                                                href="tel:+8005272553">800 LASALLE</a></span><br>
+                                        <a class="btnMap "
+                                            href="https://www.google.com/maps/place/Universidad+La+Salle/@19.4085702,-99.1810039,15z/data=!4m5!3m4!1s0x0:0x3108b5797f9c9ecd!8m2!3d19.4085702!4d-99.1810039"
+                                            target="_blank"> <span class="ing-ubicacion mr-1"></span>¿Cómo llegar?</a>
+                                    </p>
+                                </div>
+                                <div class="tab-pane fade" id="unidad2" role="tabpanel" aria-labelledby="unidad2-tab">
+                                    <p>Camino a Santa Teresa 811, Col. Rinconada del Pedregal, Alc. Tlalpan, CDMX, CP
+                                        14010 <span class="tel">Tel. <a href="tel:5552789500">55 5278-9500</a> / <a
+                                                href="tel:+8005272553">800 LASALLE</a></span><br>
+                                        <a class="btnMap "
+                                            href="https://www.google.com/maps/place/Universidad+La+Salle+Unidad+Santa+Teresa/@19.299013,-99.196093,15z/data=!4m5!3m4!1s0x0:0xdfc2b61c9b67aac2!8m2!3d19.299013!4d-99.196093"
+                                            target="_blank"> <span class="ing-ubicacion mr-1"></span>¿Cómo llegar?</a>
+                                    </p>
+                                </div>
+                                <div class="tab-pane fade" id="unidad3" role="tabpanel" aria-labelledby="unidad3-tab">
+                                    <p>Av. De Las Fuentes 17, Col. Tlalpan, Alc. Tlalpan, CDMX, CP 14000 <span
+                                            class="tel">Tel. <a href="tel:+525552789500">55 5278-9500</a> / <a
+                                                href="tel:+8005272553">800 LASALLE</a></span><br>
+                                        <a class="btnMap "
+                                            href="https://www.google.com/maps/place/Universidad+La+Salle+Facultad+de+Medicina/@19.2930318,-99.1720808,15z/data=!4m5!3m4!1s0x0:0x29b7725e5a004277!8m2!3d19.2930318!4d-99.1720808"
+                                            target="_blank"> <span class="ing-ubicacion mr-1"></span>¿Cómo llegar?</a>
+                                    </p>
+                                </div>
+                                <div class="tab-pane fade" id="unidad4" role="tabpanel" aria-labelledby="unidad4-tab">
+                                    <p>Av. Tamaulipas 3, Col. Zona Federal, Alc. Álvaro Obregón, CDMX, CP 01357 <span
+                                            class="tel">Tel. <a href="tel:5556021130">55 5602-1130</a> </span><br>
+                                        <a class="btnMap "
+                                            href="https://www.google.com/maps/place/Unidad+Deportiva+La+Salle/@19.3662852,-99.2421597,15z/data=!4m5!3m4!1s0x0:0x88e0334f044bc518!8m2!3d19.3662852!4d-99.2421597"
+                                            target="_blank"> <span class="ing-ubicacion mr-1"></span>¿Cómo llegar?</a>
+                                    </p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="redes col-12 col-sm-4">
+                        <h4>Compartir :</h4>
+                        <ul>
+                            <li><a href="https://www.facebook.com/LaSalleMXIngenieria" target="_blank"><i
+                                        class="ing-fb2 ing-fw"></i></a></li>
+                            <!--<li><a href="https://twitter.com/lasalle_mx" target="_blank"><i class="fing-tw2 ing-fw"></i></a></li>-->
+                            <li><a href="https://www.youtube.com/user/IngenieriaLaSalle/" target="_blank"><i
+                                        class="ing-youtube ing-fw"></i></a></li>
+                            <li><a href="https://www.instagram.com/ingenieria_lasalle/" target="_blank"><i
+                                        class="ing-in2 ing-fw"></i></a></li>
+                            <!--<li><a href="https://www.linkedin.com/school/universidad-la-salle?pathWildcard=24227" target="_blank"><i class="fab fa-linkedin-in fa-fw"></i></a></li>-->
+                        </ul>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="footerMiddle">
+            <div class="container marco">
+                <div class="row justify-content-md-center">
+                    <nav class="col-12 col-md-10">
+                        <a class="footerMore menuMore" href="#">Sistema y Red La Salle</a>
+                        <ul>
+                            <li><a href="http://bajio.delasalle.edu.mx/" target="_blank">Bajío</a></li>
+                            <li><a href="http://www.lasalle.mx/" target="_blank">Ciudad de México</a></li>
+                            <li><a href="http://lasallecancun.edu.mx/" target="_blank">Cancún</a></li>
+                            <li><a href="http://www.ulsapuebla.mx/" target="_blank">Puebla</a></li>
+                            <li><a href="http://www.ulsapuebla.mx/" target="_blank">Chihuahua</a></li>
+                            <li><a href="http://www.lasallecuernavaca.edu.mx/wp/" target="_blank">Cuernavaca</a></li>
+                            <li><a href="http://www.ulsalaguna.edu.mx/" target="_blank">Laguna</a></li>
+                            <li><a href="http://www.lasallemorelia.edu.mx/" target="_blank">Morelia</a></li>
+                            <li><a href="http://www.ulsaneza.edu.mx/" target="_blank">Nezahualcóyotl</a></li>
+                            <li><a href="http://www.ulsa-noroeste.edu.mx/n2015/" target="_blank">Noroeste</a></li>
+                            <li><a href="http://www.ulsaoaxaca.edu.mx/" target="_blank">Oaxaca</a></li>
+                            <li><a href="http://www.lasallep.edu.mx/" target="_blank">Pachuca</a></li>
+                            <li><a href="https://www.ulsasaltillo.edu.mx/" target="_blank">Saltillo</a></li>
+                            <li><a href="https://www.lasallevictoria.edu.mx/" target="_blank">Victoria</a></li>
+                        </ul>
+                    </nav>
+                </div>
+            </div>
+        </div>
+        <div class="footerBottom">
+            <div class="container marco">
+                <div class="logotipos">
+                    <ul>
+                        <li><a href="http://redlasalle.mx/" target="_blank"><img
+                                    src="imagenes/la-salle-logo-red-universidades.png" alt="La Salle - logotipo"
+                                    class="img-responsive" width="80"></a></li>
+                        <li><a href="http://ialu.org/english/" target="_blank"><img
+                                    src="imagenes/la-salle-logo-international-ia.png" alt="La Salle - logotipo"
+                                    class="img-responsive" width="80"></a></li>
+                    </ul>
+                </div>
+                <div class="legales">
+                    <a class="footerMore menuMore" href="#">Legales</a>
+                    <ul>
+                        <li><a href="https://lasalle.mx/globales/contacto.html" target="_blank">Contacto</a></li>
+                        <li><a href="https://lasalle.mx/globales/terminos-y-condiciones.html" target="_blank">Términos y
+                                condiciones</a></li>
+                        <li><a href="https://lasalle.mx/globales/aviso-de-privacidad.html" target="_blank">Aviso de
+                                Privacidad</a></li>
+                        <!--<li><a href="https://lasalle.mx/globales/mapa-de-sitio.html" target="_blank">Mapa de sitio</a></li>
+                        <li><a href="https://lasalle.mx/globales/preguntas-frecuentes/" target="_blank">Preguntas frecuentes</a></li>-->
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </footer>
+</div>

+ 6 - 0
template/header.html

@@ -0,0 +1,6 @@
+<header class="sticky-top bg-white bg-head">
+    <div class="menu d-flex align-items-center" style="visibility: visible;">
+        <div class="logotipo"><a href="https://lasalle.mx/" target="_blank"><img id="logo" src="imagenes/logo_lasalle.png"
+                    border="0" class="img-fluid"></a></div>
+    </div>
+</header>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels