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.
{ "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.
<?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.
// 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.
Todo lo que veis en estos ejemplos, es completamente GRATIS, al igual que el entorno de Desarrollo
Adjuntos
Archivo | Tamaño de archivo | Descargas |
---|---|---|
![]() |
225 KB | 11 |
![]() |
266 KB | 9 |
![]() |
2 KB | 7 |