From ed62af159db87d7d9e3ac4abdc28a8adc5b9bf5b Mon Sep 17 00:00:00 2001 From: Renato Date: Thu, 12 Feb 2026 04:30:15 +0100 Subject: [PATCH] Fix PDF permissions, routing, and FlujoCircular exercise - Fix PDF file permissions (403 Forbidden error) - Fix routing bug preventing access to modules 2-4 - Replace FlujoCircular with intuitive quiz version - Cap progress percentage at 100% - Add PDF viewer with modal in Recursos page --- frontend/src/App.tsx | 32 - .../exercises/modulo1/FlujoCircular.tsx | 571 +++++++----------- frontend/src/pages/Recursos.tsx | 100 ++- frontend/src/stores/progressStore.ts | 4 +- 4 files changed, 312 insertions(+), 395 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1733eb4..18d6ab7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -61,38 +61,6 @@ function App() { } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> void; } -type Agente = 'familias' | 'empresas' | 'estado' | 'exterior'; -type TipoFlujo = 'real' | 'monetario'; - -interface Elemento { - id: string; - texto: string; - tipo: TipoFlujo; - origen: Agente; - destino: Agente; +interface Pregunta { + id: number; + pregunta: string; + opciones: string[]; + respuestaCorrecta: number; + explicacion: string; } -interface Nivel { - nombre: string; - descripcion: string; - agentes: Agente[]; - elementos: Elemento[]; -} - -const NIVELES: Nivel[] = [ +const PREGUNTAS: Pregunta[] = [ { - nombre: 'Básico', - descripcion: 'Solo Familias y Empresas', - agentes: ['familias', 'empresas'], - elementos: [ - { id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' }, - { id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, - { id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' }, - { id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' }, - ] + id: 1, + pregunta: "¿Quiénes son los principales agentes económicos en el flujo circular?", + opciones: [ + "Solo el gobierno y las empresas", + "Familias y empresas", + "Bancos y familias", + "Empresas y extranjeros" + ], + respuestaCorrecta: 1, + explicacion: "Las familias (consumidores) y las empresas (productores) son los dos agentes principales en el modelo básico del flujo circular." }, { - nombre: 'Intermedio', - descripcion: 'Incluye al Estado', - agentes: ['familias', 'empresas', 'estado'], - elementos: [ - { id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' }, - { id: 'tierra', texto: '🌾 Tierra', tipo: 'real', origen: 'familias', destino: 'empresas' }, - { id: 'capital', texto: '💰 Capital', tipo: 'real', origen: 'familias', destino: 'empresas' }, - { id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, - { id: 'renta', texto: '🏠 Renta', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, - { id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, - { id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' }, - { id: 'servicios', texto: '🔧 Servicios', tipo: 'real', origen: 'empresas', destino: 'familias' }, - { id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' }, - { id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', origen: 'familias', destino: 'estado' }, - { id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', origen: 'estado', destino: 'familias' }, - { id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', origen: 'estado', destino: 'empresas' }, - ] + id: 2, + pregunta: "Las familias ofrecen a las empresas:", + opciones: [ + "Productos terminados", + "Trabajo, tierra y capital (factores de producción)", + "Dinero para invertir", + "Servicios bancarios" + ], + respuestaCorrecta: 1, + explicacion: "Las familias son propietarias de los factores de producción (trabajo, tierra, capital) y los ofrecen a las empresas a cambio de ingresos (salarios, rentas, intereses)." }, { - nombre: 'Avanzado', - descripcion: 'Todos los agentes incluyendo Sector Externo', - agentes: ['familias', 'empresas', 'estado', 'exterior'], - elementos: [ - { id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' }, - { id: 'tierra', texto: '🌾 Tierra', tipo: 'real', origen: 'familias', destino: 'empresas' }, - { id: 'capital', texto: '💰 Capital', tipo: 'real', origen: 'familias', destino: 'empresas' }, - { id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, - { id: 'renta', texto: '🏠 Renta', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, - { id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, - { id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' }, - { id: 'servicios', texto: '🔧 Servicios', tipo: 'real', origen: 'empresas', destino: 'familias' }, - { id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' }, - { id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', origen: 'familias', destino: 'estado' }, - { id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', origen: 'estado', destino: 'familias' }, - { id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', origen: 'estado', destino: 'empresas' }, - { id: 'exportaciones', texto: '📤 Exportaciones', tipo: 'real', origen: 'empresas', destino: 'exterior' }, - { id: 'importaciones', texto: '📥 Importaciones', tipo: 'real', origen: 'exterior', destino: 'empresas' }, - { id: 'divisas-ent', texto: '💱 Divisas (Ent.)', tipo: 'monetario', origen: 'exterior', destino: 'empresas' }, - { id: 'divisas-sal', texto: '💱 Divisas (Sal.)', tipo: 'monetario', origen: 'empresas', destino: 'exterior' }, - ] + id: 3, + pregunta: "Las empresas le venden a las familias:", + opciones: [ + "Acciones de la empresa", + "Bienes y servicios", + "Materias primas", + "Deudas" + ], + respuestaCorrecta: 1, + explicacion: "Las empresas producen bienes y servicios que venden a las familias en el mercado de bienes." + }, + { + id: 4, + pregunta: "En el flujo MONETARIO (de dinero), el dinero va:", + opciones: [ + "De empresas a familias (salarios) y de familias a empresas (gastos)", + "Solo de familias a empresas", + "Solo de empresas a familias", + "En círculo en una sola dirección" + ], + respuestaCorrecta: 0, + explicacion: "El flujo monetario es bidireccional: empresas pagan salarios/rentas a familias, y familias gastan dinero comprando bienes a las empresas." + }, + { + id: 5, + pregunta: "En el flujo REAL (de bienes/factores), ¿qué ofrecen las familias?", + opciones: [ + "Productos terminados", + "Factores de producción (trabajo, tierra, capital)", + "Dinero", + "Servicios financieros" + ], + respuestaCorrecta: 1, + explicacion: "En el flujo real, las familias ofrecen sus factores de producción (trabajo, tierra, capital) a las empresas." } ]; -const AGENTE_CONFIG: Record = { - familias: { icon: , label: 'Familias', color: 'bg-green-100 text-green-700 border-green-300', position: 'left-4 top-1/2 -translate-y-1/2' }, - empresas: { icon: , label: 'Empresas', color: 'bg-blue-100 text-blue-700 border-blue-300', position: 'right-4 top-1/2 -translate-y-1/2' }, - estado: { icon: , label: 'Estado', color: 'bg-orange-100 text-orange-700 border-orange-300', position: 'left-1/2 -translate-x-1/2 top-4' }, - exterior: { icon: , label: 'Sector Externo', color: 'bg-purple-100 text-purple-700 border-purple-300', position: 'left-1/2 -translate-x-1/2 bottom-4' }, -}; - export function FlujoCircular({ ejercicioId: _ejercicioId, onComplete }: FlujoCircularProps) { - const [nivelActual, setNivelActual] = useState(0); - const [elementosColocados, setElementosColocados] = useState>({}); - const [elementoSeleccionado, setElementoSeleccionado] = useState(null); + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); const [puntuacion, setPuntuacion] = useState(0); const [completado, setCompletado] = useState(false); - const [aciertos, setAciertos] = useState(0); - const [errores, setErrores] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); - const nivel = NIVELES[nivelActual]; + const pregunta = PREGUNTAS[preguntaActual]; - useEffect(() => { - const inicial: Record = {}; - nivel.elementos.forEach(el => { - inicial[el.id] = null; - }); - setElementosColocados(inicial); - }, [nivel]); - - const handleElementoClick = (elementoId: string) => { - if (elementosColocados[elementoId]) return; - setElementoSeleccionado(elementoId === elementoSeleccionado ? null : elementoId); + const handleSeleccionar = (index: number) => { + if (mostrarResultado) return; + setRespuestaSeleccionada(index); }; - const handleConexionClick = (origen: Agente, destino: Agente) => { - if (!elementoSeleccionado) return; - - const elemento = nivel.elementos.find(el => el.id === elementoSeleccionado); - if (!elemento) return; + const handleVerificar = () => { + if (respuestaSeleccionada === null) return; - const esCorrecto = elemento.origen === origen && elemento.destino === destino; - - setElementosColocados(prev => ({ - ...prev, - [elementoSeleccionado]: { origen, destino } - })); + const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta; + setMostrarResultado(true); - if (esCorrecto) { - setAciertos(prev => prev + 1); - setPuntuacion(prev => prev + 10); - } else { - setErrores(prev => prev + 1); - setPuntuacion(prev => Math.max(0, prev - 2)); + if (esCorrecta) { + setPuntuacion(prev => prev + 20); + setRespuestasCorrectas(prev => prev + 1); } - setElementoSeleccionado(null); - }; - - const verificarCompletitud = () => { - const todosColocados = nivel.elementos.every(el => elementosColocados[el.id] !== null); - if (todosColocados) { - const bonus = 50; - setPuntuacion(prev => prev + bonus); - - if (nivelActual < NIVELES.length - 1) { - setNivelActual(prev => prev + 1); - } else { + // Si es la última pregunta + if (preguntaActual === PREGUNTAS.length - 1) { + setTimeout(() => { setCompletado(true); + const puntuacionFinal = puntuacion + (esCorrecta ? 20 : 0); if (onComplete) { - onComplete(puntuacion + bonus); + onComplete(puntuacionFinal); } - } + }, 2000); } }; - useEffect(() => { - const todosColocados = nivel.elementos.every(el => elementosColocados[el.id] !== null); - if (todosColocados && !completado) { - setTimeout(verificarCompletitud, 500); - } - }, [elementosColocados]); - - const handleReiniciarNivel = () => { - const inicial: Record = {}; - nivel.elementos.forEach(el => { - inicial[el.id] = null; - }); - setElementosColocados(inicial); - setElementoSeleccionado(null); - setAciertos(0); - setErrores(0); + const handleSiguiente = () => { + setPreguntaActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarResultado(false); }; - const getConexionesPosibles = (): { origen: Agente; destino: Agente; label: string }[] => { - const conexiones: { origen: Agente; destino: Agente; label: string }[] = []; - - if (nivel.agentes.includes('familias') && nivel.agentes.includes('empresas')) { - conexiones.push({ origen: 'familias', destino: 'empresas', label: 'Familias → Empresas' }); - conexiones.push({ origen: 'empresas', destino: 'familias', label: 'Empresas → Familias' }); - } - - if (nivel.agentes.includes('estado')) { - if (nivel.agentes.includes('familias')) { - conexiones.push({ origen: 'familias', destino: 'estado', label: 'Familias → Estado' }); - conexiones.push({ origen: 'estado', destino: 'familias', label: 'Estado → Familias' }); - } - if (nivel.agentes.includes('empresas')) { - conexiones.push({ origen: 'estado', destino: 'empresas', label: 'Estado → Empresas' }); - } - } - - if (nivel.agentes.includes('exterior') && nivel.agentes.includes('empresas')) { - conexiones.push({ origen: 'empresas', destino: 'exterior', label: 'Empresas → Exterior' }); - conexiones.push({ origen: 'exterior', destino: 'empresas', label: 'Exterior → Empresas' }); - } - - return conexiones; + const handleReiniciar = () => { + setPreguntaActual(0); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + setPuntuacion(0); + setCompletado(false); + setRespuestasCorrectas(0); }; if (completado) { return ( - -
+ +
- + - -

¡Juego Completado!

+ +

+ ¡Ejercicio Completado! +

+

- Has completado todos los niveles del Flujo Circular + Has respondido {respuestasCorrectas} de {PREGUNTAS.length} preguntas correctamente

- -
-

Puntuación Final

-

{puntuacion} puntos

+ +
+

Puntuación

+

{puntuacion}

+

puntos

- -
-
-

{aciertos}

-

Aciertos

-
-
-

{errores}

-

Errores

-
-
- -
@@ -250,178 +169,128 @@ export function FlujoCircular({ ejercicioId: _ejercicioId, onComplete }: FlujoCi } return ( - - - - - } - /> - -
-
- {NIVELES.map((_, idx) => ( -
- {idx < nivelActual ? : idx + 1} -
- ))} -
-
-

Puntuación

-

{puntuacion} pts

-
-
- -
-
-
- {nivel.agentes.map((agente) => ( - - {AGENTE_CONFIG[agente].icon} - {AGENTE_CONFIG[agente].label} - - ))} - - - {getConexionesPosibles().map((conexion, idx) => ( - - - - ))} - - -
-
- {getConexionesPosibles().map((conexion, idx) => { - const elementosEnConexion = nivel.elementos.filter(el => - elementosColocados[el.id]?.origen === conexion.origen && - elementosColocados[el.id]?.destino === conexion.destino - ); - - return ( - - ); - })} -
-
+ +
+ {/* Header */} +
+
+

Flujo Circular de la Renta

+

Pregunta {preguntaActual + 1} de {PREGUNTAS.length}

+
+
+

Puntos

+

{puntuacion}

-
-

Elementos ({nivel.elementos.length})

-

- {elementoSeleccionado - ? 'Selecciona una conexión en el diagrama' - : 'Haz clic en un elemento para colocarlo'} -

- -
- {nivel.elementos.map((elemento) => { - const colocado = elementosColocados[elemento.id]; - const seleccionado = elementoSeleccionado === elemento.id; - const esCorrecto = colocado && colocado.origen === elemento.origen && colocado.destino === elemento.destino; + {/* Barra de progreso */} +
+ +
+ + {/* Pregunta */} +
+

+ {pregunta.pregunta} +

+ +
+ {pregunta.opciones.map((opcion, index) => { + const estaSeleccionada = respuestaSeleccionada === index; + const esCorrecta = index === pregunta.respuestaCorrecta; + const mostrarCorrecta = mostrarResultado && esCorrecta; + const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !esCorrecta; return ( handleElementoClick(elemento.id)} - disabled={!!colocado} - whileHover={!colocado ? { scale: 1.02 } : {}} - whileTap={!colocado ? { scale: 0.98 } : {}} - className={`w-full p-3 rounded-lg border-2 text-left transition-all ${ - colocado - ? esCorrecto - ? 'border-green-300 bg-green-50' - : 'border-red-300 bg-red-50' - : seleccionado - ? 'border-blue-500 bg-blue-50 shadow-md' + key={index} + onClick={() => handleSeleccionar(index)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.01 } : {}} + whileTap={!mostrarResultado ? { scale: 0.99 } : {}} + className={`w-full p-4 rounded-xl border-2 text-left transition-all ${ + mostrarCorrecta + ? 'border-green-500 bg-green-50' + : mostrarIncorrecta + ? 'border-red-500 bg-red-50' + : estaSeleccionada + ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white hover:border-blue-300' }`} > -
-
- {elemento.texto.split(' ')[0]} - {elemento.texto.split(' ').slice(1).join(' ')} -
- {colocado && ( - esCorrecto - ? - : - )} -
-
- +
- {elemento.tipo === 'real' ? 'Real' : 'Monetario'} + {mostrarCorrecta && } + {mostrarIncorrecta && } + {!mostrarResultado && estaSeleccionada && ( +
+ )} +
+ + {opcion} - {colocado && !esCorrecto && ( - - {AGENTE_CONFIG[elemento.origen].label} → {AGENTE_CONFIG[elemento.destino].label} - - )}
); })}
+
-
-

Leyenda:

-
- - - Flujo Real - - - - Flujo Monetario - -
-
+ {/* Explicación */} + {mostrarResultado && ( + +

+ {respuestaSeleccionada === pregunta.respuestaCorrecta + ? '¡Correcto!' + : 'Incorrecto'} +

+

{pregunta.explicacion}

+
+ )} + + {/* Botones */} +
+ {!mostrarResultado ? ( + + ) : preguntaActual < PREGUNTAS.length - 1 ? ( + + ) : null}
diff --git a/frontend/src/pages/Recursos.tsx b/frontend/src/pages/Recursos.tsx index b22090c..4a49746 100644 --- a/frontend/src/pages/Recursos.tsx +++ b/frontend/src/pages/Recursos.tsx @@ -1,6 +1,7 @@ +import { useState } from 'react'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; -import { FileText, Download, BookOpen, ArrowLeft } from 'lucide-react'; +import { FileText, Download, BookOpen, ArrowLeft, X, Eye } from 'lucide-react'; import { Link } from 'react-router-dom'; const recursos = [ @@ -39,6 +40,19 @@ const recursos = [ ]; export function RecursosPage() { + const [pdfSeleccionado, setPdfSeleccionado] = useState(null); + const [pdfTitulo, setPdfTitulo] = useState(''); + + const abrirPdf = (archivo: string, titulo: string) => { + setPdfSeleccionado(archivo); + setPdfTitulo(titulo); + }; + + const cerrarPdf = () => { + setPdfSeleccionado(null); + setPdfTitulo(''); + }; + return (
@@ -98,17 +112,29 @@ export function RecursosPage() { {recurso.descripcion}

- - - + + + + +
@@ -127,6 +153,58 @@ export function RecursosPage() {
+ + {/* Modal para visualizar PDF */} + {pdfSeleccionado && ( +
+
+ {/* Header del modal */} +
+

+ {pdfTitulo} +

+ +
+ + {/* Contenido del PDF */} +
+