Browse Source

New things

Alejandro Rosales 1 năm trước cách đây
mục cha
commit
da22d3c86b

+ 20 - 13
action/action_diasfestivos_insert.php

@@ -12,11 +12,10 @@ require_once "../include/bd_pdo.php";
 global $pdo;
 //print_r($_POST);
 if (!isset($_POST['periodo'])  || count($_POST["periodo"])==0) {
-    //header("Location: ../días_festivos.php?error=1");
-    echo "Error no hay periodo";
+    header("Location: ../días_festivos.php?error=0");
     exit();
 }
-$periodo = $_POST['periodo'];
+$periodoArr = $_POST['periodo'];
 
 if (isset($_POST['rango'])) {
     $diaInicio  = new DateTime(date("Y-m-d", strtotime(str_replace("/", "-", $_POST['diaFestivo']))));
@@ -24,27 +23,35 @@ if (isset($_POST['rango'])) {
     $cantidad = $diaFin->diff($diaInicio);
     $date = date("Y-m-d", strtotime(str_replace("/", "-", $_POST['diaFestivo'])));
     for ($dias = 0; $dias <= $cantidad->days; $dias++) {
-        $sql = "SELECT fi_diasfestivos(:periodo, :dia)";
-        $params = [':periodo' => $periodo, ':dia' => $date];
-        query($sql, $params, false);
+        
+        $db->querySingle('SELECT fi_diasfestivos({'.implode(",",$fieldName).'}, :dia)', [':dia' => $date]);
+            /*$sql = "SELECT fi_diasfestivos(:periodo, :dia)";
+            $params = [':periodo' => $periodo, ':dia' => $date];
+            query($sql, $params, false);*/
+        
         $date = date("Y-m-d", strtotime($date . "+ 1 days"));
     }
     header("Location: ../días_festivos.php");
     exit();
 } else {
-    $sql = "SELECT * FROM fs_diasfestivos(null, :dia)";
+    /*$sql = "SELECT * FROM fs_diasfestivos(null, :dia)";
     $params = [':dia' => $_POST['diaFestivo']];
     $dia_general = query($sql, $params, false);
     $sql = "SELECT * FROM fs_diasfestivos(null, null, :periodo, :dia)";
     $params = [':periodo' => $periodo, ":dia" => $_POST['diaFestivo']];
-    $dia = query($sql, $params, false);
-    if (!$dia && !$dia_general) { //no hay repetidos
-        $sql = "SELECT fi_diasfestivos(:periodo, :dia)";
-        $id = query($sql, $params, false);
+    $dia = query($sql, $params, false);*/
+    //if (!$dia && !$dia_general) { //no hay repetidos
+    foreach($periodoArr as $periodo){
+        $db->querySingle('SELECT fi_diasfestivos({'.implode(",",$fieldName).'}, :dia)', [':dia' => $_POST['diaFestivo']]);
+        /*$sql = "SELECT fi_diasfestivos(:periodo, :dia)";
+        $params = [':periodo' => $periodo, ":dia" => $_POST['diaFestivo']];
+        $id = query($sql, $params, false);*/
+    }
         header("Location: ../días_festivos.php");
         exit();
-    } else {
+    
+    /*} else {
         header("Location: ../días_festivos.php?error=1");
         exit();
-    }
+    }*/
 }

+ 135 - 0
action/profesor_faltas.php

@@ -0,0 +1,135 @@
+<?
+require_once "{$_SERVER['DOCUMENT_ROOT']}/class/c_login.php";
+header('Content-Type: application/json');
+
+if (!Login::is_logged()) {
+    header('HTTP/1.1 401 Unauthorized');
+    echo json_encode(['error' => 'No se ha iniciado sesión']);
+    exit();
+}
+$user = Login::get_user();
+
+try {
+    switch ($_SERVER['REQUEST_METHOD']) {
+        case 'GET':
+            $facultad = $_GET['facultad'] ?? $user->facultad['facultad_id'] ?? null;
+            $porcentaje = $_GET['porcentaje'] ?? null;
+            $faltas = $_GET['faltas'] ?? null;
+
+            if (!isset($facultad) || !is_numeric($facultad)) {
+                $error = 'No se ha seleccionado una facultad';
+            } else if ((!isset($faltas) || !is_numeric($faltas)) && (!isset($porcentaje) || !is_numeric($porcentaje))) {
+                $error = 'Debe especificar las faltas o el porcentaje';
+            } else if (isset($faltas) && (!is_numeric($faltas) || $faltas <= 0)) {
+                $error = 'Las faltas deben ser un número mayor a 0';
+            } else if (isset($porcentaje) && (!is_numeric($porcentaje) || $porcentaje <= 0)) {
+                $error = 'El porcentaje debe ser un número mayor a 0';
+            } else if (isset($faltas) && isset($porcentaje)) {
+                $error = 'No se puede especificar las faltas y el porcentaje al mismo tiempo';
+            } else if (!isset($facultad) || !is_numeric($facultad)) {
+                $error = 'Debe especificar una facultad';
+            }
+
+            if (isset($error)) {
+                header('HTTP/1.1 400 Bad Request');
+                echo json_encode(['error' => $error]);
+                exit();
+            }
+            // Initialize the data array
+            $data = array();
+
+            // Check if 'profesor' or 'supervisor' is set and prepare the specific part of the SQL query accordingly.
+            if (isset($_GET['profesor']) || isset($_GET['supervisor'])) {
+
+                $condition = isset($_GET['profesor'])
+                    ? "r.registro_fecha IS NULL AND NOT COALESCE(r.registro_justificada, FALSE)"
+                    : "estado_supervisor_id = 2";
+
+                $filter = isset($faltas)
+                    ? "afcp.faltas >= :faltas"
+                    : "afcp.porcentaje >= :porcentaje";
+
+                // Prepare the SQL query with placeholders for parameters
+                $data = array_column($db->query(
+                    "WITH fechas AS (
+                        SELECT 
+                            fcc.registro_fecha_ideal, 
+                            fcc.horario_id, 
+                            hp.profesor_id
+                        FROM fechas_clase_cache fcc
+                        JOIN horario_profesor hp USING (horario_id)
+                        JOIN horario h USING (horario_id)
+                        WHERE (h.PERIODO_ID, h.FACULTAD_ID) = (:periodo_id, :facultad_id) and profesor_id <> 0
+                    ),
+                    asistencia_faltas AS (
+                        SELECT 
+                            f.profesor_id,
+                            COUNT(1) AS total,
+                            COUNT(1) FILTER (WHERE $condition AND f.registro_fecha_ideal <= current_date) AS faltas
+                        FROM fechas f
+                        LEFT JOIN registro r USING (registro_fecha_ideal, horario_id, profesor_id)
+                        GROUP BY f.profesor_id
+                    ),
+                    asistencia_faltas_con_porcentaje AS (
+                        SELECT 
+                            af.profesor_id,
+                            af.faltas,
+                            af.total,
+                            CASE 
+                                WHEN af.total > 0 THEN ROUND((af.faltas::NUMERIC / af.total) * 100, 2)
+                                ELSE NULL 
+                            END AS porcentaje
+                        FROM asistencia_faltas af
+                        WHERE af.faltas > 0
+                    )
+                    SELECT 
+                        json_build_object(
+                            'profesor', json_build_object(
+                                'profesor_nombre', p.profesor_nombre,
+                                'profesor_clave', p.profesor_clave,
+                                'profesor_correo', p.profesor_correo
+                            ),
+                            'profesor_id', afcp.profesor_id,
+                            'faltas', afcp.faltas,
+                            'total', afcp.total,
+                            'porcentaje', afcp.porcentaje
+                        ) AS result_json
+                    FROM asistencia_faltas_con_porcentaje afcp
+                    JOIN profesor p USING (profesor_id)
+                    WHERE $filter
+                    ORDER BY afcp.porcentaje DESC",
+                    [
+                        'periodo_id' => $user->periodo_id,
+                        'facultad_id' => $facultad,
+                    ] + (isset($faltas)
+                        ? ['faltas' => $faltas]
+                        : ['porcentaje' => $porcentaje])
+                ), 'result_json');
+            } else {
+                // Send a 400 Bad Request header and an error message in JSON format
+                header('HTTP/1.1 400 Bad Request');
+                echo json_encode(['error' => 'Especifique si las faltas son de profesor o supervisor']);
+                exit();
+            }
+            if (empty($data)) {
+                header('HTTP/1.1 404 Not Found');
+                echo json_encode(['error' => 'No se encontraron faltas']);
+            } else {
+                echo json_encode(
+                    array_map(fn($item) => json_decode($item), $data)
+                );
+            }
+
+            break;
+
+        default:
+            header('HTTP/1.1 405 Method Not Allowed');
+            echo json_encode(['error' => 'Método no permitido']);
+            break;
+    }
+} catch (PDOException $e) {
+    echo json_encode([
+        'error' => $e->getMessage(),
+        'query' => $db->getLastQuery(),
+    ]);
+}

+ 11 - 8
action/reposicion_autoriza.php

@@ -33,26 +33,29 @@ if(isset($_POST["salon"]) && $_POST["salon"] != "")
 
 //--------------
 //Obtiene datos reposición
-//TODO , SALÓN SALIÓ PENDIENTE Y FALTA REVISAR LISTA DE CORREOS TO
-$reposicion_rs = $db->querySingle('SELECT h.materia, r.fecha_nueva, r.hora_nueva, r.fecha_clase, h.horario_hora, h.facultad_id,  h.facultad, f.clave_dependencia, s.salon_id, s.salon_array, r.motivo_cancelacion, ta.tipoaula_supervisor , ta.tipoaula_nombre 
+$reposicion_rs = $db->querySingle('SELECT h.materia, r.fecha_nueva, r.hora_nueva, r.fecha_clase, h.horario_hora, h.facultad_id,  h.facultad, f.clave_dependencia, r.motivo_cancelacion, ta.tipoaula_supervisor , ta.tipoaula_nombre 
     from reposicion_solicitud r
     inner join horario_view h on h.horario_id = r.horario_id 
     inner join facultad f on f.facultad_id = h.facultad_id
     inner join tipoaula ta on ta.tipoaula_id = r.tipoaula_id
-    left join salon_view s on r.salon_id = s.salon_id
     where r.reposicion_solicitud_id = :id_repo',
             [':id_repo' => $id_repo]
         );
 
-if($reposicion_rs["salon_id"] == "" || $reposicion_rs["salon_id"] == NULL){
+//Obtiene datos de salón asignado
+$salon_rs = $db->querySingle('SELECT s.salon_id, s.salon_array FROM salon_view s where s.salon_id  = :id_salon',
+        [':id_salon' => $salon]
+    );
+if($salon_rs["salon_id"] == "" || $salon_rs["salon_id"] == NULL){
     $salon_desc = "Pendiente";
 }else{
-    $salon_json = json_decode($reposicion_rs["salon_array"], true);
+    $salon_json = json_decode($salon_rs["salon_array"], true);
     if($salon_json[0]== "UNIVERSIDAD LA SALLE"){
         unset($salon_json[0]);
     }
     $salon_desc = join(" / ",$salon_json);
 }
+
 //Obtiene correos
 $correos_rs = $db->query('SELECT p.profesor_nombre, p.profesor_correo, u.usuario_nombre as jefe_nombre, u.usuario_correo as jefe_correo,
         coor.usuario_nombre as coordinador_nombre, coor.usuario_correo as coordinador_correo 
@@ -163,12 +166,12 @@ if($to!= "" && ENVIO_CORREOS){
         </body>';
     
     require_once('../include/phpmailer/PHPMailerAutoload.php');
-    /*if(DB_NAME == "poad_pruebas"){
+    if($_ENV['DB_NAME'] == "paad_pruebas"){
         $asunto = "PRUEBAS-".$asunto;
         Mailer::enviarCorreo("alejandro.lara@lasalle.mx", $asunto, $texto, true);
-    }else{*/
+    }else{
         Mailer::enviarCorreo($to, $asunto, $texto, true);
-    //}
+    }
 }
 
 /*

+ 13 - 6
action/reposicion_insert.php

@@ -39,6 +39,7 @@ else
 $comentario = trim(htmlspecialchars($_POST["comentario"], ENT_QUOTES, "UTF-8"));//limpia texto
 
 
+
 $duracion_rs = $db->querySingle("select * from duracion where duracion_id = :id", [":id"=>$duracion_id]);
 $duracion_tiempo = $duracion_rs["duracion_interval"];
 
@@ -73,6 +74,9 @@ if(intval($dia) != intval($dia_falta)){
     exit();
 }
 
+//Obtiene materia
+$materia_rs = $db->querySingle('SELECT materia_nombre  from materia where materia_id = :mat',[':mat' => $materia]);
+
 //Obtiene correo
 $correos_rs = $db->querySingle('SELECT coor.usuario_correo, coor.usuario_nombre  from usuario coor where rol_id = :rol_coord and facultad_id = (
 	select coalesce(facultad_id,0) from usuario u where u.usuario_id = :id_usr)',[':rol_coord' => COORDINADOR, ':id_usr' => $user->user["id"]]
@@ -98,6 +102,7 @@ if($tipo == 1){//Reposición
     if($traslape){
         //print_r($_POST);
         //echo "SELECT * from traslape_profesor_reposicion($prof,'".DateTime::createFromFormat('d/m/Y', $fecha)->format('Y-m-d')."' , '$hora', $duracion)";
+        
         header("Location:".$pag."?error=9");
         exit();
     }
@@ -110,12 +115,13 @@ if($tipo == 1){//Reposición
             ]
         );
     }catch(Exception $e){
+        
         echo $e->getMessage();
         //header("Location: ".$pag."?error=1");
         exit();
     }
     $texto = "<p>Se creó una reposición nueva.</p>";
-    $texto .= "<p><b>".mb_strtoupper($reposicion_rs["materia"])."</b> del día <b>".$fecha_falta." a las ".$hor." hrs. </b> se propone reponer el <b>".$fecha_new." a las ".$hora." hrs.</b>";
+    $texto .= "<p><b>".mb_strtoupper($materia_rs["materia_nombre"])."</b> del día <b>".$fecha_falta." a las ".$hor." hrs. </b> se propone reponer el <b>".$fecha_new." a las ".$hora." hrs.</b>";
     $texto .= "<p>Ingresa al <a href='https://paad.lci.ulsa.mx'>sistema PAAD</a> para autorizarla.</p>";
     
 /*
@@ -134,11 +140,12 @@ if($tipo == 1){//Reposición
             ]
         );
     }catch(Exception $e){
+        
         header("Location: ".$pag."?error=1");
         exit();
     }
     $texto = "<p>Se creó un cambio de salón nuevo.</p>";
-    $texto .= "<p><b>".mb_strtoupper($reposicion_rs["materia"])."</b> del día <b>".$fecha_falta." a las ".$hora." hrs. </b> se propone reponer el <b>".$fecha_nueva." a las ".$hora_nueva." hrs.</b>";
+    $texto .= "<p><b>".mb_strtoupper($materia_rs["materia_nombre"])."</b> del día <b>".$fecha_falta." a las ".$hora." hrs. </b> se propone reponer el <b>".$fecha_nueva." a las ".$hora_nueva." hrs.</b>";
     $texto .= "<p>Ingresa al <a href='https://paad.lci.ulsa.mx'>sistema PAAD</a> para autorizarlo.</p>";
 
     /*
@@ -159,14 +166,14 @@ if($to!= "" && ENVIO_CORREOS){
         </body>';
     
     require_once('../include/phpmailer/PHPMailerAutoload.php');
-    /*if(DB_NAME == "poad_pruebas"){
+    if($_ENV['DB_NAME'] == "paad_pruebas"){
         $asunto = "PRUEBAS-".$asunto;
         Mailer::enviarCorreo("alejandro.lara@lasalle.mx", $asunto, $texto, true);
-    }else{*/
+    }else{
         Mailer::enviarCorreo($to, $asunto, $texto, true);
-    //}
+    }
 }
 
-header("Location: ".$pag."?ok=0");
 exit();
+header("Location: ".$pag."?ok=0");
 ?>

+ 24 - 8
class/mailer.php

@@ -5,7 +5,7 @@
 class Mailer{
     private const FROM = "academia@lasalle.mx";
     private const FROM_NAME = "Vicerrectoría Académica";
-    private const FROM_PASS = "Foy25193";
+    private const FROM_PASS = "4c4d3m14S3gur4##";//Foy25193
     private const FOOTER = "<p style='margin-top:5em; color:#aaa;font-style:italics'><small>Este es un correo automatizado, esta cuenta no recibe correos.<small></p>";
     //private $lista_to, $asunto, $texto;
 
@@ -50,24 +50,40 @@ class Mailer{
             }else{//cadena de texto separada por ;
                 if(strpos($lista_to, ";")!==false){
                     $toArr = explode(";", $lista_to);
+                    foreach($toArr as $correo){
+                        if(trim($correo)!=""){
+                            if($bcc)
+                                $mail->addBCC($correo);
+                            else
+                                $mail->AddAddress($correo);
+                        }
+                    }
                 }elseif(strpos($lista_to, ",")!==false){
                     $toArr = explode(",", $lista_to);
+                    foreach($toArr as $correo){
+                        if(trim($correo)!=""){
+                            if($bcc)
+                                $mail->addBCC($correo);
+                            else
+                                $mail->AddAddress($correo);
+                        }
+                    }
                 }else{
-                    echo "Cadena de correos inválida";
-                    return false;
-                }
-                foreach($toArr as $correo){
-                    if(trim($correo)!=""){
+                    if(trim($lista_to)!=""){
                         if($bcc)
-                            $mail->addBCC($correo);
+                            $mail->addBCC($lista_to);
                         else
-                            $mail->AddAddress($correo);
+                            $mail->AddAddress($lista_to);
                     }
                 }
+                
             }
             //Success
             if ($mail->Send()) { 
                 return true;
+            }else{
+                echo "Error al enviar correo";
+                return false;
             }
         }catch(phpmailerException $e){
             echo $mail->ErrorInfo;

+ 142 - 0
export/faltas_excel.php

@@ -0,0 +1,142 @@
+<?php
+
+$fecha = date('d_m_Y');
+header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+header("Content-Disposition: attachment;filename=horario_$fecha.xlsx");
+header("Cache-Control: max-age=0");
+
+require_once "../vendor/autoload.php";
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+
+$spreadsheet = new Spreadsheet();
+$sheet = $spreadsheet->getActiveSheet();
+
+// Image settings
+$drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
+$drawing->setName('La Salle')
+    ->setDescription('La Salle')
+    ->setPath('../imagenes/logo.png')
+    ->setCoordinates('B1')
+    ->setHeight(100)
+    ->setOffsetX(10)
+    ->setWorksheet($spreadsheet->getActiveSheet());
+
+$json = file_get_contents('php://input');
+$data = json_decode($json, true);
+
+empty($data) and die(json_encode(['error' => 'No se recibieron datos', 'data' => $data]));
+
+$data_excel = array(
+    "CLAVE" => 'profesor_clave',
+    "PROFESOR" => 'profesor_nombre',
+    "CORREO" => 'profesor_correo',
+    "FALTAS" => 'faltas',
+    "TOTAL" => 'total',
+    "PORCENTAJE" => 'porcentaje',
+); // Same as before
+
+
+
+const ROW = 6;
+// Merge cells from A1 to C+ ROW
+$sheet->mergeCells('A1:B' . (ROW - 1));
+// Merge cells from D1 to size of $data_excel + 1
+$sheet->mergeCells('C1:' . chr(65 + count($data_excel) - 1) . (ROW - 1));
+
+// Set the title in D1 Sistema de Auditoría de Asistencia
+$sheet->setCellValue('C1', 'Sistema de Auditoría de Asistencia');
+$sheet->getStyle('C1')->applyFromArray([
+    'font' => [
+        'bold' => true,
+        'size' => 30,
+        'name' => 'Indivisa Text Sans',
+        'color' => ['argb' => '001d68'],
+    ],
+    'alignment' => [
+        'vertical' => Alignment::VERTICAL_CENTER,
+    ],
+]);
+
+
+$lastColumnLetter = chr(65 + count($data_excel) - 1);
+$headers_range = 'A' . ROW . ':' . $lastColumnLetter . ROW;
+
+$keys = array_keys($data_excel);
+array_walk($keys, function ($key, $index) use ($sheet) {
+    $sheet->setCellValue(chr(65 + $index) . ROW, $key);
+});
+// Apply the header styles
+$sheet->getStyle($headers_range)->applyFromArray([
+    'font' => [
+        'bold' => true,
+        'size' => 15,
+        'name' => 'Indivisa Text Sans',
+        'color' => ['argb' => Color::COLOR_WHITE],
+    ],
+    'alignment' => [
+        'horizontal' => Alignment::HORIZONTAL_CENTER,
+        'vertical' => Alignment::VERTICAL_CENTER,
+    ],
+    'fill' => [
+        'fillType' => Fill::FILL_SOLID,
+        'startColor' => ['argb' => '001d68'],
+    ]
+]);
+
+// set filters
+$sheet->setAutoFilter($headers_range);
+
+
+
+// Styles that are common for all rows can be set outside the loop
+
+const DEFAULT_FONT = [
+    'size' => 12,
+    'name' => 'Indivisa Text Sans',
+    'color' => ['argb' => '001d68']
+];
+
+const DEFAULT_STYLE = [
+    'alignment' => [
+        'vertical' => Alignment::VERTICAL_CENTER,
+        'wrapText' => true,
+    ],
+    'font' => DEFAULT_FONT,
+    'borders' => [
+        'outline' => [
+            'borderStyle' => Border::BORDER_THIN,
+            'color' => ['argb' => Color::COLOR_WHITE],
+        ]
+    ]
+];
+
+foreach ($data as $index => $registro) {
+    $pair = $index % 2 == 0;
+    $cellRange = 'A' . (ROW + $index + 1) . ':' . $lastColumnLetter . (ROW + $index + 1);
+    $styleArray = DEFAULT_STYLE;
+    $styleArray['fill'] = [
+        'fillType' => Fill::FILL_SOLID,
+        'startColor' => ['argb' => $pair ? 'd4d9dd' : 'f6f7f8'],
+    ];
+
+    $sheet->getStyle($cellRange)->applyFromArray($styleArray);
+    $values = array_values($data_excel);
+    array_walk($values, function ($row, $column_index) use ($sheet, $index, $registro) {
+        $cellLocation = chr(65 + $column_index) . (ROW + $index + 1);
+        $sheet->setCellValue($cellLocation, $registro[$row]);
+    });
+}
+
+
+foreach ($sheet->getColumnIterator() as $column) {
+    $sheet->getColumnDimension($column->getColumnIndex())->setAutoSize(true);
+}
+
+$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+$writer->save('php://output');

+ 275 - 0
faltas.php

@@ -0,0 +1,275 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Auditoría de faltas</title>
+    <?php
+    include 'import/html_css_files.php';
+    ?>
+    <style>
+        [v-cloak] {
+            display: none;
+        }
+    </style>
+    <script src="js/jquery.min.js"></script>
+    <script src="js/jquery-ui.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
+        integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
+        crossorigin="anonymous"></script>
+    <script src="js/bootstrap/bootstrap.min.js"></script>
+</head>
+
+<body>
+    <?
+    $redirect = $_SERVER['PHP_SELF'];
+    include "import/html_header.php";
+    global $user;
+
+    html_header(
+        "Faltas",
+        "Sistema de gestión de checador",
+    );
+
+
+
+    if (!$user->periodo_id) { ?>
+            <script defer src="js/jquery.min.js"></script>
+            <script src="js/bootstrap/bootstrap.min.js"></script>
+
+            <div class="modal" id="seleccionar-periodo" tabindex="-1">
+                <div class="modal-dialog modal-dialog-centered modal-xl">
+                    <div class="modal-content">
+                        <div class="modal-header">
+                            <h2 class="modal-title">Seleccionar periodo</h2>
+                        </div>
+                        <div class="modal-body container">
+                            <? include 'import/periodo.php' ?>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <script>
+                $('#seleccionar-periodo').modal({
+                    backdrop: 'static',
+                    keyboard: false,
+                });
+                $('#seleccionar-periodo').modal('show');
+            </script>
+            <? exit;
+    } ?>
+
+    <main class="container-fluid px-4 mt-4" id="app" v-cloak @vue:mounted="mounted" style="min-height: 60vh;"
+        v-scope="">
+        <?php include "import/periodo.php" ?>
+        <div class="card border-0 shadow-sm">
+            <div class="card-body">
+                <div class="form-box marco">
+                    <? if (!$user->facultad['facultad_id']) { ?>
+                            <div class="form-group row">
+                                <label for="dlFacultad" class="col-4 col-form-label" id="facultad">Facultad</label>
+                                <div class="col-6">
+                                    <div id="dlFacultad" class="datalist datalist-select mb-1 w-100">
+                                        <div class="datalist-input">
+                                            Selecciona una facultad
+                                        </div>
+                                        <span class="icono ing-buscar"></span>
+                                        <ul style="display:none">
+                                            <li class="datalist-option d-none" data-id="-1">
+                                                Selecciona una facultad
+                                            </li>
+                                            <li class="datalist-option" v-for="facultad in facultades"
+                                                :key="facultad.facultad_id" :data-id="facultad.facultad_id"
+                                                @click="filter.facultad = facultad.facultad_id;">
+                                                (<small> {{facultad.clave_dependencia}} </small>) {{ facultad.facultad_nombre }}
+                                            </li>
+                                        </ul>
+                                        <input type="hidden" id="facultad_id" name="id">
+                                    </div>
+                                </div>
+                            </div>
+                    <? } ?>
+                    <div class="form-group row">
+                        <label for="profesor" class="col-4 col-form-label">Profesor</label>
+                        <div class="col-6">
+                            <div class="form-row justify-content-around align-items-center">
+                                <input id="profesor" name="profesor" class="form-control col-11 mr-1 px-2"
+                                    placeholder="Seleccione una profesor" list="dlProfesor" v-model="filter.profesor">
+                                <button type="button" class="btn btn-outline-danger btn-sm form-control col ml-auto"
+                                    @click="filter.profesor = '';">
+                                    <i class="ing-borrar"></i>
+                                </button>
+                            </div>
+                            <datalist id="dlProfesor">
+                                <option v-for="profesor in profesores" :key="profesor.profesor_id"
+                                    :value="`(${profesor.profesor_clave}) ${profesor.profesor_nombre}`">
+                            </datalist>
+                        </div>
+                    </div>
+                    <div class="form-group row" v-scope ="{
+                        input_faltas: true,
+                    }">
+                        <label for="porcentaje" class="col-4 col-form-label" id="facultad">
+                            <div class="custom-control custom-switch">
+                                <span class="mr-5" :class="{'text-muted': input_faltas}">Número</span>
+                                <input type="checkbox" class="custom-control-input" id="faltas_porcentaje"
+                                    v-model="input_faltas" @change="<? if ($user->facultad['facultad_id']) { ?> filter.facultad = <?= $user->facultad['facultad_id'] ?>; <? } ?> filter.porcentaje = input_faltas ? 10 : 0; filter.faltas = input_faltas ? 0 : 1">
+                                <label class="custom-control-label" for="faltas_porcentaje"
+                                    :class="{'text-muted': !input_faltas}">
+                                    Porcentaje 
+                                </label>
+                            </div>
+                            de faltas
+                        </label>
+                        <div class="col-6">
+                            <div id="porcentaje" class="datalist datalist-select mb-1 w-100" V-if="input_faltas">
+                                <div class="datalist-input">
+                                    Selecciona una porcentaje
+                                </div>
+                                <span class="icono ing-buscar"></span>
+                                <ul style="display:none">
+                                    <li class="datalist-option d-none" data-id="-1">
+                                        Selecciona un porcentaje
+                                    </li>
+                                    <li class="datalist-option"
+                                        v-for="porcentaje in Array.from({length: 5}, (_, i) => (i + 1) * 10)"
+                                        :key="facultad.facultad_id" :data-id="facultad.facultad_id"
+                                        <? if ($user->facultad['facultad_id']) { ?>
+                                                @click="filter.porcentaje = porcentaje; filter.facultad = <?= $user->facultad['facultad_id'] ?>;"
+                                        <? } else { ?>
+                                                @click="filter.porcentaje = porcentaje;"
+                                        <? } ?> >
+                                        {{ porcentaje }}%
+                                    </li>
+                                </ul>
+                                <input type="hidden" id="facultad_id" name="id">
+                            </div>
+                            <input type="number" class="form-control" v-model="filter.faltas" v-else
+                                @change="" min="1" max="100" step="1"
+                                <? if ($user->facultad['facultad_id']) { ?>
+                                        @click="filter.facultad = <?= $user->facultad['facultad_id'] ?>"
+                                <? } ?>
+                                placeholder="Número de faltas">
+
+                        </div>
+                    </div>
+                    <div class="form-group row">
+                        <label for="porcentaje" class="col-4 col-form-label" id="facultad">Faltas</label>
+                        <div class="col-6">
+                            <div class="form-row justify-content-center align-items-center text-center">
+                                <div class="custom-control custom-switch">
+                                    <input type="checkbox" class="custom-control-input" id="tipoFaltas"
+                                        v-model="filter.tipoFaltas" @change="">
+                                    <label class="custom-control-label" for="tipoFaltas"
+                                        :class="{'text-muted': !filter.tipoFaltas}">
+                                        Faltas del {{ filter.tipoFaltas ? 'Supervisor' : 'Profesor' }}
+                                    </label>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-row justify-content-center align-items-center">
+                        <div class="col-auto">
+                            <button type="button" class="btn btn-success" @click="toExcel()" :disabled="!faltas.length" :class="{'disabled': !faltas.length}">
+                                <i class=" ing-descarga"></i>
+                                Exportar a Excel</button>
+                        </div>
+
+                        <div class="col-auto">
+                            <button type="button" class="btn btn-primary" @click="refresh()" :disabled="(filter.facultad <= 0) || (filter.faltas <= 0 && filter.porcentaje <= 0)" :class="{'disabled': !filter.facultad || !filter.faltas && !filter.porcentaje}">
+                                <i class=" ing-buscar"></i>
+                                Buscar faltas
+
+                            </button>
+                        </div>
+                    </div>
+                </div>
+
+            </div>
+        </div>
+
+
+        <div class="table-responsive marco" v-if="faltas.length > 0" v-scope="{orderBy: [
+            {column: 'Profesor', order: 'asc'},
+            {column: 'Faltas', order: 'desc'},
+            {column: 'Total', order: 'desc'},
+            {column: 'Porcentaje', order: 'desc'},
+        ]}">
+            <table class="table table-hover table-striped table-bordered table-sm">
+                <thead class="thead-dark">
+                    <tr>
+                        <th scope="col" class="text-center align-middle px-2" v-for="column in orderBy">
+                            {{ column.column }}
+                        </th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr v-for="falta in faltas.filter(({profesor}) => filter.profesor ? (profesor.profesor_nombre.toLowerCase().includes(filter.profesor.toLowerCase()) || filter.profesor.toLowerCase().includes(`(${profesor.profesor_clave}) ${profesor.profesor_nombre}`.toLowerCase())) : true)"
+                        :key="`flata-${falta.profesor_id}`">
+                        <td class="align-middle px-2">
+                            <strong>{{ falta.profesor.profesor_clave }}</strong>
+                            {{ falta.profesor.profesor_nombre }}
+                        </td>
+                        <td class="align-middle px-2 text-center">
+                            {{ falta.faltas }}
+                        </td>
+                        <td class="align-middle px-2 text-center">
+                            {{ falta.total }}
+                        </td>
+                        <td class="align-middle px-2 text-center">
+                            {{ falta.porcentaje }}%
+                        </td>
+
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+        <div class="modal" tabindex="-1" id="cargando" data-backdrop="static" data-keyboard="false">
+            <div class="modal-dialog modal-dialog-centered">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h4 class="modal-title">Cargando datos...</h4>
+
+                    </div>
+                    <div class="modal-body container">
+                        <div class="row">
+                            <div class="col-12 text-center">
+                                <span class="spinner-border spinner-border-lg"></span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="modal" tabindex="-1" id="mensaje">
+            <div class="modal-dialog modal-dialog-centered">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h4 class="modal-title">{{mensaje.titulo}}</h4>
+                        <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body container">
+                        <div class="row">
+                            <div class="col-12 text-center">
+                                {{mensaje.texto}}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+
+    </main>
+
+    <!-- <script src=" js/datalist.js"></script> -->
+    <script src="js/datepicker-es.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
+    <script src="js/faltas.js?<?= rand(0, 2) ?>" type="module"></script>
+    <script src="js/scrollables.js"></script>
+</body>
+
+</html>

+ 84 - 0
js/faltas.js

@@ -0,0 +1,84 @@
+import { createApp, reactive } from 'https://unpkg.com/petite-vue?module';
+const filter = reactive({
+    facultad: -1,
+    profesor: '',
+    porcentaje: 0,
+    faltas: 0,
+    tipoFaltas: true,
+});
+
+const app = createApp({
+    filter,
+    facultades: [],
+    profesores: [],
+    faltas: [],
+    mensaje: {
+        titulo: '',
+        texto: '',
+    },
+    async refresh() {
+        if (filter.facultad == -1 || (filter.porcentaje < 10 && filter.faltas < 1)) {
+            console.log('Facultad: ', filter.facultad, 'Porcentaje: ', filter.porcentaje, 'Faltas: ', filter.faltas);
+            return;
+        }
+        $('#cargando').modal('show');
+        try {
+
+            this.faltas = await fetch(`action/profesor_faltas.php?facultad=${this.filter.facultad}&${this.filter.tipoFaltas ? 'supervisor' : 'profesor'}&${this.filter.faltas > 0 ? 'faltas' : 'porcentaje'}=${this.filter.faltas > 0 ? this.filter.faltas : this.filter.porcentaje}`).then(res => res.json());
+            if (this.faltas.error) {
+                $('.modal#mensaje').modal('show');
+                this.mensaje.titulo = 'Información';
+                this.mensaje.texto = this.faltas.error;
+            }
+        } catch (error) {
+            $('.modal#mensaje').modal('show');
+            this.mensaje.titulo = 'Error';
+            this.mensaje.texto = 'No se pudo cargar los datos';
+        }
+        finally {
+            $('#cargando').modal('hide');
+        }
+    },
+
+    async toExcel() {
+        if (filter.facultad == -1 || filter.porcentaje < 10) {
+            return;
+        }
+        $('#cargando').modal('show');
+        try {
+            const response = await fetch(`export/faltas_excel.php`, {
+                method: 'POST',
+                body: JSON.stringify(this.faltas.map(falta => ({
+                    'profesor_clave': falta.profesor.profesor_clave,
+                    'profesor_correo': falta.profesor.profesor_correo,
+                    'profesor_nombre': falta.profesor.profesor_nombre,
+                    'faltas': falta.faltas,
+                    'porcentaje': `${falta.porcentaje}%`,
+                    'total': falta.total,
+                }))),
+            })
+
+            const blob = await response.blob();
+            window.saveAs(blob, `faltas_${this.facultades.find(facultad => facultad.facultad_id == filter.facultad).facultad_nombre}_${new Date().toISOString().slice(0, 10)}.xlsx`);
+        } catch (error) {
+            $('.modal#mensaje').modal('show');
+            this.mensaje.titulo = 'Error';
+            this.mensaje.texto = 'No se pudo cargar los datos';
+            console.log('Error: ', error);
+        }
+        finally {
+            $('#cargando').modal('hide');
+        }
+    },
+    async mounted() {
+        try {
+            this.facultades = await fetch('action/action_facultad.php').then(res => res.json());
+            this.profesores = await fetch('action/action_profesor.php').then(res => res.json());
+        } catch (error) {
+            $('.modal#mensaje').modal('show');
+            this.mensaje.titulo = 'Error';
+            this.mensaje.texto = 'No se pudo cargar los datos';
+            console.log('Error: ', error);
+        }
+    }
+}).mount('#app');

+ 38 - 0
ts/faltas.ts

@@ -0,0 +1,38 @@
+import { createApp, reactive } from 'https://unpkg.com/petite-vue?module';
+// define that $ has type any
+declare const $: any;
+
+const filter = reactive({
+    facultad: -1,
+    profesor: '',
+    porcentaje: 0
+});
+const app = createApp({
+    filter,
+    facultades: [],
+    profesores: [],
+
+    faltas: [],
+    openModal() {
+        const modal = document.getElementById('cargando');
+        $(modal).modal('show');
+    },
+    closeModal() {
+        const modal = document.getElementById('cargando');
+        $(modal).modal('hide');
+    },
+
+    async refresh() {
+        if(filter.facultad == -1 || filter.porcentaje < 10) {
+            return;
+        }
+
+        this.openModal();
+        this.faltas = await fetch(`action/profesor_faltas.php?facultad=${this.filter.facultad}&profesor=${this.filter.profesor}&porcentaje=${this.filter.porcentaje}`).then(res => res.json());
+        this.closeModal();
+    },
+    async mounted() {
+        this.facultades = await fetch('action/action_facultad.php').then(res => res.json());
+        this.profesores = await fetch('action/action_profesor.php').then(res => res.json());
+    }
+}).mount('#app');