|
|
|
|
@@ -1,248 +1,167 @@
|
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
import { Card, CardHeader } from '../../ui/Card';
|
|
|
|
|
import { Card } from '../../ui/Card';
|
|
|
|
|
import { Button } from '../../ui/Button';
|
|
|
|
|
import { CheckCircle, XCircle, Trophy, Users, Building2, Landmark, Globe, RefreshCw } from 'lucide-react';
|
|
|
|
|
import { CheckCircle, XCircle, Trophy, RotateCcw, ArrowRight } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
interface FlujoCircularProps {
|
|
|
|
|
ejercicioId: string;
|
|
|
|
|
onComplete?: (puntuacion: number) => 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<Agente, { icon: React.ReactNode; label: string; color: string; position: string }> = {
|
|
|
|
|
familias: { icon: <Users size={24} />, label: 'Familias', color: 'bg-green-100 text-green-700 border-green-300', position: 'left-4 top-1/2 -translate-y-1/2' },
|
|
|
|
|
empresas: { icon: <Building2 size={24} />, label: 'Empresas', color: 'bg-blue-100 text-blue-700 border-blue-300', position: 'right-4 top-1/2 -translate-y-1/2' },
|
|
|
|
|
estado: { icon: <Landmark size={24} />, label: 'Estado', color: 'bg-orange-100 text-orange-700 border-orange-300', position: 'left-1/2 -translate-x-1/2 top-4' },
|
|
|
|
|
exterior: { icon: <Globe size={24} />, 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<Record<string, { origen: Agente; destino: Agente } | null>>({});
|
|
|
|
|
const [elementoSeleccionado, setElementoSeleccionado] = useState<string | null>(null);
|
|
|
|
|
const [preguntaActual, setPreguntaActual] = useState(0);
|
|
|
|
|
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<number | null>(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<string, { origen: Agente; destino: Agente } | null> = {};
|
|
|
|
|
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<string, { origen: Agente; destino: Agente } | null> = {};
|
|
|
|
|
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 (
|
|
|
|
|
<Card className="w-full max-w-3xl mx-auto">
|
|
|
|
|
<div className="text-center py-8">
|
|
|
|
|
<Card className="w-full max-w-2xl mx-auto">
|
|
|
|
|
<div className="text-center py-8 px-4">
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ scale: 0 }}
|
|
|
|
|
animate={{ scale: 1 }}
|
|
|
|
|
transition={{ type: "spring", stiffness: 200 }}
|
|
|
|
|
className="inline-flex items-center justify-center w-20 h-20 bg-yellow-100 rounded-full mb-4"
|
|
|
|
|
className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full mb-6"
|
|
|
|
|
>
|
|
|
|
|
<Trophy size={40} className="text-yellow-600" />
|
|
|
|
|
<Trophy size={40} className="text-white" />
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Juego Completado!</h3>
|
|
|
|
|
|
|
|
|
|
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
|
|
|
|
¡Ejercicio Completado!
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
<p className="text-gray-600 mb-6">
|
|
|
|
|
Has completado todos los niveles del Flujo Circular
|
|
|
|
|
Has respondido {respuestasCorrectas} de {PREGUNTAS.length} preguntas correctamente
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div className="bg-blue-50 rounded-lg p-6 mb-6">
|
|
|
|
|
<p className="text-sm text-blue-600 mb-1">Puntuación Final</p>
|
|
|
|
|
<p className="text-4xl font-bold text-blue-700">{puntuacion} puntos</p>
|
|
|
|
|
|
|
|
|
|
<div className="bg-blue-50 rounded-xl p-6 mb-6">
|
|
|
|
|
<p className="text-sm text-blue-600 mb-1">Puntuación</p>
|
|
|
|
|
<p className="text-4xl font-bold text-blue-700">{puntuacion}</p>
|
|
|
|
|
<p className="text-sm text-blue-500">puntos</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4 max-w-xs mx-auto mb-6">
|
|
|
|
|
<div className="bg-green-50 rounded-lg p-3">
|
|
|
|
|
<p className="text-2xl font-bold text-green-600">{aciertos}</p>
|
|
|
|
|
<p className="text-sm text-green-700">Aciertos</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-red-50 rounded-lg p-3">
|
|
|
|
|
<p className="text-2xl font-bold text-red-600">{errores}</p>
|
|
|
|
|
<p className="text-sm text-red-700">Errores</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button onClick={() => {
|
|
|
|
|
setNivelActual(0);
|
|
|
|
|
setPuntuacion(0);
|
|
|
|
|
setCompletado(false);
|
|
|
|
|
setAciertos(0);
|
|
|
|
|
setErrores(0);
|
|
|
|
|
handleReiniciarNivel();
|
|
|
|
|
}} variant="outline">
|
|
|
|
|
<RefreshCw size={16} className="mr-2" />
|
|
|
|
|
Jugar de Nuevo
|
|
|
|
|
|
|
|
|
|
<Button onClick={handleReiniciar} variant="outline">
|
|
|
|
|
<RotateCcw size={16} className="mr-2" />
|
|
|
|
|
Intentar de Nuevo
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
@@ -250,178 +169,128 @@ export function FlujoCircular({ ejercicioId: _ejercicioId, onComplete }: FlujoCi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card className="w-full max-w-4xl mx-auto">
|
|
|
|
|
<CardHeader
|
|
|
|
|
title={`Nivel ${nivelActual + 1}: ${nivel.nombre}`}
|
|
|
|
|
subtitle={nivel.descripcion}
|
|
|
|
|
action={
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={handleReiniciarNivel}>
|
|
|
|
|
<RefreshCw size={16} />
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div className="mb-4 flex items-center justify-between">
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
{NIVELES.map((_, idx) => (
|
|
|
|
|
<div
|
|
|
|
|
key={idx}
|
|
|
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
|
|
|
|
|
idx < nivelActual
|
|
|
|
|
? 'bg-green-500 text-white'
|
|
|
|
|
: idx === nivelActual
|
|
|
|
|
? 'bg-blue-500 text-white'
|
|
|
|
|
: 'bg-gray-200 text-gray-500'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{idx < nivelActual ? <CheckCircle size={16} /> : idx + 1}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
<p className="text-sm text-gray-600">Puntuación</p>
|
|
|
|
|
<p className="text-xl font-bold text-blue-600">{puntuacion} pts</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
|
<div className="lg:col-span-2">
|
|
|
|
|
<div className="relative bg-gray-50 rounded-xl p-8 min-h-[400px]">
|
|
|
|
|
{nivel.agentes.map((agente) => (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={agente}
|
|
|
|
|
initial={{ scale: 0 }}
|
|
|
|
|
animate={{ scale: 1 }}
|
|
|
|
|
className={`absolute ${AGENTE_CONFIG[agente].position} w-24 h-24 rounded-xl border-2 ${AGENTE_CONFIG[agente].color} flex flex-col items-center justify-center gap-1 cursor-pointer hover:shadow-lg transition-shadow`}
|
|
|
|
|
>
|
|
|
|
|
{AGENTE_CONFIG[agente].icon}
|
|
|
|
|
<span className="text-xs font-bold text-center">{AGENTE_CONFIG[agente].label}</span>
|
|
|
|
|
</motion.div>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
<svg className="absolute inset-0 w-full h-full pointer-events-none" style={{ zIndex: 0 }}>
|
|
|
|
|
{getConexionesPosibles().map((conexion, idx) => (
|
|
|
|
|
<g key={idx}>
|
|
|
|
|
<line
|
|
|
|
|
x1={conexion.origen === 'familias' ? '15%' : conexion.origen === 'empresas' ? '85%' : conexion.origen === 'estado' ? '50%' : '50%'}
|
|
|
|
|
y1={conexion.origen === 'familias' ? '50%' : conexion.origen === 'empresas' ? '50%' : conexion.origen === 'estado' ? '15%' : '85%'}
|
|
|
|
|
x2={conexion.destino === 'familias' ? '15%' : conexion.destino === 'empresas' ? '85%' : conexion.destino === 'estado' ? '50%' : '50%'}
|
|
|
|
|
y2={conexion.destino === 'familias' ? '50%' : conexion.destino === 'empresas' ? '50%' : conexion.destino === 'estado' ? '15%' : '85%'}
|
|
|
|
|
stroke="#e5e7eb"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeDasharray="5,5"
|
|
|
|
|
/>
|
|
|
|
|
</g>
|
|
|
|
|
))}
|
|
|
|
|
</svg>
|
|
|
|
|
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
{getConexionesPosibles().map((conexion, idx) => {
|
|
|
|
|
const elementosEnConexion = nivel.elementos.filter(el =>
|
|
|
|
|
elementosColocados[el.id]?.origen === conexion.origen &&
|
|
|
|
|
elementosColocados[el.id]?.destino === conexion.destino
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={idx}
|
|
|
|
|
onClick={() => handleConexionClick(conexion.origen, conexion.destino)}
|
|
|
|
|
disabled={!elementoSeleccionado}
|
|
|
|
|
className={`p-2 rounded-lg border-2 text-xs font-medium transition-all min-w-[120px] ${
|
|
|
|
|
elementoSeleccionado
|
|
|
|
|
? 'border-blue-300 bg-blue-50 hover:bg-blue-100 cursor-pointer'
|
|
|
|
|
: 'border-gray-200 bg-white'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="text-gray-500 mb-1">{conexion.label}</div>
|
|
|
|
|
<div className="flex flex-wrap gap-1 justify-center">
|
|
|
|
|
{elementosEnConexion.map((el, i) => (
|
|
|
|
|
<span key={i} className="text-lg" title={el.texto}>
|
|
|
|
|
{el.texto.split(' ')[0]}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Card className="w-full max-w-3xl mx-auto">
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-xl font-bold text-gray-900">Flujo Circular de la Renta</h3>
|
|
|
|
|
<p className="text-sm text-gray-500">Pregunta {preguntaActual + 1} de {PREGUNTAS.length}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
<p className="text-xs text-gray-500">Puntos</p>
|
|
|
|
|
<p className="text-xl font-bold text-blue-600">{puntuacion}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="font-semibold text-gray-700 mb-3">Elementos ({nivel.elementos.length})</h4>
|
|
|
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
|
|
|
{elementoSeleccionado
|
|
|
|
|
? 'Selecciona una conexión en el diagrama'
|
|
|
|
|
: 'Haz clic en un elemento para colocarlo'}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
|
|
|
|
{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 */}
|
|
|
|
|
<div className="w-full bg-gray-200 rounded-full h-2 mb-6">
|
|
|
|
|
<motion.div
|
|
|
|
|
className="h-2 bg-blue-500 rounded-full"
|
|
|
|
|
initial={{ width: 0 }}
|
|
|
|
|
animate={{ width: `${((preguntaActual + 1) / PREGUNTAS.length) * 100}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Pregunta */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h4 className="text-lg font-medium text-gray-800 mb-4">
|
|
|
|
|
{pregunta.pregunta}
|
|
|
|
|
</h4>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{pregunta.opciones.map((opcion, index) => {
|
|
|
|
|
const estaSeleccionada = respuestaSeleccionada === index;
|
|
|
|
|
const esCorrecta = index === pregunta.respuestaCorrecta;
|
|
|
|
|
const mostrarCorrecta = mostrarResultado && esCorrecta;
|
|
|
|
|
const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !esCorrecta;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<motion.button
|
|
|
|
|
key={elemento.id}
|
|
|
|
|
onClick={() => 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'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-xl">{elemento.texto.split(' ')[0]}</span>
|
|
|
|
|
<span className="text-sm font-medium">{elemento.texto.split(' ').slice(1).join(' ')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{colocado && (
|
|
|
|
|
esCorrecto
|
|
|
|
|
? <CheckCircle size={16} className="text-green-600" />
|
|
|
|
|
: <XCircle size={16} className="text-red-600" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2 mt-1">
|
|
|
|
|
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
|
|
|
elemento.tipo === 'real' ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
|
|
|
|
|
mostrarCorrecta
|
|
|
|
|
? 'border-green-500 bg-green-500 text-white'
|
|
|
|
|
: mostrarIncorrecta
|
|
|
|
|
? 'border-red-500 bg-red-500 text-white'
|
|
|
|
|
: estaSeleccionada
|
|
|
|
|
? 'border-blue-500 bg-blue-500 text-white'
|
|
|
|
|
: 'border-gray-300'
|
|
|
|
|
}`}>
|
|
|
|
|
{elemento.tipo === 'real' ? 'Real' : 'Monetario'}
|
|
|
|
|
{mostrarCorrecta && <CheckCircle size={14} />}
|
|
|
|
|
{mostrarIncorrecta && <XCircle size={14} />}
|
|
|
|
|
{!mostrarResultado && estaSeleccionada && (
|
|
|
|
|
<div className="w-2 h-2 bg-white rounded-full" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<span className={`font-medium ${
|
|
|
|
|
mostrarCorrecta ? 'text-green-800' :
|
|
|
|
|
mostrarIncorrecta ? 'text-red-800' :
|
|
|
|
|
'text-gray-700'
|
|
|
|
|
}`}>
|
|
|
|
|
{opcion}
|
|
|
|
|
</span>
|
|
|
|
|
{colocado && !esCorrecto && (
|
|
|
|
|
<span className="text-xs text-red-600">
|
|
|
|
|
{AGENTE_CONFIG[elemento.origen].label} → {AGENTE_CONFIG[elemento.destino].label}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
|
|
|
|
<p className="text-xs text-gray-600 mb-2">Leyenda:</p>
|
|
|
|
|
<div className="flex gap-4 text-xs">
|
|
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<span className="w-3 h-3 bg-blue-100 border border-blue-300 rounded"></span>
|
|
|
|
|
Flujo Real
|
|
|
|
|
</span>
|
|
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<span className="w-3 h-3 bg-green-100 border border-green-300 rounded"></span>
|
|
|
|
|
Flujo Monetario
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Explicación */}
|
|
|
|
|
{mostrarResultado && (
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: 10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
className={`p-4 rounded-xl mb-6 ${
|
|
|
|
|
respuestaSeleccionada === pregunta.respuestaCorrecta
|
|
|
|
|
? 'bg-green-50 border border-green-200'
|
|
|
|
|
: 'bg-red-50 border border-red-200'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<p className={`font-medium mb-2 ${
|
|
|
|
|
respuestaSeleccionada === pregunta.respuestaCorrecta
|
|
|
|
|
? 'text-green-800'
|
|
|
|
|
: 'text-red-800'
|
|
|
|
|
}`}>
|
|
|
|
|
{respuestaSeleccionada === pregunta.respuestaCorrecta
|
|
|
|
|
? '¡Correcto!'
|
|
|
|
|
: 'Incorrecto'}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-gray-700">{pregunta.explicacion}</p>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Botones */}
|
|
|
|
|
<div className="flex justify-end">
|
|
|
|
|
{!mostrarResultado ? (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleVerificar}
|
|
|
|
|
disabled={respuestaSeleccionada === null}
|
|
|
|
|
>
|
|
|
|
|
Verificar Respuesta
|
|
|
|
|
</Button>
|
|
|
|
|
) : preguntaActual < PREGUNTAS.length - 1 ? (
|
|
|
|
|
<Button onClick={handleSiguiente}>
|
|
|
|
|
Siguiente
|
|
|
|
|
<ArrowRight size={16} className="ml-2" />
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|