Ya hace un tiempo que escribí este ejemplo de aplicación para el móvil en PHPrunner, Gestión de la Lista de la Compra.
Además de poner a prueba PHPRunner para aplicaciones móviles, expliqué que cambios había que hacer para poderla instalar como una aplicación Web en el móvil.
Yo pude contemplar, y espero que vosotros también, que estas aplicaciones creadas con PHPRunner podían funcionar, pero se alejaban bastante de las típicas aplicaciones que disfrutábamos en los móviles y estaban entre una aplicación de escritorio y de móvil.
Cuando ya dispuse de conocimientos en React, me propuse ver esta misma aplicación en este nuevo entorno de desarrollo y este artículo viene a explicar cómo lo he hecho.
Objetivo
Pasar una aplicación típica de móvil realizada en PHPrunner a React, para su funcionamiento, principalmente, para el móvil y para usuarios del móvil.
DEMO (PHPRunner): https://fhumanes.com/compra/
DEMO (REACT): https://fhumanes.com/compra-react/
Comparten la base de datos y si no deseáis daros de alta podéis utilizar los usuarios «user1» y «user2». El Login/User Name y Password son iguales. Os pido que no destruyáis los datos de estos usuarios.
Por favor, dedicar un momento para leer la explicación del funcionamiento del aplicativo para diferenciar qué es un «Producto» y qué un «Artículo de la Lista» y la gestión de los Grupos.
Solución Técnica
Para desarrollar este ejemplo practiqué con la nueva bibliotecas de Material UI y además tuve que resolver los siguientes apartados:
- Disponer de opciones de menú para usuario identificado y sin identificar. Menú dinámico según su estado.
- Establecer una solución para la identificación del usuario y definición y gestión de un «token» que identifica en el Server las credenciales del usuario.
- Definir y establecer un mecanismo similar a las cookie de sesión y almacenamiento del estado para cada usuario. Se mantiene el estado asociado al «token» generado y la persistencia se hace en una tabla de base de datos.
- Definir un mecanismo (ahora en el cliente) para mantener el estado del aplicativo. Nunca se almacena ni en «contexto» y en el «Almacén local», la password y datos significativos del usuario conectado.
- Gestión de subida de imágenes y reducción de estas para ajustarse a las características de la aplicación. Se ha resulto un problema (general y de siempre) que tiene PHPRunner que las imágenes de móvil, si es vertical y se recorta, se pierde la orientación de la imagen.
- Se establece formar dinámicas de los botones de acciones, dependiendo del estado del producto o del usuario conectado y sus privilegios.
- Se establece cómo hacer cambios dinámicos en texto y botones dependiendo del estado o usuario.
- Se resuelve la configuración para la instalación automática de la aplicación en el móvil.
Para hacer este ejemplo he tenido que revisar muchas publicaciones (existe una cantidad muy grande y de calidad, de publicaciones) y combinarlas para hacer una gestión de identidad parecida a la que hace PHPRunner. Tenemos que tener siempre presente, que ambas soluciones comparten el modelo de datos y sus datos serán gestionable por ambos sistemas.
La parte del Server en PHP he seguido las indicaciones de este artículo publicado hace un tiempo.
La parte que se ha añadido para responder a la aplicación REACT es:
La parte del «Include», es la configuración de la solución, para disponer de la personalización del entorno de Desarrollo y el de Producción.
Los otros 2 ficheros son las funciones que se utilizan. En «DbCompra.php», son todas las que acceden a la base de datos y el otro, las funciones que no acceden a base de datos.
En directorio «libs» es el FrameWork SLIM v4.X y en V1, es el interfaz de la aplicación con su «.htaccess» para que todas las peticiones vayan al fichero «index.php» y el fichero «index.php», todas la peticiones posibles y sus controles de validación de parámetros y y llamadas al resto de funciones.
Para la parte de REACT, que es nueva en el Blog, voy a mostrar su contenido y luego mostraré y explicaré algunos de sus códigos.
Esta figura representa el proyecto en MS Visual Studio, una vez que el ejemplo ha sido «instalado». ver comandos aquí.
(1).- En donde hay que configurar para cuando la aplicación no va a ejecutarse en el «Document Root» , del servidor web.
(2).- Ficheros que se utilizarán para construir el directorio «build» y donde se define el fichero «index.html», «manifiesf.json», los logos e iconos, así como el fichero «.htaccess», que van a requerir para su despliegue.
(3).- Son los fuentes del programa. Donde vamos a trabajar para construir la solución.
(4),(5) y (6).- son los directorio que yo he definido para distribuir mis ficheros . En (4) tengo la definición de todas las pantalla y sus controles.
(5).- Defino en «Contexto», para guardar información entre pantallas, del usuario conectado
(6).- Definido el control de que opciones son de usuario sin identificación y usuario identificado
(7).- Primer ejecutable que se lanza cuando se accede y que tiene la relación de las url’s y los ficheros del código a ejecutar.
Su función es enrutar la URL al programa que debe ejecutarse. También controla si puede acceder o no, al programa, dependiendo de la identificación del usuario.
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import * as React from 'react'; import Config from './components/config'; import Login from "./components/login"; import Logout from "./components/logout"; import Register from "./components/register"; import SelectGroup from "./components/selectGroup"; import CompraList from "./components/compraList"; import CompraAdd from "./components/compraAdd"; import CompraEdit from "./components/compraEdit"; import CompraView from "./components/compraView"; import ProductoList from "./components/productoList"; import ProductoAdd from "./components/productoAdd"; import ProductoEdit from "./components/productoEdit"; import ProductoView from "./components/productoView"; import GrupoList from "./components/grupoList"; import GrupoAdd from "./components/grupoAdd"; import GrupoEdit from "./components/grupoEdit"; import UserList from "./components/userList"; import UserAdd from "./components/userAdd"; import UserConnect from "./components/userConnect"; import NotFound from "./components/notFound"; import AuthProvider from "./hooks/AuthProvider"; import PrivateRoute from "./router/route"; // import AdminRoute from "./router/routeAdmin"; function App() { return ( <div className="App"> <Router> <AuthProvider> <Routes> <Route path={`${Config.URL_APP}/`} element={<Login />} /> <Route path={`${Config.URL_APP}/login`} element={<Login />} /> <Route path={`${Config.URL_APP}/register`} element={<Register />} /> <Route element={<PrivateRoute />}> <Route path={`${Config.URL_APP}/userconnect`} element={<UserConnect />} /> <Route path={`${Config.URL_APP}/logout`} element={<Logout />} /> <Route path={`${Config.URL_APP}/selectgroup`} element={<SelectGroup />} /> <Route path={`${Config.URL_APP}/compraList`} element={<CompraList />} /> <Route path={`${Config.URL_APP}/compraAdd`} element={<CompraAdd />} /> <Route path={`${Config.URL_APP}/compraEdit/:id`} element={<CompraEdit />} /> <Route path={`${Config.URL_APP}/compraView/:id`} element={<CompraView />} /> <Route path={`${Config.URL_APP}/productoList`} element={<ProductoList />} /> <Route path={`${Config.URL_APP}/productoAdd`} element={<ProductoAdd />} /> <Route path={`${Config.URL_APP}/productoEdit/:id`} element={<ProductoEdit />} /> <Route path={`${Config.URL_APP}/productoView/:id`} element={<ProductoView />} /> <Route path={`${Config.URL_APP}/grupoList`} element={<GrupoList />} /> <Route path={`${Config.URL_APP}/grupoAdd`} element={<GrupoAdd />} /> <Route path={`${Config.URL_APP}/grupoEdit/:id`} element={<GrupoEdit />} /> <Route path={`${Config.URL_APP}/userList`} element={<UserList />} /> if (auth.adminGroup !== null ) { // Administrador del Grupo <Route path={`${Config.URL_APP}/userAdd`} element={<UserAdd />} /> } </Route> {/* Other routes */} <Route path="*" element={<NotFound />} /> </Routes> </AuthProvider> </Router> </div> ); } export default App;
import { useContext, createContext, useState } from "react"; const AuthContext = createContext(); const AuthProvider = ({ children }) => { const [user, setUser] = useState(localStorage.getItem("siteUser") || null); const [token, setToken] = useState(localStorage.getItem("site") || ""); const [groupName, setGroupName] = useState(localStorage.getItem("siteGroup") || null); const [adminGroup, setAdminGroup] = useState(localStorage.getItem("siteAdmin") || null); const loginAction = (user,token) => { setUser(user); setToken(token); localStorage.setItem("site", token); // si se quita, no se puede hacer "reload" de la página localStorage.setItem("siteUser", user); // si se quita, no se puede hacer "reload" de la página }; const groupAction = (group,admin) => { setGroupName(group); setAdminGroup(admin); localStorage.setItem("siteGroup", group); // si se quita, no se puede hacer "reload" de la página localStorage.setItem("siteAdmin", admin); // si se quita, no se puede hacer "reload" de la página }; const logOut = () => { setUser(null); setToken(""); setGroupName(null); setAdminGroup(null); localStorage.removeItem("site"); localStorage.removeItem("siteUser"); localStorage.removeItem("siteGroup"); localStorage.removeItem("siteAdmin"); localStorage.removeItem("listaCompra"); localStorage.removeItem("situacionLista"); }; return ( <AuthContext.Provider value={{ token, user, groupName, adminGroup, loginAction, logOut, groupAction }}> {children} </AuthContext.Provider> ); }; export default AuthProvider; export const useAuth = () => { return useContext(AuthContext); };
import React from "react"; import { Navigate, Outlet } from "react-router-dom"; import { useAuth } from "../hooks/AuthProvider"; import Config from '../components/config'; const PrivateRoute = () => { const user = useAuth(); const urlLogin = Config.URL_APP+"/login"; if (!user.token) return <Navigate to={urlLogin} />; return <Outlet />; }; export default PrivateRoute;
// Parámetros de configuración de la App const configuration = 'production'; // 'test' or 'production' var Config =null; if (configuration === 'test') { Config = { RUTA_API: "http://localhost/compra/restapi/v1", URL_APP: "/compra-react", TITLE_APP: "COMPRA", AUTHORIZATION: "3d524a53c110e4c22463b10ed32cef9d", URL_FILES: "http://localhost/files/" }; } else { // Config edl Server Linux Config = { RUTA_API: "https://fhumanes.com/compra/restapi/v1", URL_APP: "/compra-react", TITLE_APP: "COMPRA", AUTHORIZATION: "3d524a53c110e4c22463b10ed32cef9d", URL_FILES: "https://fhumanes.com/files/" }; }; 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 Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import AddIcon from '@mui/icons-material/Add'; import EditIcon from '@mui/icons-material/Edit'; // import CheckedIcon from '@mui/icons-material/ShoppingCart'; // import FilterIcon from '@mui/icons-material/Search'; import CachedIcon from '@mui/icons-material/Cached'; import DeleteIcon from '@mui/icons-material/Delete'; import Swal from 'sweetalert2' import withReactContent from 'sweetalert2-react-content' import { createTheme, ThemeProvider } from '@mui/material/styles'; import axios from 'axios'; import { useAuth } from "../../hooks/AuthProvider"; import {useEffect, useState} from 'react'; // 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'; 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 GrupoList(props) { const [rows, setRows] = useState(null); // const [users, setUsers] = useState(null); // API /usersGroupList const [refresh, setRefresh] = useState(true); const [isMobile, setIsMobile] = useState(false); const auth = useAuth(); // 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+'/logout'); } // Cargar los datos del Server cuando se carga y recarga la página const fetchGrupo= async () => { // console.log("Valor de la variable Refresh: ",refresh); 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); // console.log("Configuarado para móvil "); } const url = Config.RUTA_API + "/grupoList"; const formData = new FormData(); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; try { const response = await axios.post(url,formData,config) var data = response.data; // console.log("Respuesta del fetch: ",data); if (data.error === true ) { console.log("Hay un error: ",data.message); errorNotification(data.error, data.message); } else { setRows(data); } } catch(error) { console.error("Error get Lista Grupos ", error); errorNotification('000', error); } } }; // Cargar los datos del Server cuando se carga la página useEffect(() => { // console.log("Ejecutándose 'useEffect"); fetchGrupo(); }, [refresh]); const deleteRow = React.useCallback((id) => () => { // console.log("Registro a eliminar: ",id); const row = rows.find((rowid) => rowid.id === id); var msg = ''; if (row.Login === null ) { msg = `¿<b>Te quieres dar de baja</b> <br>del Grupo: ${row.Nombre} ?`; } else { msg = `¿Quieres <b>ELIMINAR el Grupo:</b> ${row.Nombre}? <br><br><b>TODA LA INFORMACIÓN ASOCIADA (Lista, Productos, Usuarios del Grupo) será Eliminada</b>`; } 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 + "/grupo/delete/"+id; const formData = new FormData(); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; 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: 'Grupo eliminado!!!!', text: '', timer: 1000, timerProgressBar: true, toast: true, position: "center", footer: '' }) if ( row.Nombre === auth.groupName ) { // Si ha eliminado del grupo al que está conectado props.navigate(Config.URL_APP+'/logout'); } } else { errorNotification(res.code, res.message); } }) .catch((error) => { console.error("Error delete Grupo: ", error); setRefresh(true); // Reload Data from Server }) } }) }, [rows, auth.token], ); const editRow = React.useCallback( (id) => () => { const row = rows.find((rowid) => rowid.id === id); if (row.Login === null) { // No es administrador del Grupo y no puede modificar const text = `<p>¡¡¡ No eres administrador de: <b> ${row.Nombre}</b> !!!</p><p><b>No puedes modificar el Grupo.</b></p>`; // ${row.nombre} const MySwal = withReactContent(Swal); MySwal.fire({ title: "<h3>Acción no se puede ejecutar</h3>", icon: "info", html: text, showCloseButton: true, showCancelButton: false, focusConfirm: false, closeButtonText: 'OK', // confirmButtonAriaLabel: "Thumbs up, great!", // cancelButtonText: `<i class="fa fa-thumbs-down"></i>`, // cancelButtonAriaLabel: "Thumbs down" }); } else { props.navigate(Config.URL_APP+'/grupoEdit/'+id); } }, [rows], ); const addRow = (event) => { props.navigate(Config.URL_APP+'/grupoAdd'); }; function CustomToolbar() { return ( <GridToolbarContainer> <GridToolbarColumnsButton /> <GridToolbarFilterButton /> { isMobile ? '' : <GridToolbarDensitySelector /> } <GridToolbarExport /> </GridToolbarContainer> ); } const isRowEditable = (row) => { // console.log(" Datos del registro: ",row); if (row.Login === null) { return false } else { return true } } const columns = React.useMemo( () => [ { field: 'actions', hideable: false, headerName: 'Acciones', type: 'actions', minWidth: 75, getActions: ({ id, row }) => { if (isRowEditable(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" />, ]; } else { return[ <GridActionsCellItem icon={<DeleteIcon />} label="Delete" // className="textPrimary" onClick={deleteRow(id)} color="inherit" />, ]} } }, // { field: 'groupId', type: 'number', width: 40}, { field: 'Nombre', hideable: false, headerName: 'Nombre', type: 'string', minWidth: 200 }, { field: 'NombreUsuario', headerName: 'Administrador', type: 'string', minWidth: 200 }, ], [deleteRow, editRow], ); return ( <> <Nav /> <div className='titlePage'> <h3 className="head_title">Estos son tus Grupos / Listas de Compra </h3> </div> <ThemeProvider theme={theme}> <div className='titleButtons' > <Button color="primary" startIcon={<CachedIcon />} onClick={(event) => setRefresh(true)}></Button> <Button color="primary" startIcon={<AddIcon />} onClick={(event) => addRow(event)}> Nuevo </Button> </div> <div className='panel'> <div className='box-compraList'> <DataGrid columns={columns} rows={rows} initialState={{ pagination: { paginationModel: { page: 0, pageSize: 100 }, }, columns: { columnVisibilityModel: { }, }, }} autosizeOptions={{ columns: ['Nombre', 'Login'], includeOutliers: true, includeHeaders: true, }} pageSizeOptions={[5, 10, 15, 20, 100]} // enableRowNumbers slots={{ toolbar: CustomToolbar, }} /> </div> </div> </ThemeProvider> </> ); } // IMPORTANT ---------------------------------------- export default withRouter( GrupoList );
Funcionalmente se utiliza para dar de alta un Grupo, donde el usuario conectado será el Administrador de dicho Grupo.
Podemos ver que los campos de entrada los guardamos en formato JSON/array ne este caso y en la parte de Edit, como campos independientes.
También, de forma general, se utilizan 2 variables para el control global de formulario «validacionForms» y «errorForms». De forma similar, se utilizan otros 2 campos de control para cada uno de los campos de entrada «errorXXXXX» y «helperXXXXXX».
Lo normal en estos ejemplos es que las validaciones de la información se hagan a través de «Expresiones Regulares».
import * as React from 'react'; import {useEffect, useState} from 'react'; import './style.css'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { esES } from '@mui/material/locale'; // import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Grid2'; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import ReplySharpIcon from '@mui/icons-material/ReplySharp'; // import { Select, MenuItem, InputLabel, FormHelperText } from '@mui/material'; import Alert from '@mui/material/Alert'; import Swal from 'sweetalert2' import withReactContent from 'sweetalert2-react-content' import axios from 'axios'; import { useAuth } from "../../hooks/AuthProvider"; import { useNavigate } from 'react-router-dom'; import Nav from '../nav'; import Config from '../config'; const withRouter = (Component) => { const Wrapper = (props) => { const navigate = useNavigate(); return <Component navigate={navigate} {...props} />; }; return Wrapper; }; const theme = createTheme( { palette: { primary: { main: '#1976d2' }, }, }, esES, ); function GrupoAdd(props) { const auth = useAuth(); // console.log("Grupo al que está identificado: ",auth.groupName); const [validacionForms, setValidacionForms] = useState(true); const [errorForms, setErrorForms] = useState(''); const [input, setInput] = useState({ grupo: "", descripcion: "" }); const [errorGrupo, setErrorGrupo] = useState(false); const [helperGrupo, setHelperGrupo] = useState(''); const [errorDescripcion, setErrorDescripcion] = useState(false); const [helperDescripcion, setHelperDescripcion] = useState(''); const [menu, setMenu] = 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+'/logout'); } // Cargar los datos del Server cuando se carga la página useEffect(() => { // console.log("Ejecutando 'useEffect() en grupoAdd", auth); if (auth.groupName === null) { setMenu(false); } else { setMenu(true); } }, [auth]); // Envío del Formulario const saveForms = (e) => { e.preventDefault(); setValidacionForms(true); // Quitamos errores del formulario setErrorForms(''); var existeError = false; var Error = ''; if (input.grupo === '' || errorGrupo ) { setHelperGrupo('Hay que facilitar un Grupo Válido'); setErrorGrupo(true); existeError = true; Error += ' (Grupo) '; } if ( errorDescripcion ) { setHelperDescripcion('Hay que facilitar una Descripción Válida'); setErrorDescripcion(true); existeError = true; Error += ' (Descripción) '; } if (!(existeError)) { // se han introducido los valores de los campos // console.log("accediendo al Server"); // Acceso al server y status de la operación const url = Config.RUTA_API + "/grupo/add/0"; const formData = new FormData(); formData.append('Nombre', input.grupo); formData.append('Descripcion', input.descripcion); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; axios.post(url, formData, config) .then((response) => { const res = response.data; // console.log("Respuesta del fetch: ",res); if (res.error === false) { setValidacionForms(true); // Quitamos errores del formulario setErrorForms(''); postSave(); // Alert y navegar a la aplicación } else { setValidacionForms(false); setErrorForms(res.message); } }) .catch((error) => { console.error("Error send Register: ", error); setValidacionForms(false); setErrorForms("Error send Register: ", error); errorNotification('000', error) }); } else { setValidacionForms(false); // Ponemos los errores del formulario Error = 'Falta la entrada de los datos o la información de estos campos está mal: '+ Error; setErrorForms(Error); } }; const postSave = (e) => { const MySwal = withReactContent(Swal); MySwal.fire({ icon: 'info', title: 'Nuevo Grupo creado!!!!', text: '', timer: 1000, timerProgressBar: true, toast: true, position: "center", footer: '' }) if (auth.groupName === null ) { props.navigate(Config.URL_APP+'/selectgroup'); } else { props.navigate(Config.URL_APP+'/grupoList'); } } const handleValidationGrupo = (e) => { const { name, value } = e.target; setInput((prev) => ( { ...prev, [name]: value, })); const reg = new RegExp("^([a-zA-Z0-9_áéíóúüñÑ -]){3,50}$"); if (! reg.test(e.target.value)) { setHelperGrupo('Entrada inválida para "Nombre de grupo"'); } else { setHelperGrupo(''); } setErrorGrupo(!reg.test(e.target.value)); }; const handleValidationDescripcion = (e) => { const { name, value } = e.target; setInput((prev) => ( { ...prev, [name]: value, })); const reg = new RegExp("^([a-zA-Z0-9_áéíóúüñÑ, -]){0,200}$"); if (! reg.test(e.target.value)) { setHelperDescripcion('Entrada inválida para "Descripción"'); } else { setHelperDescripcion(''); } setErrorDescripcion(!reg.test(e.target.value)); }; return ( <> <Nav /> <div className='titlePage'> <h3 className="head_title">Añadir Grupo</h3> </div> <ThemeProvider theme={theme}> <div className='panel-compraAdd'> <div className='box-compraAdd'> <Box component="form" noValidate autoComplete="off" > <Grid container spacing={2}> <Grid xs={12}> <TextField error={errorGrupo} required={false} id="grupo" name="grupo" label="Grupo" variant="standard" type= 'text' className= 'CustomField' helperText={helperGrupo} value={input.grupo } onChange={(e) => handleValidationGrupo(e)} /> </Grid> <Grid xs={12}> <TextField error={errorDescripcion} required={false} id="descripcion" name="descripcion" label="Descripcion" variant="standard" type= 'text' multiline maxRows={4} className= 'CustomField' helperText={helperDescripcion} value={input.Descripcion } onChange={(e) => handleValidationDescripcion(e)} /> </Grid> </Grid> <Stack spacing={2} direction="row" sx={{ width: '100%', padding: '15px 10px 15px 10px'}} > <Button variant="contained" type='submit' onClick={(e) => saveForms(e)}>Guardar</Button> {menu? <Button variant="outlined" startIcon={<ReplySharpIcon />} onClick={(e) =>props.navigate(Config.URL_APP+'/grupoList')}>Volver</Button> :''} </Stack> <Stack sx={{ width: '100%' }} spacing={2}> { validacionForms? '': <Alert severity="error">{errorForms}</Alert>} </Stack> </Box> </div> </div> </ThemeProvider> </> ); } // IMPORTANT ---------------------------------------- export default withRouter( GrupoAdd);
Funcionalmente, se verá el icono de edición si el usuario conectado es el administrador del grupo que se desea editar.
import * as React from 'react'; import {useEffect, useState} from 'react'; import './style.css'; // import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Grid2'; import ReplySharpIcon from '@mui/icons-material/ReplySharp'; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { esES } from '@mui/material/locale'; import { Select, MenuItem, InputLabel, FormHelperText } from '@mui/material'; import Alert from '@mui/material/Alert'; import Swal from 'sweetalert2' import withReactContent from 'sweetalert2-react-content' import axios from 'axios'; import { useAuth } from "../../hooks/AuthProvider"; import { useNavigate } from 'react-router-dom'; import { useParams } from "react-router-dom"; import Nav from '../nav'; import Config from '../config'; const withRouter = (Component) => { const Wrapper = (props) => { const navigate = useNavigate(); return <Component navigate={navigate} {...props} />; }; return Wrapper; }; const theme = createTheme( { palette: { primary: { main: '#1976d2' }, }, }, esES, ); function GrupoEdit(props) { const [users, setUsers] = useState([{ idct_usuario: '', NombreyApellidos: '' }]); const [grupo, setGrupo] = useState(null); const [refresh, setRefresh] = useState(true); const auth = useAuth(); const [validacionForms, setValidacionForms] = useState(true); const [errorForms, setErrorForms] = useState(''); const [in_grupo, setIn_grupo] = useState(''); const [in_administrador, setIn_administrador] = useState(null); const [errorGrupo, setErrorGrupo] = useState(false); const [helperGrupo, setHelperGrupo] = useState(''); const [errorAdministrador, setErrorAdministrador] = useState(false); const [helperAdministrador, setHelperAdministrador] = useState(''); const { id } = useParams() // Parámetro con el "id" de la Compra // 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+'/logout'); } // const sleep = ms => new Promise(r => setTimeout(r, ms)); // Cargar los datos del Server cuando se carga y recarga la página const fetchGrupo = async () => { if (refresh) { // Para intentar que sólo sea una vez setRefresh(false); fetchUsuarios(); const url = Config.RUTA_API + "/grupo/view/"+id; // console.log("Valor de URL: ",url); const formData = new FormData(); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; try { const response = await axios.post(url,formData,config) var data = response.data; var datos = response.data.data; console.log("Respuesta del fetch(compra): ",data); // console.log("Respuesta del fetch(compra/Datos): ",datos); if (data.error === true ) { console.log("Hay un error: ",data.message); errorNotification(data.error,data.message); } else { setGrupo(datos); setIn_grupo(datos.Nombre); setIn_administrador(datos.Administrador); } } catch(error) { console.log("Error get View Grupo", error); errorNotification('000',error); } } }; // Cargar los datos del Server cuando se carga y recarga la página function fetchUsuarios() { const url = Config.RUTA_API + "/usersGroupList"; const formData = new FormData(); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; axios.post(url,formData,config) .then((response) => { var data = response.data; if (data.error === true ) { console.log("Hay un error: ",data.message); errorNotification(data.error,data.message); } else { console.log("Usuarios del grupo: ",data) setUsers(data); } }) .catch((error) => { console.log("Error get Usuarios", error); errorNotification('000',error); }) }; // Cargar los datos del Server cuando se carga la página useEffect(() => { console.log("Ejecutándose 'useEffect"); fetchGrupo(); }, [refresh]); // Envío del Formulario const saveForms = (e) => { e.preventDefault(); setValidacionForms(true); // Quitamos errores del formulario setErrorForms(''); var existeError = false; var Error = ''; if (in_grupo === '' || errorGrupo ) { setHelperGrupo('Hay que facilitar un Grupo Valido'); setErrorGrupo(true); existeError = true; Error += ' (Grupo) '; } if (in_administrador === '') { setHelperAdministrador('Hay indicar un Administrador'); setErrorAdministrador(true); existeError = true; Error += ' (Administrador) '; } if (!(existeError)) { // se han introducido los valores de los campos // console.log("accediendo al Server"); // Acceso al server y status de la operación const url = Config.RUTA_API + "/grupo/update/"+id; const formData = new FormData(); formData.append('Nombre', in_grupo); formData.append('Administrador', in_administrador); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; axios.post(url, formData, config) .then((response) => { const res = response.data; // console.log("Respuesta del fetch: ",res); if (res.error === false) { setValidacionForms(true); // Quitamos errores del formulario setErrorForms(''); postSave(); // Alert y navegar a la aplicación } else { setValidacionForms(false); setErrorForms(res.message); } }) .catch((error) => { console.error("Error send Register: ", error); setValidacionForms(false); setErrorForms("Error send Register: ", error); errorNotification('000', error) }); } else { setValidacionForms(false); // Ponemos los errores del formulario Error = 'Falta la entrada de los datos o la información de estos campos está mal: '+ Error; setErrorForms(Error); } }; const postSave = (e) => { const MySwal = withReactContent(Swal); MySwal.fire({ icon: 'info', title: 'Grupo actualizado!!!!', text: '', timer: 1000, timerProgressBar: true, toast: true, position: "center", footer: '' }) props.navigate(Config.URL_APP+'/grupoList'); } const handleValidationAdministrador = (e) => { const { name, value } = e.target; setIn_administrador(value); }; const handleValidationGrupo = (e) => { const { name, value } = e.target; setIn_grupo(value); // var regxp = /^([a-zA-Z0-9_-áéíóúñÑ]){3,60}$/; const reg = new RegExp("^([a-zA-Z0-9_áéíóúüñÑ -]){3,50}$"); if (! reg.test(e.target.value)) { setHelperGrupo('Entrada inválida para "Nombre de Grupo"'); } else { setHelperGrupo(''); } setErrorGrupo(!reg.test(e.target.value)); }; return ( <> <Nav /> <div className='titlePage'> <h3 className="head_title">Editar Grupo</h3> </div> <ThemeProvider theme={theme}> <div className='panel-compraAdd'> <div className='box-compraAdd'> <Box component="form" noValidate autoComplete="off" > <Grid container spacing={2}> <Grid xs={12}> <TextField error={errorGrupo} required={true} id="grupo" name="grupo" label="Grupo" variant="standard" type= 'text' className= 'CustomField' helperText={helperGrupo} value={in_grupo || ''} onChange={(e) => handleValidationGrupo(e)} /> </Grid> <Grid xs={12}> <InputLabel id="select-label">Administrador</InputLabel> <Select error={errorAdministrador} id="administrador" name="administrador" label="Administrador" required={true} variant="standard" className= 'CustomField' value={in_administrador || ''} onChange={handleValidationAdministrador} > {users.map((row, index) => ( <MenuItem key={row.idct_usuario} value={row.idct_usuario}> {row.NombreyApellidos} </MenuItem> ))} </Select> {errorAdministrador && <FormHelperText>{helperAdministrador}</FormHelperText>} </Grid> </Grid> <Stack spacing={2} direction="row" sx={{ width: '100%', padding: '15px 10px 15px 10px'}} > <Button variant="contained" type='submit' onClick={(e) => saveForms(e)}>Guardar</Button> <Button variant="outlined" startIcon={<ReplySharpIcon />} onClick={(e) =>props.navigate(Config.URL_APP+'/grupoList')}>Volver</Button> </Stack> <Stack spacing={2} direction="row" sx={{ width: '100%', padding: '15px 10px 15px 10px'}} > </Stack> { <Stack sx={{ width: '100%' }} spacing={2}> { validacionForms? '': <Alert severity="error">{errorForms}</Alert>} </Stack> } </Box> </div> </div> </ThemeProvider> </> ); } // IMPORTANT ---------------------------------------- export default withRouter(GrupoEdit);
Como los Grupos no disponen de una página de View, muestro la de «Producto». La característica más relevante es que muestra la imagen del producto.
import * as React from 'react'; import {useEffect, useState} from 'react'; import './style.css'; // import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Grid2'; import EditIcon from '@mui/icons-material/Edit'; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; // import Rating from '@mui/material/Rating'; // import Typography from '@mui/material/Typography'; // import StarIcon from '@mui/icons-material/Star'; import { Select, MenuItem, InputLabel } from '@mui/material'; // import Alert from '@mui/material/Alert'; import Swal from 'sweetalert2' import withReactContent from 'sweetalert2-react-content' import axios from 'axios'; import { useAuth } from "../../hooks/AuthProvider"; import { useNavigate } from 'react-router-dom'; import { useParams } from "react-router-dom"; import Nav from '../nav'; import Config from '../config'; const withRouter = (Component) => { const Wrapper = (props) => { const navigate = useNavigate(); return <Component navigate={navigate} {...props} />; }; return Wrapper; }; function ProductoView(props) { const [unidad, setUnidad] = useState([ { "idct_unidad": 0, "Titulo": "" } ]); const [producto, setProducto] = useState( { } ); const [refresh, setRefresh] = useState(true); const auth = useAuth(); const [in_producto, setIn_producto] = useState(''); const [in_cantidad, setIn_cantidad] = useState(''); const [in_unidad, setIn_unidad] = useState(''); const { id } = useParams() // Parámetro con el "id" de la Compra // var formWitdh = window.innerWidth; // console.log("Ancho de pantalla: ",formWitdh); // const auth = useAuth(); // console.log("Función AUTH: ",auth); // 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+'/logout'); } function renderFoto() { if (producto.foto !== null ) { var imagen= Config.URL_FILES+producto.foto; // console.log("URL de la imagen: ", imagen); return (<div> <img className="imageProducto" src={imagen} alt="imagen de Producto" /> </div>) } } // Cargar los datos del Server cuando se carga y recarga la página const fetchProducto = async () => { if (refresh) { // Para intentar que sólo sea una vez setRefresh(false); // fetchProductos(); fetchUnidades(); const url = Config.RUTA_API + "/producto/view/"+id; // console.log("Valor de URL: ",url); const formData = new FormData(); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; try { const response = await axios.post(url,formData,config) var data = response.data; var datos = response.data.data; // console.log("Respuesta del fetch(compra): ",data); // console.log("Respuesta del fetch(compra/Datos): ",datos); setProducto(datos); // console.log("Valor de 'compra':",compras); setIn_producto(datos.Nombre); setIn_cantidad(datos.Cantidad); setIn_unidad(datos.ct_unidad_idct_unidad); // console.log("valor de 'in_producto':",in_producto); // console.log("Valor de Data:",datos.Nombre); if (data.error === true ) { console.log("Hay un error: ",data.message); errorNotification(data.error,data.message); } } catch(error) { console.log("Error get Producto", error); errorNotification('000',error); } } }; // Cargar los datos del Server cuando se carga y recarga la página function fetchUnidades() { const url = Config.RUTA_API + "/listadoUnidades"; const formData = new FormData(); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "token-user": auth.token }, }; axios.post(url,formData,config) .then((response) => { var data = response.data; // console.log("Respuesta del fetch(Unidades): ",data); setUnidad(data); if (data.error === true ) { console.log("Hay un error: ",data.message); errorNotification(data.error,data.message); } else { // console.log("Registros de respuesta: ", data.length); } }) .catch((error) => { console.log("Error get Listados Producto", error); errorNotification('000',error); }) }; // Cargar los datos del Server cuando se carga la página useEffect(() => { console.log("Ejecutándose 'useEffect"); fetchProducto(); }, [refresh]); return ( <> <Nav /> <div className='titlePage'> <h3 className="head_title">Visualizar Producto</h3> </div> <div className='panel-compraView'> <div className='box-compraView'> <Box component="form" noValidate autoComplete="off" > <Grid container spacing={2}> <Grid xs={12}> <TextField required={true} id="producto" name="producto" label="Producto" variant="standard" type= 'text' className= 'CustomField' value={in_producto || ''} slotProps={{ input: { readOnly: true, }, }} /> </Grid> <Grid xs={12} > <TextField required={true} id="cantidad" name="cantidad" label="Cantidad" variant="standard" type= 'text' className= 'CustomField' value={in_cantidad || ''} slotProps={{ input: { readOnly: true, }, }} /> </Grid> <Grid xs={12}> <InputLabel id="select-label">Unidad/Medida *</InputLabel> <Select id="unidad" name="unidad" label="Unidad" required={true} variant="standard" className= 'CustomField' value={in_unidad || ''} slotProps={{ input: { readOnly: true, }, }} > {unidad.map((row, index) => ( <MenuItem key={row.idct_unidad} value={row.idct_unidad}> {row.Titulo} </MenuItem> ))} </Select> </Grid> <Grid xs={12}> <InputLabel id="select-label">Foto</InputLabel> {renderFoto()} </Grid> </Grid> <Stack spacing={2} direction="row" sx={{ width: '100%', padding: '15px 10px 15px 10px'}} > <Button variant="contained" onClick={(e) =>props.navigate(Config.URL_APP+'/productoList')}>Volver</Button> <Button variant="outlined" startIcon={<EditIcon />} onClick={(e) =>props.navigate(Config.URL_APP+'/productoEdit/'+id)}>Editar</Button> </Stack> </Box> </div> </div> </> ); } // IMPORTANT ---------------------------------------- export default withRouter(ProductoView);
Os facilito los fuentes de ambos proyectos.
11/12/2024:
Se ha cambiado en PHP, la distribución de las funciones. Todas las que acceden a Base de Datos, están en el fichero «DbCompra.php».
Se ha añadido en JavaScript, la consulta del usuario conectado y la función de «Logout», porque como estaba, en algunos momentos daba un error porque no le daba tiempo a «repintar» una pantalla.
Adjuntos
Archivo | Tamaño de archivo | Descargas |
---|---|---|
compra-react (React Source) - Actualización: 11/12/2024 | 335 KB | 67 |
PHPRunner 10.91 +Backup Dase Datos+ Imágenes - Actualización: 11/12/2024 | 3 MB | 57 |