Guía R-005 – Caso práctico de React y FullCalendar

Como os había indicado, aquí está un ejemplo completo de utilización de la biblioteca FullCalendar utilizada desde React.

He tardado en publicar el ejemplo porque he estado probando distintas formas de hacer la funcionalidad de List, Add, Edit, View y Delete. Verás que las 3 tablas que contiene el ejemplo, tienen 3 formas distintas de visualización/construcción de estas funcionalidades, más si se tiene en cuenta su funcionamiento en pantallas pequeñas como la de los móviles.

También, me ha costado «afinar» el funcionamiento del Calendario, en los siguientes apartados:

  • Utilización de los Tooltip, para mostrar información del caso/evento sin tener que hacer clic para visualizarlo. He encontrado poca información y me he ayudado de la IA de DeepSeek, muchas veces me ha dado soluciones incorrectas, pero siempre me ha ayudado para explorar otras alternativas y por fin, di con la solución. Es una buena solución si estás sólo y no puedes consultar con nadie.
  • Para las acciones de «Delete» he estado utilizando Sweetalert, desde las páginas de «List», pero al utilizarlas desde página de diálogo, como «View», teniendo que «cerrar» esa página, me daba y da, muchos problemas, porque destruyo el registro antes de terminar el diálogo de Sweetalert, ya que es asíncrono y da control a otra parte del programa sin haberse terminado el diálogo. Lo he cambiado por la función «Dialog» de «Material UI»

Objetivo

Tener un ejemplo completo de React y FullCalendar con similares características que este ejemplo que había hecho en PHPRunner, Guía 64. También, disponer de la parte de SERVER sin depender/conectarse con PHPRunner.

DEMO: https://fhumanes.com/incidentes-react

Solución Técnica

Si habéis hecho algún ejemplo, ya conocéis que en el fichero «package.json» del proyecto se indica todos los componentes que he instalado. Si no conocéis esto, por favor, leer este artículo.

package.json
{
  "name": "incidentes-react",
  "version": "0.1.0",
  "private": true,
  "homepage": "/incidentes-react",
  "dependencies": {
    "@emotion/react": "^11.14.0",
    "@emotion/styled": "^11.14.0",
    "@fullcalendar/core": "^6.1.15",
    "@fullcalendar/react": "^6.1.15",
    "@mui/icons-material": "^6.4.2",
    "@mui/material": "^6.4.2",
    "@mui/x-data-grid": "^7.24.1",
    "@mui/x-date-pickers": "^7.27.3",
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "ajv": "^8.17.1",
    "axios": "^1.7.9",
    "dayjs": "^1.11.13",
    "fullcalendar": "^6.1.15",
    "react": "^19.0.0",
    "react-colorful": "^5.6.1",
    "react-dom": "^19.0.0",
    "react-router-dom": "^7.1.3",
    "react-scripts": "5.0.1",
    "sweetalert2": "^11.15.10",
    "sweetalert2-react-content": "^5.1.0",
    "tippy.js": "^6.3.7",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Este ejemplo comparte la base de datos de la Guía 64 por lo que a nivel funcional y modelo de datos, podéis obtener esa información en el artículo.

Una de las cosas que más he disfrutado, por su gran diferencia con PHPRunner, es cuando he hecho las páginas de Add, Edit y View, y los datos para estas páginas se los he pasado de los datos que tenía cargados en la página LIST (sin tener que ir al Server, para obtener los datos) y además mezclar la presentación de estas paginas con la de LIST.

Poniéndolas:

  • Al lado
  • Sustituyendo la de List.
  • En Popup.

En cada una de las tablas, he puesto diferentes soluciones, para que dispongáis de ejemplo de estas funcionalidades.

También, como he indicado, el código del Server está desligado de PHPrunner (no requiere de PHPrunner). Podéis comprobar que es muy sencillo trabajar con este código y el framework SLIM 4.0

Os muestro el código más relevante del server.

incidentes-server
Config.phpindex.phpDbincidentes.php
<?php

//
//  Database configuration
//

//
//  URI raíz de la aplicación
//

define('SCRIPTS_DIR', '/incidentes-server/v1'); 

$server = 'test';  // 'test' or 'production'

if ($server == 'test' ) {
// Path root of Directory files in File System or Disc
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASSWORD', 'humanes'); 
define('DB_NAME', 'pruebas'); 
} else {
// In Server Linux
define('DB_HOST', 'localhost');
define('DB_USER', 'u637977917_factu');
define('DB_PASSWORD', 'humanes'); 
define('DB_NAME', 'u637977917_factu'); 
} 

//referencia generado con MD5(uniqueid(<some_string>, true))
define('API_KEY','3d524a53c110e4c22463b10ed32cef9d');

/**
 * API Response HTTP CODE
 * Used as reference for API REST Response Header
 *
 */
/*
200 	OK
201 	Created
304 	Not Modified
400 	Bad Request
401 	Unauthorized
403 	Forbidden
404 	Not Found
422 	Unprocessable Entity
500 	Internal Server Error
*/

// Error messages to facilitate their translation

$errorMessages = array(
    "001" => "Authorization Token Missing",
    "002" => "The authorization token is incorrect",
    "003" => "Required field(s) or attribute(s) {1} is missing or empty",
    "004" => "Required Action (add,view,update,delete)",
    "005" => "Error access Data Base: ?",
    "006" => "Operation performed successfully",
    "007" => "",
    "008" => "",
    "009" => "",
    "010" => "",
    "011" => "",
    "012" => "",
    "013" => "",
    "014" => "",
    "015" => "",
    "016" => "",
    "017" => "",
    "018" => "",
    "019" => "",
    "020" => "",
    "021" => "",
    "022" => "",
    "023" => "",
    "024" => "",
    "025" => "",
    "026" => ""
);
<?php
/**
 *
 * @About:      API Interface
 * @File:       index.php
 * @Date:       $Date:$ Ene 2025
 * @Version:    $Rev:$ 1.0
 * @Developer:  Federico Guzman || Modificado por Fernando Humanes para PHP 8.1
 **/

/* Los headers permiten acceso desde otro dominio (CORS) a nuestro REST API o desde un cliente remoto via HTTP
 * Removiendo las lineas header() limitamos el acceso a nuestro RESTfull API a el mismo dominio
 * Nótese los métodos permitidos en Access-Control-Allow-Methods. Esto nos permite limitar los métodos de consulta a nuestro RESTfull API
 * Mas información: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
 **/

// $dominioPermitido = "http://localhost:3000";

// header("Access-Control-Allow-Origin: $dominioPermitido"); // Para restringir desde dónde se pueden hacer peticines
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: Content-Type, authorization, Authorization, token-user ");
// header("Access-Control-Allow-Headers: *");

header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS');
// header("Access-Control-Allow-Headers: X-Requested-With");
header('Content-Type: text/html; charset=utf-8');
header('Content-Type: multipart/form-data');
header('Content-Type: application/x-www-form-urlencoded');
header('Content-Type: application/json');
header('P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"');

// session_cache_limiter(false);

include_once '../include/Config.php';  // Configuration Rest Api
//     require_once("../../include/dbcommon.php"); // DataBase PHPRunner

// Debug
// $debugCode = false;
// custom_error(1,"URL ejecutada: ".$_SERVER["REQUEST_URI"]); // To debug
// $debugCode = false;

// use App\Models\Db;  // Utilizamos la conexión de PHPRunner
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Selective\BasePath\BasePathMiddleware;
use Slim\Factory\AppFactory;

require_once __DIR__ . '/../libs/autoload.php';   // Library SLIM v4

$app = AppFactory::create();

$app->addRoutingMiddleware();
// $app->add(new BasePathMiddleware($app)); // No usar si se ejecuta en subdirectorio
$app->addErrorMiddleware(true, true, true);
$app->addBodyParsingMiddleware();

$app->setBasePath(SCRIPTS_DIR);             // Indica el directorio desde donde está trabajando

// --------------------------------------------------------------------------------------

/* Usando POST pata obtener los Tipos de la Base de datos */

$app->post('/tipoList', function(Request $request, Response $response) {
    global $errorMessages;
    /* Include functions required for the execution of the actions */
    include_once '../include/Function.php';
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbIncidentes.php';
    $db = new DbIncidentes();
    
    // Verify Token Authenticate Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    
    /* Obtener el listado de Tipo */
    $responseBody = $db->tipoList($request, $response);

    $response->getBody()->write(json_encode($responseBody,JSON_NUMERIC_CHECK));
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
});
// --------------------------------------------------------------------------------------
/* Usando POST para add, view, update, delete de Tipos de Incidentes
*/
$app->post('/tipo/{action}/{id}', function(Request $request, Response $response, array $args) {
    global $errorMessages;
    /* Include functions required for the execution of the actions */
    include_once '../include/Function.php';
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbIncidentes.php';
    $db = new DbIncidentes();
    
    // Verify Token Authenticate Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    // check for required params
    $param = array();
    $param['action'] = $args['action'];
    $param['id'] = $args['id'];

    $verify = verifyRequiredParams(array('action','id'), $param, $request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    // Verificación de los valores de los parámetros || attributes
    if( !in_array($param['action'],array('add','view','update','delete')) )  { // Si la acción no es correcta
        $responseBody["error"] = true;
        $responseBody["message_num"] = '004';
        $responseBody["message"] = $errorMessages['004'];
        $response->getBody()->write(json_encode($responseBody));
        return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
    }
        
    switch ($param['action']) {
    case 'add':
        // check for required params
        $param2 = $request->getParsedBody();

        $verify = verifyRequiredParams(array('titulo','color'), $param2, $request, $response);
        if (is_array($verify)) { // Si es una array, es que hay error
            $response->getBody()->write(json_encode($verify));
            return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);
        }
        $data = $db->addTipo($param2,$request, $response );
        break;
    case 'view':
        $data = $db->viewTipo($param,$request, $response );
        break;
    case 'update':
        // check for required params
        $param2 = $request->getParsedBody();
        $param2['action'] = $args['action'];
        $param2['id'] = $args['id'];

        $verify = verifyRequiredParams(array('id','titulo','color'), $param2, $request, $response);
        if (is_array($verify)) { // Si es una array, es que hay error
            $response->getBody()->write(json_encode($verify));
            return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);
        }
        $data = $db->updateTipo($param2,$request, $response );
        break;
    case 'delete':
        $data = $db->deleteTipo($param,$request, $response );
        break;
    }
    // $response->getBody()->write($data);
    $response->getBody()->write(json_encode($data,JSON_NUMERIC_CHECK));
    
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);  
});
// --------------------------------------------------------------------------------------

/* Usando POST pata obtener las Usuarios de la Base de datos */

$app->post('/usuarioList', function(Request $request, Response $response) {
    global $errorMessages;
    /* Include functions required for the execution of the actions */
    include_once '../include/Function.php';
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbIncidentes.php';
    $db = new DbIncidentes();
    
    // Verify Token Authenticate Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    
    /* Obtener el listado de Tipo */
    $responseBody = $db->usuarioList($request, $response);

    $response->getBody()->write(json_encode($responseBody,JSON_NUMERIC_CHECK));
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
});
// --------------------------------------------------------------------------------------
/* Usando POST para add, view, update, delete de Usuarios de Incidentes
*/
$app->post('/usuario/{action}/{id}', function(Request $request, Response $response, array $args) {
    global $errorMessages;
    /* Include functions required for the execution of the actions */
    include_once '../include/Function.php';
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbIncidentes.php';
    $db = new DbIncidentes();
    
    // Verify Token Authenticate Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    // check for required params
    $param = array();
    $param['action'] = $args['action'];
    $param['id'] = $args['id'];

    $verify = verifyRequiredParams(array('action','id'), $param, $request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    // Verificación de los valores de los parámetros || attributes
    if( !in_array($param['action'],array('add','view','update','delete')) )  { // Si la acción no es correcta
        $responseBody["error"] = true;
        $responseBody["message_num"] = '004';
        $responseBody["message"] = $errorMessages['004'];
        $response->getBody()->write(json_encode($responseBody));
        return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
    }
        
    switch ($param['action']) {
    case 'add':
        // check for required params
        $param2 = $request->getParsedBody();

        $verify = verifyRequiredParams(array('login','nombre_apellidos'), $param2, $request, $response);
        if (is_array($verify)) { // Si es una array, es que hay error
            $response->getBody()->write(json_encode($verify));
            return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);
        }
        $data = $db->addUsuario($param2,$request, $response );
        break;
    case 'view':
        $data = $db->viewUsuario($param,$request, $response );
        break;
    case 'update':
        // check for required params
        $param2 = $request->getParsedBody();
        $param2['action'] = $args['action'];
        $param2['id'] = $args['id'];

        $verify = verifyRequiredParams(array('id','login','nombre_apellidos'), $param2, $request, $response);
        if (is_array($verify)) { // Si es una array, es que hay error
            $response->getBody()->write(json_encode($verify));
            return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);
        }
        $data = $db->updateUsuario($param2,$request, $response );
        break;
    case 'delete':
        $data = $db->deleteUsuario($param,$request, $response );
        break;
    }
    // $response->getBody()->write($data);
    $response->getBody()->write(json_encode($data,JSON_NUMERIC_CHECK));
    
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);  
});

// --------------------------------------------------------------------------------------
/* Usando POST pata obtener los Casos de la Base de datos */

$app->post('/casoList', function(Request $request, Response $response) {
    global $errorMessages;
    /* Include functions required for the execution of the actions */
    include_once '../include/Function.php';
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbIncidentes.php';
    $db = new DbIncidentes();
    
    // Verify Token Authenticate Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    
    /* Obtener el listado de Tipo */
    $responseBody = $db->casoList($request, $response);

    $response->getBody()->write(json_encode($responseBody,JSON_NUMERIC_CHECK));
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
});

// --------------------------------------------------------------------------------------
/* Usando POST para add, view, update, delete de Casos de Incidentes
*/
$app->post('/caso/{action}/{id}', function(Request $request, Response $response, array $args) {
    global $errorMessages;
    /* Include functions required for the execution of the actions */
    include_once '../include/Function.php';
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbIncidentes.php';
    $db = new DbIncidentes();
    
    // Verify Token Authenticate Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    // check for required params
    $param = array();
    $param['action'] = $args['action'];
    $param['id'] = $args['id'];

    $verify = verifyRequiredParams(array('action','id'), $param, $request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    // Verificación de los valores de los parámetros || attributes
    if( !in_array($param['action'],array('add','view','update','delete')) )  { // Si la acción no es correcta
        $responseBody["error"] = true;
        $responseBody["message_num"] = '004';
        $responseBody["message"] = $errorMessages['004'];
        $response->getBody()->write(json_encode($responseBody));
        return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
    }
        
    switch ($param['action']) {
    case 'add':
        // check for required params
        $param2 = $request->getParsedBody();

        $verify = verifyRequiredParams(array('incidente_usuario_id','incidente_tipo_id','fechaDesde','fechaHasta','titulo','descripcion'), $param2, $request, $response);
        if (is_array($verify)) { // Si es una array, es que hay error
            $response->getBody()->write(json_encode($verify));
            return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);
        }
        $data = $db->addCaso($param2,$request, $response );
        break;
    case 'view':
        $data = $db->viewCaso($param,$request, $response );
        break;
    case 'update':
        // check for required params
        $param2 = $request->getParsedBody();
        $param2['action'] = $args['action'];
        $param2['id'] = $args['id'];

        $verify = verifyRequiredParams(array('id','incidente_usuario_id','incidente_tipo_id','fechaDesde','fechaHasta','titulo','descripcion'), $param2, $request, $response);
        if (is_array($verify)) { // Si es una array, es que hay error
            $response->getBody()->write(json_encode($verify));
            return $response
                ->withHeader('content-type', 'application/json')
                ->withStatus(200);
        }
        $data = $db->updateCaso($param2,$request, $response );
        break;
    case 'delete':
        $data = $db->deleteCaso($param,$request, $response );
        break;
    }
    // $response->getBody()->write($data);
    $response->getBody()->write(json_encode($data,JSON_NUMERIC_CHECK));
    
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);  
});

// --------------------------------------------------------------------------------------

$app->post('/casoCalendar', function(Request $request, Response $response) {
    global $errorMessages;
    /* Include functions required for the execution of the actions */
    include_once '../include/Function.php';
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbIncidentes.php';
    $db = new DbIncidentes();
    
    $param = $request->getParsedBody();
    
    // Verify Token Authenticate Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    }
    // check for required params
    $verify = verifyRequiredParams(array('id_user','start','end'), $param, $request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    } 
    
    /* Obtener el listado de Tipo */
    $responseBody = $db->casoCalendar($param,$request, $response);

    $response->getBody()->write(json_encode($responseBody,JSON_NUMERIC_CHECK));
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
});

/* Runner the aplication */
$app->run();
<?php

// include_once '../include/Config.php';  // Configuration Rest Api

/**
 *
 * @About:      Gestión de Datos Electorales de la Comunidad de Madrid
 * @File:       DbElecciones.php
 * @Date:       $Date:$ Ene 2025
 * @Version:    $Rev:$ 1.0
 * @Developer:  fernando humanes
 **/

    use Psr\Http\Message\ResponseInterface as Response;
    use Psr\Http\Message\ServerRequestInterface as Request;
    use Selective\BasePath\BasePathMiddleware;
    use Slim\Factory\AppFactory;

class DbIncidentes
{

    private $connDB;

    function __construct()
    {
        /* You should enable error reporting for mysqli before attempting to make a connection */
        mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
        $mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
        /* Set the desired charset after establishing a connection */
        $mysqli->set_charset('utf8mb4');
        
        $this->connDB = $mysqli;      // No se usa esta conexión si está integrado con PHPRunner
    }
    
    private function responseKO ($data_arr) {
        global $errorMessages;
        $data_arr['error'] = true;
        $data_arr["message_num"] = '005';
        $data_arr["message"] = str_replace("?",$this->connDB->error,$errorMessages['005']);
        $this->connDB->close();
        return $data_arr;
    }
    private function responseOK ($data_arr) {
        global $errorMessages;
        $data_arr['error'] = false;
        $data_arr["message_num"] = '006';
        $data_arr["message"] = $errorMessages['006'];
        $this->connDB->close();
        return $data_arr;
    }
    private function diffDates ($date_from, $date_to) {
        // $date1 = DateTimeImmutable::createFromFormat('d/m/Y', $date_from);
        // $date2 = DateTimeImmutable::createFromFormat('d/m/Y', $date_to);
        // Calculation of the days
        $sql = "Select
        DATEDIFF('$date_to','$date_from') NaturalWorkingDays,
        workdaydiff('$date_to','$date_from') WorkingDays
        from dual";
        $data_arr = array();
        $rs = $this->connDB->query($sql);
        $data = $rs->fetch_assoc();
        $data['NaturalWorkingDays'] += 1; // se incluyen los dias desde y hasta en la fecha
        $data_arr['NaturalWorkingDays'] = $data['NaturalWorkingDays'];
        $data_arr['DaysWeekend'] = $data['NaturalWorkingDays'] - $data['WorkingDays'];
        return $data_arr;      
    }
    // ---------------------------------------------------------------------------
    /*
    * Listado de Tipos
    */
    public function tipoList(Request $request,Response $response)
    {
        global $errorMessages;
        
        $sql = "
        SELECT
            id_incidente_tipo id,
            titulo,
            color
        FROM incidente_tipo
        ORDER BY titulo ";
        $data_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
           $data_arr[] = $data; 
        }
        $data = ["error" => '' ,"message_num" => '',"message"=> '', "data" => $data_arr];
        return $this->responseOK($data);
        
    }
    /*
    * ADD de Tipos
    */
    public function addTipo($param, Request $request,Response $response)
    {
        global $errorMessages;
        $titulo = $param['titulo'];
        $color = $param['color'];
        
        $sql = "
        INSERT INTO incidente_tipo (titulo, color)
        VALUES (?,?)";
        
        $data_arr = array();
        $stmt = $this->connDB->prepare($sql);
        /*
        if ( $stmt <> true ) { // Error access database
            return $this->responseKO($data_arr);
        }
         */
        $stmt->bind_param('ss',$titulo, $color);
        $stmt->execute();  
        return $this->responseOK($data_arr);     
    }
    /*
    * View de Tipos
    */
    public function viewTipo($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        
        $sql = "
        SELECT
        id_incidente_tipo id,
        titulo,
        color
        FROM incidente_tipo
        WHERE id_incidente_tipo = ?";
        
        $data_arr = array();
        $data_all = array();
        
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('i',$id);
        $stmt->execute();
        $result = $stmt->get_result();
        while ($data = $result->fetch_array(MYSQLI_ASSOC)) 
        {
           $data_arr[] = $data; 
        }

        $data = ["error" => '' ,"message_num" => '',"message"=> '', "data" => $data_arr];
        return $this->responseOK($data);  
    }
    /*
    * UPDATE de Tipos
    */
    public function updateTipo($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        $titulo = $param['titulo'];
        $color = $param['color'];
        
        $sql = "
        UPDATE incidente_tipo
        SET
        titulo = ?,
        color = ?
        WHERE id_incidente_tipo = ?";
        
        $data_arr = array();
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('ssi',$titulo, $color, $id);
        $stmt->execute();
        
        $data_arr = $this->viewTipo($param, $request, $response);  // Visualizar resultado
        
        return $data_arr;     
    }
    /*
    * Delete de Tipos
    */
    public function deleteTipo($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        
        $sql = "
        DELETE FROM incidente_tipo
        WHERE id_incidente_tipo = ?";
        
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('i',$id);
        $stmt->execute();
        
        $data = ["error" => '' ,"message_num" => '',"message"=> ''];
        return $this->responseOK($data);   
    }
    
    // ---------------------------------------------------------------------------
    /*
    * Listado de Casos
    */
    public function casoList(Request $request,Response $response)
    {
        global $errorMessages;
        
        $sql = "
        SELECT
            id_incidente_caso id,
            incidente_usuario_id,
            login,
            nombre_apellidos,
            incidente_tipo_id,
            incidente_tipo.titulo titulo_tipo,
            incidente_tipo.color,
            fechaDesde,
            fechaHasta,
            incidente_caso.titulo,
            descripcion,
            diasLaborables,
            diasFinSemana
        FROM incidente_caso
        JOIN incidente_tipo on (id_incidente_tipo = incidente_tipo_id )
        JOIN incidente_usuario on ( incidente_usuario_id = id_incidente_usuario )
        ORDER BY id_incidente_caso";
        $data1_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
           $data1_arr[] = $data; 
        }
        // Tipo LIST
        $sql = "
        SELECT
            id_incidente_tipo id,
            titulo,
            color
        FROM incidente_tipo
        ORDER BY id_incidente_tipo ";
        $data2_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
           $data2_arr[] = $data; 
        }
        // Usuarios LIST
        $sql = "
        SELECT
            id_incidente_usuario id,
            login,
            nombre_apellidos
        FROM incidente_usuario
        ORDER BY id_incidente_usuario ";
        $data3_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
           $data3_arr[] = $data; 
        }
              
        $data = ["error" => '' ,"message_num" => '',"message"=> '', "data"=> $data1_arr, "dataTipo"=> $data2_arr, "dataUsuario" => $data3_arr];
        return $this->responseOK($data); 
        
    }
    /*
    * ADD de Casos
    */
    public function addCaso($param, Request $request,Response $response)
    {
        global $errorMessages;
        // field: id,incidente_usuario_id,incidente_tipo_id,fechaDesde,fechaHasta,titulo,descripcion,diasLaborables,diasFinSemana
        $incidente_usuario_id = $param['incidente_usuario_id'];
        $incidente_tipo_id = $param['incidente_tipo_id'];
        $fechaDesde = $param['fechaDesde'];
        $fechaHasta = $param['fechaHasta'];
        $titulo = $param['titulo'];
        $descripcion = $param['descripcion'];
        $calc_array = $this->diffDates ($fechaDesde, $fechaHasta);  // Calculo de días
        $diasLaborables = $calc_array['NaturalWorkingDays'];
        $diasFinSemana = $calc_array['DaysWeekend'];
     
        $sql = "
        INSERT INTO incidente_caso (incidente_usuario_id,incidente_tipo_id,fechaDesde,fechaHasta,titulo,descripcion,diasLaborables,diasFinSemana)
        VALUES (?,?,?,?,?,?,?,?)";
        
        $data_arr = array();
        $stmt = $this->connDB->prepare($sql);

        $stmt->bind_param('iissssii',$incidente_usuario_id,$incidente_tipo_id,$fechaDesde,$fechaHasta,$titulo,$descripcion,$diasLaborables,$diasFinSemana);
        $stmt->execute();  
        return $this->responseOK($data_arr);     
    }
    /*
    * View de Casos
    */
    public function viewCaso($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        
        $sql = "
        SELECT
            id_incidente_caso id,
            incidente_usuario_id,
            login,
            nombre_apellidos,
            incidente_tipo_id,
            incidente_tipo.titulo titulo_tipo,
            incidente_tipo.color,
            fechaDesde,
            fechaHasta,
            incidente_caso.titulo,
            descripcion,
            diasLaborables,
            diasFinSemana
        FROM incidente_caso
        JOIN incidente_tipo on (id_incidente_tipo = incidente_tipo_id )
        JOIN incidente_usuario on ( incidente_usuario_id = id_incidente_usuario ) 
        WHERE id_incidente_caso = ?";
        
        $data_arr = array();
        $data_all = array();
        
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('i',$id);
        $stmt->execute();
        $result = $stmt->get_result();
        while ($data = $result->fetch_array(MYSQLI_ASSOC)) 
        {
           $data_arr[] = $data; 
        }
        $data_all['data'] = $data_arr;
        return $this->responseOK($data_all);     
    }
    /*
    * UPDATE de Casos
    */
    public function updateCaso($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        // field: id,incidente_usuario_id,incidente_tipo_id,fechaDesde,fechaHasta,titulo,descripcion,diasLaborables,diasFinSemana
        $incidente_usuario_id = $param['incidente_usuario_id'];
        $incidente_tipo_id = $param['incidente_tipo_id'];
        $fechaDesde = $param['fechaDesde'];
        $fechaHasta = $param['fechaHasta'];
        $titulo = $param['titulo'];
        $descripcion = $param['descripcion'];
        $calc_array = $this->diffDates ($fechaDesde, $fechaHasta);  // Calculo de días
        $diasLaborables = $calc_array['NaturalWorkingDays'];
        $diasFinSemana = $calc_array['DaysWeekend'];

        $sql = "
        UPDATE incidente_caso
        SET
        incidente_usuario_id = ?,
        incidente_tipo_id = ?,
        fechaDesde = ?,
        fechaHasta = ?,
        titulo = ?,
        descripcion = ?,
        diasLaborables = ?,
        diasFinSemana  = ?
        WHERE id_incidente_caso = ?";
        
        $data_arr = array();
        $stmt = $this->connDB->prepare($sql);

        $stmt->bind_param('iissssiii',$incidente_usuario_id,$incidente_tipo_id,$fechaDesde,$fechaHasta,$titulo,$descripcion,$diasLaborables,$diasFinSemana,$id);
        $stmt->execute();
        
        $data_arr = $this->viewCaso($param, $request, $response);  // Visualizar resultado
        
        return $data_arr;     
    }
    /*
    * Delete de Casos
    */
    public function deleteCaso($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        
        $sql = "
        DELETE FROM incidente_caso
        WHERE id_incidente_caso = ?";
        
        $data_arr = array();
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('i',$id);
        $stmt->execute();  
        return $this->responseOK($data_arr);     
    }
    // ---------------------------------------------------------------------------
    /*
    * Listado de Usuarios
    */
    public function usuarioList(Request $request,Response $response)
    {
        global $errorMessages;
        
        $sql = "
        SELECT
            id_incidente_usuario id,
            login,
            nombre_apellidos
        FROM incidente_usuario
        ORDER BY login ";
        $data_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
           $data_arr[] = $data; 
        }
        $data = ["error" => '' ,"message_num" => '',"message"=> '', "data" => $data_arr];
        return $this->responseOK($data);
        
    }
    /*
    * ADD de Usuarios
    */
    public function addUsuario($param, Request $request,Response $response)
    {
        global $errorMessages;
        $login = $param['login'];
        $nombre_apellidos = $param['nombre_apellidos'];
        
        $sql = "
        INSERT INTO incidente_usuario (login, nombre_apellidos)
        VALUES (?,?)";
        
        $stmt = $this->connDB->prepare($sql);

        $stmt->bind_param('ss',$login, $nombre_apellidos);
        $stmt->execute();

        /*
         * Falta controlar si está duplicado el usuario
         */
          
        $data = ["error" => '' ,"message_num" => '',"message"=> ''];
        return $this->responseOK($data);
    
    }
    /*
    * View de Usuario
    */
    public function viewUsuario($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        
        $sql = "
        SELECT
            id_incidente_usuario id,
            login,
            nombre_apellidos
        FROM incidente_usuario
        WHERE id_incidente_usuario = ?";
        
        $data_arr = array();
        $data_all = array();
        
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('i',$id);
        $stmt->execute();
        $result = $stmt->get_result();
        while ($data = $result->fetch_array(MYSQLI_ASSOC)) 
        {
           $data_arr[] = $data; 
        }
        $data = ["error" => '' ,"message_num" => '',"message"=> '', "data" => $data_arr];
        return $this->responseOK($data);    
    }
    /*
    * UPDATE de Usuario
    */
    public function updateUsuario($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        $login = $param['login'];
        $nombre_apellidos = $param['nombre_apellidos'];
        
        $sql = "
        UPDATE incidente_usuario
        SET
        login = ?,
        nombre_apellidos = ?
        WHERE id_incidente_usuario = ?";
        
        $data_arr = array();
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('ssi',$login, $nombre_apellidos, $id);
        $stmt->execute();
        
        $data_arr = $this->viewUsuario($param, $request, $response);  // Visualizar resultado
        
        return $data_arr;     
    }
    /*
    * Delete de Usuario
    */
    public function deleteUsuario($param, Request $request,Response $response)
    {
        global $errorMessages;
        $id = $param['id'];
        
        $sql = "
        DELETE FROM incidente_usuario
        WHERE id_incidente_usuario = ?";
        
        $stmt = $this->connDB->prepare($sql);
        $stmt->bind_param('i',$id);
        $stmt->execute();  
        $data = ["error" => '' ,"message_num" => '',"message"=> ''];
        return $this->responseOK($data);    
    }
    // ---------------------------------------------------------------------------
    /*
    * Listado de Casos para Calendar
    */
    public function casoCalendar($param, Request $request,Response $response)
    {
        global $errorMessages;
        
        $id_user = $param['id_user'];
        $start = $param['start'].' 00:00:00';
        $end = $param['end'].' 23:59:59';
        
        $sql = "
        SELECT
            id_incidente_caso id,
            incidente_usuario_id,
            login,
            nombre_apellidos,
            incidente_tipo_id,
            incidente_tipo.titulo titulo_tipo,
            incidente_tipo.color,
            fechaDesde ,
            DATE_ADD(fechaHasta, INTERVAL 1 DAY) end,
            fechaHasta,
            incidente_caso.titulo,
            descripcion,
            diasLaborables,
            diasFinSemana
        FROM incidente_caso
        JOIN incidente_tipo on (id_incidente_tipo = incidente_tipo_id )
        JOIN incidente_usuario on ( incidente_usuario_id = id_incidente_usuario )
        WHERE incidente_usuario_id = $id_user 
            AND fechaHasta >= '$start' AND FechaDesde <= '$end' 
        ORDER BY id_incidente_caso";
        $data1_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
            // $data['allDay'] = true;

            $data1_arr[] = $data; 
        }
        // Tipo LIST
        $sql = "
        SELECT
            id_incidente_tipo id,
            titulo,
            color
        FROM incidente_tipo
        ORDER BY id_incidente_tipo ";
        $data2_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
           $data2_arr[] = $data; 
        }
        // Usuarios LIST
        $sql = "
        SELECT
            id_incidente_usuario id,
            login,
            nombre_apellidos
        FROM incidente_usuario
        ORDER BY id_incidente_usuario ";
        $data3_arr = array();
        $rs = $this->connDB->query($sql);
        while( $data = $rs->fetch_assoc() )
        {
           $data3_arr[] = $data; 
        }
              
        $data = ["error" => '' ,"message_num" => '',"message"=> '', "data"=> $data1_arr, "dataTipo"=> $data2_arr, "dataUsuario" => $data3_arr];
        return $this->responseOK($data); 
        
    }
}

También, de la parte de Front-End realizada en React, os dejo algunos de los ficheros más relevantes.

incidentes-react
src/components/config/index.jssrc/components/usuarioList/index.jssrc/components/casoList/index.jssrc/components/fullcalendar/index.js
// Parámetros de configuración de la App
const configuration = 'linux';  // 'test' or 'production'
var Config =null;

if (configuration === 'test') {
    Config = {
        RUTA_API: "http://localhost/incidentes-server/v1",
        URL_APP: "/incidentes-react",
        TITLE_APP: "INCIDENTES",
        AUTHORIZATION: "3d524a53c110e4c22463b10ed32cef9d",
    };
} else {
    // Config edl Server Linux
    Config = {
        RUTA_API: "https://fhumanes.com/incidentes-server/v1",
        URL_APP: "/incidentes-react",
        TITLE_APP: "INCIDENTES",
        AUTHORIZATION: "3d524a53c110e4c22463b10ed32cef9d",
    };
};

export default Config;
import * as React from 'react';
import './style.css';

import {
  DataGrid,
  GridToolbarContainer,
  GridToolbarColumnsButton,
  GridToolbarFilterButton,
  GridToolbarDensitySelector,
  GridToolbarExport,
  GridActionsCellItem,
  // GridToolbar,
} from '@mui/x-data-grid';

import Button from '@mui/material/Button';

import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import CachedIcon from '@mui/icons-material/Cached';
import DeleteIcon from '@mui/icons-material/Delete';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';

import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content'

import { createTheme, ThemeProvider } from '@mui/material/styles';
import axios from 'axios';
import {useEffect, useState} from 'react';

import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';

// import { esES } from '@mui/material/locale';
import { esES } from '@mui/x-data-grid/locales';

import { useNavigate } from 'react-router-dom';

import Nav from '../nav';
import Config from '../config';
import UsuarioAdd from '../usuarioAdd';
import UsuarioEdit from '../usuarioEdit';

const theme = createTheme(
  {
    palette: {
      primary: { main: '#1976d2' },
    },
  },
  esES,
);

const styleModal = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: 555,
  bgcolor: 'background.paper',
  // border: '2px solid #000',
  // boxShadow: 24,
  p: 4,
};
const styleModalMobile = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: '100%',
  bgcolor: 'background.paper',
  // border: '2px solid #000',
  // boxShadow: 24,
  // p: 1,
};

const withRouter = (Component) => {
  const Wrapper = (props) => {
    const navigate = useNavigate();
    return <Component navigate={navigate} {...props} />;
  };
  return Wrapper;
};

function  UsuarioList(props) {
 
  const [rows, setRows] = useState(null);
  const [row, setRow] = useState(null);

  // const [rowSelectionModel, setRowSelectionModel] = React.useState([]); // Para control del registro seleccionado en el GRID
  const [action, setAction] = useState(''); // Acción sobre el registro (row) - add, edit, view 
  
  const [open, setOpen] = React.useState(false);   // Open / Close Modal window

  const [refresh, setRefresh] = useState(true);
  const [isMobile, setIsMobile] = useState(false);

  // Notificación error grave
  function errorNotification(code, message) {
    const MySwal = withReactContent(Swal); 
    MySwal.fire({
      icon: 'error',
      title: message,
      text: '',
      timer: 5000,
      timerProgressBar: true,
      toast: true,
      position: "center",
      footer: ''
    })
    // props.navigate(Config.URL_APP+'/');
  }

  // Cargar los datos del Server cuando se carga y recarga la página
  const fetchUsuarios= async () => {
    if (refresh) {        // Para intentar que sólo sea una vez
      setRefresh(false);
      var formWitdh = window.innerWidth;
      // console.log("Ancho de pantalla: ",formWitdh);
      if (formWitdh < 768) {
        setIsMobile(true);
      }

      const url = Config.RUTA_API + "/usuarioList";
      const formData = new FormData();
      const config = {
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": Config.AUTHORIZATION
        },
      };
      try {  
        const response = await axios.post(url,formData,config)     
            var msg = response.data;
            // console.log("Respuesta del fetch: ",msg); 
            if (msg.error === true )  { 
              console.log("Hay un error: ",msg.message);
              errorNotification(msg.error, msg.message);
            } else {
              setRows(msg.data);
            }           
          }
      catch(error) {
            console.error("Error get Lista Usuarios ", error);
            errorNotification('000', error);
          }
    }
  };

  // Cargar los datos del Server cuando se carga la página
  useEffect(() => {
    // console.log("Ejecutándose 'useEffect");
    fetchUsuarios();
    setAction('');
     }, [refresh]);

  const deleteRow = React.useCallback((id) => () => 
    {
      // console.log("Registro a eliminar: ",id);
      setAction('');   // Elimina ventanas de Add o Edit
      const row = rows.find((rowid) => rowid.id === id);

      var msg = `¿Quieres <b>ELIMINAR el Usuario:</b> id:${row.id} de ${row.nombre_apellidos}? `;

      const MySwal = withReactContent(Swal); 
      MySwal.fire({
        title: 'Confirmación',
        html: msg,
        icon: 'warning',
        showCancelButton: true,
        confirmButtonColor: '#3298dc',
        cancelButtonColor: '#f14668',
        cancelButtonText: 'No',
        confirmButtonText: 'Sí, eliminar'
      })

        .then(response => {
          if (response.isConfirmed) {

             // Acceso al server y status de la operación
            const url = Config.RUTA_API + "/usuario/delete/"+id;
            const formData = new FormData();
            const config = {
              headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Authorization": Config.AUTHORIZATION
                },
              };  
            axios.post(url, formData, config)
        
              .then((response) => {
                const res = response.data;
                // console.log("Respuesta del fetch: ",res);
                if (res.error === false) {
                    setRows((prevRows) => prevRows.filter((row) => row.id !== id));
                    // console.log("Delete row ID: ",id);
      
                    MySwal.fire({
                      icon: 'info',
                      title: 'Usuario eliminado!!!!',
                      text: '',
                      timer: 1000,
                      timerProgressBar: true,
                      toast: true,
                      position: "center",
                      footer: ''
                    })
                  } else {
                    errorNotification(res.code, res.message);
                }
              })
            .catch((error) => {
              console.error("Error delete Usuario: ", error);
              errorNotification('000', error);
              setRefresh(true);  // Reload Data from Server
            })
          }
        })
    },
    [rows],
  );

  const calendarRow = React.useCallback(
    (id) => () => {
      props.navigate(Config.URL_APP+'/fullcalendar/'+id);
    },
    [rows,action],
  );

 
  const editRow = React.useCallback(
    (id) => () => {
      const row = rows.find((rowid) => rowid.id === id);
      if (action === '') {
          setRow(row);
          setAction('edit');
          setOpen(true);
      } else {
          setAction('');
          setOpen(false);
      }
    },
    [rows,action],
  );

  const addRow = React.useCallback(
    () => {
      if (action === '') {
          setAction('add');
          setOpen(true);
      } else {
          setAction('');
          setOpen(false);
      }
    },
    [action],
  );

  // Gestión de respuesta de páginas modales
  const handleButtonClickFromItem = (action,newAction = '') => {
    console.log("Regreso formulario remoto: ",action);
    setOpen(false);
    if (action === 'add') {
      setAction('');
      setRefresh(true);
    }
    if (action === 'edit') {
      setAction('');
      setRefresh(true);
    }
  };

  function CustomToolbar() {
    if (isMobile) {
      return (
          <GridToolbarContainer>
          <Button color="primary"  startIcon={<CachedIcon />} onClick={(event) => setRefresh(true)}></Button>
          <Button color="primary"  startIcon={<AddIcon />} onClick={(event) => addRow(event)}>Nuevo</Button>
          <MoreVertIcon sx={{ color:"#1976d2" }} />
          <GridToolbarExport />
        </GridToolbarContainer>
      )
    } else {
    return (
      <GridToolbarContainer>
        <Button color="primary"  startIcon={<CachedIcon />} onClick={(event) => setRefresh(true)}></Button>
        <Button color="primary"  startIcon={<AddIcon />} onClick={(event) => addRow(event)}>Nuevo</Button>
        <MoreVertIcon sx={{ color:"#1976d2" }} />
      
        <GridToolbarColumnsButton />
        <GridToolbarFilterButton />
        <GridToolbarDensitySelector />
        <GridToolbarExport />             
      </GridToolbarContainer>
      );
    }
  }

  const columns = React.useMemo(
    () => [
      {
        field: 'actions',
        hideable: false,
        headerName: 'Acciones',
        type: 'actions',
        flex: 0.5,
        minWidth:74,
        getActions: ({ id, row }) => {
   
        return [
            <GridActionsCellItem
              icon={<CalendarMonthIcon color="primary"/>}
              label="Calendar"
              // className="textPrimary"
              onClick={calendarRow(id)}
              color="inherit"
            />,
            <GridActionsCellItem
              icon={<EditIcon />}
              label="Edit"
              // className="textPrimary"
              onClick={editRow(id)}
              color="inherit"
            />,
            <GridActionsCellItem
              icon={<DeleteIcon />}
              label="Delete"
              // className="textPrimary"
              onClick={deleteRow(id)}
              color="inherit"
          />,
        ];   
        }
      },
      // { field: 'incidente_usuario_id', hideable: false, headerName: 'Usuario', type: 'singleSelect',
      //  getOptionValue: (key) => key.id, getOptionLabel: (value) => value.nombre_apellidos,  valueOptions: rowsUser , flex: 0.7, minWidth: 100 },
      { field: 'login', hideable: false, headerName: 'login', type: 'string',flex: 0.7, minWidth: 100 },
      { field: 'nombre_apellidos', hideable: false, headerName: 'Usuario', type: 'string', flex: 1.5, minWidth: 100 },
    ],
    [deleteRow, editRow, calendarRow],
  );


  return (
    <>
    <Nav />

    <ThemeProvider theme={theme}>
      <div className='contenedor'>

        <div className='panel'>
          <div className='box-itemList'>
            <div className='titlePage'>
                <h3 className="head_title">Listado de los Usuarios</h3>
            </div>

           <DataGrid  columns={columns} rows={rows}
              initialState={{
                pagination: {
                  paginationModel: { page: 0, pageSize: 5 },
                },
                columns: {
                  columnVisibilityModel: {
                  },
                },
              }}
              autosizeOptions={{
                columns: ['login', 'nombre_apellidos'],
                includeOutliers: true,
                includeHeaders: true,
              }}
              pageSizeOptions={[2,5 , 10, 15, 20, 100]}
              hideFooterSelectedRowCount={true}
              // enableRowNumbers
              disableRowSelectionOnClick
              /*
              onRowSelectionModelChange={(newRowSelectionModel) => {
                    // Aquí el control del registro seleccionado. 
                    setRowSelectionModel(newRowSelectionModel);
                    // console.log("Array de registros seleccionados: ",newRowSelectionModel);
                    const idSelected = newRowSelectionModel[0];
                    // console.log("Id seleccionado: ",idSelected);
                    viewRow(idSelected);
                    // props.navigate(Config.URL_APP+'/compraView/'+idSelected);
              }}
              */
              slots={{
                toolbar: CustomToolbar
              }}

            />
          </div>  
        </div>
      </div>
    </ThemeProvider>
        <Modal
          open={open}
          // onClose={handleButtonClickFromItem('add')}
          // aria-labelledby="modal-modal-title"
          // aria-describedby="modal-modal-description"
        >
          { isMobile?
         <Box sx={styleModalMobile}>
          { action === 'add'?<UsuarioAdd  rows={rows} onButtonClick={handleButtonClickFromItem} />:''}
          { action === 'edit'? <UsuarioEdit  row={row} rows={rows} onButtonClick={handleButtonClickFromItem}/> :''}
        </Box>
        :
        <Box sx={styleModal}>
        { action === 'add'?<UsuarioAdd  rows={rows} onButtonClick={handleButtonClickFromItem} />:''}
        { action === 'edit'? <UsuarioEdit  row={row} rows={rows} onButtonClick={handleButtonClickFromItem}/> :''}
      </Box>
          }
        </Modal>
    </>
  );
}
// IMPORTANT ----------------------------------------
export default withRouter( UsuarioList );
import * as React from 'react';
import './style.css';

import {
  DataGrid,
  GridToolbarContainer,
  GridToolbarColumnsButton,
  GridToolbarFilterButton,
  GridToolbarDensitySelector,
  GridToolbarExport,
  GridActionsCellItem,
  // GridToolbar,
} from '@mui/x-data-grid';

import Button from '@mui/material/Button';

import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import CachedIcon from '@mui/icons-material/Cached';
import DeleteIcon from '@mui/icons-material/Delete';
import MoreVertIcon from '@mui/icons-material/MoreVert';

import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content'

import { createTheme, ThemeProvider } from '@mui/material/styles';
import axios from 'axios';
import {useEffect, useState} from 'react';

// import { esES } from '@mui/material/locale';
import { esES } from '@mui/x-data-grid/locales';

// import 'dayjs/locale/es';
// import dayjs from 'dayjs';

import { useNavigate } from 'react-router-dom';

import Nav from '../nav';
import Config from '../config';
import CasosAdd from '../casosAdd';
import CasosEdit from '../casosEdit';
import CasosView from '../casosView';


const theme = createTheme(
  {
    palette: {
      primary: { main: '#1976d2' },
    },
  },
  esES,
);

const withRouter = (Component) => {
  const Wrapper = (props) => {
    const navigate = useNavigate();
    return <Component navigate={navigate} {...props} />;
  };
  return Wrapper;
};

function  CasosList(props) {
 
  const [rows, setRows] = useState(null);
  const [row, setRow] = useState(null);
  const [rowsTipo, setRowsTipo] = useState(null);
  const [rowsUser, setRowsUser] = useState(null);

   const [rowSelectionModel, setRowSelectionModel] = React.useState([]); // Para control del registro seleccionado en el GRID
  const [action, setAction] = useState(''); // Acción sobre el registro (row) - add, edit, view -

  const [refresh, setRefresh] = useState(true);
  const [isMobile, setIsMobile] = useState(false);
  const [isDouble, setIsDouble] = useState(true);
  const [viewList, setViewList] = useState(true);

  // Notificación error grave
  function errorNotification(code, message) {
    const MySwal = withReactContent(Swal); 
    MySwal.fire({
      icon: 'error',
      title: message,
      text: '',
      timer: 5000,
      timerProgressBar: true,
      toast: true,
      position: "center",
      footer: ''
    })
    // props.navigate(Config.URL_APP+'/');
  }

  // Cargar los datos del Server cuando se carga y recarga la página
  const fetchCasos = async () => {
    if (refresh) {        // Para intentar que sólo sea una vez
      setRefresh(false);
      var formWitdh = window.innerWidth;
      // console.log("Ancho de pantalla: ",formWitdh);
      if (formWitdh < 768) {
        // console.log("pone isMobile a 'true'");
        setIsMobile(true);
      }
      if (formWitdh < 1390) { // Para ver si tenemos que ocultar el GRID
        setIsDouble(false);
      }

      const url = Config.RUTA_API + "/casoList";
      const formData = new FormData();
      const config = {
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": Config.AUTHORIZATION
        },
      };
      try {  
        const response = await axios.post(url,formData,config)     
            var msg = response.data;
            // console.log("Respuesta del fetch: ",msg); 
            if (msg.error === true )  { 
              console.log("Hay un error: ",msg.message);
              errorNotification(msg.error, msg.message);
            } else {
              setRows(msg.data);
              setRowsTipo(msg.dataTipo);
              setRowsUser(msg.dataUsuario);
              // console.log("Casos: ",msg.data);
              // console.log("Tipos: ",msg.dataTipo);
              // console.log("Usuarios: ",msg.dataUsuario);

            }           
          }
      catch(error) {
            console.error("Error get Lista Casos ", error);
            errorNotification('000', error);
          }
    }
  };

  // Cargar los datos del Server cuando se carga la página
  useEffect(() => {
    // console.log("Ejecutándose 'useEffect");
    fetchCasos();
    setAction('');
     }, [refresh]);


// CallBack de Delete registro
  const deleteRow = React.useCallback(
    (id)  => () =>
    {
      // console.log("Registro a eliminar: ",id);

      setAction('');   // Elimina ventanas de Add o Edit

      const row = rows.find((rowid) => Number(rowid.id) === Number(id));


      var msg = `¿Quieres <b>ELIMINAR el Caso:</b> id:${row.id} de ${row.nombre_apellidos}? `;

      const MySwal = withReactContent(Swal); 
      MySwal.fire({
        title: 'Confirmación',
        html: msg,
        icon: 'warning',
        showCancelButton: true,
        confirmButtonColor: '#3298dc',
        cancelButtonColor: '#f14668',
        cancelButtonText: 'No',
        confirmButtonText: 'Sí, eliminar'
      })

        .then(response => {
          if (response.isConfirmed) {
            // console.log("Comfirmed OK",response);

             // Acceso al server y status de la operación
            const url = Config.RUTA_API + "/caso/delete/"+id;
            const formData = new FormData();
            const config = {
              headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Authorization": Config.AUTHORIZATION
                },
              };  
            axios.post(url, formData, config)
        
              .then((response) => {
                const res = response.data;
                // console.log("Respuesta del fetch: ",res);
                if (res.error === false) {
                    setRows((prevRows) => prevRows.filter((row) => Number(row.id) !== Number(id)));
                    // console.log("Delete row ID: ",id);
      
                    MySwal.fire({
                      icon: 'info',
                      title: 'Caso eliminado!!!!',
                      text: '',
                      timer: 1000,
                      timerProgressBar: true,
                      toast: true,
                      position: "center",
                      footer: ''
                    })
                  } else {
                    errorNotification(res.code, res.message);
                }
              })
            .catch((error) => {
              console.error("Error delete Caso: ", error);
              setRefresh(true);  // Reload Data from Server
            })
          }
        })
      },[rows],
    );
  

 
  const editRow = React.useCallback(
    (id) => () => {
      const row = rows.find((rowid) => rowid.id === id);
      if (action === '') {
          setRow(row);
          setAction('edit');
          if (isMobile) {setViewList(false)}
      } else {
          setAction('');
      }
    },
    [rows,action],
  );

   
  const viewRow = React.useCallback(
    (id)=> {
      const row = rows.find((rowid) => rowid.id === id);
      // console.log("El ID selecionado: ",id);
      // console.log("estamos en VIEW, record: ",rows);
      if (action === '') {
          setRow(row);
          setAction('view');
          if (isMobile) {setViewList(false)}
      } else {
          setAction('');
      }
    },
    [rows,action],
  );

  const addRow = React.useCallback(
    () => {
      if (action === '') {
          setAction('add');
          if (isMobile) {setViewList(false)}
      } else {
          setAction('');
      }
    },
    [action],
  );

  const handleButtonClickFromItem = (actionRb,newAction = '') => {
    console.log("Regreso formulario remoto: ",actionRb);
    setAction('');
    
    if (actionRb === 'add') {
      setRefresh(true);
      if (isMobile) { setViewList(true)}
    }
    
    if (actionRb === 'edit') {
      setRefresh(true);
      if (isMobile) { setViewList(true)}
    }
    
    if (actionRb === 'view') {
      // console.log("Valor de Nueva Acción: ",newAction);
      if (newAction !== '') {
        switch (newAction) {
          case "edit":
            setAction(newAction);
            break;
          case "delete":
            // console.log("Registro eliminado:");
            // deleteRow(id)(); // Proceso de eliminación del registro. Require +(), por el tipo de Callaback
            setRefresh(true);
            // fetchCasos();  // Recuperando nuevos registros
            // console.log("En 'delete', después de Refresh");
            if (isMobile) { setViewList(true)}
            break;
          default:
            // console.log("Pasó por default");
          }
        } else {
          if (isMobile) { setViewList(true)}
        }
    }    
  };

  function CustomToolbar() {
    if (isMobile) {
      return (
          <GridToolbarContainer>
          <Button color="primary"  startIcon={<CachedIcon />} onClick={(event) => setRefresh(true)}></Button>
          <Button color="primary"  startIcon={<AddIcon />} onClick={(event) => addRow(event)}>Nuevo</Button>
          <MoreVertIcon sx={{ color:"#1976d2" }} />
          <GridToolbarExport />
        </GridToolbarContainer>
      )
    } else {
    return (
      <GridToolbarContainer>
        <Button color="primary"  startIcon={<CachedIcon />} onClick={(event) => setRefresh(true)}></Button>
        <Button color="primary"  startIcon={<AddIcon />} onClick={(event) => addRow(event)}>Nuevo</Button>
        <MoreVertIcon sx={{ color:"#1976d2" }} />
      
        <GridToolbarColumnsButton />
        <GridToolbarFilterButton />
        <GridToolbarDensitySelector />
        <GridToolbarExport />             
      </GridToolbarContainer>
      );
    }
  }

  const columns = React.useMemo(
    () => [
      {
        field: 'actions',
        hideable: false,
        headerName: 'Acciones',
        type: 'actions',
        flex: 0.5,
        minWidth:74,
        getActions: ({ id, row }) => {
   
        return [

            <GridActionsCellItem
              icon={<EditIcon />}
              label="Edit"
              // className="textPrimary"
              onClick={editRow(id)}
              color="inherit"
            />,
            <GridActionsCellItem
              icon={<DeleteIcon />}
              label="Delete"
              // className="textPrimary"
              onClick={deleteRow(id)}
              color="inherit"
          />,
        ];   
        }
      },
      // { field: 'incidente_usuario_id', hideable: false, headerName: 'Usuario', type: 'singleSelect',
      //  getOptionValue: (key) => key.id, getOptionLabel: (value) => value.nombre_apellidos,  valueOptions: rowsUser , flex: 0.7, minWidth: 100 },
      { field: 'nombre_apellidos', hideable: false, headerName: 'Usuario', type: 'string', flex: 0.7, minWidth: 100 },
      { field: 'titulo_tipo', hideable: false, headerName: 'Tipo permiso', type: 'string',flex: 0.7, minWidth: 100 },
      
      /* 
      { field: 'fechaDesde', hideable: true, headerName: 'Fecha Inicial', type: 'date', valueGetter: (value) => value && new Date(value),renderCell: renderDate, flex: 0.6, minWidth:100 },
      { field: 'fechaHasta', hideable: true, headerName: 'Fecha final',type: 'date', valueGetter: (value) => value && new Date(value),renderCell: renderDate, flex: 0.6, minWidth: 100 },
       */
      { field: 'fechaDesde', hideable: true, headerName: 'Fecha Inicial', type: 'date', valueGetter: (value) => value && new Date(value), flex: 0.6, minWidth:100 },
      { field: 'fechaHasta', hideable: true, headerName: 'Fecha final',type: 'date', valueGetter: (value) => value && new Date(value) , flex: 0.6, minWidth: 100 },

      { field: 'diasLaborables', hideable: true, headerName: 'Laborables', type: 'int',  flex: 0.3, minWidth: 30, align: 'right' },
      { field: 'diasFinSemana', hideable: true, headerName: 'Festivos', type: 'int',  flex: 0.3, minWidth: 30, align: 'right' },

    ],
    [deleteRow, editRow],
  );


  return (
    <>
    <Nav />

    <ThemeProvider theme={theme}>
      <div className='contenedor'>

      {viewList?
        <div className={`panel ${ viewList ? "" : "hide_div"}`}>
          <div className='box-itemList'>
            <div className='titlePage'>
                <h3 className="head_title">Estos son los Casos de los Incidentes </h3>
            </div>

           <DataGrid  columns={columns} rows={rows}
              initialState={{
                pagination: {
                  paginationModel: { page: 0, pageSize: 5 },
                },
                columns: {
                  columnVisibilityModel: {
                  },
                },
              }}
              autosizeOptions={{
                columns: ['incidente_usuario_id', 'incidente_tipo_id','fechaDesde','fechaHasta','diasLaborables','diasFinSemana'],
                includeOutliers: true,
                includeHeaders: true,
              }}
              pageSizeOptions={[2,5 , 10, 15, 20, 100]}
              hideFooterSelectedRowCount={true}
              // enableRowNumbers
              // disableRowSelectionOnClick
              onRowSelectionModelChange={(newRowSelectionModel) => {
                    // Aquí el control del registro seleccionado. 
                    setRowSelectionModel(newRowSelectionModel);
                    // console.log("Array de registros seleccionados: ",newRowSelectionModel);
                    const idSelected = newRowSelectionModel[0];
                    // console.log("Id seleccionado: ",idSelected);
                    viewRow(idSelected);
                    // props.navigate(Config.URL_APP+'/compraView/'+idSelected);
              }}
              slots={{
                toolbar: CustomToolbar
              }}

            />
          </div>  
        </div>
        :''}
        { action === 'add'? <CasosAdd  rowsTipo={rowsTipo}  rowsUser={rowsUser}  onButtonClick={handleButtonClickFromItem} /> :''}
        { action === 'edit'? <CasosEdit  row={row} rowsUser={rowsUser} rowsTipo={rowsTipo} onButtonClick={handleButtonClickFromItem}/> :''}
        { action === 'view' && row !== undefined? <CasosView row={row} rowsUser={rowsUser} rowsTipo={rowsTipo} onButtonClick={handleButtonClickFromItem}/> :''}
      </div>

        
      </ThemeProvider>
    </>
  );
}
// IMPORTANT ----------------------------------------
export default withRouter( CasosList );
import {useEffect, useState, useCallback, useRef} from 'react';
import './style.css';

import Swal from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';

import { useNavigate } from 'react-router-dom';
import { useParams } from "react-router-dom";

import axios from 'axios';

import Config from '../config';
import Nav from '../nav';
import CasosAdd from '../casosAdd';
import CasosEdit from '../casosEdit';
import CasosView from '../casosView';

import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';

// import { formatDate } from '@fullcalendar/core'
import FullCalendar from '@fullcalendar/react'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import multiMonthPlugin from '@fullcalendar/multimonth'
// import { INITIAL_EVENTS, createEventId } from './event-utils'
import esLocale from '@fullcalendar/core/locales/es';

import 'dayjs/locale/es';
import dayjs from 'dayjs';

import tippy from 'tippy.js'
import 'tippy.js/dist/tippy.css';
import 'tippy.js/themes/light.css'; // Opcional: tema adicional


const styleModal = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: 555,
  bgcolor: 'background.paper',
  // border: '2px solid #000',
  // boxShadow: 24,
  p: 4,
};
const styleModalMobile = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: '100%',
  bgcolor: 'background.paper',
  // border: '2px solid #000',
  // boxShadow: 24,
  // p: 1,
};

const withRouter = (Component) => {
  const Wrapper = (props) => {
    const navigate = useNavigate();
    return <Component navigate={navigate} {...props} />;
  };
  return Wrapper;
};

function Calendario(props) {

  const calendarRef = useRef(null);

  const [weekendsVisible, setWeekendsVisible] = useState(true);
  const [currentEvents, setCurrentEvents] = useState([]);

  // const [isData,setIsData] = useState(false);  // Control de datos cargados
  const [startEvent, setStartEvent ]= useState('');
  const [endEvent, setEndEvent ]= useState('');
  // For ADD Event
  const [startAddEvent, setStartAddEvent ]= useState('');
  const [endAddEvent, setEndAddEvent ]= useState('');

  const [rows, setRows] = useState([]);
  const [row, setRow] = useState({});
  const [rowsTipo, setRowsTipo] = useState([]);
  const [rowsUser, setRowsUser] = useState([]);
  const [user, setUser] = useState({});


  const [rowSelectionModel, setRowSelectionModel] = useState([]); // Para control del registro seleccionado en el GRID
  const [action, setAction] = useState(''); // Acción sobre el registro (row) - add, edit, view -
   const [open, setOpen] = useState(false);   // Open / Close Modal window

  const [refresh, setRefresh] = useState(true);
  const [isMobile, setIsMobile] = useState(false);
  const [isDouble, setIsDouble] = useState(true);
  const [viewList, setViewList] = useState(true);

  const { id } = useParams();

  // Notificación error grave
  function errorNotification(code, message) {
    const MySwal = withReactContent(Swal); 
    MySwal.fire({
      icon: 'error',
      title: message,
      text: '',
      timer: 5000,
      timerProgressBar: true,
      toast: true,
      position: "center",
      footer: ''
    })
    // props.navigate(Config.URL_APP+'/');
  }
  // Cargar los datos del Server cuando se carga la página
  useEffect(() => {
    // console.log("Ejecutándose 'useEffect");
    setRefresh(false);
    setAction('');
    // console.log(" Valores iniciales de datos de prueba: ",INITIAL_EVENTS);

    var formWitdh = window.innerWidth;
    // console.log("Ancho de pantalla: ",formWitdh);
    if (formWitdh < 768) {
      // console.log("pone isMobile a 'true'");
      setIsMobile(true);
    }
    if (formWitdh < 1390) { // Para ver si tenemos que ocultar el GRID
      setIsDouble(false);
    }

    }, [refresh]);


    // Función auxiliar para formatear fechas
    const formatDate = (date) => {
      return new Date(date).toLocaleDateString('es-ES');
    };

  // Función para cargar eventos dinámicamente
  const fetchEvents = (start_fech, end_fech) => {

    console.log('Cargando eventos desde:', start_fech, ' hasta ', end_fech);
    // console.log(" Objeto fechInfo: ",fetchInfo);

    const url = Config.RUTA_API + "/casoCalendar";
    const formData = new FormData();
    formData.append('start',  start_fech);
    formData.append('end'  ,  end_fech);
    formData.append('id_user',  id);
    const config = {
      headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          "Authorization": Config.AUTHORIZATION
      },
    };
           
    axios.post(url,formData,config) 
    .then((response) => {    
        var msg = response.data;
        // console.log("Respuesta del fetch: ",msg); 
        if (msg.error === true )  { 
          console.log("Hay un error: ",msg.message);
          errorNotification(msg.error, msg.message);
        } else {
          // Transforma los datos al formato que FullCalendar espera
          const formattedEvents = msg.data.map(event => (
            {
            id: event.id.toString(),
            title: event.titulo|| 'Sin título',
            start: event.fechaDesde,       // Asegúrate que estos campos existan
            // end: dayjs(event.FechaHasta, "YYYY-MM-DD").add(1, 'day').format('YYYY-MM-DD'),  // en tu respuesta del servidor
            end: event.end,
            allDay: true,             // Para eventos de varios días
            color: event.color,       // Opcional
            extendedProps: {          // Datos adicionales
              description: event.descripcion
            }
          }));
          setCurrentEvents(formattedEvents);
          // console.log("Datos formateados: ", formattedEvents);
          setRows(msg.data);
          setRowsTipo(msg.dataTipo);
          setRowsUser(msg.dataUsuario);

          userFind(id,msg.dataUsuario);

          // console.log("Casos: ",msg.data);
          // console.log("Tipos: ",msg.dataTipo);
          // console.log("Usuarios: ",msg.dataUsuario);
        }           
      })
      .catch((error) => {
        console.error("Error get Lista Casos ", error);
        errorNotification('000', error);
      })
      
  }; 
  // Función para buscar un usuario por ID y actualizar el estado
  const userFind = (id, rows) => {
    // Verifica si `rows` es un array y no está vacío
    if (!Array.isArray(rows) || rows.length === 0) {
      console.error("Error: 'rows' no es un array válido o está vacío");
      return;
    }
    const row = rows.find((row) => Number(row.id) === Number(id));
    /* console.log(
      "Datos de los usuarios:", rows,
      "ID del usuario:", id,
      "Datos del seleccionado:", row
    );
    */
    if (row !== undefined && row !== null) {
      setUser(row);
    }
  };

  // For Add Event with selection detes
  function handleDateSelect(selectInfo) {
    console.log("Selección de evento: ",selectInfo);
    const end = dayjs(selectInfo.endStr).subtract(1, 'day').format("YYYY-MM-DD");   // Paras visualizar día completo hay que añadir un día
    setRow({
      "incidente_usuario_id": id,
      "incidente_tipo_id": '',
      "fechaDesde": selectInfo.startStr,
      "fechaHasta": end ,
      "titulo":'',
      "descripcion": ''
  });

    setOpen(true);
    setAction('add');



    /*
    let title = prompt('Please enter a new title for your event')
    let calendarApi = selectInfo.view.calendar

    calendarApi.unselect() // clear date selection

    if (title) {
      calendarApi.addEvent({
        id: createEventId(),
        title,
        start: selectInfo.startStr,
        end: selectInfo.endStr,
        allDay: selectInfo.allDay
      })
    }
      */
  }

  function handleEventClick(clickInfo) {
   
    const id_caso = clickInfo.event.id;
    const row = rows.find((row) => Number(row.id) === Number(id_caso));
    setRow(row);
    setOpen(true);
    setAction('view');
  }

  // Manejar cambio de fecha/vista
  const handleDatesSet =  (dateInfo) => {
    /*
    console.log('Vista cambiada:', dateInfo.view.type, 
                'Fecha inicio:', dateInfo.start, 
                'Fecha fin:', dateInfo.end);
    */
    const start_fech = dateInfo.start.toISOString().split('T')[0];
    const end_fech = dateInfo.end.toISOString().split('T')[0];

    if ( start_fech !== startEvent || end_fech !== endEvent ) {
 
      setStartEvent(start_fech);
      setEndEvent(end_fech);

      fetchEvents(start_fech,end_fech);  // Carga los eventos del nuevo rango
    }
  };

  function renderEventContent(eventInfo) {
    return (
      <>
        <b>{eventInfo.timeText}</b>
        <i>{eventInfo.event.title}</i>
      </>
    )
  }
  
   // Actualizar tooltips cuando cambian los eventos Necesario para que los Tooltip se actualicen cuando se recargam los datos
   const handleEventSet = (events) => {
    // console.log("Estoy en EventSet: ",events);

    // Usamos el selector directamente ya que no necesitamos calendarApi aquí
    const eventElements = document.querySelectorAll('.fc-event');
        
    eventElements.forEach(el => {
      const eventId = el.getAttribute('data-event-id');
      const c_event = currentEvents.find(e => e.id === eventId);
      // console.log("Datos del Evento: ",c_event);
      tippy(el, {
        content: createTooltipContent(c_event),
        allowHTML: true,
        interactive: true,
        appendTo: () => document.body,
        theme: 'light',
        placement: 'top',
        delay: [300, 0],
        duration: [200, 0],
        zIndex: 9999
      });  
    })
  }

  const createTooltipContent = (event) => {
    // console.log('Event data for tooltip:', event); // Verifica que los datos sean correctos
    const end = dayjs(event.end).subtract(1, 'day');   // Paras visualizar día completo hay que añadir un día
    return `
      <div class="custom-tooltip">
        <h4>${event.title || 'Sin título'}</h4>
        ${event.extendedProps?.description ? 
          `<p>${event.extendedProps.description}</p>` : ''}
        <small>${formatDate(event.start)} - ${formatDate(end)}</small>
      </div>
    `;
  };


  const handleButtonClickFromItem = (actionRb,newAction = '') => {
    console.log("Regreso formulario remoto: ",action);
    setOpen(false);
    
    if (actionRb === 'add') {
      setAction('');
      fetchEvents(startEvent,endEvent);  // Carga los eventos del nuevo rango
    }
    
    if (actionRb === 'edit') {
      setAction('');
      fetchEvents(startEvent,endEvent);  // Carga los eventos del nuevo rango
    }
    
    if (actionRb === 'view') {
      setAction('');
      // console.log("Valor de Nueva Acción: ",newAction);
      if (newAction !== '') {
        switch (newAction) {
          case "edit":
            setOpen(true);
            setAction(newAction);
            break;
          case "delete":

            fetchEvents(startEvent,endEvent);  // Carga los eventos del nuevo rango
            // console.log("Después del 'FetchEvents' : ");
            break;
          default:
            // console.log("Pasó por default");
          }
        } 
    }    
  };

  /*
  // Gestión de respuesta de páginas modales
  const handleButtonClickFromItem = (action,newAction = '') => {
    console.log("Regreso formulario remoto: ",action);
    setOpen(false);
    if (action === 'add') {
      setAction('');
      fetchEvents(startEvent,endEvent);  // Carga los eventos del nuevo rango
    }
    if (action === 'edit') {
      setAction('');
      fetchEvents(startEvent,endEvent);  // Carga los eventos del nuevo rango
    }
    if (action === 'view') {
      setAction('');
      if (newAction !== '') {
        setOpen(true);
        setAction(newAction);
      }
    }
  };
  */

  return (
    <>
    <Nav />
    <div>
      <div className='panel_calendar'>
        <div className='box-itemList_calendar'>
              <div className='titlePage'>
                  <h3 className="head_title">Estos son los Casos de los Incidentes del usuario: {user.nombre_apellidos} </h3>
              </div>
        </div>
      </div>

      <div className='calendar'>
        <FullCalendar
          plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin, multiMonthPlugin]}
          headerToolbar= {{ center: 'dayGridMonth Trimestre multiMonthYear' }} // buttons for switching between views

          locales= {[esLocale]}
          timeZone= 'UTC+1' 
          locale= 'es' // the initial locale. if not specified, uses the first one
          height= 'auto'

          views = {{
            Trimestre: {
             type: 'multiMonth' ,
             duration: { months: 3 },
            }
          }}
          // multiMonthMaxEvents={true}
          initialView='dayGridMonth'    
          multiMonthMinWidth= {50}
          multiMonthMaxColumns= {3}

          // nowIndicator={true}
          // displayEventTime={false} // Para eventos de todo el día
          // eventDisplay="block"    // Muestra como bloque sólido
          // dayMaxEventRows={3}     // Máximo de eventos por día
          dayHeaders={true}

          editable={false}
          selectable={true}
          selectMirror={false}
          dayMaxEvents={true}
          weekends={weekendsVisible}
          events={currentEvents} 
          select={handleDateSelect}
          eventContent={renderEventContent} // custom render function
          eventClick={handleEventClick}
          datesSet={handleDatesSet} // Para detectar cambios de vista/fecha
          eventsSet={handleEventSet} // Se ejecuta cuando -  events are initialized/added/changed/removed
          // eventDidMount={handleTooltip} // Event managment the Tooltip
          eventDidMount={(arg) => {
            arg.el.setAttribute('data-event-id', arg.event.id);
            arg.el.style.cursor = 'pointer';
            // console.log("Estoy en DidMount: ",arg);          
          }}

          /* you can update a remote database when these fire:
          eventAdd={function(){}}
          eventChange={function(){}}
          eventRemove={function(){}}
          */
        />

      </div>
        <Modal
            open={open}
            // onClose={handleButtonClickFromItem('add')}
            // aria-labelledby="modal-modal-title"
            // aria-describedby="modal-modal-description"
          >
            <Box sx={styleModal}>
              { action === 'add'?<CasosAdd  rowsTipo={rowsTipo}  rowsUser={rowsUser} row={row} onButtonClick={handleButtonClickFromItem} />:''}
              { action === 'edit'? <CasosEdit  row={row} rowsUser={rowsUser} rowsTipo={rowsTipo} onButtonClick={handleButtonClickFromItem}/> :''}
              { action === 'view'? <CasosView row={row} rowsUser={rowsUser} rowsTipo={rowsTipo}  onButtonClick={handleButtonClickFromItem}/> :''}
          </Box>
        </Modal>
    </div>
    </>
  )
}

// IMPORTANT ----------------------------------------
export default withRouter( Calendario);

Para acceder a la visualización del calendario se hace clic en el icono de calendario azul de la página List de Usuarios.

Como siempre, os facilito todos los fuentes, para que lo podáis reproducir y modificar en vuestros PC’s.

Si tenéis curiosidad, preguntad a cualquier IA por el presente y futuro de REACT y, por ejemplo, de PHPrunner. Ya os digo que muchas veces producen «alucinaciones», pero algunas veces es importante elegir la línea técnica de las aplicaciones que realicéis.

REACT es para todo tipo de desarrollador?
REACT no es para todo tipo de desarrollador, pues es mucho más exigente de conocimientos que, por ejemplo, PHPrunner, pero es de los más sencillos de utilizar.

Todo lo que veis en estos ejemplos, es completamente GRATIS, al igual que el entorno de Desarrollo

Adjuntos

Archivo Tamaño de archivo Descargas
zip incidentes-server 225 KB 11
zip incidentes-react 266 KB 9
zip Backup de la Base de datos 2 KB 7

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner