diff --git a/dev-dist/sw.js b/dev-dist/sw.js index d7855ba..17dfc54 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -79,7 +79,7 @@ define(['./workbox-fde070c5'], (function (workbox) { 'use strict'; */ workbox.precacheAndRoute([{ "url": "/offline.html", - "revision": "0.4c31ogilivg" + "revision": "0.tp82avot2f" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/offline.html"), { diff --git a/package-lock.json b/package-lock.json index 6fe6cc1..498455a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@stripe/stripe-js": "^4.1.0", "axios": "^1.6.7", "bcryptjs": "^2.4.3", - "chart.js": "^4.4.2", + "chart.js": "^4.4.6", "date-fns": "^2.30.0", "formik": "^2.4.5", "jest-environment-jsdom": "^29.7.0", @@ -8019,9 +8019,10 @@ } }, "node_modules/chart.js": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", - "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -18304,6 +18305,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "license": "MIT", "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" @@ -30361,9 +30363,9 @@ "dev": true }, "chart.js": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", - "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", "requires": { "@kurkle/color": "^0.3.0" } diff --git a/package.json b/package.json index a4a6e6d..2c3b2a8 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@stripe/stripe-js": "^4.1.0", "axios": "^1.6.7", "bcryptjs": "^2.4.3", - "chart.js": "^4.4.2", + "chart.js": "^4.4.6", "date-fns": "^2.30.0", "formik": "^2.4.5", "jest-environment-jsdom": "^29.7.0", diff --git a/src/App.jsx b/src/App.jsx index a0ac853..f3676f7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -64,6 +64,11 @@ import Pedidos from './views/Perfil/Pedidos'; import PaginaSuccess from './views/Lente/SuccessPage'//cuando se realiza el pago de mercadopago import Stripe from './views/Metodopago/stripe' +import Noencontrados from "./views/bus/noencontrados"; +import Reporte from "./views/Admin/reportes"; + + + @@ -153,6 +158,9 @@ function App() { } /> } /> } /> + } /> + + } /> } /> @@ -168,6 +176,7 @@ function App() { } /> } /> } /> + } /> } /> { if (event.key === "Enter") { @@ -9,30 +10,81 @@ function Busqueda({ busqueda, setBusqueda, handleSearch }) { } }; + useEffect(() => { + // Ejecuta la búsqueda cada vez que el estado 'busqueda' cambie, pero no borra el texto + if (busqueda && busqueda.trim() !== "") { + handleSearch(); + } + }, [busqueda, handleSearch]); + + const startListening = () => { + const SpeechRecognition = + window.SpeechRecognition || window.webkitSpeechRecognition; + + if (!SpeechRecognition) { + console.error("El reconocimiento de voz no está soportado en este navegador."); + return; + } + + const recognition = new SpeechRecognition(); + recognition.lang = "es-ES"; + recognition.start(); + + recognition.onstart = () => { + setIsListening(true); + }; + + recognition.onresult = (event) => { + const transcript = event.results[0][0].transcript; + // Solo actualiza el estado si el texto reconocido no es vacío + if (transcript.trim()) { + setBusqueda(transcript); + } + }; + + recognition.onerror = () => { + setIsListening(false); + }; + + recognition.onend = () => { + setIsListening(false); + }; + }; + return ( -
- {/* Barra de búsqueda que se ajusta a todos los tamaños de pantalla */} -
-
- - setBusqueda(e.target.value)} - onKeyDown={handleKeyDown} - className="flex-grow outline-none bg-transparent placeholder-gray-500 pl-1" +
+
+ + + setBusqueda(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-grow outline-none bg-transparent placeholder-gray-500 text-sm md:text-base px-2 " + /> + + {/* Botón de micrófono para búsqueda por voz */} +
-
- -
+ + + {/* Botón de búsqueda tradicional */} +
); diff --git a/src/components/Navegacion/barra.jsx b/src/components/Navegacion/barra.jsx index dbd25f0..ad59518 100644 --- a/src/components/Navegacion/barra.jsx +++ b/src/components/Navegacion/barra.jsx @@ -107,7 +107,7 @@ function Barra() { try { const response = await fetch( - `http://localhost:3000/productos/Buscar_productos?busqueda=${busqueda}` + `https://backopt-production.up.railway.app/productos/Buscar_productos?busqueda=${busqueda}` ); const data = await response.json(); if (data.length > 0) { @@ -115,6 +115,7 @@ function Barra() { navigate("/productos-encontrados", { state: { productos: data } }); } else { console.log("No se encontraron productos."); + navigate("/productos-Noencontrados") // Podemos mostrar un mensaje al usuario indicando que no se encontraron productos setProductosEncontrados([]); } @@ -139,7 +140,7 @@ function Barra() { icono -
+
Reportes diff --git a/src/img/noEncontrados.png b/src/img/noEncontrados.png new file mode 100644 index 0000000..5d08b6d Binary files /dev/null and b/src/img/noEncontrados.png differ diff --git a/src/img/nocita.jpg b/src/img/nocita.jpg new file mode 100644 index 0000000..7c64eac Binary files /dev/null and b/src/img/nocita.jpg differ diff --git a/src/views/Admin/reportes.jsx b/src/views/Admin/reportes.jsx new file mode 100644 index 0000000..ceb2faf --- /dev/null +++ b/src/views/Admin/reportes.jsx @@ -0,0 +1,136 @@ +import React, { useState, useEffect } from "react"; +import { Bar } from "react-chartjs-2"; // Importamos el componente de gráfico de barras +import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'; + +// Registramos los componentes necesarios de Chart.js para el gráfico de barras +ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale); + +function ResultadosEncuestas() { + const [resultados, setResultados] = useState([]); + const [cargando, setCargando] = useState(true); + + useEffect(() => { + // Obtener los resultados de las encuestas completadas + const obtenerResultados = async () => { + try { + const response = await fetch("https://backopt-production.up.railway.app/resultados", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + if (response.ok) { + const data = await response.json(); + + // Verificar que data contiene los resultados + if (data && data.data && Array.isArray(data.data)) { + setResultados(data.data); // Almacenar los resultados de las encuestas + } else { + console.error("La respuesta no contiene un arreglo de resultados"); + setResultados([]); // Manejo de error si la respuesta no es válida + } + } else { + alert("Error al obtener los resultados"); + } + } catch (error) { + console.error("Error al obtener los resultados:", error); + alert("Hubo un error al obtener los resultados"); + } finally { + setCargando(false); // Finalizar el estado de carga + } + }; + + obtenerResultados(); + }, []); + + if (cargando) { + return
Cargando resultados...
; + } + + // Procesar las respuestas para construir los datos de la gráfica + const procesarDatos = () => { + const respuestasPorCalificacion = { "1": 0, "2": 0, "3": 0, "4": 0, "5": 0 }; + + // Contamos las respuestas por calificación + resultados.forEach((encuesta) => { + const { respuestas } = encuesta; + + // Contamos las respuestas de cada calificación + Object.entries(respuestas).forEach(([calificacion, cantidad]) => { + respuestasPorCalificacion[calificacion] += cantidad; // Incrementamos el contador + }); + }); + + // Asignamos colores diferentes a cada calificación + const colores = ["#FF6F61", "#FF9F40", "#FFCD44", "#4BC0C0", "#36A2EB"]; // Colores para las barras + + // Creamos los datos para el gráfico de barras + return { + labels: ["1", "2", "3", "4", "5"], // Las respuestas posibles + datasets: [ + { + label: "Distribución de Respuestas", + data: Object.values(respuestasPorCalificacion), // Datos de cada calificación + backgroundColor: colores, // Asignamos los colores diferentes a cada barra + borderColor: '#D66F58', // Color de los bordes de las barras + borderWidth: 1, + }, + ], + }; + }; + + // Contamos el total de encuestas + const totalEncuestas = resultados.reduce((total, encuesta) => total + Object.values(encuesta.respuestas).reduce((sum, cantidad) => sum + cantidad, 0), 0); + + const data = procesarDatos(); + + return ( +
+

Resultados de Encuestas

+ {resultados.length === 0 ? ( +

No se han completado encuestas aún.

+ ) : ( +
+ {/* Mostrar el total de encuestas */} +

Total de Encuestas Completadas: {totalEncuestas}

+ + +
+ )} +
+ ); +} + +export default ResultadosEncuestas; diff --git a/src/views/Citas/AgendarCita.jsx b/src/views/Citas/AgendarCita.jsx index 1a15894..c77f500 100644 --- a/src/views/Citas/AgendarCita.jsx +++ b/src/views/Citas/AgendarCita.jsx @@ -18,6 +18,7 @@ import axios from "axios"; import useFetchHorarios from "./horariosDisp"; import Barra from "../../components/Navegacion/barra"; import Fot from "../../components/Footer"; +import Encuesta from "../feedback/encuesta" // Función para decodificar JWT function parseJwt(token) { @@ -47,6 +48,8 @@ const CrearCita = () => { const { horarios, loading, error } = useFetchHorarios( selectFecha ? format(selectFecha, "yyyy-MM-dd") : null ); + + const [showFeedback, setShowFeedback] = useState(false); // Nuevo estado const navigate = useNavigate(); const [descripcionTLength, setDescripcionTLength] = useState(0); @@ -111,7 +114,7 @@ const CrearCita = () => { try { // Crear la cita const citaResponse = await axios.post( - "http://localhost:3000/cita", + "https://backopt-production.up.railway.app/cita", { Fecha: format(selectFecha, "yyyy-MM-dd"), Hora: selectHora, @@ -127,7 +130,7 @@ const CrearCita = () => { // Reservar el horario try { const reservaResponse = await axios.post( - "http://localhost:3000/horarios/reservar", + "https://backopt-production.up.railway.app/horarios/reservar", { Fecha: format(selectFecha, "yyyy-MM-dd"), Hora: selectHora, @@ -136,9 +139,7 @@ const CrearCita = () => { if (reservaResponse.status === 200) { toast.success("Cita agendada y horario reservado exitosamente"); - setTimeout(() => { - navigate("/inicio"); // Redirige al usuario a la página de inicio de sesión - }, 5000); + setShowFeedback(true); //Muestra la encuenta } else { toast.error( "Cita agendada, pero hubo un problema al reservar el horario" @@ -170,6 +171,8 @@ const CrearCita = () => { } }; + + const renderHeader = () => { return (
@@ -240,10 +243,23 @@ const CrearCita = () => { return
{rows}
; }; + const handleFeedbackComplete = () =>{ + setShowFeedback(false); + navigate("/inicio"); + } + return (
+ {showFeedback ? ( + + ):( +
+ {/* Aquí va el resto de la lógica para mostrar el formulario */} +
+ )} +

Agendar cita

diff --git a/src/views/Citas/horariosDisp.jsx b/src/views/Citas/horariosDisp.jsx index 8851de9..8a0fd8d 100644 --- a/src/views/Citas/horariosDisp.jsx +++ b/src/views/Citas/horariosDisp.jsx @@ -17,7 +17,7 @@ const useFetchHorarios = (fecha) => { setLoading(true); try { const response = await axios.get( - `http://localhost:3000/horarios/HrPorFecha?fecha=${fecha}` + `https://backopt-production.up.railway.app/horarios/HrPorFecha?fecha=${fecha}` ); setHorarios(response.data); diff --git a/src/views/Perfil/Citas/cancelarCita.jsx b/src/views/Perfil/Citas/cancelarCita.jsx index 0255a9b..db7a9a0 100644 --- a/src/views/Perfil/Citas/cancelarCita.jsx +++ b/src/views/Perfil/Citas/cancelarCita.jsx @@ -21,7 +21,7 @@ const CancelarCita = ({ citaId, onCancelSuccess }) => { setLoading(true); // Hacer la solicitud de cancelación al backend const response = await axios.put( - `http://localhost:3000/cita/cancelar/${citaId}` + `https://backopt-production.up.railway.app/cita/cancelar/${citaId}` ); if (response.status === 200) { diff --git a/src/views/Perfil/Citas/modificarCita.jsx b/src/views/Perfil/Citas/modificarCita.jsx index d7a333f..6e6d60a 100644 --- a/src/views/Perfil/Citas/modificarCita.jsx +++ b/src/views/Perfil/Citas/modificarCita.jsx @@ -66,7 +66,7 @@ const ModificarCita = () => { const fetchCita = async () => { try { const response = await axios.get( - `http://localhost:3000/cita/${id}` + `https://backopt-production.up.railway.app/cita/${id}` ); const cita = response.data; @@ -119,7 +119,7 @@ const ModificarCita = () => { try { // Actualizar la cita const citaResponse = await axios.put( - `http://localhost:3000/cita/${id}`, + `https://backopt-production.up.railway.app/cita/${id}`, { Fecha: format(selectFecha, "yyyy-MM-dd"), Hora: selectHora, @@ -138,7 +138,7 @@ const ModificarCita = () => { // Reservar el horario después de actualizar la cita try { const reservaResponse = await axios.post( - "http://localhost:3000/horarios/reservar", + "https://backopt-production.up.railway.app/horarios/reservar", { Fecha: format(selectFecha, "yyyy-MM-dd"), Hora: selectHora, diff --git a/src/views/Perfil/Citas/verCitas.jsx b/src/views/Perfil/Citas/verCitas.jsx index 96348e8..0c55d32 100644 --- a/src/views/Perfil/Citas/verCitas.jsx +++ b/src/views/Perfil/Citas/verCitas.jsx @@ -4,8 +4,8 @@ import Barra from "../../../components/Navegacion/barra"; import Fot from "../../../components/Footer"; import { Link } from "react-router-dom"; import CancelarCita from "./cancelarCita"; +import Nocita from "../../../img/nocita.jpg"; -// Función para decodificar JWT function parseJwt(token) { var base64Url = token.split(".")[1]; var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); @@ -29,15 +29,19 @@ const VerCitas = () => { const [usuarioLogueado, setUsuarioLogueado] = useState(false); const [idUsuario, setIdUsuario] = useState(""); - // Función para obtener citas + const estadoCitaMap = { + 1: "Programada", + 2: "Cancelada", + 3: "Completada", + }; + const fetchCitas = async () => { if (idUsuario) { try { setLoading(true); const response = await axios.get( - `http://localhost:3000/cita/usuario/${idUsuario}` + `https://backopt-production.up.railway.app/cita/usuario/${idUsuario}` ); - // Ordenar citas por fecha en orden descendente const citasOrdenadas = response.data.sort( (a, b) => new Date(b.Fecha) - new Date(a.Fecha) ); @@ -51,7 +55,6 @@ const VerCitas = () => { }; useEffect(() => { - // Verificar el tipo de usuario al cargar la página const token = localStorage.getItem("token"); if (token) { const decodedToken = parseJwt(token); @@ -64,117 +67,97 @@ const VerCitas = () => { fetchCitas(); }, [idUsuario]); - // Función para manejar la eliminación de la cita const handleCancelSuccess = async (citaId) => { try { - // Llamar al endpoint de cancelación de cita await axios.put( - `http://localhost:3000/cita/cancelar/${citaId}` + `https://backopt-production.up.railway.app/cita/cancelar/${citaId}` ); - - // Volver a obtener las citas actualizadas fetchCitas(); } catch (error) { console.error("Error al cancelar la cita:", error); } }; - if (loading) { + /* if (loading) { return
Cargando...
; - } + } */ - if (error) { + /* if (error) { return
Error: {error}
; - } + } */ return ( -
+
-

Mis Citas

+

Mis Citas

{citas.length > 0 ? ( -
- - - - - - - - - - - - - - - - {citas.map((cita) => ( - - - - - - - - - - - - ))} - -
- ID - - Fecha - - Hora - - Tipo de Cita - - Costo - - Estado - - Observaciones - - Descripción - - Acciones -
- {cita.IdCita} - - {cita.Fecha} - - {cita.Hora} - - {cita.IdTipoCita ? cita.IdTipoCita : "No disponible"} - - {cita.Costo} - - {cita.IdEstadoCita ? cita.IdEstadoCita : "No disponible"} - - {cita.Observaciones} - - {cita.DescripcionT} - -
- - Modificar - - handleCancelSuccess(cita.IdCita)} - /> -
-
+
+ {citas.map((cita) => ( +
+

+ ID: {cita.IdCita} +

+

+ Fecha: {cita.Fecha} +

+

+ Hora: {cita.Hora} +

+

+ Tipo de Cita:{" "} + {cita.IdTipoCita || "No disponible"} +

+

+ Costo: {cita.Costo} +

+

+ Estado:{" "} + + {estadoCitaMap[cita.IdEstadoCita] || "Cita cancelada"} + +

+

+ Observaciones:{" "} + {cita.Observaciones} +

+

+ Descripción:{" "} + {cita.DescripcionT} +

+
+ + Modificar + + handleCancelSuccess(cita.IdCita)} + /> +
+
+ ))}
) : ( -
- No se encontraron citas para este usuario. +
+ No has creado ninguna cita. + No se encontraron citas
)}
diff --git a/src/views/bus/noencontrados.jsx b/src/views/bus/noencontrados.jsx new file mode 100644 index 0000000..2da3337 --- /dev/null +++ b/src/views/bus/noencontrados.jsx @@ -0,0 +1,38 @@ +import { useLocation } from "react-router-dom"; +import Fot from "../../components/Footer"; +import { Link } from "react-router-dom"; +import Barra from "../../components/Navegacion/barra"; +import Noencontrado from "../../img/noEncontrados.png"; + +function ProductosEncontrados() { + return ( +
+ + +
+ Imagen de productos no encontrados +

+ No hemos encontrado productos que coincidan con tu búsqueda. +

+

+ Puede que el producto esté agotado o que no esté disponible en este momento. +

+ + + Volver al Inicio + +
+ + {/* Footer */} + +
+ ); +} + +export default ProductosEncontrados; diff --git a/src/views/feedback/encuesta.jsx b/src/views/feedback/encuesta.jsx new file mode 100644 index 0000000..9b712b6 --- /dev/null +++ b/src/views/feedback/encuesta.jsx @@ -0,0 +1,144 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; + +function EncuestaCitas() { + const [respuestas, setRespuestas] = useState({}); + const [enviada, setEnviada] = useState(false); + const [cargando, setCargando] = useState(true); // Estado para indicar que se está verificando + const [encuestaCompletada, setEncuestaCompletada] = useState(false); // Estado para verificar si ya se completó la encuesta + const navigate = useNavigate(); + + const preguntas = [ + "¿Qué tan difícil te pareció el proceso para agendar tu cita?", + "¿Cómo calificarías la facilidad de uso del sistema para agendar tu cita?", + "¿En qué medida encontraste complicado el proceso de agendar tu cita?", + "¿Consideras que el proceso de agendar tu cita fue intuitivo?", + "¿Cuánto tiempo te llevó completar el proceso de agendar tu cita?", + ]; + + // Verificar si el usuario ya completó la encuesta al montar el componente + useEffect(() => { + const verificarEncuesta = async () => { + try { + const response = await fetch("https://backopt-production.up.railway.app/Encuesta/completada?idUsuario=1", { // Ajusta el parámetro idUsuario + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + if (response.ok) { + const data = await response.json(); + if (data.completada) { + setEncuestaCompletada(true); // Establecer que la encuesta ya fue completada + } + } + } catch (error) { + console.error("Error al verificar encuesta:", error); + } finally { + setCargando(false); // Finaliza el estado de carga + } + }; + + verificarEncuesta(); + }, [navigate]); + + const handleRespuestaChange = (index, value) => { + setRespuestas((prev) => ({ + ...prev, + [index]: value, + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (Object.keys(respuestas).length !== preguntas.length) { + alert("Por favor responde todas las preguntas antes de enviar."); + return; + } + + try { + const response = await fetch("https://backopt-production.up.railway.app/Encuesta", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ idUsuario: 1, respuestas }), // Ajusta idUsuario + }); + + if (response.ok) { + setEnviada(true); + + // Redirigir al inicio después de 3 segundos + setTimeout(() => { + navigate("/ver-cita"); + }, 3000); // Retrasar la redirección 3 segundos + } else { + alert("Hubo un error al enviar tu feedback. Intenta nuevamente."); + } + } catch (error) { + console.error("Error:", error); + alert("Hubo un error al enviar tu feedback. Intenta nuevamente."); + } + }; + + if (cargando) { + return ( +
+
Cargando...
+
+ ); + } + + // Si la encuesta ya fue completada, redirige a la página de la cita sin mostrar la encuesta + if (encuestaCompletada) { + setTimeout(() => { + navigate("/ver-cita"); // Redirigir a la página de cita después de un breve retraso + }, 2000); // 2 segundos de espera antes de redirigir + return null; // No renderizar nada, solo redirigir + } + + return ( +
+
+ {enviada ? ( +
+

¡Gracias por tu feedback!

+

Hemos recibido tus respuestas. Serás redirigido al inicio.

+
+ ) : ( + <> +

Encuesta

+
+ {preguntas.map((pregunta, index) => ( +
+ +
+ {[1, 2, 3, 4, 5].map((estrella) => ( + + ))} +
+
+ ))} + +
+ +
+
+ + )} +
+
+ ); +} + +export default EncuestaCitas;