Fix login blank screen and progress persistence

- Fix authStore to persist user data, not just isAuthenticated
- Fix progressStore handling of undefined API responses
- Remove minimax.md documentation file
- All progress now properly saves to PostgreSQL
- Login flow working correctly
This commit is contained in:
Renato
2026-02-12 03:38:33 +01:00
parent d31575a143
commit a2ed69fdb8
68 changed files with 14321 additions and 397 deletions

View File

@@ -103,8 +103,7 @@ func main() {
progreso := protected.Group("/progreso")
{
progreso.GET("", progresoHandler.GetProgreso)
progreso.GET("/modulo/:numero", progresoHandler.GetProgresoModulo)
progreso.PUT("/:ejercicioId", progresoHandler.UpdateProgreso)
progreso.POST("", progresoHandler.SaveProgreso)
progreso.GET("/resumen", progresoHandler.GetResumen)
}
@@ -214,12 +213,12 @@ func runMigrations(ctx context.Context, dbPool *pgxpool.Pool) {
func seedEjercicios(ctx context.Context, pool *pgxpool.Pool) {
ejercicios := []struct {
ID string
ID string
ModuloNumero int
Titulo string
Tipo string
Contenido string
Orden int
Titulo string
Tipo string
Contenido string
Orden int
}{
// Módulo 1
{"m1e1", 1, "Simulador de Disyuntivas", "interactivo", `{"tipo":"slider","descripcion":"Elige cuanto producir de cada bien"}`, 1},

View File

@@ -2,7 +2,6 @@ package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -34,7 +33,7 @@ func (h *ProgresoHandler) GetProgreso(c *gin.Context) {
return
}
progresos, err := h.progresoRepo.GetByUsuario(c.Request.Context(), userID.(uuid.UUID))
progresos, err := h.progresoRepo.GetProgresoByUsuarioID(userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"})
return
@@ -47,72 +46,32 @@ func (h *ProgresoHandler) GetProgreso(c *gin.Context) {
c.JSON(http.StatusOK, progresos)
}
// GetProgresoModulo godoc
// @Summary Obtener progreso por módulo
// @Description Obtiene el progreso del usuario en un módulo específico
// @Tags progreso
// @Produce json
// @Security BearerAuth
// @Param numero path int true "Número del módulo"
// @Success 200 {array} models.Progreso
// @Router /api/progreso/modulo/{numero} [get]
func (h *ProgresoHandler) GetProgresoModulo(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"})
return
}
moduloNumero, err := strconv.Atoi(c.Param("numero"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Número de módulo inválido"})
return
}
progresos, err := h.progresoRepo.GetByModulo(c.Request.Context(), userID.(uuid.UUID), moduloNumero)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"})
return
}
if progresos == nil {
progresos = []models.Progreso{}
}
c.JSON(http.StatusOK, progresos)
}
// UpdateProgreso godoc
// @Summary Guardar avance
// @Description Guarda el progreso de un ejercicio
// SaveProgreso godoc
// @Summary Guardar/actualizar progreso
// @Description Guarda o actualiza el progreso de un ejercicio
// @Tags progreso
// @Accept json
// @Produce json
// @Param ejercicioId path int true "ID del ejercicio"
// @Param progreso body models.ProgresoUpdate true "Datos del progreso"
// @Param progreso body models.Progreso true "Datos del progreso"
// @Security BearerAuth
// @Success 200 {object} map[string]string
// @Router /api/progreso/{ejercicioId} [put]
func (h *ProgresoHandler) UpdateProgreso(c *gin.Context) {
// @Router /api/progreso [post]
func (h *ProgresoHandler) SaveProgreso(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"})
return
}
ejercicioID, err := strconv.Atoi(c.Param("ejercicioId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de ejercicio inválido"})
return
}
var req models.ProgresoUpdate
if err := c.ShouldBindJSON(&req); err != nil {
var progreso models.Progreso
if err := c.ShouldBindJSON(&progreso); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = h.progresoRepo.Upsert(c.Request.Context(), userID.(uuid.UUID), ejercicioID, &req)
progreso.UsuarioID = userID.(uuid.UUID)
err := h.progresoRepo.SaveProgreso(&progreso)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al guardar progreso: " + err.Error()})
return
@@ -123,11 +82,11 @@ func (h *ProgresoHandler) UpdateProgreso(c *gin.Context) {
// GetResumen godoc
// @Summary Obtener resumen
// @Description Obtiene estadísticas del progreso del usuario
// @Description Obtiene estadísticas del progreso del usuario (puntos totales, etc.)
// @Tags progreso
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.ProgresoResumen
// @Success 200 {object} models.ResumenProgreso
// @Router /api/progreso/resumen [get]
func (h *ProgresoHandler) GetResumen(c *gin.Context) {
userID, exists := c.Get("user_id")
@@ -136,7 +95,7 @@ func (h *ProgresoHandler) GetResumen(c *gin.Context) {
return
}
resumen, err := h.progresoRepo.GetResumen(c.Request.Context(), userID.(uuid.UUID))
resumen, err := h.progresoRepo.GetResumen(userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener resumen"})
return

View File

@@ -216,7 +216,7 @@ func (h *UsersHandler) GetUserProgreso(c *gin.Context) {
return
}
progresos, err := h.progresoRepo.GetByUsuarioID(c.Request.Context(), id)
progresos, err := h.progresoRepo.GetProgresoByUsuarioID(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"})
return

View File

@@ -7,15 +7,31 @@ import (
)
type Progreso struct {
ID uuid.UUID `json:"id"`
UsuarioID uuid.UUID `json:"usuario_id"`
ModuloNumero int `json:"modulo_numero"`
EjercicioID int `json:"ejercicio_id"`
Completado bool `json:"completado"`
Puntuacion int `json:"puntuacion"`
Intentos int `json:"intentos"`
UltimaVez time.Time `json:"ultima_vez"`
RespuestaJSON string `json:"respuesta_json,omitempty"`
ID uuid.UUID `json:"id"`
UsuarioID uuid.UUID `json:"usuario_id"`
ModuloNumero int `json:"modulo_numero"`
EjercicioID string `json:"ejercicio_id"`
Completado bool `json:"completado"`
Puntuacion int `json:"puntuacion"`
Intentos int `json:"intentos"`
UltimaVez time.Time `json:"ultima_vez"`
}
type Badge struct {
ID string `json:"id"`
Nombre string `json:"nombre"`
Descripcion string `json:"descripcion"`
Icono string `json:"icono"`
Desbloqueado bool `json:"desbloqueado"`
}
type ResumenProgreso struct {
PuntosTotales int `json:"puntos_totales"`
EjerciciosCompletados int `json:"ejercicios_completados"`
TotalEjercicios int `json:"total_ejercicios"`
TotalPuntuacion int `json:"totalPuntuacion"`
Badges []Badge `json:"badges"`
Nivel string `json:"nivel"`
}
type ProgresoUpdate struct {
@@ -25,8 +41,8 @@ type ProgresoUpdate struct {
}
type ProgresoResumen struct {
TotalEjercicios int `json:"total_ejercicios"`
TotalEjercicios int `json:"total_ejercicios"`
EjerciciosCompletados int `json:"ejercicios_completados"`
PromedioPuntuacion int `json:"promedio_puntuacion"`
ModulosCompletados int `json:"modulos_completados"`
PromedioPuntuacion int `json:"promedio_puntuacion"`
ModulosCompletados int `json:"modulos_completados"`
}

View File

@@ -17,13 +17,13 @@ func NewProgresoRepository(db *pgxpool.Pool) *ProgresoRepository {
return &ProgresoRepository{db: db}
}
func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UUID) ([]models.Progreso, error) {
func (r *ProgresoRepository) GetProgresoByUsuarioID(usuarioID uuid.UUID) ([]models.Progreso, error) {
query := `
SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json
SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez
FROM progreso_usuario WHERE usuario_id = $1
ORDER BY ultima_vez DESC
`
rows, err := r.db.Query(ctx, query, usuarioID)
rows, err := r.db.Query(context.Background(), query, usuarioID)
if err != nil {
return nil, err
}
@@ -34,7 +34,7 @@ func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UU
var p models.Progreso
err := rows.Scan(
&p.ID, &p.UsuarioID, &p.ModuloNumero, &p.EjercicioID,
&p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez, &p.RespuestaJSON)
&p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez)
if err != nil {
return nil, err
}
@@ -43,99 +43,80 @@ func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UU
return progresos, nil
}
func (r *ProgresoRepository) GetByModulo(ctx context.Context, usuarioID uuid.UUID, moduloNumero int) ([]models.Progreso, error) {
func (r *ProgresoRepository) SaveProgreso(progreso *models.Progreso) error {
query := `
SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json
FROM progreso_usuario WHERE usuario_id = $1 AND modulo_numero = $2
ORDER BY ejercicio_id
`
rows, err := r.db.Query(ctx, query, usuarioID, moduloNumero)
if err != nil {
return nil, err
}
defer rows.Close()
var progresos []models.Progreso
for rows.Next() {
var p models.Progreso
err := rows.Scan(
&p.ID, &p.UsuarioID, &p.ModuloNumero, &p.EjercicioID,
&p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez, &p.RespuestaJSON)
if err != nil {
return nil, err
}
progresos = append(progresos, p)
}
return progresos, nil
}
func (r *ProgresoRepository) GetByEjercicio(ctx context.Context, usuarioID uuid.UUID, ejercicioID int) (*models.Progreso, error) {
query := `
SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json
FROM progreso_usuario WHERE usuario_id = $1 AND ejercicio_id = $2
`
var p models.Progreso
err := r.db.QueryRow(ctx, query, usuarioID, ejercicioID).Scan(
&p.ID, &p.UsuarioID, &p.ModuloNumero, &p.EjercicioID,
&p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez, &p.RespuestaJSON)
if err != nil {
return nil, err
}
return &p, nil
}
func (r *ProgresoRepository) Upsert(ctx context.Context, usuarioID uuid.UUID, ejercicioID int, update *models.ProgresoUpdate) error {
query := `
INSERT INTO progreso_usuario (id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
INSERT INTO progreso_usuario (id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (usuario_id, modulo_numero, ejercicio_id)
DO UPDATE SET completado = $5, puntuacion = $6, intentos = $7, ultima_vez = $8, respuesta_json = $9
DO UPDATE SET completado = $5, puntuacion = $6, intentos = progreso_usuario.intentos + 1, ultima_vez = $8
`
moduloNumero, err := r.getModuloByEjercicio(ctx, ejercicioID)
if err != nil {
return err
if progreso.ID == uuid.Nil {
progreso.ID = uuid.New()
}
if progreso.UltimaVez.IsZero() {
progreso.UltimaVez = time.Now()
}
if progreso.Intentos == 0 {
progreso.Intentos = 1
}
existing, _ := r.GetByEjercicio(ctx, usuarioID, ejercicioID)
var intentos int
if existing != nil {
intentos = existing.Intentos + 1
} else {
intentos = 1
}
_, err = r.db.Exec(ctx, query,
uuid.New(), usuarioID, moduloNumero, ejercicioID,
update.Completado, update.Puntuacion, intentos, time.Now(), update.RespuestaJSON)
_, err := r.db.Exec(context.Background(), query,
progreso.ID, progreso.UsuarioID, progreso.ModuloNumero, progreso.EjercicioID,
progreso.Completado, progreso.Puntuacion, progreso.Intentos, progreso.UltimaVez)
return err
}
func (r *ProgresoRepository) getModuloByEjercicio(ctx context.Context, ejercicioID int) (int, error) {
var moduloNumero int
err := r.db.QueryRow(ctx, "SELECT modulo_numero FROM ejercicios WHERE id = $1", ejercicioID).Scan(&moduloNumero)
return moduloNumero, err
}
func (r *ProgresoRepository) GetResumen(ctx context.Context, usuarioID uuid.UUID) (*models.ProgresoResumen, error) {
func (r *ProgresoRepository) GetResumen(usuarioID uuid.UUID) (*models.ResumenProgreso, error) {
query := `
SELECT
COUNT(DISTINCT ejercicio_id) as total,
COUNT(CASE WHEN completado THEN 1 END) as completados,
COALESCE(AVG(CASE WHEN completado THEN puntuacion END), 0)::int as promedio,
COUNT(DISTINCT CASE WHEN completado THEN modulo_numero END) as modulos
COALESCE(SUM(puntuacion), 0) as puntos_totales,
COUNT(CASE WHEN completado THEN 1 END) as ejercicios_completados,
COUNT(*) as total_ejercicios
FROM progreso_usuario WHERE usuario_id = $1
`
var resumen models.ProgresoResumen
err := r.db.QueryRow(ctx, query, usuarioID).Scan(
&resumen.TotalEjercicios, &resumen.EjerciciosCompletados,
&resumen.PromedioPuntuacion, &resumen.ModulosCompletados)
var resumen models.ResumenProgreso
err := r.db.QueryRow(context.Background(), query, usuarioID).Scan(
&resumen.PuntosTotales, &resumen.EjerciciosCompletados, &resumen.TotalEjercicios)
if err != nil {
return nil, err
}
// Alias para compatibilidad con frontend
resumen.TotalPuntuacion = resumen.PuntosTotales
// Calcular nivel basado en puntuación
resumen.Nivel = calcularNivel(resumen.PuntosTotales)
// Generar badges basados en progreso
resumen.Badges = generarBadges(resumen.PuntosTotales, resumen.EjerciciosCompletados)
return &resumen, nil
}
func (r *ProgresoRepository) GetByUsuarioID(ctx context.Context, usuarioID uuid.UUID) ([]models.Progreso, error) {
return r.GetByUsuario(ctx, usuarioID)
func calcularNivel(puntuacion int) string {
if puntuacion >= 2000 {
return "Maestro"
}
if puntuacion >= 1000 {
return "Experto"
}
if puntuacion >= 300 {
return "Aprendiz"
}
return "Novato"
}
func generarBadges(puntuacion, ejerciciosCompletados int) []models.Badge {
badges := []models.Badge{
{ID: "primer-ejercicio", Nombre: "Primer Ejercicio", Descripcion: "Completa tu primer ejercicio", Icono: "star", Desbloqueado: ejerciciosCompletados >= 1},
{ID: "primer-modulo", Nombre: "Primer Módulo", Descripcion: "Completa todas las lecciones de un módulo", Icono: "award", Desbloqueado: ejerciciosCompletados >= 3},
{ID: "aprendiz", Nombre: "Aprendiz", Descripcion: "Alcanza el nivel Aprendiz", Icono: "book", Desbloqueado: puntuacion >= 300},
{ID: "experto", Nombre: "Experto", Descripcion: "Alcanza el nivel Experto", Icono: "trophy", Desbloqueado: puntuacion >= 1000},
{ID: "maestro", Nombre: "Maestro", Descripcion: "Alcanza el nivel Maestro", Icono: "crown", Desbloqueado: puntuacion >= 2000},
{ID: "puntos-500", Nombre: "500 Puntos", Descripcion: "Acumula 500 puntos", Icono: "target", Desbloqueado: puntuacion >= 500},
{ID: "puntos-1000", Nombre: "1000 Puntos", Descripcion: "Acumula 1000 puntos", Icono: "zap", Desbloqueado: puntuacion >= 1000},
{ID: "puntos-2000", Nombre: "2000 Puntos", Descripcion: "Acumula 2000 puntos", Icono: "flame", Desbloqueado: puntuacion >= 2000},
}
return badges
}

View File

@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"axios": "^1.6.2",
"framer-motion": "^12.34.0",
"lucide-react": "^0.294.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -1809,6 +1810,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.34.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz",
"integrity": "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.34.0",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2153,6 +2181,21 @@
"node": ">= 0.6"
}
},
"node_modules/motion-dom": {
"version": "12.34.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz",
"integrity": "sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.29.2"
}
},
"node_modules/motion-utils": {
"version": "12.29.2",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
"integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2850,6 +2893,12 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"axios": "^1.6.2",
"framer-motion": "^12.34.0",
"lucide-react": "^0.294.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useAuthStore } from './stores/authStore';
import { Login } from './pages/Login';
@@ -5,6 +6,7 @@ import { Dashboard } from './pages/Dashboard';
import { Modulos } from './pages/Modulos';
import { Modulo } from './pages/Modulo';
import { AdminPanel } from './pages/admin/AdminPanel';
import { RecursosPage } from './pages/Recursos';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthStore();
@@ -25,6 +27,12 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
}
function App() {
const { checkAuth } = useAuthStore();
useEffect(() => {
checkAuth();
}, [checkAuth]);
return (
<BrowserRouter>
<Routes>
@@ -53,6 +61,38 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/modulo/1"
element={
<ProtectedRoute>
<Modulo />
</ProtectedRoute>
}
/>
<Route
path="/modulo/2"
element={
<ProtectedRoute>
<Modulo />
</ProtectedRoute>
}
/>
<Route
path="/modulo/3"
element={
<ProtectedRoute>
<Modulo />
</ProtectedRoute>
}
/>
<Route
path="/modulo/4"
element={
<ProtectedRoute>
<Modulo />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
@@ -61,6 +101,14 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/recursos"
element={
<ProtectedRoute>
<RecursosPage />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>

View File

@@ -0,0 +1,184 @@
import React, { useState, isValidElement } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../ui/Card';
import { Button } from '../ui/Button';
import { Trophy, Star, RotateCcw, Home, ArrowRight, CheckCircle } from 'lucide-react';
import { useEjercicioProgreso } from '../../hooks/useEjercicioProgreso';
interface EjercicioWrapperProps {
moduloId: string;
ejercicioId: string;
titulo: string;
descripcion: string;
puntosMaximos: number;
onComplete?: (puntuacion?: number) => void;
onRetry?: () => void;
onExit?: () => void;
children: React.ReactNode;
}
export function EjercicioWrapper({
moduloId,
ejercicioId,
titulo,
descripcion,
puntosMaximos,
onComplete,
onRetry,
onExit,
children,
}: EjercicioWrapperProps) {
const { puntuacionAnterior, intentos, guardarProgreso } = useEjercicioProgreso({
moduloId,
ejercicioId,
onComplete,
});
const [mostrarCompletado, setMostrarCompletado] = useState(false);
const [puntuacionActual, setPuntuacionActual] = useState(0);
const handleCompletar = (puntuacion: number) => {
guardarProgreso(puntuacion);
setPuntuacionActual(puntuacion);
setMostrarCompletado(true);
};
const esMejorPuntuacion = puntuacionAnterior !== undefined && puntuacionActual > puntuacionAnterior;
// Pasar handleCompletar a los hijos
const childrenWithProps = isValidElement(children)
? React.cloneElement(children as React.ReactElement<any>, { onCompletar: handleCompletar })
: children;
return (
<div className="space-y-6">
<AnimatePresence mode="wait">
{!mostrarCompletado ? (
<motion.div
key="ejercicio"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div className="mb-6">
<div className="flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">{titulo}</h2>
<p className="text-gray-600 mt-1">{descripcion}</p>
</div>
<div className="text-right">
<div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-1 rounded-full">
<Trophy size={16} />
<span className="font-semibold">{puntosMaximos} pts máx.</span>
</div>
{puntuacionAnterior !== undefined && (
<div className="mt-2 text-sm text-gray-500">
Mejor puntuación: {puntuacionAnterior} pts
<span className="text-gray-400"> ({intentos} {intentos === 1 ? 'intento' : 'intentos'})</span>
</div>
)}
</div>
</div>
</div>
{childrenWithProps}
</motion.div>
) : (
<motion.div
key="completado"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", stiffness: 200 }}
>
<Card className="text-center py-12">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 200,
delay: 0.2
}}
className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full mb-6 shadow-lg"
>
<Trophy size={48} className="text-white" />
</motion.div>
<h3 className="text-3xl font-bold text-gray-900 mb-2">
¡Ejercicio Completado!
</h3>
<p className="text-gray-600 mb-8 max-w-md mx-auto">
Has completado el ejercicio. Revisa tu puntuación y decide si quieres intentarlo de nuevo para mejorar tu marca.
</p>
<div className="grid grid-cols-3 gap-4 max-w-lg mx-auto mb-8">
<div className="bg-blue-50 rounded-xl p-4">
<Star className="w-6 h-6 text-blue-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-blue-700">{puntuacionActual}</p>
<p className="text-sm text-blue-600">Puntuación</p>
</div>
<div className="bg-purple-50 rounded-xl p-4">
<Trophy className="w-6 h-6 text-purple-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-purple-700">{puntosMaximos}</p>
<p className="text-sm text-purple-600">Máximo</p>
</div>
<div className={`rounded-xl p-4 ${esMejorPuntuacion ? 'bg-green-50' : 'bg-gray-50'}`}>
<CheckCircle className={`w-6 h-6 mx-auto mb-2 ${esMejorPuntuacion ? 'text-green-500' : 'text-gray-400'}`} />
<p className={`text-2xl font-bold ${esMejorPuntuacion ? 'text-green-700' : 'text-gray-700'}`}>
{Math.round((puntuacionActual / puntosMaximos) * 100)}%
</p>
<p className={`text-sm ${esMejorPuntuacion ? 'text-green-600' : 'text-gray-500'}`}>
{esMejorPuntuacion ? '¡Récord!' : 'Precisión'}
</p>
</div>
</div>
{esMejorPuntuacion && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6"
>
<div className="inline-flex items-center gap-2 px-4 py-2 bg-green-100 text-green-700 rounded-full font-medium">
<Star size={18} />
¡Nueva mejor puntuación! +{puntuacionActual - (puntuacionAnterior || 0)} pts
</div>
</motion.div>
)}
<div className="flex flex-wrap justify-center gap-4">
<Button variant="outline" onClick={onExit}>
<Home size={18} className="mr-2" />
Volver al módulo
</Button>
<Button
variant="outline"
onClick={() => {
setMostrarCompletado(false);
if (onRetry) onRetry();
}}
>
<RotateCcw size={18} className="mr-2" />
Intentar de nuevo
</Button>
{!esMejorPuntuacion && puntuacionActual < puntosMaximos && (
<Button onClick={onExit}>
Siguiente ejercicio
<ArrowRight size={18} className="ml-2" />
</Button>
)}
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default EjercicioWrapper;

View File

@@ -0,0 +1 @@
export { EjercicioWrapper } from './EjercicioWrapper';

View File

@@ -0,0 +1,431 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Trophy, Users, Building2, Landmark, Globe, RefreshCw } 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 Nivel {
nombre: string;
descripcion: string;
agentes: Agente[];
elementos: Elemento[];
}
const NIVELES: Nivel[] = [
{
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' },
]
},
{
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' },
]
},
{
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' },
]
}
];
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 [puntuacion, setPuntuacion] = useState(0);
const [completado, setCompletado] = useState(false);
const [aciertos, setAciertos] = useState(0);
const [errores, setErrores] = useState(0);
const nivel = NIVELES[nivelActual];
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 handleConexionClick = (origen: Agente, destino: Agente) => {
if (!elementoSeleccionado) return;
const elemento = nivel.elementos.find(el => el.id === elementoSeleccionado);
if (!elemento) return;
const esCorrecto = elemento.origen === origen && elemento.destino === destino;
setElementosColocados(prev => ({
...prev,
[elementoSeleccionado]: { origen, destino }
}));
if (esCorrecto) {
setAciertos(prev => prev + 1);
setPuntuacion(prev => prev + 10);
} else {
setErrores(prev => prev + 1);
setPuntuacion(prev => Math.max(0, prev - 2));
}
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 {
setCompletado(true);
if (onComplete) {
onComplete(puntuacion + bonus);
}
}
}
};
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 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;
};
if (completado) {
return (
<Card className="w-full max-w-3xl mx-auto">
<div className="text-center py-8">
<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"
>
<Trophy size={40} className="text-yellow-600" />
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Juego Completado!</h3>
<p className="text-gray-600 mb-6">
Has completado todos los niveles del Flujo Circular
</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>
<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>
</div>
</Card>
);
}
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>
</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;
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'
: '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'
}`}>
{elemento.tipo === 'real' ? 'Real' : 'Monetario'}
</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 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>
</div>
</div>
</Card>
);
}
export default FlujoCircular;

View File

@@ -0,0 +1,310 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, ArrowRight, Trophy, BookOpen } from 'lucide-react';
interface QuizBienesProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Pregunta {
id: string;
bien: string;
descripcion: string;
opciones: string[];
respuestaCorrecta: string;
explicacionDetallada: string;
}
const PREGUNTAS: Pregunta[] = [
{
id: 'p1',
bien: 'Carne de primera calidad',
descripcion: 'Carne de res premium vendida en supermercados de alta gama',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacionDetallada: 'La carne premium es considerada un bien de lujo porque cuando el ingreso aumenta significativamente, las familias aumentan su consumo de este tipo de carne sustituyendo carnes de menor calidad.'
},
{
id: 'p2',
bien: 'Pan',
descripcion: 'Pan básico de consumo diario',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien normal',
explicacionDetallada: 'El pan es un bien normal porque su consumo aumenta moderadamente con el ingreso, aunque llega un punto donde se estabiliza (saturación).'
},
{
id: 'p3',
bien: 'Transporte público (autobús)',
descripcion: 'Servicio de autobuses urbanos',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacionDetallada: 'El transporte público es un bien inferior porque cuando los ingresos aumentan, las personas tienden a comprar automóviles o usar taxis/Uber, reduciendo el uso del autobús.'
},
{
id: 'p4',
bien: 'Fideos instantáneos',
descripcion: 'Comida rápida económica',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacionDetallada: 'Los fideos instantáneos son claramente un bien inferior. A medida que aumentan los ingresos, las personas prefieren alimentos más nutritivos y de mejor calidad.'
},
{
id: 'p5',
bien: 'Vacaciones en el extranjero',
descripcion: 'Viajes turísticos internacionales',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacionDetallada: 'Las vacaciones internacionales son un bien de lujo porque su consumo aumenta significativamente cuando el ingreso crece, incluso más que proporcionalmente.'
},
{
id: 'p6',
bien: 'Ropa de marca',
descripcion: 'Vestimenta de diseñador',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacionDetallada: 'La ropa de marca es un bien de lujo porque su demanda crece más rápido que el ingreso, especialmente en rangos de ingreso altos.'
},
{
id: 'p7',
bien: 'Cine',
descripcion: 'Entradas a salas de cine',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien normal',
explicacionDetallada: 'El cine es un bien normal. Aunque con el auge del streaming podría debatirse, generalmente el consumo de entretenimiento aumenta con el ingreso de forma moderada.'
},
{
id: 'p8',
bien: 'Productos de marca blanca',
descripcion: 'Productos genéricos de supermercado',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacionDetallada: 'Los productos de marca blanca son bienes inferiores porque son sustituidos por marcas reconocidas cuando el consumidor tiene mayores ingresos.'
}
];
export function QuizBienes({ ejercicioId: _ejercicioId, onComplete }: QuizBienesProps) {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<string | null>(null);
const [mostrarRetroalimentacion, setMostrarRetroalimentacion] = useState(false);
const [puntuacion, setPuntuacion] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const [progreso, setProgreso] = useState(0);
useEffect(() => {
setProgreso(((preguntaActual + (completado ? 1 : 0)) / PREGUNTAS.length) * 100);
}, [preguntaActual, completado]);
const handleSeleccionarRespuesta = (opcion: string) => {
if (mostrarRetroalimentacion) return;
setRespuestaSeleccionada(opcion);
};
const handleValidar = () => {
if (!respuestaSeleccionada) return;
const esCorrecta = respuestaSeleccionada === PREGUNTAS[preguntaActual].respuestaCorrecta;
setMostrarRetroalimentacion(true);
if (esCorrecta) {
setRespuestasCorrectas(prev => prev + 1);
setPuntuacion(prev => prev + 100);
}
};
const handleSiguiente = () => {
if (preguntaActual < PREGUNTAS.length - 1) {
setPreguntaActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarRetroalimentacion(false);
} else {
setCompletado(true);
const puntuacionFinal = puntuacion + (respuestaSeleccionada === PREGUNTAS[preguntaActual].respuestaCorrecta ? 100 : 0);
if (onComplete) {
onComplete(puntuacionFinal);
}
}
};
const handleReiniciar = () => {
setPreguntaActual(0);
setRespuestaSeleccionada(null);
setMostrarRetroalimentacion(false);
setPuntuacion(0);
setRespuestasCorrectas(0);
setCompletado(false);
};
const pregunta = PREGUNTAS[preguntaActual];
const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta;
if (completado) {
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="text-center py-8">
<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"
>
<Trophy size={40} className="text-yellow-600" />
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Quiz Completado!</h3>
<p className="text-gray-600 mb-6">
Respondiste correctamente {respuestasCorrectas} de {PREGUNTAS.length} preguntas
</p>
<div className="bg-blue-50 rounded-lg p-6 mb-6">
<p className="text-sm text-blue-600 mb-1">Puntuación Total</p>
<p className="text-4xl font-bold text-blue-700">{puntuacion} puntos</p>
</div>
<Button onClick={handleReiniciar} variant="outline">
Intentar de Nuevo
</Button>
</div>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader
title="Quiz: Clasificación de Bienes"
subtitle={`Pregunta ${preguntaActual + 1} de ${PREGUNTAS.length}`}
/>
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Progreso</span>
<span>{Math.round(progreso)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<motion.div
className="bg-blue-600 h-2.5 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progreso}%` }}
transition={{ duration: 0.3 }}
/>
</div>
</div>
<div className="mb-6">
<div className="flex items-center gap-2 text-blue-600 mb-3">
<BookOpen size={20} />
<span className="font-medium">Clasifica el siguiente bien:</span>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">{pregunta.bien}</h3>
<p className="text-gray-600">{pregunta.descripcion}</p>
</div>
<div className="space-y-3 mb-6">
{pregunta.opciones.map((opcion, index) => {
const isSelected = respuestaSeleccionada === opcion;
const isCorrect = opcion === pregunta.respuestaCorrecta;
const showCorrect = mostrarRetroalimentacion && isCorrect;
const showIncorrect = mostrarRetroalimentacion && isSelected && !isCorrect;
return (
<motion.button
key={index}
onClick={() => handleSeleccionarRespuesta(opcion)}
disabled={mostrarRetroalimentacion}
whileHover={!mostrarRetroalimentacion ? { scale: 1.02 } : {}}
whileTap={!mostrarRetroalimentacion ? { scale: 0.98 } : {}}
className={`w-full p-4 rounded-lg border-2 text-left transition-all ${
showCorrect
? 'border-green-500 bg-green-50'
: showIncorrect
? 'border-red-500 bg-red-50'
: isSelected
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-700 font-semibold text-sm">
{String.fromCharCode(65 + index)}
</span>
<span className="font-medium">{opcion}</span>
</div>
{showCorrect && <CheckCircle size={20} className="text-green-600" />}
{showIncorrect && <XCircle size={20} className="text-red-600" />}
</div>
</motion.button>
);
})}
</div>
<AnimatePresence>
{mostrarRetroalimentacion && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden mb-6"
>
<div className={`p-4 rounded-lg border ${
esCorrecta
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}>
<div className="flex items-center gap-2 mb-2">
{esCorrecta ? (
<CheckCircle size={20} className="text-green-600" />
) : (
<XCircle size={20} className="text-red-600" />
)}
<span className={`font-semibold ${
esCorrecta ? 'text-green-800' : 'text-red-800'
}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className={`text-sm ${
esCorrecta ? 'text-green-700' : 'text-red-700'
}`}>
{pregunta.explicacionDetallada}
</p>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
Puntuación: <span className="font-bold text-blue-600">{puntuacion}</span> pts
</div>
{!mostrarRetroalimentacion ? (
<Button
onClick={handleValidar}
disabled={!respuestaSeleccionada}
>
Validar Respuesta
</Button>
) : (
<Button onClick={handleSiguiente}>
{preguntaActual < PREGUNTAS.length - 1 ? (
<>
Siguiente
<ArrowRight size={16} className="ml-2" />
</>
) : (
'Finalizar'
)}
</Button>
)}
</div>
</Card>
);
}
export default QuizBienes;

View File

@@ -0,0 +1,248 @@
import { useState, useCallback } from 'react';
interface SimuladorDisyuntivasProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function SimuladorDisyuntivas({ ejercicioId: _ejercicioId, onComplete }: SimuladorDisyuntivasProps) {
const [bienX, setBienX] = useState(50);
const [bienY, setBienY] = useState(50);
const [validacion, setValidacion] = useState<'eficiente' | 'ineficiente' | 'inalcanzable' | null>(null);
const [completado, setCompletado] = useState(false);
const MAX_X = 100;
const MAX_Y = 80;
const calcularFPP = useCallback((x: number): number => {
const ratio = x / MAX_X;
const y = MAX_Y * Math.pow(1 - ratio, 0.7);
return Math.max(0, Math.min(MAX_Y, y));
}, []);
const validarPosicion = useCallback(() => {
const yFPP = calcularFPP(bienX);
const diferencia = Math.abs(bienY - yFPP);
const tolerancia = 3;
if (bienY > yFPP + tolerancia) {
return 'inalcanzable';
} else if (diferencia <= tolerancia) {
return 'eficiente';
} else {
return 'ineficiente';
}
}, [bienX, bienY, calcularFPP]);
const handleValidar = () => {
const resultado = validarPosicion();
setValidacion(resultado);
if (resultado === 'eficiente' && !completado) {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const handleReset = () => {
setBienX(50);
setBienY(50);
setValidacion(null);
setCompletado(false);
};
// Generar puntos para la curva FPP
const puntosFPP: string[] = [];
for (let x = 0; x <= MAX_X; x += 2) {
const y = calcularFPP(x);
const svgX = 40 + (x / MAX_X) * 260;
const svgY = 200 - (y / MAX_Y) * 180;
puntosFPP.push(`${svgX},${svgY}`);
}
const pathData = puntosFPP.length > 0
? `M ${puntosFPP.join(' L ')}`
: '';
const colorValidacion = validacion === 'eficiente'
? 'text-green-600 bg-green-50 border-green-200'
: validacion === 'ineficiente'
? 'text-yellow-600 bg-yellow-50 border-yellow-200'
: validacion === 'inalcanzable'
? 'text-red-600 bg-red-50 border-red-200'
: 'text-gray-600 bg-gray-50 border-gray-200';
const mensajeValidacion = validacion === 'eficiente'
? '¡Excelente! Estás sobre la FPP (Asignación eficiente)'
: validacion === 'ineficiente'
? 'Punto ineficiente: Estás dentro de la FPP, hay recursos sin usar'
: validacion === 'inalcanzable'
? 'Punto inalcanzable: No tienes suficientes recursos'
: 'Ajusta los sliders para explorar la FPP';
const puntoColor = validacion === 'eficiente'
? '#10b981'
: validacion === 'ineficiente'
? '#f59e0b'
: validacion === 'inalcanzable'
? '#ef4444'
: '#6b7280';
return (
<div className="w-full max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900">Simulador de Disyuntivas Económicas</h3>
<p className="text-sm text-gray-500">Explora la Frontera de Posibilidades de Producción (FPP)</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6">
{/* Slider X */}
<div>
<label className="flex justify-between text-sm font-medium text-gray-700 mb-2">
<span>Alimentos (X)</span>
<span className="text-blue-600 font-bold">{bienX} millones de toneladas</span>
</label>
<input
type="range"
min="0"
max={MAX_X}
value={bienX}
onChange={(e) => setBienX(Number(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer"
style={{ accentColor: '#2563eb' }}
/>
</div>
{/* Slider Y */}
<div>
<label className="flex justify-between text-sm font-medium text-gray-700 mb-2">
<span>Tecnología (Y)</span>
<span className="text-green-600 font-bold">{bienY} millones de unidades</span>
</label>
<input
type="range"
min="0"
max={MAX_Y}
value={bienY}
onChange={(e) => setBienY(Number(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer"
style={{ accentColor: '#16a34a' }}
/>
</div>
{/* Mensaje de validación */}
<div className={`p-4 rounded-lg border-2 ${colorValidacion}`}>
<span className="font-semibold capitalize">{validacion || 'Selecciona'}:</span>
<p className="text-sm mt-1">{mensajeValidacion}</p>
</div>
{/* Botones */}
<div className="flex gap-3">
<button
onClick={handleValidar}
disabled={completado}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
>
Validar Posición
</button>
<button
onClick={handleReset}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Reiniciar
</button>
</div>
{/* Mensaje de éxito */}
{completado && (
<div className="bg-green-100 border border-green-300 rounded-lg p-4 text-center">
<p className="text-green-800 font-semibold">¡Ejercicio Completado!</p>
<p className="text-green-700 text-2xl font-bold mt-1">100 puntos</p>
</div>
)}
</div>
{/* Gráfico SVG */}
<div className="bg-gray-50 rounded-lg p-4 border-2 border-gray-200">
<svg viewBox="0 0 340 240" className="w-full" style={{ minHeight: '300px' }}>
{/* Grid */}
<defs>
<pattern id="grid" width="30" height="27" patternUnits="userSpaceOnUse">
<path d="M 30 0 L 0 0 0 27" fill="none" stroke="#e5e7eb" strokeWidth="0.5"/>
</pattern>
</defs>
<rect width="340" height="240" fill="url(#grid)" />
{/* Ejes */}
<line x1="40" y1="210" x2="320" y2="210" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="210" x2="40" y2="30" stroke="#374151" strokeWidth="2" />
{/* Flechas */}
<polygon points="320,210 315,207 315,213" fill="#374151" />
<polygon points="40,30 37,35 43,35" fill="#374151" />
{/* Etiquetas */}
<text x="180" y="235" textAnchor="middle" fill="#6b7280" fontSize="12">
Alimentos (millones de toneladas)
</text>
<text x="15" y="120" textAnchor="middle" fill="#6b7280" fontSize="12" transform="rotate(-90, 15, 120)">
Tecnología (millones de unidades)
</text>
{/* Marcas X */}
{[0, 25, 50, 75, 100].map((val, i) => (
<g key={`x-${val}`}>
<line x1={40 + i * 70} y1="210" x2={40 + i * 70} y2="215" stroke="#374151" />
<text x={40 + i * 70} y="228" textAnchor="middle" fill="#6b7280" fontSize="11">{val}</text>
</g>
))}
{/* Marcas Y */}
{[0, 20, 40, 60, 80].map((val, i) => (
<g key={`y-${val}`}>
<line x1="35" y1={210 - i * 45} x2="40" y2={210 - i * 45} stroke="#374151" />
<text x="30" y={210 - i * 45 + 4} textAnchor="end" fill="#6b7280" fontSize="11">{val}</text>
</g>
))}
{/* Curva FPP */}
{pathData && (
<path
d={pathData}
fill="none"
stroke="#2563eb"
strokeWidth="3"
/>
)}
{/* Punto actual */}
<circle
cx={40 + (bienX / MAX_X) * 280}
cy={210 - (bienY / MAX_Y) * 180}
r="8"
fill={puntoColor}
stroke="white"
strokeWidth="3"
/>
{/* Coordenadas */}
<text
x={40 + (bienX / MAX_X) * 280}
y={210 - (bienY / MAX_Y) * 180 - 15}
textAnchor="middle"
fill="#1f2937"
fontSize="12"
fontWeight="bold"
>
({bienX}, {bienY})
</text>
</svg>
</div>
</div>
</div>
);
}
export default SimuladorDisyuntivas;

View File

@@ -0,0 +1,3 @@
export { SimuladorDisyuntivas } from './SimuladorDisyuntivas';
export { QuizBienes } from './QuizBienes';
export { FlujoCircular } from './FlujoCircular';

View File

@@ -0,0 +1,505 @@
import React, { useState, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { LineChart, Check, X, RotateCcw, Trophy, ArrowRight, HelpCircle } from 'lucide-react';
interface Punto {
x: number;
y: number;
id: string;
}
interface ConstructorCurvasProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
type Nivel = 'demanda' | 'oferta' | 'equilibrio';
type TipoCurva = 'demanda' | 'oferta';
interface NivelConfig {
tipo: Nivel;
titulo: string;
descripcion: string;
tipoCurvaEsperada: TipoCurva | 'ambas';
mensajeExito: string;
}
const niveles: NivelConfig[] = [
{
tipo: 'demanda',
titulo: 'Nivel 1: Curva de Demanda',
descripcion: 'La demanda tiene pendiente negativa (cuando el precio sube, la cantidad demandada baja). Coloca al menos 2 puntos y traza la línea.',
tipoCurvaEsperada: 'demanda',
mensajeExito: '¡Correcto! La curva de demanda tiene pendiente negativa.'
},
{
tipo: 'oferta',
titulo: 'Nivel 2: Curva de Oferta',
descripcion: 'La oferta tiene pendiente positiva (cuando el precio sube, los productores quieren vender más). Coloca al menos 2 puntos.',
tipoCurvaEsperada: 'oferta',
mensajeExito: '¡Correcto! La curva de oferta tiene pendiente positiva.'
},
{
tipo: 'equilibrio',
titulo: 'Nivel 3: Equilibrio de Mercado',
descripcion: 'Dibuja ambas curvas para encontrar el punto de equilibrio donde se cruzan.',
tipoCurvaEsperada: 'ambas',
mensajeExito: '¡Excelente! Has encontrado el equilibrio de mercado.'
}
];
const GRID_SIZE = 300;
const PADDING = 40;
const MAX_PRECIO = 100;
const MAX_CANTIDAD = 100;
export const ConstructorCurvas: React.FC<ConstructorCurvasProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [nivelActual, setNivelActual] = useState(0);
const [puntosDemanda, setPuntosDemanda] = useState<Punto[]>([]);
const [puntosOferta, setPuntosOferta] = useState<Punto[]>([]);
const [modoActivo, setModoActivo] = useState<TipoCurva>('demanda');
const [mensaje, setMensaje] = useState<string>('');
const [showSuccess, setShowSuccess] = useState(false);
const [score, setScore] = useState(0);
const [_startTime] = useState(Date.now());
const [, setPuntosDibujados] = useState<{ demanda: boolean; oferta: boolean }>({ demanda: false, oferta: false });
const svgRef = useRef<SVGSVGElement>(null);
const [draggedPoint, setDraggedPoint] = useState<string | null>(null);
const nivel = niveles[nivelActual];
const cartesianToSvg = useCallback((x: number, y: number) => {
const svgX = PADDING + (x / MAX_CANTIDAD) * GRID_SIZE;
const svgY = PADDING + GRID_SIZE - (y / MAX_PRECIO) * GRID_SIZE;
return { x: svgX, y: svgY };
}, []);
const svgToCartesian = useCallback((svgX: number, svgY: number) => {
const x = ((svgX - PADDING) / GRID_SIZE) * MAX_CANTIDAD;
const y = ((PADDING + GRID_SIZE - svgY) / GRID_SIZE) * MAX_PRECIO;
return {
x: Math.max(0, Math.min(MAX_CANTIDAD, Math.round(x))),
y: Math.max(0, Math.min(MAX_PRECIO, Math.round(y)))
};
}, []);
const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
if (draggedPoint || nivelActual === 2) return;
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const svgX = e.clientX - rect.left;
const svgY = e.clientY - rect.top;
const cartesian = svgToCartesian(svgX, svgY);
if (nivelActual === 0 && puntosDemanda.length >= 4) {
setMensaje('Máximo 4 puntos para la demanda');
return;
}
if (nivelActual === 1 && puntosOferta.length >= 4) {
setMensaje('Máximo 4 puntos para la oferta');
return;
}
const newPoint: Punto = {
x: cartesian.x,
y: cartesian.y,
id: `point-${Date.now()}-${Math.random()}`
};
if (modoActivo === 'demanda') {
setPuntosDemanda(prev => [...prev, newPoint]);
} else {
setPuntosOferta(prev => [...prev, newPoint]);
}
setMensaje('');
};
const handlePointDrag = (pointId: string, _tipo: TipoCurva) => {
setDraggedPoint(pointId);
};
const handlePointMove = (e: React.MouseEvent<SVGSVGElement>) => {
if (!draggedPoint) return;
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const svgX = e.clientX - rect.left;
const svgY = e.clientY - rect.top;
const cartesian = svgToCartesian(svgX, svgY);
const updatePoint = (puntos: Punto[]) =>
puntos.map(p => p.id === draggedPoint ? { ...p, x: cartesian.x, y: cartesian.y } : p);
if (puntosDemanda.some(p => p.id === draggedPoint)) {
setPuntosDemanda(updatePoint);
} else if (puntosOferta.some(p => p.id === draggedPoint)) {
setPuntosOferta(updatePoint);
}
};
const handlePointUp = () => {
setDraggedPoint(null);
};
const calcularPendiente = (puntos: Punto[]): number | null => {
if (puntos.length < 2) return null;
const sorted = [...puntos].sort((a, b) => a.x - b.x);
const first = sorted[0];
const last = sorted[sorted.length - 1];
if (last.x === first.x) return 0;
return (last.y - first.y) / (last.x - first.x);
};
const validarCurva = () => {
const puntos = modoActivo === 'demanda' ? puntosDemanda : puntosOferta;
if (puntos.length < 2) {
setMensaje('Necesitas al menos 2 puntos para trazar una curva');
return;
}
const pendiente = calcularPendiente(puntos);
if (pendiente === null) return;
if (modoActivo === 'demanda') {
if (pendiente >= 0) {
setMensaje('La demanda debe tener pendiente negativa (bajar de izquierda a derecha)');
return;
}
setPuntosDibujados(prev => ({ ...prev, demanda: true }));
} else {
if (pendiente <= 0) {
setMensaje('La oferta debe tener pendiente positiva (subir de izquierda a derecha)');
return;
}
setPuntosDibujados(prev => ({ ...prev, oferta: true }));
}
setMensaje('');
setShowSuccess(true);
setScore(prev => prev + 33);
setTimeout(() => {
if (nivelActual < 2) {
setNivelActual(prev => prev + 1);
setShowSuccess(false);
setMensaje('');
if (nivelActual === 0) setModoActivo('oferta');
}
}, 2000);
};
const validarEquilibrio = () => {
if (puntosDemanda.length < 2 || puntosOferta.length < 2) {
setMensaje('Necesitas trazar ambas curvas con al menos 2 puntos cada una');
return;
}
setShowSuccess(true);
setScore(100);
setTimeout(() => {
if (onComplete) {
onComplete(100);
}
}, 2000);
};
const reiniciar = () => {
setPuntosDemanda([]);
setPuntosOferta([]);
setNivelActual(0);
setModoActivo('demanda');
setMensaje('');
setShowSuccess(false);
setScore(0);
setPuntosDibujados({ demanda: false, oferta: false });
};
const eliminarPunto = (id: string, tipo: TipoCurva) => {
if (tipo === 'demanda') {
setPuntosDemanda(prev => prev.filter(p => p.id !== id));
} else {
setPuntosOferta(prev => prev.filter(p => p.id !== id));
}
};
const renderLineaCurva = (puntos: Punto[], color: string) => {
if (puntos.length < 2) return null;
const sorted = [...puntos].sort((a, b) => a.x - b.x);
const points = sorted.map(p => {
const svg = cartesianToSvg(p.x, p.y);
return `${svg.x},${svg.y}`;
}).join(' ');
return (
<polyline
points={points}
fill="none"
stroke={color}
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
);
};
return (
<div className="w-full max-w-4xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<LineChart className="w-8 h-8 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-800">{nivel.titulo}</h2>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">Nivel {nivelActual + 1} de 3</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${score}%` }}
/>
</div>
<button
onClick={reiniciar}
className="p-2 text-gray-500 hover:text-blue-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
</div>
<p className="text-gray-600">{nivel.descripcion}</p>
</div>
{nivelActual === 2 && (
<div className="mb-4 p-3 bg-blue-50 rounded-lg flex items-center gap-2">
<HelpCircle className="w-5 h-5 text-blue-600" />
<span className="text-sm text-blue-700">
Nivel Avanzado: Dibuja ambas curvas. La demanda (azul) con pendiente negativa,
y la oferta (verde) con pendiente positiva.
</span>
</div>
)}
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1">
<svg
ref={svgRef}
width={GRID_SIZE + 2 * PADDING}
height={GRID_SIZE + 2 * PADDING}
className="border-2 border-gray-300 rounded-lg bg-white cursor-crosshair"
onClick={handleSvgClick}
onMouseMove={handlePointMove}
onMouseUp={handlePointUp}
onMouseLeave={handlePointUp}
>
{/* Grid */}
{Array.from({ length: 11 }).map((_, i) => (
<g key={i}>
<line
x1={PADDING + (i * GRID_SIZE) / 10}
y1={PADDING}
x2={PADDING + (i * GRID_SIZE) / 10}
y2={PADDING + GRID_SIZE}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={PADDING}
y1={PADDING + (i * GRID_SIZE) / 10}
x2={PADDING + GRID_SIZE}
y2={PADDING + (i * GRID_SIZE) / 10}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line
x1={PADDING}
y1={PADDING + GRID_SIZE}
x2={PADDING + GRID_SIZE}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
<line
x1={PADDING}
y1={PADDING}
x2={PADDING}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
{/* Labels ejes */}
<text x={PADDING + GRID_SIZE / 2} y={PADDING + GRID_SIZE + 25} textAnchor="middle" className="text-sm fill-gray-600">
Cantidad
</text>
<text x={15} y={PADDING + GRID_SIZE / 2} textAnchor="middle" transform={`rotate(-90, 15, ${PADDING + GRID_SIZE / 2})`} className="text-sm fill-gray-600">
Precio
</text>
{/* Curvas */}
{(nivelActual === 0 || nivelActual === 2) && renderLineaCurva(puntosDemanda, '#3b82f6')}
{(nivelActual === 1 || nivelActual === 2) && renderLineaCurva(puntosOferta, '#22c55e')}
{/* Puntos Demanda */}
{(nivelActual === 0 || nivelActual === 2) && puntosDemanda.map(punto => {
const svg = cartesianToSvg(punto.x, punto.y);
return (
<motion.g key={punto.id}>
<circle
cx={svg.x}
cy={svg.y}
r="8"
fill="#3b82f6"
stroke="white"
strokeWidth="2"
className="cursor-move hover:r-10"
onMouseDown={() => handlePointDrag(punto.id, 'demanda')}
onClick={(e) => {
e.stopPropagation();
eliminarPunto(punto.id, 'demanda');
}}
/>
<text x={svg.x} y={svg.y - 12} textAnchor="middle" className="text-xs fill-gray-500">
({punto.x}, {punto.y})
</text>
</motion.g>
);
})}
{/* Puntos Oferta */}
{(nivelActual === 1 || nivelActual === 2) && puntosOferta.map(punto => {
const svg = cartesianToSvg(punto.x, punto.y);
return (
<motion.g key={punto.id}>
<circle
cx={svg.x}
cy={svg.y}
r="8"
fill="#22c55e"
stroke="white"
strokeWidth="2"
className="cursor-move"
onMouseDown={() => handlePointDrag(punto.id, 'oferta')}
onClick={(e) => {
e.stopPropagation();
eliminarPunto(punto.id, 'oferta');
}}
/>
<text x={svg.x} y={svg.y - 12} textAnchor="middle" className="text-xs fill-gray-500">
({punto.x}, {punto.y})
</text>
</motion.g>
);
})}
</svg>
</div>
<div className="w-full md:w-64 space-y-4">
{nivelActual === 2 && (
<div className="flex gap-2 p-3 bg-gray-100 rounded-lg">
<button
onClick={() => setModoActivo('demanda')}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition-colors ${
modoActivo === 'demanda'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50'
}`}
>
Demanda
</button>
<button
onClick={() => setModoActivo('oferta')}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition-colors ${
modoActivo === 'oferta'
? 'bg-green-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50'
}`}
>
Oferta
</button>
</div>
)}
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold text-gray-700 mb-2">Puntos colocados:</h3>
{modoActivo === 'demanda' || nivelActual === 2 ? (
<div className="mb-2">
<span className="text-sm text-blue-600 font-medium">Demanda: </span>
<span className="text-sm text-gray-600">{puntosDemanda.length} puntos</span>
</div>
) : null}
{(modoActivo === 'oferta' || nivelActual === 2) && (
<div>
<span className="text-sm text-green-600 font-medium">Oferta: </span>
<span className="text-sm text-gray-600">{puntosOferta.length} puntos</span>
</div>
)}
</div>
{mensaje && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2"
>
<X className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{mensaje}</p>
</motion.div>
)}
{nivelActual < 2 ? (
<button
onClick={validarCurva}
disabled={showSuccess}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
>
<Check className="w-5 h-5" />
Validar Curva
</button>
) : (
<button
onClick={validarEquilibrio}
disabled={showSuccess}
className="w-full py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
>
<Check className="w-5 h-5" />
Validar Equilibrio
</button>
)}
<AnimatePresence>
{showSuccess && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="p-4 bg-green-50 border border-green-200 rounded-lg text-center"
>
<Trophy className="w-8 h-8 text-green-500 mx-auto mb-2" />
<p className="font-semibold text-green-700">{nivel.mensajeExito}</p>
{nivelActual === 2 && (
<div className="mt-3 flex items-center justify-center gap-2 text-green-600">
<span>Completado</span>
<ArrowRight className="w-4 h-4" />
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
);
};
export default ConstructorCurvas;

View File

@@ -0,0 +1,467 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Brain, ArrowRight, ArrowLeft, TrendingUp, TrendingDown, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen } from 'lucide-react';
interface IdentificarShocksProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
type DireccionShock = 'oferta-up' | 'oferta-down' | 'demanda-up' | 'demanda-down';
type CurvaTipo = 'oferta' | 'demanda';
type Direccion = 'arriba' | 'abajo';
interface Escenario {
id: number;
descripcion: string;
respuesta: DireccionShock;
curva: CurvaTipo;
direccion: Direccion;
explicacion: string;
dificultad: 'facil' | 'medio' | 'dificil';
}
const escenarios: Escenario[] = [
{
id: 1,
descripcion: 'Una nueva tecnología permite producir smartphones más rápido y barato.',
respuesta: 'oferta-up',
curva: 'oferta',
direccion: 'arriba',
explicacion: 'La tecnología mejora la productividad, reduciendo costos. Esto aumenta la oferta (la curva se desplaza a la derecha).',
dificultad: 'facil'
},
{
id: 2,
descripcion: 'Se anuncia que el café causa cáncer y la demanda disminuye drásticamente.',
respuesta: 'demanda-down',
curva: 'demanda',
direccion: 'abajo',
explicacion: 'Las preferencias de los consumidores cambian negativamente. La demanda disminuye (la curva se desplaza a la izquierda).',
dificultad: 'facil'
},
{
id: 3,
descripcion: 'Una sequía severa destruye el 40% de la cosecha de trigo.',
respuesta: 'oferta-down',
curva: 'oferta',
direccion: 'abajo',
explicacion: 'La sequía reduce la cantidad disponible de trigo. La oferta disminuye (la curva se desplaza a la izquierda).',
dificultad: 'facil'
},
{
id: 4,
descripcion: 'El ingreso promedio de la población aumenta un 15% (bien normal).',
respuesta: 'demanda-up',
curva: 'demanda',
direccion: 'arriba',
explicacion: 'Para bienes normales, al aumentar el ingreso, aumenta la demanda (la curva se desplaza a la derecha).',
dificultad: 'medio'
},
{
id: 5,
descripcion: 'El precio del petróleo (insumo) sube un 30%.',
respuesta: 'oferta-down',
curva: 'oferta',
direccion: 'abajo',
explicacion: 'Al subir el costo de los insumos, producir es más caro. La oferta disminuye (la curva se desplaza a la izquierda).',
dificultad: 'medio'
},
{
id: 6,
descripcion: 'El gobierno subsidia la compra de autos eléctricos con $5,000.',
respuesta: 'demanda-up',
curva: 'demanda',
direccion: 'arriba',
explicacion: 'El subsidio reduce el precio efectivo para consumidores. La demanda aumenta (la curva se desplaza a la derecha).',
dificultad: 'dificil'
}
];
interface Opcion {
value: DireccionShock;
label: string;
icon: React.ReactNode;
color: string;
}
const opciones: Opcion[] = [
{ value: 'oferta-up', label: 'Oferta ↑', icon: <TrendingUp className="w-5 h-5" />, color: 'green' },
{ value: 'oferta-down', label: 'Oferta ↓', icon: <TrendingDown className="w-5 h-5" />, color: 'red' },
{ value: 'demanda-up', label: 'Demanda ↑', icon: <TrendingUp className="w-5 h-5" />, color: 'blue' },
{ value: 'demanda-down', label: 'Demanda ↓', icon: <TrendingDown className="w-5 h-5" />, color: 'orange' },
];
export const IdentificarShocks: React.FC<IdentificarShocksProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<DireccionShock | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [score, setScore] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [_startTime] = useState(Date.now());
const [completado, setCompletado] = useState(false);
const escenario = escenarios[escenarioActual];
const handleSeleccionar = (respuesta: DireccionShock) => {
if (mostrarResultado) return;
setRespuestaSeleccionada(respuesta);
};
const handleVerificar = () => {
if (!respuestaSeleccionada) return;
const esCorrecta = respuestaSeleccionada === escenario.respuesta;
setMostrarResultado(true);
if (esCorrecta) {
setScore(prev => prev + Math.round(100 / escenarios.length));
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(score);
}
}
};
const handleReiniciar = () => {
setEscenarioActual(0);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
setScore(0);
setRespuestasCorrectas(0);
setCompletado(false);
};
const getDificultadColor = (dificultad: string) => {
switch (dificultad) {
case 'facil': return 'bg-green-100 text-green-700';
case 'medio': return 'bg-yellow-100 text-yellow-700';
case 'dificil': return 'bg-red-100 text-red-700';
default: return 'bg-gray-100 text-gray-700';
}
};
const renderGraficoShock = () => {
const { curva, direccion } = escenario;
const isOferta = curva === 'oferta';
const isUp = direccion === 'arriba';
return (
<svg width="300" height="250" className="mx-auto">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line x1={50 + i * 40} y1="30" x2={50 + i * 40} y2="210" stroke="#e5e7eb" strokeWidth="1" />
<line x1="50" y1={30 + i * 36} x2="250" y2={30 + i * 36} stroke="#e5e7eb" strokeWidth="1" />
</g>
))}
{/* Ejes */}
<line x1="50" y1="210" x2="250" y2="210" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="30" x2="50" y2="210" stroke="#374151" strokeWidth="2" />
{/* Curva original */}
{isOferta ? (
<line x1="80" y1="180" x2="220" y2="80" stroke="#22c55e" strokeWidth="3" />
) : (
<line x1="80" y1="80" x2="220" y2="180" stroke="#3b82f6" strokeWidth="3" />
)}
<text x={isOferta ? 230 : 230} y={isOferta ? 75 : 190} className={`text-sm ${isOferta ? 'fill-green-600' : 'fill-blue-600'}`}>
{isOferta ? 'S₁' : 'D₁'}
</text>
{/* Curva desplazada */}
<motion.g
initial={{ opacity: 0, x: isUp ? 30 : -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
{isOferta ? (
<line x1={isUp ? 110 : 50} y1="180" x2={isUp ? 250 : 190} y2="80" stroke="#22c55e" strokeWidth="3" strokeDasharray="5,5" />
) : (
<line x1={isUp ? 110 : 50} y1="80" x2={isUp ? 250 : 190} y2="180" stroke="#3b82f6" strokeWidth="3" strokeDasharray="5,5" />
)}
<text x={isUp ? 260 : 200} y={isOferta ? 75 : 190} className={`text-sm ${isOferta ? 'fill-green-600' : 'fill-blue-600'}`}>
{isOferta ? 'S₂' : 'D₂'}
</text>
</motion.g>
{/* Flecha de dirección */}
<motion.path
d={isUp ? 'M 280 130 L 300 130' : 'M 300 130 L 280 130'}
stroke={isOferta ? '#22c55e' : '#3b82f6'}
strokeWidth="3"
markerEnd={`url(#arrowhead-${isOferta ? 'green' : 'blue'})`}
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
/>
{/* Defs para flechas */}
<defs>
<marker id="arrowhead-green" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#22c55e" />
</marker>
<marker id="arrowhead-blue" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6" />
</marker>
</defs>
</svg>
);
};
if (completado) {
const porcentaje = Math.round((respuestasCorrectas / escenarios.length) * 100);
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-2xl mx-auto p-8 bg-white rounded-xl shadow-lg text-center"
>
<Trophy className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
<h2 className="text-3xl font-bold text-gray-800 mb-2">¡Ejercicio Completado!</h2>
<p className="text-gray-600 mb-6">Has identificado shocks del mercado</p>
<div className="bg-gray-50 rounded-lg p-6 mb-6">
<div className="text-5xl font-bold text-purple-600 mb-2">{porcentaje}%</div>
<p className="text-gray-600">
{respuestasCorrectas} de {escenarios.length} respuestas correctas
</p>
</div>
<button
onClick={handleReiniciar}
className="inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors"
>
<RotateCcw className="w-5 h-5" />
Intentar de nuevo
</button>
</motion.div>
);
}
return (
<div className="w-full max-w-4xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Brain className="w-8 h-8 text-purple-600" />
<h2 className="text-2xl font-bold text-gray-800">Identificar Shocks del Mercado</h2>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getDificultadColor(escenario.dificultad)}`}>
{escenario.dificultad.toUpperCase()}
</span>
<span className="text-sm text-gray-500">
{escenarioActual + 1} de {escenarios.length}
</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-purple-600"
initial={{ width: 0 }}
animate={{ width: `${((escenarioActual + 1) / escenarios.length) * 100}%` }}
/>
</div>
</div>
</div>
<p className="text-gray-600">
Lee cada escenario e identifica qué curva se desplaza y en qué dirección.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<div className="bg-gradient-to-br from-purple-50 to-blue-50 rounded-lg p-6 mb-4">
<div className="flex items-start gap-3">
<BookOpen className="w-6 h-6 text-purple-600 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-gray-800 mb-2">Escenario {escenario.id}</h3>
<p className="text-gray-700 text-lg">{escenario.descripcion}</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{opciones.map((opcion) => {
const isSelected = respuestaSeleccionada === opcion.value;
const isCorrect = mostrarResultado && opcion.value === escenario.respuesta;
const isWrong = mostrarResultado && isSelected && opcion.value !== escenario.respuesta;
let buttonClass = 'p-4 rounded-lg border-2 transition-all flex flex-col items-center gap-2 ';
if (isCorrect) {
buttonClass += 'border-green-500 bg-green-50';
} else if (isWrong) {
buttonClass += 'border-red-500 bg-red-50';
} else if (isSelected) {
buttonClass += `border-${opcion.color}-500 bg-${opcion.color}-50`;
} else {
buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50';
}
return (
<motion.button
key={opcion.value}
onClick={() => handleSeleccionar(opcion.value)}
disabled={mostrarResultado}
whileHover={!mostrarResultado ? { scale: 1.02 } : {}}
whileTap={!mostrarResultado ? { scale: 0.98 } : {}}
className={buttonClass}
>
{opcion.icon}
<span className={`font-semibold ${
isCorrect ? 'text-green-700' :
isWrong ? 'text-red-700' :
isSelected ? `text-${opcion.color}-700` : 'text-gray-700'
}`}>
{opcion.label}
</span>
{isCorrect && <CheckCircle2 className="w-5 h-5 text-green-600" />}
{isWrong && <XCircle className="w-5 h-5 text-red-600" />}
</motion.button>
);
})}
</div>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`mt-4 p-4 rounded-lg ${
respuestaSeleccionada === escenario.respuesta
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}
>
<div className="flex items-start gap-2">
{respuestaSeleccionada === escenario.respuesta ? (
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
) : (
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-semibold ${
respuestaSeleccionada === escenario.respuesta ? 'text-green-800' : 'text-red-800'
}`}>
{respuestaSeleccionada === escenario.respuesta ? '¡Correcto!' : 'Incorrecto'}
</p>
<p className="text-sm mt-1 text-gray-700">{escenario.explicacion}</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="mt-4 flex gap-3">
{!mostrarResultado ? (
<button
onClick={handleVerificar}
disabled={!respuestaSeleccionada}
className="flex-1 py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Verificar respuesta
</button>
) : (
<button
onClick={handleSiguiente}
className="flex-1 py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
>
{escenarioActual < escenarios.length - 1 ? (
<>
Siguiente
<ArrowRight className="w-5 h-5" />
</>
) : (
'Finalizar'
)}
</button>
)}
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-4 text-center">Visualización del Shock</h3>
{renderGraficoShock()}
<div className="mt-4 p-3 bg-white rounded-lg">
<h4 className="font-medium text-gray-700 mb-2">Leyenda:</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 bg-blue-600"></div>
<span className="text-gray-600">Curva de Demanda (D)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 bg-green-600"></div>
<span className="text-gray-600">Curva de Oferta (S)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 border-t-2 border-dashed border-gray-400"></div>
<span className="text-gray-600">Curva después del shock</span>
</div>
</div>
</div>
<div className="mt-4 p-3 bg-yellow-50 rounded-lg border border-yellow-200">
<p className="text-sm text-yellow-800">
<strong>Tip:</strong> Recuerda que:
</p>
<ul className="text-sm text-yellow-700 mt-1 space-y-1">
<li> Factores de oferta: tecnología, insumos, número de vendedores</li>
<li> Factores de demanda: ingreso, preferencias, precios relacionados</li>
</ul>
</div>
</div>
</div>
<div className="mt-6 flex justify-between items-center">
<button
onClick={() => setEscenarioActual(Math.max(0, escenarioActual - 1))}
disabled={escenarioActual === 0}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Anterior
</button>
<div className="flex items-center gap-1">
{escenarios.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full ${
index === escenarioActual
? 'bg-purple-600'
: index < escenarioActual
? 'bg-green-500'
: 'bg-gray-300'
}`}
/>
))}
</div>
<button
onClick={() => setEscenarioActual(Math.min(escenarios.length - 1, escenarioActual + 1))}
disabled={escenarioActual === escenarios.length - 1}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Siguiente
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
);
};
export default IdentificarShocks;

View File

@@ -0,0 +1,454 @@
import React, { useState, useMemo, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { TrendingUp, TrendingDown, AlertTriangle, Calculator, RotateCcw, Info } from 'lucide-react';
interface SimuladorPreciosProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface CurvaParams {
pendienteDemanda: number;
interceptoDemanda: number;
pendienteOferta: number;
interceptoOferta: number;
}
const DEFAULT_PARAMS: CurvaParams = {
pendienteDemanda: -1.5,
interceptoDemanda: 90,
pendienteOferta: 1.2,
interceptoOferta: 10
};
const calcularEquilibrio = (params: CurvaParams) => {
const { pendienteDemanda, interceptoDemanda, pendienteOferta, interceptoOferta } = params;
// Pd = Po => a + b*Q = c + d*Q
const Q = (interceptoOferta - interceptoDemanda) / (pendienteDemanda - pendienteOferta);
const P = interceptoDemanda + pendienteDemanda * Q;
return { Q: Math.max(0, Q), P: Math.max(0, P) };
};
const calcularCantidadEnPrecio = (precio: number, params: CurvaParams) => {
// P = a + b*Q => Q = (P - a) / b
const Qd = (precio - params.interceptoDemanda) / params.pendienteDemanda;
const Qo = (precio - params.interceptoOferta) / params.pendienteOferta;
return { Qd: Math.max(0, Qd), Qo: Math.max(0, Qo) };
};
export const SimuladorPrecios: React.FC<SimuladorPreciosProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [params, _setParams] = useState<CurvaParams>(DEFAULT_PARAMS);
const [precioMaximo, setPrecioMaximo] = useState<number | null>(null);
const [precioMinimo, setPrecioMinimo] = useState<number | null>(null);
const [showInfo, setShowInfo] = useState(true);
const [startTime] = useState(Date.now());
const [hasInteracted, setHasInteracted] = useState(false);
const equilibrio = useMemo(() => calcularEquilibrio(params), [params]);
const analisis = useMemo(() => {
if (precioMaximo !== null && precioMaximo < equilibrio.P) {
const { Qd, Qo } = calcularCantidadEnPrecio(precioMaximo, params);
const excesoDemanda = Qd - Qo;
const cantidadTransada = Qo;
// Pérdida de peso muerto: área del triángulo
const base = equilibrio.Q - cantidadTransada;
const altura = precioMaximo - (params.interceptoOferta + params.pendienteOferta * cantidadTransada);
const deadweightLoss = 0.5 * base * altura;
return {
tipo: 'precio-maximo' as const,
excesoDemanda: Math.max(0, excesoDemanda),
excesoOferta: 0,
cantidadTransada,
deadweightLoss: Math.max(0, deadweightLoss),
mensaje: 'Precio máximo crea escasez (exceso de demanda)'
};
}
if (precioMinimo !== null && precioMinimo > equilibrio.P) {
const { Qd, Qo } = calcularCantidadEnPrecio(precioMinimo, params);
const excesoOferta = Qo - Qd;
const cantidadTransada = Qd;
const base = equilibrio.Q - cantidadTransada;
const altura = (params.interceptoDemanda + params.pendienteDemanda * cantidadTransada) - precioMinimo;
const deadweightLoss = 0.5 * base * altura;
return {
tipo: 'precio-minimo' as const,
excesoDemanda: 0,
excesoOferta: Math.max(0, excesoOferta),
cantidadTransada,
deadweightLoss: Math.max(0, deadweightLoss),
mensaje: 'Precio mínimo crea superávit (exceso de oferta)'
};
}
return {
tipo: 'equilibrio' as const,
excesoDemanda: 0,
excesoOferta: 0,
cantidadTransada: equilibrio.Q,
deadweightLoss: 0,
mensaje: 'Mercado en equilibrio'
};
}, [precioMaximo, precioMinimo, equilibrio, params]);
useEffect(() => {
if (hasInteracted && (precioMaximo !== null || precioMinimo !== null)) {
const timer = setTimeout(() => {
if (onComplete) {
onComplete(100);
}
}, 5000);
return () => clearTimeout(timer);
}
}, [hasInteracted, precioMaximo, precioMinimo, startTime, onComplete]);
const reset = () => {
setPrecioMaximo(null);
setPrecioMinimo(null);
setHasInteracted(false);
};
// Generar puntos para las curvas
const generateCurvePoints = () => {
const points = [];
for (let Q = 0; Q <= 60; Q += 2) {
const Pd = params.interceptoDemanda + params.pendienteDemanda * Q;
const Po = params.interceptoOferta + params.pendienteOferta * Q;
points.push({ Q, Pd: Math.max(0, Pd), Po: Math.max(0, Po) });
}
return points;
};
const curvePoints = generateCurvePoints();
// Escalar para SVG
const scaleX = (Q: number) => 50 + (Q / 60) * 400;
const scaleY = (P: number) => 350 - (P / 100) * 300;
const demandaPath = curvePoints.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.Pd)}`
).join(' ');
const ofertaPath = curvePoints.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.Po)}`
).join(' ');
return (
<div className="w-full max-w-5xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-3">
<Calculator className="w-8 h-8 text-purple-600" />
Simulador de Precios Intervenidos
</h2>
<p className="text-gray-600 mt-2">
Experimenta con precios máximos y mínimos para ver cómo afectan el equilibrio de mercado.
</p>
</div>
{showInfo && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3"
>
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Cómo usar:</h4>
<ul className="text-sm text-blue-700 space-y-1">
<li> Ajusta los sliders para establecer un precio máximo o mínimo</li>
<li> Observa cómo cambian las cantidades demandadas y ofrecidas</li>
<li> Identifica escasez (exceso de demanda) o superávit (exceso de oferta)</li>
<li> La pérdida de peso muerto representa la ineficiencia creada</li>
</ul>
</div>
<button
onClick={() => setShowInfo(false)}
className="text-blue-600 hover:text-blue-800"
>
×
</button>
</motion.div>
)}
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex justify-between items-center mb-4">
<h3 className="font-semibold text-gray-700">Gráfico de Mercado</h3>
<button
onClick={reset}
className="p-2 text-gray-500 hover:text-purple-600 transition-colors"
>
<RotateCcw className="w-4 h-4" />
</button>
</div>
<svg width="500" height="400" className="w-full">
{/* Grid */}
{Array.from({ length: 11 }).map((_, i) => (
<g key={i}>
<line
x1={50 + (i * 400) / 10}
y1={50}
x2={50 + (i * 400) / 10}
y2={350}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={50}
y1={50 + (i * 300) / 10}
x2={450}
y2={50 + (i * 300) / 10}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line x1="50" y1="350" x2="450" y2="350" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="50" x2="50" y2="350" stroke="#374151" strokeWidth="2" />
{/* Labels */}
<text x="250" y="385" textAnchor="middle" className="text-sm fill-gray-600">Cantidad</text>
<text x="15" y="200" textAnchor="middle" transform="rotate(-90, 15, 200)" className="text-sm fill-gray-600">Precio</text>
{/* Curva de Demanda */}
<path d={demandaPath} fill="none" stroke="#3b82f6" strokeWidth="3" />
<text x="420" y={scaleY(15)} className="text-sm fill-blue-600 font-medium">D</text>
{/* Curva de Oferta */}
<path d={ofertaPath} fill="none" stroke="#22c55e" strokeWidth="3" />
<text x="420" y={scaleY(80)} className="text-sm fill-green-600 font-medium">S</text>
{/* Punto de equilibrio */}
<circle
cx={scaleX(equilibrio.Q)}
cy={scaleY(equilibrio.P)}
r="6"
fill="#8b5cf6"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(equilibrio.Q) + 10} y={scaleY(equilibrio.P)} className="text-xs fill-purple-600">
E
</text>
{/* Línea de precio máximo */}
{precioMaximo !== null && (
<g>
<line
x1="50"
y1={scaleY(precioMaximo)}
x2="450"
y2={scaleY(precioMaximo)}
stroke="#ef4444"
strokeWidth="2"
strokeDasharray="5,5"
/>
<text x="460" y={scaleY(precioMaximo)} className="text-xs fill-red-500">Pmáx</text>
{/* Zona de escasez */}
{analisis.excesoDemanda > 0 && (
<polygon
points={`
${scaleX(analisis.cantidadTransada)} ${scaleY(precioMaximo)}
${scaleX(analisis.cantidadTransada + analisis.excesoDemanda)} ${scaleY(precioMaximo)}
`}
fill="#fef3c7"
opacity="0.5"
/>
)}
</g>
)}
{/* Línea de precio mínimo */}
{precioMinimo !== null && (
<g>
<line
x1="50"
y1={scaleY(precioMinimo)}
x2="450"
y2={scaleY(precioMinimo)}
stroke="#f59e0b"
strokeWidth="2"
strokeDasharray="5,5"
/>
<text x="460" y={scaleY(precioMinimo)} className="text-xs fill-amber-500">Pmín</text>
</g>
)}
{/* Indicador de pérdida de peso muerto */}
{analisis.deadweightLoss > 0 && (
<motion.text
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
x={scaleX(equilibrio.Q / 2)}
y={scaleY((equilibrio.P + (precioMaximo || precioMinimo || 0)) / 2)}
textAnchor="middle"
className="text-xs fill-red-600 font-medium"
>
Pérdida de peso muerto
</motion.text>
)}
</svg>
</div>
<div className="space-y-6">
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-4">Controles de Precio</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Precio Máximo (Pmáx)
</label>
<input
type="range"
min="0"
max={Math.round(equilibrio.P)}
value={precioMaximo ?? Math.round(equilibrio.P)}
onChange={(e) => {
setPrecioMaximo(Number(e.target.value) || null);
setPrecioMinimo(null);
setHasInteracted(true);
}}
className="w-full accent-red-500"
/>
<div className="flex justify-between text-sm text-gray-500 mt-1">
<span>$0</span>
<span className="font-medium text-red-600">
{precioMaximo !== null ? `$${precioMaximo}` : 'Desactivado'}
</span>
<span>${Math.round(equilibrio.P)}</span>
</div>
</div>
<div className="border-t border-gray-200 pt-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Precio Mínimo (Pmín)
</label>
<input
type="range"
min={Math.round(equilibrio.P)}
max="100"
value={precioMinimo ?? Math.round(equilibrio.P)}
onChange={(e) => {
setPrecioMinimo(Number(e.target.value) || null);
setPrecioMaximo(null);
setHasInteracted(true);
}}
className="w-full accent-amber-500"
/>
<div className="flex justify-between text-sm text-gray-500 mt-1">
<span>${Math.round(equilibrio.P)}</span>
<span className="font-medium text-amber-600">
{precioMinimo !== null ? `$${precioMinimo}` : 'Desactivado'}
</span>
<span>$100</span>
</div>
</div>
</div>
</div>
<AnimatePresence mode="wait">
<motion.div
key={analisis.tipo}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`p-4 rounded-lg border ${
analisis.tipo === 'equilibrio'
? 'bg-green-50 border-green-200'
: analisis.tipo === 'precio-maximo'
? 'bg-red-50 border-red-200'
: 'bg-amber-50 border-amber-200'
}`}
>
<div className="flex items-center gap-2 mb-3">
{analisis.tipo === 'equilibrio' ? (
<TrendingUp className="w-5 h-5 text-green-600" />
) : analisis.tipo === 'precio-maximo' ? (
<TrendingDown className="w-5 h-5 text-red-600" />
) : (
<AlertTriangle className="w-5 h-5 text-amber-600" />
)}
<h4 className={`font-semibold ${
analisis.tipo === 'equilibrio'
? 'text-green-800'
: analisis.tipo === 'precio-maximo'
? 'text-red-800'
: 'text-amber-800'
}`}>
{analisis.mensaje}
</h4>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="bg-white rounded p-2">
<span className="text-gray-500">Precio de equilibrio:</span>
<p className="font-semibold text-gray-800">${equilibrio.P.toFixed(1)}</p>
</div>
<div className="bg-white rounded p-2">
<span className="text-gray-500">Cantidad de equilibrio:</span>
<p className="font-semibold text-gray-800">{equilibrio.Q.toFixed(1)} unidades</p>
</div>
{precioMaximo !== null && (
<div className="bg-white rounded p-2">
<span className="text-red-500">Precio máximo:</span>
<p className="font-semibold text-gray-800">${precioMaximo}</p>
</div>
)}
{precioMinimo !== null && (
<div className="bg-white rounded p-2">
<span className="text-amber-500">Precio mínimo:</span>
<p className="font-semibold text-gray-800">${precioMinimo}</p>
</div>
)}
{analisis.excesoDemanda > 0 && (
<div className="bg-red-100 rounded p-2 col-span-2">
<span className="text-red-700">Exceso de demanda (escasez):</span>
<p className="font-semibold text-red-800">{analisis.excesoDemanda.toFixed(1)} unidades</p>
</div>
)}
{analisis.excesoOferta > 0 && (
<div className="bg-amber-100 rounded p-2 col-span-2">
<span className="text-amber-700">Exceso de oferta (superávit):</span>
<p className="font-semibold text-amber-800">{analisis.excesoOferta.toFixed(1)} unidades</p>
</div>
)}
{analisis.deadweightLoss > 0 && (
<div className="bg-red-100 rounded p-2 col-span-2">
<span className="text-red-700">Pérdida de peso muerto:</span>
<p className="font-semibold text-red-800">${analisis.deadweightLoss.toFixed(1)}</p>
</div>
)}
</div>
</motion.div>
</AnimatePresence>
<div className="p-4 bg-purple-50 rounded-lg">
<h4 className="font-semibold text-purple-800 mb-2">Resultado</h4>
<p className="text-sm text-purple-700">
Cantidad transada: <span className="font-bold">{analisis.cantidadTransada.toFixed(1)} unidades</span>
</p>
<p className="text-xs text-purple-600 mt-1">
{analisis.tipo === 'precio-maximo'
? 'Con precio máximo, los vendedores quieren vender menos cantidad.'
: analisis.tipo === 'precio-minimo'
? 'Con precio mínimo, los compradores quieren comprar menos cantidad.'
: 'En equilibrio, la cantidad demandada = cantidad ofrecida.'}
</p>
</div>
</div>
</div>
</div>
);
};
export default SimuladorPrecios;

View File

@@ -0,0 +1,3 @@
export { ConstructorCurvas } from './ConstructorCurvas';
export { SimuladorPrecios } from './SimuladorPrecios';
export { IdentificarShocks } from './IdentificarShocks';

View File

@@ -0,0 +1,266 @@
import { useState, useEffect, useCallback } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
type ElasticidadTipo = 'precio' | 'ingreso' | 'cruzada';
interface CalculadoraElasticidadProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function CalculadoraElasticidad({ ejercicioId: _ejercicioId, onComplete }: CalculadoraElasticidadProps) {
const [tipo, setTipo] = useState<ElasticidadTipo>('precio');
const [p1, setP1] = useState('');
const [p2, setP2] = useState('');
const [q1, setQ1] = useState('');
const [q2, setQ2] = useState('');
const [error, setError] = useState('');
const [resultado, setResultado] = useState<{
deltaQ: number;
deltaP: number;
qPromedio: number;
pPromedio: number;
porcentajeQ: number;
porcentajeP: number;
elasticidad: number;
interpretacion: string;
} | null>(null);
const calcular = useCallback(() => {
const numP1 = parseFloat(p1);
const numP2 = parseFloat(p2);
const numQ1 = parseFloat(q1);
const numQ2 = parseFloat(q2);
if (isNaN(numP1) || isNaN(numP2) || isNaN(numQ1) || isNaN(numQ2)) {
setError('Todos los valores deben ser numéricos');
setResultado(null);
return;
}
if (numP1 === numP2 && tipo === 'precio') {
setError('P1 y P2 no pueden ser iguales para calcular elasticidad');
setResultado(null);
return;
}
setError('');
// Método del punto medio
const deltaQ = numQ2 - numQ1;
const deltaP = numP2 - numP1;
const qPromedio = (numQ1 + numQ2) / 2;
const pPromedio = (numP1 + numP2) / 2;
const porcentajeQ = (deltaQ / qPromedio) * 100;
const porcentajeP = (deltaP / pPromedio) * 100;
const elasticidad = porcentajeQ / porcentajeP;
let interpretacion = '';
const absE = Math.abs(elasticidad);
if (tipo === 'precio') {
if (absE > 1) interpretacion = 'Demanda ELÁSTICA: |E| > 1. El consumo responde más que proporcionalmente al cambio de precio.';
else if (absE < 1) interpretacion = 'Demanda INELÁSTICA: |E| < 1. El consumo responde menos que proporcionalmente al cambio de precio.';
else interpretacion = 'Demanda UNITARIA: |E| = 1. El consumo responde exactamente proporcional al cambio de precio.';
} else if (tipo === 'ingreso') {
if (elasticidad > 1) interpretacion = 'Bien de LUJO: Ei > 1. El gasto en el bien aumenta más que proporcionalmente al ingreso.';
else if (elasticidad > 0 && elasticidad < 1) interpretacion = 'Bien NECESARIO: 0 < Ei < 1. El gasto aumenta menos que proporcionalmente al ingreso.';
else if (elasticidad < 0) interpretacion = 'Bien INFERIOR: Ei < 0. El consumo disminuye cuando aumenta el ingreso.';
else interpretacion = 'Bien NEUTRO: Ei = 0. El consumo no cambia con el ingreso.';
} else {
if (elasticidad > 0) interpretacion = 'BIENES SUSTITUTOS: Ecr > 0. El aumento del precio de Y aumenta la demanda de X.';
else if (elasticidad < 0) interpretacion = 'BIENES COMPLEMENTARIOS: Ecr < 0. El aumento del precio de Y disminuye la demanda de X.';
else interpretacion = 'BIENES INDEPENDIENTES: Ecr = 0. No existe relación entre los bienes.';
}
setResultado({
deltaQ,
deltaP,
qPromedio,
pPromedio,
porcentajeQ,
porcentajeP,
elasticidad,
interpretacion
});
if (onComplete) {
onComplete(100);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [p1, p2, q1, q2, tipo]);
useEffect(() => {
if (p1 && p2 && q1 && q2) {
calcular();
}
}, [p1, p2, q1, q2, tipo, calcular]);
const getLabelPrecio = () => {
if (tipo === 'cruzada') return 'P1/Py1 (Precio del otro bien)';
return 'P1 (Precio inicial)';
};
const getLabelCantidad = () => {
if (tipo === 'ingreso') return 'Q1 (Cantidad con ingreso I1)';
return 'Q1 (Cantidad inicial)';
};
const tipoLabels: Record<ElasticidadTipo, string> = {
precio: 'Elasticidad Precio de la Demanda',
ingreso: 'Elasticidad Ingreso',
cruzada: 'Elasticidad Cruzada'
};
return (
<Card className="max-w-2xl mx-auto">
<CardHeader
title="Calculadora de Elasticidad"
subtitle="Método del punto medio (Arco)"
/>
<div className="space-y-6">
<div className="flex flex-wrap gap-2">
{(['precio', 'ingreso', 'cruzada'] as ElasticidadTipo[]).map((t) => (
<Button
key={t}
variant={tipo === t ? 'primary' : 'outline'}
size="sm"
onClick={() => setTipo(t)}
>
{tipoLabels[t]}
</Button>
))}
</div>
<div className="bg-blue-50 p-4 rounded-lg">
<p className="text-sm text-blue-800 font-medium">{tipoLabels[tipo]}</p>
<p className="text-xs text-blue-600 mt-1">
{tipo === 'precio' && 'Mide la sensibilidad de la cantidad demandada ante cambios en el precio del propio bien'}
{tipo === 'ingreso' && 'Mide la sensibilidad de la cantidad demandada ante cambios en el ingreso del consumidor'}
{tipo === 'cruzada' && 'Mide la sensibilidad de la cantidad demandada de X ante cambios en el precio de Y'}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={getLabelCantidad()}
type="number"
value={q1}
onChange={(e) => setQ1(e.target.value)}
placeholder="Ej: 100"
/>
<Input
label={tipo === 'ingreso' ? 'Q2 (Cantidad con ingreso I2)' : 'Q2 (Cantidad final)'}
type="number"
value={q2}
onChange={(e) => setQ2(e.target.value)}
placeholder="Ej: 80"
/>
<Input
label={getLabelPrecio()}
type="number"
value={p1}
onChange={(e) => setP1(e.target.value)}
placeholder="Ej: 10"
/>
<Input
label={tipo === 'cruzada' ? 'P2/Py2 (Precio final del otro bien)' : 'P2 (Precio final)'}
type="number"
value={p2}
onChange={(e) => setP2(e.target.value)}
placeholder="Ej: 12"
/>
</div>
{error && (
<p className="text-red-500 text-sm bg-red-50 p-3 rounded-lg">{error}</p>
)}
{resultado && (
<div className="space-y-4">
<div className="bg-gray-50 p-4 rounded-lg space-y-3">
<h4 className="font-semibold text-gray-700">Desarrollo paso a paso:</h4>
<div className="space-y-2 text-sm">
<div className="bg-white p-3 rounded border">
<p className="font-medium text-gray-600">Paso 1: Calcular cambios</p>
<p className="font-mono text-gray-800">
ΔQ = Q2 - Q1 = {q2} - {q1} = {resultado.deltaQ.toFixed(2)}
</p>
<p className="font-mono text-gray-800">
ΔP = P2 - P1 = {p2} - {p1} = {resultado.deltaP.toFixed(2)}
</p>
</div>
<div className="bg-white p-3 rounded border">
<p className="font-medium text-gray-600">Paso 2: Calcular promedios</p>
<p className="font-mono text-gray-800">
= (Q1 + Q2) / 2 = ({q1} + {q2}) / 2 = {resultado.qPromedio.toFixed(2)}
</p>
<p className="font-mono text-gray-800">
= (P1 + P2) / 2 = ({p1} + {p2}) / 2 = {resultado.pPromedio.toFixed(2)}
</p>
</div>
<div className="bg-white p-3 rounded border">
<p className="font-medium text-gray-600">Paso 3: Calcular variaciones porcentuales</p>
<p className="font-mono text-gray-800">
%ΔQ = (ΔQ / ) × 100 = ({resultado.deltaQ.toFixed(2)} / {resultado.qPromedio.toFixed(2)}) × 100 = {resultado.porcentajeQ.toFixed(2)}%
</p>
<p className="font-mono text-gray-800">
%ΔP = (ΔP / ) × 100 = ({resultado.deltaP.toFixed(2)} / {resultado.pPromedio.toFixed(2)}) × 100 = {resultado.porcentajeP.toFixed(2)}%
</p>
</div>
<div className="bg-blue-100 p-3 rounded border border-blue-300">
<p className="font-medium text-blue-800">Paso 4: Calcular elasticidad</p>
<p className="font-mono text-blue-900 text-lg">
E = %ΔQ / %ΔP = {resultado.porcentajeQ.toFixed(2)} / {resultado.porcentajeP.toFixed(2)} = <strong>{resultado.elasticidad.toFixed(4)}</strong>
</p>
</div>
</div>
</div>
<div className={`p-4 rounded-lg ${
tipo === 'precio'
? (Math.abs(resultado.elasticidad) > 1
? 'bg-green-100 border border-green-300'
: Math.abs(resultado.elasticidad) < 1
? 'bg-orange-100 border border-orange-300'
: 'bg-blue-100 border border-blue-300')
: tipo === 'ingreso'
? (resultado.elasticidad > 1
? 'bg-purple-100 border border-purple-300'
: resultado.elasticidad > 0
? 'bg-yellow-100 border border-yellow-300'
: 'bg-red-100 border border-red-300')
: (resultado.elasticidad > 0
? 'bg-green-100 border border-green-300'
: 'bg-red-100 border border-red-300')
}`}>
<p className="font-semibold text-gray-800">Interpretación:</p>
<p className="text-gray-700 mt-1">{resultado.interpretacion}</p>
{tipo === 'precio' && Math.abs(resultado.elasticidad) > 1 && (
<p className="text-sm text-gray-600 mt-2">
El ingreso total aumentará si se reduce el precio (efecto cantidad domina).
</p>
)}
{tipo === 'precio' && Math.abs(resultado.elasticidad) < 1 && (
<p className="text-sm text-gray-600 mt-2">
El ingreso total aumentará si se aumenta el precio (efecto precio domina).
</p>
)}
</div>
</div>
)}
</div>
</Card>
);
}
export default CalculadoraElasticidad;

View File

@@ -0,0 +1,228 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface Bien {
id: string;
nombre: string;
descripcion: string;
elasticidad: number;
categoriaCorrecta: Categoria;
}
type Categoria = 'lujo' | 'necesario' | 'inferior';
interface ClasificadorBienesProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
const bienes: Bien[] = [
{ id: '1', nombre: 'Caviar', descripcion: 'Alimento de lujo', elasticidad: 3.5, categoriaCorrecta: 'lujo' },
{ id: '2', nombre: 'Arroz', descripcion: 'Grano básico de consumo', elasticidad: 0.3, categoriaCorrecta: 'necesario' },
{ id: '3', nombre: 'Viajes en primera clase', descripcion: 'Transporte de lujo', elasticidad: 2.8, categoriaCorrecta: 'lujo' },
{ id: '4', nombre: 'Pasta de dientes', descripcion: 'Higiene personal básica', elasticidad: 0.15, categoriaCorrecta: 'necesario' },
{ id: '5', nombre: 'Autobuses', descripcion: 'Transporte público urbano', elasticidad: -0.5, categoriaCorrecta: 'inferior' },
{ id: '6', nombre: 'Frijoles', descripcion: 'Proteína básica', elasticidad: 0.4, categoriaCorrecta: 'necesario' },
{ id: '7', nombre: 'Yates privados', descripcion: 'Embarcaciones recreativas', elasticidad: 4.2, categoriaCorrecta: 'lujo' },
{ id: '8', nombre: 'Pan de bagazo', descripcion: 'Pan económico de baja calidad', elasticidad: -0.8, categoriaCorrecta: 'inferior' },
{ id: '9', nombre: 'Sal', descripcion: 'Condimento esencial', elasticidad: 0.05, categoriaCorrecta: 'necesario' },
{ id: '10', nombre: 'Joyería fina', descripcion: 'Accesorios de oro/plata', elasticidad: 2.2, categoriaCorrecta: 'lujo' },
{ id: '11', nombre: 'Comida rápida barata', descripcion: 'Hamburguesas de bajo costo', elasticidad: -0.3, categoriaCorrecta: 'inferior' },
{ id: '12', nombre: 'Medicinas genéricas', descripcion: 'Productos farmacéuticos básicos', elasticidad: 0.2, categoriaCorrecta: 'necesario' }
];
const categorias: { id: Categoria; nombre: string; descripcion: string; rango: string; color: string }[] = [
{
id: 'lujo',
nombre: 'Bienes de Lujo',
descripcion: 'El gasto aumenta más que proporcionalmente al ingreso',
rango: 'Ei > 1',
color: 'bg-purple-100 border-purple-300 text-purple-800'
},
{
id: 'necesario',
nombre: 'Bienes Necesarios',
descripcion: 'El gasto aumenta menos que proporcionalmente al ingreso',
rango: '0 < Ei < 1',
color: 'bg-blue-100 border-blue-300 text-blue-800'
},
{
id: 'inferior',
nombre: 'Bienes Inferiores',
descripcion: 'El consumo disminuye cuando aumenta el ingreso',
rango: 'Ei < 0',
color: 'bg-red-100 border-red-300 text-red-800'
}
];
export function ClasificadorBienes({ ejercicioId: _ejercicioId, onComplete }: ClasificadorBienesProps) {
const [clasificaciones, setClasificaciones] = useState<Record<string, Categoria | null>>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const [bienesActuales] = useState(() =>
[...bienes].sort(() => Math.random() - 0.5).slice(0, 8)
);
const seleccionarCategoria = (bienId: string, categoria: Categoria) => {
setClasificaciones(prev => ({
...prev,
[bienId]: categoria
}));
};
const verificarResultados = () => {
setMostrarResultados(true);
const correctas = bienesActuales.filter(
bien => clasificaciones[bien.id] === bien.categoriaCorrecta
).length;
const score = Math.round((correctas / bienesActuales.length) * 100);
if (onComplete) {
onComplete(score);
}
return score;
};
const reiniciar = () => {
setClasificaciones({});
setMostrarResultados(false);
};
const getEstadoBien = (bien: Bien) => {
if (!mostrarResultados || !clasificaciones[bien.id]) {
return 'bg-white border-gray-200';
}
const esCorrecto = clasificaciones[bien.id] === bien.categoriaCorrecta;
return esCorrecto
? 'bg-green-50 border-green-400'
: 'bg-red-50 border-red-400';
};
const getIconoEstado = (bien: Bien) => {
if (!mostrarResultados || !clasificaciones[bien.id]) return null;
const esCorrecto = clasificaciones[bien.id] === bien.categoriaCorrecta;
return esCorrecto ? (
<span className="text-green-600 text-xl"></span>
) : (
<span className="text-red-600 text-xl"></span>
);
};
const puntuacion = mostrarResultados
? bienesActuales.filter(bien => clasificaciones[bien.id] === bien.categoriaCorrecta).length
: 0;
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Clasificador de Bienes"
subtitle="Clasifica cada bien según su elasticidad ingreso"
/>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{categorias.map((cat) => (
<div key={cat.id} className={`p-4 rounded-lg border-2 ${cat.color}`}>
<h4 className="font-bold text-lg">{cat.nombre}</h4>
<p className="text-sm font-mono font-semibold">{cat.rango}</p>
<p className="text-xs mt-1 opacity-80">{cat.descripcion}</p>
</div>
))}
</div>
<div className="space-y-3">
{bienesActuales.map((bien) => (
<div
key={bien.id}
className={`p-4 rounded-lg border-2 transition-all ${getEstadoBien(bien)}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-semibold text-gray-800">{bien.nombre}</h4>
{getIconoEstado(bien)}
</div>
<p className="text-sm text-gray-500">{bien.descripcion}</p>
{mostrarResultados && (
<p className="text-xs font-mono mt-1 text-gray-600">
Ei = {bien.elasticidad} {categorias.find(c => c.id === bien.categoriaCorrecta)?.nombre}
</p>
)}
</div>
<div className="flex gap-2">
{categorias.map((cat) => (
<button
key={cat.id}
onClick={() => !mostrarResultados && seleccionarCategoria(bien.id, cat.id)}
disabled={mostrarResultados}
className={`px-3 py-2 rounded text-xs font-medium transition-all ${
clasificaciones[bien.id] === cat.id
? cat.color + ' border-2'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 border-2 border-transparent'
} ${mostrarResultados ? 'cursor-default' : 'cursor-pointer'}`}
>
{cat.nombre.split(' ')[1]}
</button>
))}
</div>
</div>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-gray-600">
Progreso: {Object.keys(clasificaciones).length} / {bienesActuales.length}
</div>
{!mostrarResultados ? (
<Button
onClick={verificarResultados}
disabled={Object.keys(clasificaciones).length < bienesActuales.length}
>
Verificar Resultados
</Button>
) : (
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm text-gray-600">Puntuación</p>
<p className="text-2xl font-bold text-gray-800">
{puntuacion} / {bienesActuales.length}
</p>
</div>
<Button variant="outline" onClick={reiniciar}>
Reiniciar
</Button>
</div>
)}
</div>
{mostrarResultados && (
<div className={`p-4 rounded-lg ${
puntuacion === bienesActuales.length
? 'bg-green-100 border border-green-300'
: puntuacion >= bienesActuales.length / 2
? 'bg-yellow-100 border border-yellow-300'
: 'bg-red-100 border border-red-300'
}`}>
<p className="font-semibold text-gray-800">
{puntuacion === bienesActuales.length
? '¡Perfecto! Has clasificado todos los bienes correctamente.'
: puntuacion >= bienesActuales.length / 2
? '¡Buen trabajo! Sigue practicando para mejorar.'
: 'Necesitas más práctica. Revisa las categorías y vuelve a intentar.'}
</p>
</div>
)}
</div>
</Card>
);
}
export default ClasificadorBienes;

View File

@@ -0,0 +1,404 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface Problema {
id: number;
titulo: string;
descripcion: string;
datos: { etiqueta: string; valor: string }[];
preguntas: {
id: string;
texto: string;
tipo: 'numero' | 'seleccion';
opciones?: string[];
respuestaCorrecta: number | string;
tolerancia?: number;
solucion: string[];
}[];
}
const problemas: Problema[] = [
{
id: 1,
titulo: "Elasticidad Precio de la Demanda",
descripcion: "Una tienda de electrónica observa que cuando el precio de un modelo de laptop aumenta de $800 a $900, la cantidad demandada disminuye de 500 a 400 unidades por mes.",
datos: [
{ etiqueta: "P1", valor: "$800" },
{ etiqueta: "P2", valor: "$900" },
{ etiqueta: "Q1", valor: "500 unidades" },
{ etiqueta: "Q2", valor: "400 unidades" }
],
preguntas: [
{
id: "p1_a",
texto: "Calcule la elasticidad precio de la demanda usando el método del punto medio.",
tipo: "numero",
respuestaCorrecta: -1.89,
tolerancia: 0.05,
solucion: [
"Paso 1: ΔQ = 400 - 500 = -100",
"Paso 2: ΔP = 900 - 800 = 100",
"Paso 3: Q̄ = (500 + 400) / 2 = 450",
"Paso 4: P̄ = (800 + 900) / 2 = 850",
"Paso 5: %ΔQ = (-100 / 450) × 100 = -22.22%",
"Paso 6: %ΔP = (100 / 850) × 100 = 11.76%",
"Paso 7: Ed = -22.22% / 11.76% = -1.89"
]
},
{
id: "p1_b",
texto: "¿Qué tipo de demanda presenta este producto?",
tipo: "seleccion",
opciones: [
"Elástica (|Ed| > 1)",
"Inelástica (|Ed| < 1)",
"Unitaria (|Ed| = 1)"
],
respuestaCorrecta: "Elástica (|Ed| > 1)",
solucion: [
"Como |Ed| = 1.89 > 1, la demanda es ELÁSTICA.",
"Esto significa que los consumidores son sensibles al cambio de precio.",
"Un aumento de precio del 1% reduce la cantidad demandada en aproximadamente 1.89%"
]
}
]
},
{
id: 2,
titulo: "Elasticidad Ingreso",
descripcion: "En una economía, cuando el ingreso promedio de los hogares aumenta de $2,000 a $2,500 mensuales, el consumo de restaurantes de alta categoría aumenta de 2 a 4 visitas mensuales por hogar.",
datos: [
{ etiqueta: "I1", valor: "$2,000" },
{ etiqueta: "I2", valor: "$2,500" },
{ etiqueta: "Q1", valor: "2 visitas" },
{ etiqueta: "Q2", valor: "4 visitas" }
],
preguntas: [
{
id: "p2_a",
texto: "Calcule la elasticidad ingreso.",
tipo: "numero",
respuestaCorrecta: 2.33,
tolerancia: 0.05,
solucion: [
"Paso 1: ΔQ = 4 - 2 = 2",
"Paso 2: ΔI = 2500 - 2000 = 500",
"Paso 3: Q̄ = (2 + 4) / 2 = 3",
"Paso 4: Ī = (2000 + 2500) / 2 = 2250",
"Paso 5: %ΔQ = (2 / 3) × 100 = 66.67%",
"Paso 6: %ΔI = (500 / 2250) × 100 = 22.22%",
"Paso 7: Ei = 66.67% / 22.22% = 3.00"
]
},
{
id: "p2_b",
texto: "¿Qué tipo de bien representa los restaurantes de alta categoría?",
tipo: "seleccion",
opciones: [
"Bien necesario (0 < Ei < 1)",
"Bien de lujo (Ei > 1)",
"Bien inferior (Ei < 0)"
],
respuestaCorrecta: "Bien de lujo (Ei > 1)",
solucion: [
"Como Ei = 3.00 > 1, se trata de un BIEN DE LUJO.",
"El gasto en este bien aumenta más que proporcionalmente al ingreso.",
"Cuando el ingreso crece 10%, el consumo de restaurantes crece 30%"
]
}
]
},
{
id: 3,
titulo: "Elasticidad Cruzada",
descripcion: "Cuando el precio del café aumenta de $4 a $6 por libra, la cantidad demandada de té aumenta de 100 a 150 libras mensuales en el mismo mercado.",
datos: [
{ etiqueta: "Pcafé1", valor: "$4/libra" },
{ etiqueta: "Pcafé2", valor: "$6/libra" },
{ etiqueta: "Qté1", valor: "100 libras" },
{ etiqueta: "Qté2", valor: "150 libras" }
],
preguntas: [
{
id: "p3_a",
texto: "Calcule la elasticidad cruzada entre café y té.",
tipo: "numero",
respuestaCorrecta: 1.0,
tolerancia: 0.05,
solucion: [
"Paso 1: ΔQté = 150 - 100 = 50",
"Paso 2: ΔPcafé = 6 - 4 = 2",
"Paso 3: Qté̄ = (100 + 150) / 2 = 125",
"Paso 4: Pcafé̄ = (4 + 6) / 2 = 5",
"Paso 5: %ΔQté = (50 / 125) × 100 = 40%",
"Paso 6: %ΔPcafé = (2 / 5) × 100 = 40%",
"Paso 7: Ecr = 40% / 40% = 1.0"
]
},
{
id: "p3_b",
texto: "¿Qué relación existe entre café y té?",
tipo: "seleccion",
opciones: [
"Son bienes sustitutos (Ecr > 0)",
"Son bienes complementarios (Ecr < 0)",
"Son bienes independientes (Ecr = 0)"
],
respuestaCorrecta: "Son bienes sustitutos (Ecr > 0)",
solucion: [
"Como Ecr = 1.0 > 0, café y té son BIENES SUSTITUTOS.",
"Cuando sube el precio del café, los consumidores compran más té.",
"Los consumidores pueden sustituir uno por otro según los precios"
]
}
]
}
];
interface Respuesta {
valor: string;
esCorrecta: boolean | null;
}
interface EjerciciosExamenProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function EjerciciosExamen({ ejercicioId: _ejercicioId, onComplete }: EjerciciosExamenProps) {
const [respuestas, setRespuestas] = useState<Record<string, Respuesta>>({});
const [mostrarSolucion, setMostrarSolucion] = useState<Record<number, boolean>>({});
const [problemaActual, setProblemaActual] = useState(0);
const handleRespuesta = (preguntaId: string, valor: string) => {
setRespuestas(prev => ({
...prev,
[preguntaId]: { valor, esCorrecta: null }
}));
};
const verificarRespuesta = (pregunta: Problema['preguntas'][0]) => {
const respuesta = respuestas[pregunta.id];
if (!respuesta) return;
let esCorrecta = false;
if (pregunta.tipo === 'numero') {
const valorNum = parseFloat(respuesta.valor);
const tolerancia = pregunta.tolerancia || 0.05;
esCorrecta = Math.abs(valorNum - (pregunta.respuestaCorrecta as number)) <= tolerancia;
} else {
esCorrecta = respuesta.valor === pregunta.respuestaCorrecta;
}
setRespuestas(prev => ({
...prev,
[pregunta.id]: { ...respuesta, esCorrecta }
}));
};
const toggleSolucion = (problemaId: number) => {
setMostrarSolucion(prev => ({
...prev,
[problemaId]: !prev[problemaId]
}));
};
const calcularPuntuacion = () => {
let correctas = 0;
let total = 0;
problemas.forEach(problema => {
problema.preguntas.forEach(pregunta => {
total++;
if (respuestas[pregunta.id]?.esCorrecta) {
correctas++;
}
});
});
return Math.round((correctas / total) * 100);
};
const finalizarExamen = () => {
const score = calcularPuntuacion();
if (onComplete) {
onComplete(score);
}
return score;
};
const problema = problemas[problemaActual];
const progreso = ((problemaActual + 1) / problemas.length) * 100;
return (
<Card className="max-w-3xl mx-auto">
<CardHeader
title="Ejercicios Tipo Examen"
subtitle={`Problema ${problemaActual + 1} de ${problemas.length}`}
/>
<div className="mb-4">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${progreso}%` }}
/>
</div>
</div>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-lg text-blue-900">{problema.titulo}</h3>
<p className="text-gray-700 mt-2">{problema.descripcion}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-4">
{problema.datos.map((dato, idx) => (
<div key={idx} className="bg-white p-2 rounded text-center">
<span className="font-mono text-sm font-bold">{dato.etiqueta}</span>
<p className="text-xs text-gray-600">{dato.valor}</p>
</div>
))}
</div>
</div>
<div className="space-y-6">
{problema.preguntas.map((pregunta, idx) => {
const respuesta = respuestas[pregunta.id];
const estado = respuesta?.esCorrecta;
return (
<div key={pregunta.id} className="border rounded-lg p-4">
<div className="flex items-start gap-2">
<span className="bg-primary text-white rounded-full w-6 h-6 flex items-center justify-center text-sm flex-shrink-0">
{idx + 1}
</span>
<p className="text-gray-800 font-medium">{pregunta.texto}</p>
</div>
<div className="mt-3 ml-8">
{pregunta.tipo === 'numero' ? (
<div className="flex gap-2">
<Input
type="number"
step="0.01"
value={respuesta?.valor || ''}
onChange={(e) => handleRespuesta(pregunta.id, e.target.value)}
className="w-48"
placeholder="Respuesta numérica"
/>
<Button
variant="outline"
size="sm"
onClick={() => verificarRespuesta(pregunta)}
disabled={!respuesta?.valor}
>
Verificar
</Button>
</div>
) : (
<div className="space-y-2">
{pregunta.opciones?.map((opcion) => (
<label
key={opcion}
className={`flex items-center gap-2 p-2 rounded cursor-pointer transition-colors ${
respuesta?.valor === opcion
? 'bg-blue-100 border-blue-300 border'
: 'hover:bg-gray-50 border border-transparent'
}`}
>
<input
type="radio"
name={pregunta.id}
value={opcion}
checked={respuesta?.valor === opcion}
onChange={(e) => handleRespuesta(pregunta.id, e.target.value)}
className="text-primary"
/>
<span className="text-sm">{opcion}</span>
</label>
))}
<Button
variant="outline"
size="sm"
onClick={() => verificarRespuesta(pregunta)}
disabled={!respuesta?.valor}
>
Verificar
</Button>
</div>
)}
{estado !== null && (
<div className={`mt-3 p-3 rounded ${
estado
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{estado ? '¡Correcto!' : 'Incorrecto. Intenta de nuevo.'}
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => toggleSolucion(parseInt(pregunta.id.split('_')[0]))}
className="mt-2"
>
{mostrarSolucion[parseInt(pregunta.id.split('_')[0])] ? 'Ocultar' : 'Ver'} solución
</Button>
{mostrarSolucion[parseInt(pregunta.id.split('_')[0])] && (
<div className="mt-2 bg-gray-50 p-3 rounded text-sm">
<p className="font-semibold mb-2">Solución paso a paso:</p>
<ul className="space-y-1 text-gray-700">
{pregunta.solucion.map((paso, i) => (
<li key={i} className="font-mono text-xs">{paso}</li>
))}
</ul>
</div>
)}
</div>
</div>
);
})}
</div>
<div className="flex justify-between pt-4 border-t">
<Button
variant="outline"
onClick={() => setProblemaActual(Math.max(0, problemaActual - 1))}
disabled={problemaActual === 0}
>
Anterior
</Button>
{problemaActual < problemas.length - 1 ? (
<Button
onClick={() => setProblemaActual(problemaActual + 1)}
>
Siguiente
</Button>
) : (
<Button
onClick={finalizarExamen}
variant="primary"
>
Finalizar Examen
</Button>
)}
</div>
{problemaActual === problemas.length - 1 && (
<div className="bg-gray-50 p-4 rounded-lg text-center">
<p className="text-gray-600">Puntuación actual: <strong>{calcularPuntuacion()}%</strong></p>
</div>
)}
</div>
</Card>
);
}
export default EjerciciosExamen;

View File

@@ -0,0 +1,3 @@
export { CalculadoraElasticidad } from './CalculadoraElasticidad';
export { ClasificadorBienes } from './ClasificadorBienes';
export { EjerciciosExamen } from './EjerciciosExamen';

View File

@@ -0,0 +1,328 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, RotateCcw, Calculator } from 'lucide-react';
interface FilaCostos {
q: number;
cv: number;
}
interface FilaCalculada extends FilaCostos {
cf: number;
ct: number;
cfme: number;
cvme: number;
cme: number;
cmg: number | null;
}
interface CalculadoraCostosProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function CalculadoraCostos({ ejercicioId: _ejercicioId, onComplete }: CalculadoraCostosProps) {
const CF_BASE = 200;
const [filas, setFilas] = useState<FilaCostos[]>([
{ q: 0, cv: 0 },
{ q: 1, cv: 50 },
{ q: 2, cv: 90 },
{ q: 3, cv: 120 },
{ q: 4, cv: 160 },
{ q: 5, cv: 220 },
{ q: 6, cv: 300 },
{ q: 7, cv: 400 },
{ q: 8, cv: 520 },
]);
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const datosCalculados: FilaCalculada[] = useMemo(() => {
return filas.map((fila, index) => {
const ct = CF_BASE + fila.cv;
const cfme = fila.q > 0 ? CF_BASE / fila.q : 0;
const cvme = fila.q > 0 ? fila.cv / fila.q : 0;
const cme = fila.q > 0 ? ct / fila.q : 0;
const cmg = index > 0 ? ct - (CF_BASE + filas[index - 1].cv) : null;
return {
...fila,
cf: CF_BASE,
ct,
cfme,
cvme,
cme,
cmg,
};
});
}, [filas]);
const handleCvChange = (index: number, valor: string) => {
const numValor = parseFloat(valor) || 0;
const nuevasFilas = [...filas];
nuevasFilas[index] = { ...nuevasFilas[index], cv: numValor };
setFilas(nuevasFilas);
setValidado(false);
};
const validarCalculos = () => {
const nuevosErrores: string[] = [];
datosCalculados.forEach((fila, index) => {
if (fila.ct !== fila.cf + fila.cv) {
nuevosErrores.push(`Fila ${index + 1}: CT no coincide con CF + CV`);
}
if (fila.q > 0 && Math.abs(fila.cme - fila.ct / fila.q) > 0.01) {
nuevosErrores.push(`Fila ${index + 1}: CMe calculado incorrectamente`);
}
});
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0) {
if (onComplete) {
onComplete(100);
}
}
};
const reiniciar = () => {
setFilas([
{ q: 0, cv: 0 },
{ q: 1, cv: 50 },
{ q: 2, cv: 90 },
{ q: 3, cv: 120 },
{ q: 4, cv: 160 },
{ q: 5, cv: 220 },
{ q: 6, cv: 300 },
{ q: 7, cv: 400 },
{ q: 8, cv: 520 },
]);
setValidado(false);
setErrores([]);
};
const maxCT = Math.max(...datosCalculados.map(d => d.ct));
const maxCMe = Math.max(...datosCalculados.filter(d => d.q > 0).map(d => d.cme));
const maxCMg = Math.max(...datosCalculados.filter(d => d.cmg !== null).map(d => d.cmg || 0));
const escalaCT = maxCT > 0 ? 150 / maxCT : 1;
const escalaCMe = maxCMe > 0 ? 150 / maxCMe : 1;
const escalaCMg = maxCMg > 0 ? 150 / maxCMg : 1;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Calculadora de Costos"
subtitle="Ingresa los Costos Variables (CV) y observa los cálculos automáticos"
/>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CF</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 bg-blue-50">CV</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CT</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CFMe</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CVMe</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CMe</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CMg</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((fila, index) => (
<tr key={index} className="border-b hover:bg-gray-50">
<td className="px-3 py-2 font-medium">{fila.q}</td>
<td className="px-3 py-2 text-gray-600">{fila.cf}</td>
<td className="px-3 py-2 bg-blue-50">
<input
type="number"
value={fila.cv}
onChange={(e) => handleCvChange(index, e.target.value)}
className="w-20 px-2 py-1 border rounded text-sm focus:ring-2 focus:ring-primary focus:border-transparent"
min="0"
disabled={fila.q === 0}
/>
</td>
<td className="px-3 py-2 font-medium text-primary">{fila.ct}</td>
<td className="px-3 py-2 text-gray-600">
{fila.q > 0 ? fila.cfme.toFixed(2) : '-'}
</td>
<td className="px-3 py-2 text-gray-600">
{fila.q > 0 ? fila.cvme.toFixed(2) : '-'}
</td>
<td className="px-3 py-2 font-medium text-secondary">
{fila.q > 0 ? fila.cme.toFixed(2) : '-'}
</td>
<td className="px-3 py-2 font-medium text-success">
{fila.cmg !== null ? fila.cmg : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarCalculos} variant="primary">
<Calculator className="w-4 h-4 mr-2" />
Validar Cálculos
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
{validado && errores.length === 0 && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">¡Todos los cálculos son correctos!</span>
</div>
</div>
)}
{validado && errores.length > 0 && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="font-medium text-error mb-2">Se encontraron errores:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card>
<CardHeader
title="Visualización de Curvas de Costos"
subtitle="Gráfico de CT, CMe y CMg"
/>
<div className="space-y-6">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Total (CT)</h4>
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
<svg className="w-full h-full" viewBox="0 0 400 160">
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">CT</text>
{datosCalculados.map((d, i) => (
<text key={i} x={30 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
<polyline
fill="none"
stroke="#2563eb"
strokeWidth="2"
points={datosCalculados.map((d, i) => `${30 + i * 40},${140 - d.ct * escalaCT}`).join(' ')}
/>
{datosCalculados.map((d, i) => (
<circle
key={i}
cx={30 + i * 40}
cy={140 - d.ct * escalaCT}
r="4"
fill="#2563eb"
/>
))}
</svg>
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Medio (CMe) vs Costo Marginal (CMg)</h4>
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
<svg className="w-full h-full" viewBox="0 0 400 160">
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">Costo</text>
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
<text key={i} x={70 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="2"
points={datosCalculados
.filter(d => d.q > 0)
.map((d, i) => `${70 + i * 40},${140 - d.cme * escalaCMe}`)
.join(' ')}
/>
<polyline
fill="none"
stroke="#16a34a"
strokeWidth="2"
strokeDasharray="4"
points={datosCalculados
.filter(d => d.cmg !== null)
.map((d, i) => `${70 + i * 40},${140 - (d.cmg || 0) * escalaCMg}`)
.join(' ')}
/>
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
<circle
key={`cme-${i}`}
cx={70 + i * 40}
cy={140 - d.cme * escalaCMe}
r="4"
fill="#7c3aed"
/>
))}
{datosCalculados.filter(d => d.cmg !== null).map((d, i) => (
<circle
key={`cmg-${i}`}
cx={70 + i * 40}
cy={140 - (d.cmg || 0) * escalaCMg}
r="4"
fill="#16a34a"
/>
))}
<g transform="translate(280, 30)">
<line x1="0" y1="0" x2="20" y2="0" stroke="#7c3aed" strokeWidth="2" />
<text x="25" y="4" className="text-xs fill-gray-600">CMe</text>
<line x1="0" y1="15" x2="20" y2="15" stroke="#16a34a" strokeWidth="2" strokeDasharray="4" />
<text x="25" y="19" className="text-xs fill-gray-600">CMg</text>
</g>
</svg>
</div>
</div>
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas utilizadas:</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li><strong>CT</strong> = CF + CV (Costo Total)</li>
<li><strong>CFMe</strong> = CF / Q (Costo Fijo Medio)</li>
<li><strong>CVMe</strong> = CV / Q (Costo Variable Medio)</li>
<li><strong>CMe</strong> = CT / Q (Costo Medio)</li>
<li><strong>CMg</strong> = ΔCT / ΔQ (Costo Marginal)</li>
</ul>
</Card>
</div>
);
}
export default CalculadoraCostos;

View File

@@ -0,0 +1,318 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Target, TrendingUp, DollarSign } from 'lucide-react';
interface FilaProduccion {
q: number;
ct: number;
}
interface FilaCalculada {
q: number;
precio: number;
it: number;
ct: number;
bt: number;
img: number | null;
cmg: number | null;
}
interface SimuladorProduccionProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function SimuladorProduccion({ ejercicioId: _ejercicioId, onComplete }: SimuladorProduccionProps) {
const [precio, setPrecio] = useState(80);
const datosBase: FilaProduccion[] = [
{ q: 0, ct: 200 },
{ q: 1, ct: 250 },
{ q: 2, ct: 290 },
{ q: 3, ct: 320 },
{ q: 4, ct: 360 },
{ q: 5, ct: 420 },
{ q: 6, ct: 500 },
{ q: 7, ct: 600 },
{ q: 8, ct: 720 },
];
const datosCalculados: FilaCalculada[] = useMemo(() => {
return datosBase.map((fila, index) => {
const it = precio * fila.q;
const bt = it - fila.ct;
const img = index > 0 ? precio : null;
const cmg = index > 0 ? fila.ct - datosBase[index - 1].ct : null;
return {
q: fila.q,
precio,
it,
ct: fila.ct,
bt,
img,
cmg,
};
});
}, [precio]);
const qOptima = useMemo(() => {
let maxBT = -Infinity;
let qOpt = 0;
datosCalculados.forEach((fila) => {
if (fila.bt > maxBT) {
maxBT = fila.bt;
qOpt = fila.q;
}
});
return qOpt;
}, [datosCalculados]);
const verificacionIMgCMg = useMemo(() => {
const filasValidas = datosCalculados.filter(f => f.img !== null && f.cmg !== null);
const filaOptima = filasValidas.find(f => f.q === qOptima);
if (!filaOptima) return null;
return {
img: filaOptima.img,
cmg: filaOptima.cmg,
diferencia: Math.abs((filaOptima.img || 0) - (filaOptima.cmg || 0)),
cumple: Math.abs((filaOptima.img || 0) - (filaOptima.cmg || 0)) < 5,
};
}, [datosCalculados, qOptima]);
const maxValor = Math.max(
...datosCalculados.map(d => Math.max(d.it, d.ct, d.bt > 0 ? d.bt : 0))
);
const escala = maxValor > 0 ? 140 / maxValor : 1;
const handleCompletar = () => {
if (onComplete) {
onComplete(100);
}
return 100;
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Simulador de Decisión de Producción"
subtitle="Encuentra la cantidad óptima que maximiza el beneficio"
/>
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
<label className="block text-sm font-medium text-gray-700 mb-2">
Precio de Mercado (P)
</label>
<div className="flex items-center gap-4">
<DollarSign className="w-5 h-5 text-gray-400" />
<Input
type="number"
value={precio}
onChange={(e) => setPrecio(parseFloat(e.target.value) || 0)}
className="w-32"
min="0"
/>
<span className="text-sm text-gray-500">
Ajusta el precio para ver cómo cambia la decisión óptima
</span>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Precio (P)</th>
<th className="px-3 py-2 text-left font-medium text-primary">IT = P × Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CT</th>
<th className="px-3 py-2 text-left font-medium text-success">BT = IT - CT</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">IMg</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">CMg</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((fila) => (
<tr
key={fila.q}
className={`border-b hover:bg-gray-50 ${
fila.q === qOptima ? 'bg-green-50' : ''
}`}
>
<td className="px-3 py-2 font-medium">{fila.q}</td>
<td className="px-3 py-2">{fila.precio}</td>
<td className="px-3 py-2 font-medium text-primary">{fila.it}</td>
<td className="px-3 py-2">{fila.ct}</td>
<td className={`px-3 py-2 font-bold ${fila.bt >= 0 ? 'text-success' : 'text-error'}`}>
{fila.bt}
</td>
<td className="px-3 py-2 text-gray-600">
{fila.img !== null ? fila.img : '-'}
</td>
<td className="px-3 py-2 text-gray-600">
{fila.cmg !== null ? fila.cmg : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-3">
<Target className="w-6 h-6 text-green-600" />
<div>
<p className="font-semibold text-green-800">
Cantidad Óptima: Q = {qOptima}
</p>
<p className="text-sm text-green-700">
Beneficio Máximo: BT = {datosCalculados.find(d => d.q === qOptima)?.bt}
{' '}(${precio} × {qOptima} - {datosCalculados.find(d => d.q === qOptima)?.ct})
</p>
</div>
</div>
</div>
{verificacionIMgCMg && (
<div className={`mt-4 p-4 rounded-lg ${
verificacionIMgCMg.cumple
? 'bg-success/10 border border-success'
: 'bg-yellow-50 border border-yellow-200'
}`}>
<div className="flex items-center gap-2">
{verificacionIMgCMg.cumple ? (
<CheckCircle className="w-5 h-5 text-success" />
) : (
<TrendingUp className="w-5 h-5 text-yellow-600" />
)}
<span className={`font-medium ${
verificacionIMgCMg.cumple ? 'text-success' : 'text-yellow-800'
}`}>
Verificación IMg CMg:
</span>
</div>
<p className="mt-1 text-sm text-gray-600">
IMg = {verificacionIMgCMg.img}, CMg = {verificacionIMgCMg.cmg}
{' '}(Diferencia: {verificacionIMgCMg.diferencia.toFixed(1)})
</p>
<p className="mt-1 text-sm text-gray-500">
{verificacionIMgCMg.cumple
? '✓ La condición de optimalidad se cumple: IMg ≈ CMg'
: 'La diferencia es significativa, pero el beneficio sigue siendo máximo en Q = ' + qOptima}
</p>
</div>
)}
</Card>
<Card>
<CardHeader
title="Gráfico de IT y CT"
subtitle="Visualiza el punto donde la distancia entre IT y CT es máxima"
/>
<div className="h-64 bg-gray-50 rounded-lg p-4">
<svg className="w-full h-full" viewBox="0 0 500 220">
<line x1="50" y1="190" x2="480" y2="190" stroke="#374151" strokeWidth="1" />
<line x1="50" y1="190" x2="50" y2="20" stroke="#374151" strokeWidth="1" />
<text x="265" y="210" textAnchor="middle" className="text-sm fill-gray-600">Cantidad (Q)</text>
<text x="20" y="105" textAnchor="middle" className="text-sm fill-gray-600" transform="rotate(-90 20 105)">$</text>
{datosCalculados.map((d, i) => (
<text key={i} x={80 + i * 45} y="205" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
<polyline
fill="none"
stroke="#2563eb"
strokeWidth="3"
points={datosCalculados.map((d, i) => `${80 + i * 45},${190 - d.it * escala}`).join(' ')}
/>
<polyline
fill="none"
stroke="#dc2626"
strokeWidth="3"
points={datosCalculados.map((d, i) => `${80 + i * 45},${190 - d.ct * escala}`).join(' ')}
/>
{datosCalculados.map((d, i) => (
<g key={i}>
<circle
cx={80 + i * 45}
cy={190 - d.it * escala}
r="5"
fill="#2563eb"
/>
<circle
cx={80 + i * 45}
cy={190 - d.ct * escala}
r="5"
fill="#dc2626"
/>
</g>
))}
<g transform={`translate(${80 + datosCalculados.findIndex(d => d.q === qOptima) * 45}, ${
190 - (datosCalculados.find(d => d.q === qOptima)?.it || 0) * escala - 20
})`}>
<polygon points="0,0 -8,-15 8,-15" fill="#16a34a" />
<text x="0" y="-20" textAnchor="middle" className="text-xs fill-green-600 font-bold">
Óptimo Q={qOptima}
</text>
</g>
<g transform="translate(380, 40)">
<line x1="0" y1="0" x2="30" y2="0" stroke="#2563eb" strokeWidth="3" />
<text x="40" y="5" className="text-sm fill-gray-700">IT (Ingreso Total)</text>
<line x1="0" y1="20" x2="30" y2="20" stroke="#dc2626" strokeWidth="3" />
<text x="40" y="25" className="text-sm fill-gray-700">CT (Costo Total)</text>
</g>
</svg>
</div>
</Card>
<Card className="bg-gradient-to-r from-green-50 to-blue-50">
<h4 className="font-semibold text-gray-900 mb-3 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
Conceptos Clave
</h4>
<div className="grid md:grid-cols-2 gap-4 text-sm">
<div>
<p className="font-medium text-gray-700 mb-1">Ingreso Total (IT)</p>
<p className="text-gray-600">IT = P × Q</p>
</div>
<div>
<p className="font-medium text-gray-700 mb-1">Beneficio Total (BT)</p>
<p className="text-gray-600">BT = IT - CT</p>
</div>
<div>
<p className="font-medium text-gray-700 mb-1">Ingreso Marginal (IMg)</p>
<p className="text-gray-600">IMg = ΔIT / ΔQ = P (en competencia perfecta)</p>
</div>
<div>
<p className="font-medium text-gray-700 mb-1">Condición de Optimalidad</p>
<p className="text-gray-600">IMg = CMg (producir hasta que el ingreso marginal iguale al costo marginal)</p>
</div>
</div>
</Card>
<div className="flex justify-end">
<Button onClick={handleCompletar} size="lg">
<CheckCircle className="w-5 h-5 mr-2" />
Marcar como Completado
</Button>
</div>
</div>
);
}
export default SimuladorProduccion;

View File

@@ -0,0 +1,344 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Info, TrendingUp } from 'lucide-react';
interface VisualizadorExcedentesProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function VisualizadorExcedentes({ ejercicioId: _ejercicioId, onComplete }: VisualizadorExcedentesProps) {
const [precio, setPrecio] = useState(50);
const demandaParams = { a: 100, b: 1 };
const ofertaParams = { c: 10, d: 0.8 };
const puntoEquilibrio = useMemo(() => {
const { a, b } = demandaParams;
const { c, d } = ofertaParams;
const pEq = (a - c) / (b + d);
const qEq = a - b * pEq;
return { pEq, qEq };
}, []);
const datosCurvas = useMemo(() => {
const puntos = [];
const { a, b } = demandaParams;
const { c, d } = ofertaParams;
for (let q = 0; q <= 100; q += 5) {
const pDemanda = (a - q) / b;
const pOferta = q > 0 ? (q - c) / d : 0;
puntos.push({ q, pDemanda: Math.max(0, pDemanda), pOferta: Math.max(0, pOferta) });
}
return puntos;
}, []);
const excedentes = useMemo(() => {
const { a } = demandaParams;
const { c } = ofertaParams;
const qAlPrecio = Math.max(0, a - demandaParams.b * precio);
const qOfrecida = Math.max(0, c + ofertaParams.d * precio);
const excedenteConsumidor = 0.5 * qAlPrecio * (a - precio);
const excedenteProductor = 0.5 * qOfrecida * (precio - c);
return {
ec: excedenteConsumidor,
ep: excedenteProductor,
total: excedenteConsumidor + excedenteProductor,
qAlPrecio,
qOfrecida,
};
}, [precio]);
const excedentesEquilibrio = useMemo(() => {
const { pEq, qEq } = puntoEquilibrio;
const { a } = demandaParams;
const { c } = ofertaParams;
const ec = 0.5 * qEq * (a - pEq);
const ep = 0.5 * qEq * (pEq - c);
return { ec, ep, total: ec + ep };
}, [puntoEquilibrio]);
const maxP = 100;
const maxQ = 100;
const escalaX = 350 / maxQ;
const escalaY = 180 / maxP;
const handleCompletar = () => {
if (onComplete) {
onComplete(100);
}
return 100;
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Visualizador de Excedentes"
subtitle="Ajusta el precio para ver cómo cambian los excedentes del consumidor y productor"
/>
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
<label className="block text-sm font-medium text-gray-700 mb-2">
Precio de Mercado (P)
</label>
<div className="flex items-center gap-4">
<Input
type="range"
min="20"
max="90"
value={precio}
onChange={(e) => setPrecio(parseFloat(e.target.value))}
className="flex-1"
/>
<span className="text-2xl font-bold text-primary min-w-[80px]">${precio}</span>
</div>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>$20</span>
<span>Precio de equilibrio: ${puntoEquilibrio.pEq.toFixed(1)}</span>
<span>$90</span>
</div>
</div>
<div className="relative h-80 bg-gray-50 rounded-lg p-4 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 400 220" preserveAspectRatio="xMidYMid meet">
<line x1="50" y1="200" x2="380" y2="200" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="200" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
<text x="215" y="215" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad (Q)</text>
<text x="15" y="110" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 15 110)">Precio (P)</text>
{[0, 25, 50, 75, 100].map((q) => (
<g key={q}>
<line
x1={50 + q * escalaX}
y1="200"
x2={50 + q * escalaX}
y2="205"
stroke="#374151"
strokeWidth="1"
/>
<text
x={50 + q * escalaX}
y="215"
textAnchor="middle"
className="text-xs fill-gray-500"
>
{q}
</text>
</g>
))}
{[0, 25, 50, 75, 100].map((p) => (
<g key={p}>
<line
x1="45"
y1={200 - p * escalaY}
x2="50"
y2={200 - p * escalaY}
stroke="#374151"
strokeWidth="1"
/>
<text
x="40"
y={200 - p * escalaY + 4}
textAnchor="end"
className="text-xs fill-gray-500"
>
{p}
</text>
</g>
))}
<line
x1="50"
y1={200 - precio * escalaY}
x2="380"
y2={200 - precio * escalaY}
stroke="#7c3aed"
strokeWidth="2"
strokeDasharray="5,5"
opacity="0.7"
/>
<text x="385" y={200 - precio * escalaY + 4} className="text-xs fill-purple-600 font-medium">
P = {precio}
</text>
{precio > puntoEquilibrio.pEq && (
<polygon
points={`
50,${200 - precio * escalaY}
${50 + excedentes.qAlPrecio * escalaX},${200 - precio * escalaY}
${50 + excedentes.qAlPrecio * escalaX},${200 - ((100 - excedentes.qAlPrecio)) * escalaY}
`}
fill="rgba(37, 99, 235, 0.3)"
stroke="#2563eb"
strokeWidth="1"
/>
)}
{precio < puntoEquilibrio.pEq && (
<polygon
points={`
50,${200 - 12.5 * escalaY}
${50 + excedentes.qOfrecida * escalaX},${200 - precio * escalaY}
50,${200 - precio * escalaY}
`}
fill="rgba(22, 163, 74, 0.3)"
stroke="#16a34a"
strokeWidth="1"
/>
)}
{Math.abs(precio - puntoEquilibrio.pEq) < 2 && (
<>
<polygon
points={`
50,${200 - puntoEquilibrio.pEq * escalaY}
${50 + puntoEquilibrio.qEq * escalaX},${200 - puntoEquilibrio.pEq * escalaY}
${50 + puntoEquilibrio.qEq * escalaX},${200 - 100 * escalaY}
`}
fill="rgba(37, 99, 235, 0.3)"
stroke="#2563eb"
strokeWidth="1"
/>
<polygon
points={`
50,${200 - 12.5 * escalaY}
${50 + puntoEquilibrio.qEq * escalaX},${200 - puntoEquilibrio.pEq * escalaY}
50,${200 - puntoEquilibrio.pEq * escalaY}
`}
fill="rgba(22, 163, 74, 0.3)"
stroke="#16a34a"
strokeWidth="1"
/>
</>
)}
<polyline
fill="none"
stroke="#2563eb"
strokeWidth="3"
points={datosCurvas.map(d => `${50 + d.q * escalaX},${200 - d.pDemanda * escalaY}`).join(' ')}
/>
<polyline
fill="none"
stroke="#16a34a"
strokeWidth="3"
points={datosCurvas.filter(d => d.pOferta >= 0).map(d => `${50 + d.q * escalaX},${200 - d.pOferta * escalaY}`).join(' ')}
/>
<circle
cx={50 + puntoEquilibrio.qEq * escalaX}
cy={200 - puntoEquilibrio.pEq * escalaY}
r="6"
fill="#dc2626"
stroke="white"
strokeWidth="2"
/>
<text
x={50 + puntoEquilibrio.qEq * escalaX}
y={200 - puntoEquilibrio.pEq * escalaY - 12}
textAnchor="middle"
className="text-xs fill-red-600 font-bold"
>
E
</text>
<g transform="translate(320, 40)">
<line x1="0" y1="0" x2="25" y2="0" stroke="#2563eb" strokeWidth="3" />
<text x="30" y="5" className="text-xs fill-gray-700">Demanda</text>
<line x1="0" y1="15" x2="25" y2="15" stroke="#16a34a" strokeWidth="3" />
<text x="30" y="20" className="text-xs fill-gray-700">Oferta</text>
<rect x="0" y="30" width="15" height="15" fill="rgba(37, 99, 235, 0.3)" stroke="#2563eb" />
<text x="20" y="42" className="text-xs fill-gray-700">EC</text>
<rect x="0" y="50" width="15" height="15" fill="rgba(22, 163, 74, 0.3)" stroke="#16a34a" />
<text x="20" y="62" className="text-xs fill-gray-700">EP</text>
</g>
</svg>
</div>
</Card>
<div className="grid md:grid-cols-3 gap-4">
<Card className="bg-blue-50 border-blue-200">
<div className="flex items-center gap-2 mb-2">
<div className="w-4 h-4 bg-blue-500/30 border border-blue-500 rounded" />
<h4 className="font-semibold text-blue-900">Excedente del Consumidor</h4>
</div>
<p className="text-3xl font-bold text-blue-700">${excedentes.ec.toFixed(0)}</p>
<p className="text-sm text-blue-600 mt-1">
Área bajo la curva de demanda y sobre el precio
</p>
</Card>
<Card className="bg-green-50 border-green-200">
<div className="flex items-center gap-2 mb-2">
<div className="w-4 h-4 bg-green-500/30 border border-green-500 rounded" />
<h4 className="font-semibold text-green-900">Excedente del Productor</h4>
</div>
<p className="text-3xl font-bold text-green-700">${excedentes.ep.toFixed(0)}</p>
<p className="text-sm text-green-600 mt-1">
Área sobre la curva de oferta y bajo el precio
</p>
</Card>
<Card className="bg-purple-50 border-purple-200">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-purple-600" />
<h4 className="font-semibold text-purple-900">Excedente Total</h4>
</div>
<p className="text-3xl font-bold text-purple-700">${excedentes.total.toFixed(0)}</p>
<p className="text-sm text-purple-600 mt-1">
EC + EP = Bienestar social total
</p>
</Card>
</div>
<Card className="bg-yellow-50 border-yellow-200">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-yellow-600 mt-0.5" />
<div>
<h4 className="font-semibold text-yellow-900 mb-2">En el Equilibrio de Mercado:</h4>
<div className="grid md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-yellow-800">Precio: </span>
<span className="font-bold">${puntoEquilibrio.pEq.toFixed(1)}</span>
</div>
<div>
<span className="text-yellow-800">Cantidad: </span>
<span className="font-bold">{puntoEquilibrio.qEq.toFixed(1)}</span>
</div>
<div>
<span className="text-yellow-800">Excedente Total: </span>
<span className="font-bold">${excedentesEquilibrio.total.toFixed(0)}</span>
</div>
</div>
<p className="mt-3 text-yellow-800 text-sm">
El equilibrio de mercado maximiza el bienestar social (excedente total).
Cualquier desviación del precio de equilibrio genera pérdida de eficiencia.
</p>
</div>
</div>
</Card>
<div className="flex justify-end">
<Button onClick={handleCompletar} size="lg">
<CheckCircle className="w-5 h-5 mr-2" />
Marcar como Completado
</Button>
</div>
</div>
);
}
export default VisualizadorExcedentes;

View File

@@ -0,0 +1,3 @@
export { CalculadoraCostos } from './CalculadoraCostos';
export { SimuladorProduccion } from './SimuladorProduccion';
export { VisualizadorExcedentes } from './VisualizadorExcedentes';

View File

@@ -0,0 +1,225 @@
import { motion } from 'framer-motion';
import {
Footprints,
BookOpen,
Scale,
StretchHorizontal,
Factory,
GraduationCap,
Target,
Award,
Lock,
Unlock,
Trophy
} from 'lucide-react';
import type { Badge } from '../../types';
const ICON_MAP: Record<string, React.ComponentType<{ size?: number | string; className?: string }>> = {
Footprints,
BookOpen,
Scale,
StretchHorizontal,
Factory,
GraduationCap,
Target,
Award,
};
interface BadgeCardProps {
badge: Badge;
size?: 'sm' | 'md' | 'lg';
}
export function BadgeCard({ badge, size = 'md' }: BadgeCardProps) {
const Icon = ICON_MAP[badge.icono] || Trophy;
const sizeClasses = {
sm: {
container: 'p-3',
icon: 20,
title: 'text-xs',
desc: 'text-[10px]',
},
md: {
container: 'p-4',
icon: 28,
title: 'text-sm',
desc: 'text-xs',
},
lg: {
container: 'p-5',
icon: 36,
title: 'text-base',
desc: 'text-sm',
},
};
if (badge.desbloqueado) {
return (
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
whileHover={{ scale: 1.05 }}
className={`${sizeClasses[size].container} bg-gradient-to-br from-yellow-50 to-orange-50 rounded-xl border-2 border-yellow-300 shadow-sm hover:shadow-md transition-all`}
>
<div className="flex flex-col items-center text-center">
<div className="relative">
<motion.div
animate={{
rotate: [0, -5, 5, 0],
scale: [1, 1.1, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
repeatDelay: 3
}}
className="w-12 h-12 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full flex items-center justify-center mb-2"
>
<Icon size={sizeClasses[size].icon} className="text-white" />
</motion.div>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className="absolute -top-1 -right-1 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center"
>
<Unlock size={12} className="text-white" />
</motion.div>
</div>
<h4 className={`${sizeClasses[size].title} font-bold text-gray-800 mb-1`}>
{badge.titulo}
</h4>
<p className={`${sizeClasses[size].desc} text-gray-600 line-clamp-2`}>
{badge.descripcion}
</p>
{badge.fechaDesbloqueo && (
<p className="text-[10px] text-gray-400 mt-2">
Desbloqueado: {new Date(badge.fechaDesbloqueo).toLocaleDateString()}
</p>
)}
</div>
</motion.div>
);
}
return (
<div className={`${sizeClasses[size].container} bg-gray-50 rounded-xl border-2 border-gray-200 opacity-70`}>
<div className="flex flex-col items-center text-center">
<div className="relative">
<div className="w-12 h-12 bg-gray-300 rounded-full flex items-center justify-center mb-2">
<Icon size={sizeClasses[size].icon} className="text-gray-500" />
</div>
<div className="absolute -top-1 -right-1 w-5 h-5 bg-gray-400 rounded-full flex items-center justify-center">
<Lock size={12} className="text-white" />
</div>
</div>
<h4 className={`${sizeClasses[size].title} font-bold text-gray-500 mb-1`}>
{badge.titulo}
</h4>
<p className={`${sizeClasses[size].desc} text-gray-400 line-clamp-2`}>
{badge.descripcion}
</p>
</div>
</div>
);
}
interface BadgesGridProps {
badges: Badge[];
columns?: 2 | 3 | 4;
size?: 'sm' | 'md' | 'lg';
}
export function BadgesGrid({ badges, columns = 4, size = 'md' }: BadgesGridProps) {
const columnClasses = {
2: 'grid-cols-2',
3: 'grid-cols-2 md:grid-cols-3',
4: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
};
return (
<div className={`grid ${columnClasses[columns]} gap-4`}>
{badges.map((badge, index) => (
<motion.div
key={badge.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<BadgeCard badge={badge} size={size} />
</motion.div>
))}
</div>
);
}
interface BadgesSectionProps {
badgesDesbloqueados: Badge[];
badgesBloqueados: Badge[];
}
export function BadgesSection({ badgesDesbloqueados, badgesBloqueados }: BadgesSectionProps) {
const totalBadges = badgesDesbloqueados.length + badgesBloqueados.length;
const porcentaje = totalBadges > 0 ? Math.round((badgesDesbloqueados.length / totalBadges) * 100) : 0;
return (
<div className="space-y-6">
{/* Resumen */}
<div className="bg-white rounded-xl p-4 border border-gray-200">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
<Trophy size={20} className="text-yellow-600" />
</div>
<div>
<h3 className="font-bold text-gray-900">Logros</h3>
<p className="text-sm text-gray-500">
{badgesDesbloqueados.length} de {totalBadges} desbloqueados
</p>
</div>
</div>
<span className="text-2xl font-bold text-yellow-600">
{porcentaje}%
</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-3 overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${porcentaje}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
/>
</div>
</div>
{/* Badges Desbloqueados */}
{badgesDesbloqueados.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<Unlock size={16} className="text-green-500" />
Desbloqueados ({badgesDesbloqueados.length})
</h4>
<BadgesGrid badges={badgesDesbloqueados} columns={4} />
</div>
)}
{/* Badges Bloqueados */}
{badgesBloqueados.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-500 mb-3 flex items-center gap-2">
<Lock size={16} className="text-gray-400" />
Por desbloquear ({badgesBloqueados.length})
</h4>
<BadgesGrid badges={badgesBloqueados} columns={4} />
</div>
)}
</div>
);
}
export default BadgesGrid;

View File

@@ -0,0 +1,81 @@
import { motion } from 'framer-motion';
interface ProgressBarProps {
porcentaje: number;
moduloNumero: number;
showLabel?: boolean;
size?: 'sm' | 'md' | 'lg';
}
export function ProgressBar({
porcentaje,
moduloNumero,
showLabel = true,
size = 'md'
}: ProgressBarProps) {
// Determinar color según progreso
const getColor = () => {
if (porcentaje < 30) return 'bg-red-500';
if (porcentaje < 70) return 'bg-yellow-500';
return 'bg-green-500';
};
// Determinar color del borde/fondo según progreso
const getBgColor = () => {
if (porcentaje < 30) return 'bg-red-100';
if (porcentaje < 70) return 'bg-yellow-100';
return 'bg-green-100';
};
const getTextColor = () => {
if (porcentaje < 30) return 'text-red-700';
if (porcentaje < 70) return 'text-yellow-700';
return 'text-green-700';
};
const sizeClasses = {
sm: 'h-2',
md: 'h-4',
lg: 'h-6',
};
return (
<div className="w-full">
{showLabel && (
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">
Módulo {moduloNumero}
</span>
<span className={`text-sm font-bold ${getTextColor()}`}>
{porcentaje}%
</span>
</div>
)}
<div className={`w-full ${getBgColor()} rounded-full overflow-hidden ${sizeClasses[size]}`}>
<motion.div
className={`${getColor()} ${sizeClasses[size]} rounded-full`}
initial={{ width: 0 }}
animate={{ width: `${porcentaje}%` }}
transition={{
duration: 0.8,
ease: "easeOut",
delay: 0.2
}}
/>
</div>
{porcentaje === 100 && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="text-xs text-green-600 mt-1 font-medium"
>
¡Módulo completado!
</motion.p>
)}
</div>
);
}
export default ProgressBar;

View File

@@ -0,0 +1,212 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Star } from 'lucide-react';
import type { NivelUsuario } from '../../types';
interface ScoreDisplayProps {
puntos: number;
animar?: boolean;
showNivel?: boolean;
size?: 'sm' | 'md' | 'lg';
}
const NIVELES_CONFIG: Record<NivelUsuario, { color: string; bgColor: string; icon: string }> = {
Novato: {
color: 'text-gray-600',
bgColor: 'bg-gray-100',
icon: '🌱'
},
Aprendiz: {
color: 'text-blue-600',
bgColor: 'bg-blue-100',
icon: '📚'
},
Experto: {
color: 'text-purple-600',
bgColor: 'bg-purple-100',
icon: '🏆'
},
Maestro: {
color: 'text-yellow-600',
bgColor: 'bg-yellow-100',
icon: '👑'
},
};
function calcularNivel(puntuacion: number): NivelUsuario {
if (puntuacion >= 2000) return 'Maestro';
if (puntuacion >= 1000) return 'Experto';
if (puntuacion >= 300) return 'Aprendiz';
return 'Novato';
}
function calcularProgresoNivel(puntuacion: number): { actual: number; siguiente: number; porcentaje: number } {
if (puntuacion >= 2000) {
return { actual: 2000, siguiente: 2000, porcentaje: 100 };
} else if (puntuacion >= 1000) {
return { actual: puntuacion, siguiente: 2000, porcentaje: ((puntuacion - 1000) / 1000) * 100 };
} else if (puntuacion >= 300) {
return { actual: puntuacion, siguiente: 1000, porcentaje: ((puntuacion - 300) / 700) * 100 };
} else {
return { actual: puntuacion, siguiente: 300, porcentaje: (puntuacion / 300) * 100 };
}
}
export function ScoreDisplay({
puntos,
animar = false,
showNivel = true,
size = 'md'
}: ScoreDisplayProps) {
const [puntosAnimados, setPuntosAnimados] = useState(0);
const [puntosPrevios, setPuntosPrevios] = useState(puntos);
const [cambioReciente, setCambioReciente] = useState(0);
const nivel = calcularNivel(puntos);
const configNivel = NIVELES_CONFIG[nivel];
const progresoNivel = calcularProgresoNivel(puntos);
useEffect(() => {
if (animar && puntos !== puntosPrevios) {
const diferencia = puntos - puntosPrevios;
setCambioReciente(diferencia);
// Animar contador
const duracion = 1500;
const pasos = 60;
let pasoActual = 0;
const intervalo = setInterval(() => {
pasoActual++;
const progreso = pasoActual / pasos;
// Función de easing
const easeOutQuart = 1 - Math.pow(1 - progreso, 4);
setPuntosAnimados(Math.round(puntosPrevios + (diferencia * easeOutQuart)));
if (pasoActual >= pasos) {
clearInterval(intervalo);
setPuntosAnimados(puntos);
setPuntosPrevios(puntos);
// Ocultar el cambio después de 3 segundos
setTimeout(() => setCambioReciente(0), 3000);
}
}, duracion / pasos);
return () => clearInterval(intervalo);
} else {
setPuntosAnimados(puntos);
setPuntosPrevios(puntos);
}
}, [puntos, animar, puntosPrevios]);
const sizeClasses = {
sm: {
container: 'p-2',
puntos: 'text-xl',
label: 'text-xs',
icon: 16,
},
md: {
container: 'p-4',
puntos: 'text-3xl',
label: 'text-sm',
icon: 24,
},
lg: {
container: 'p-6',
puntos: 'text-5xl',
label: 'text-base',
icon: 32,
},
};
return (
<div className={`${sizeClasses[size].container} bg-white rounded-xl border border-gray-200 shadow-sm`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${configNivel.bgColor}`}>
<Star size={sizeClasses[size].icon} className={configNivel.color} />
</div>
<div>
<p className={`${sizeClasses[size].label} text-gray-500 font-medium`}>Puntuación Total</p>
<div className="flex items-baseline gap-2">
<motion.span
className={`${sizeClasses[size].puntos} font-bold text-gray-900`}
key={puntosAnimados}
initial={animar ? { scale: 1.2 } : false}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 300 }}
>
{puntosAnimados.toLocaleString()}
</motion.span>
<span className="text-sm text-gray-400">pts</span>
<AnimatePresence>
{cambioReciente !== 0 && (
<motion.span
initial={{ opacity: 0, y: 20, scale: 0.5 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20 }}
className={`text-sm font-bold ${cambioReciente > 0 ? 'text-green-600' : 'text-red-600'}`}
>
{cambioReciente > 0 ? '+' : ''}{cambioReciente}
</motion.span>
)}
</AnimatePresence>
</div>
</div>
</div>
{showNivel && (
<div className="text-right">
<motion.div
key={nivel}
initial={animar ? { rotate: -10 } : false}
animate={{ rotate: 0 }}
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full ${configNivel.bgColor}`}
>
<span className="text-lg">{configNivel.icon}</span>
<span className={`text-sm font-bold ${configNivel.color}`}>{nivel}</span>
</motion.div>
</div>
)}
</div>
{showNivel && progresoNivel.siguiente > progresoNivel.actual && (
<div className="mt-4">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>Progreso hacia {calcularNivel(progresoNivel.siguiente)}</span>
<span>{Math.round(progresoNivel.porcentaje)}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-2 overflow-hidden">
<motion.div
className={`h-full rounded-full ${configNivel.color.replace('text-', 'bg-')}`}
initial={{ width: 0 }}
animate={{ width: `${progresoNivel.porcentaje}%` }}
transition={{ duration: 0.5, delay: 0.3 }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">
{progresoNivel.siguiente - progresoNivel.actual} puntos para el siguiente nivel
</p>
</div>
)}
{progresoNivel.porcentaje === 100 && nivel === 'Maestro' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-3 p-2 bg-yellow-50 rounded-lg border border-yellow-200"
>
<p className="text-xs text-yellow-700 text-center font-medium">
🎉 ¡Has alcanzado el nivel máximo!
</p>
</motion.div>
)}
</div>
);
}
export default ScoreDisplay;

View File

@@ -0,0 +1,3 @@
export { ProgressBar } from './ProgressBar';
export { ScoreDisplay } from './ScoreDisplay';
export { BadgesGrid, BadgesSection, BadgeCard } from './Badges';

View File

@@ -3,11 +3,17 @@ import { ReactNode } from 'react';
interface CardProps {
children: ReactNode;
className?: string;
onClick?: () => void;
}
export function Card({ children, className = '' }: CardProps) {
export function Card({ children, className = '', onClick }: CardProps) {
return (
<div className={`bg-white rounded-xl shadow-md p-6 ${className}`}>
<div
className={`bg-white rounded-xl shadow-md p-6 ${className}`}
onClick={onClick}
role={onClick ? "button" : undefined}
tabIndex={onClick ? 0 : undefined}
>
{children}
</div>
);

View File

@@ -0,0 +1,20 @@
import { Loader2 } from 'lucide-react';
interface LoaderProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-8 h-8',
lg: 'w-12 h-12',
};
export function Loader({ size = 'md', className = '' }: LoaderProps) {
return (
<Loader2
className={`animate-spin text-blue-600 ${sizeClasses[size]} ${className}`}
/>
);
}

View File

@@ -0,0 +1,141 @@
import type { ModuloContenido } from './introduccion';
export const agentes: ModuloContenido = {
titulo: 'Agentes Económicos',
contenido: [
{
titulo: 'Las Familias o Hogares',
contenido: `Las familias constituyen la unidad básica de consumo en la economía. Sus funciones principales son:
**Como consumidores:**
- Adquieren bienes y servicios para satisfacer sus necesidades
- Toman decisiones sobre qué comprar, cuánto y a qué precio
- Maximizan su utilidad (satisfacción) dado su presupuesto
**Como oferentes de factores productivos:**
- Proporcionan trabajo a cambio de salarios
- Ofrecen capital (ahorros) a cambio de intereses
- Entregan tierra/naturaleza a cambio de renta
- Aportan habilidades empresariales a cambio de beneficios
Las decisiones de las familias están influenciadas por sus ingresos, preferencias, precios y expectativas futuras.`
},
{
titulo: 'Las Empresas',
contenido: `Las empresas son las unidades productivas que transforman insumos en bienes y servicios. Sus características principales:
**Funciones:**
- Compran factores de producción (trabajo, capital, materias primas)
- Organizan el proceso productivo
- Venden bienes y servicios en los mercados
**Objetivo principal:**
Maximizar beneficios (diferencia entre ingresos y costos)
**Clasificación por tamaño:**
- Microempresas: menos de 10 trabajadores
- Pequeñas empresas: 10-50 trabajadores
- Medianas empresas: 50-250 trabajadores
- Grandes empresas: más de 250 trabajadores
**Clasificación por sector:**
- Sector primario: extracción de recursos naturales
- Sector secundario: industria y manufactura
- Sector terciario: servicios`
},
{
titulo: 'El Estado o Gobierno',
contenido: `El Estado interviene en la economía para corregir fallas del mercado, redistribuir ingresos y estabilizar la economía:
**Funciones económicas:**
1. **Función rectora o reguladora:**
- Establece normas y leyes (ley de competencia, protección al consumidor)
- Regula sectores estratégicos
- Protege la propiedad intelectual
2. **Función productiva:**
- Produce bienes y servicios públicos (educación, salud, defensa)
- Gestiona empresas públicas
3. **Función redistributiva:**
- Recauda impuestos
- Transfiere recursos a quienes más lo necesitan
- Proporciona seguridad social
4. **Función estabilizadora:**
- Política fiscal (gasto e impuestos)
- Política monetaria (a través del banco central)
- Control de inflación y desempleo`
},
{
titulo: 'El Sector Externo',
contenido: `El sector externo comprende todas las transacciones económicas con el resto del mundo:
**Intercambios principales:**
- **Exportaciones:** Bienes y servicios vendidos al exterior (generan entrada de divisas)
- **Importaciones:** Bienes y servicios comprados del exterior (generan salida de divisas)
**Agentes del sector externo:**
- Empresas multinacionales
- Inversionistas extranjeros
- Turistas
- Organismos internacionales (FMI, Banco Mundial)
**Impacto económico:**
- Aporta divisas necesarias para importaciones
- Genera competencia para empresas locales
- Transfiere tecnología y conocimiento
- Crea empleo (zonas francas, exportaciones)
**Balanza comercial:**
- Superávit: Exportaciones > Importaciones
- Déficit: Importaciones > Exportaciones`
},
{
titulo: 'El Flujo Circular de la Renta',
contenido: `El flujo circular de la renta es un modelo que muestra cómo interactúan los agentes económicos y cómo circulan bienes, servicios y dinero en la economía.
**Flujos reales (bienes y servicios):**
1. Familias → Empresas: Factores de producción (trabajo, capital, tierra)
2. Empresas → Familias: Bienes y servicios para consumo
**Flujos monetarios (dinero):**
1. Empresas → Familias: Pagos por factores (salarios, rentas, intereses, beneficios)
2. Familias → Empresas: Gasto en consumo
**Inclusión del Estado:**
- El Estado recauda impuestos de familias y empresas
- El Estado gasta en bienes públicos y transferencias
**Inclusión del sector externo:**
- Exportaciones: Dinero entra al país
- Importaciones: Dinero sale del país
**Identidad macroeconómica básica:**
Ingreso = Producción = Gasto`
}
],
ejercicios: [
{
id: 'flujo-circular-juego',
tipo: 'juego',
titulo: 'Juego del Flujo Circular',
descripcion: 'Arrastra cada elemento a su lugar correcto en el diagrama del flujo circular de la renta',
config: {
agentes: ['Familias', 'Empresas', 'Estado', 'Sector Externo'],
flujos: [
{ origen: 'Familias', destino: 'Empresas', tipo: 'factor', nombre: 'Trabajo' },
{ origen: 'Empresas', destino: 'Familias', tipo: 'monetario', nombre: 'Salarios' },
{ origen: 'Empresas', destino: 'Familias', tipo: 'real', nombre: 'Bienes' },
{ origen: 'Familias', destino: 'Empresas', tipo: 'monetario', nombre: 'Consumo' },
{ origen: 'Familias', destino: 'Estado', tipo: 'monetario', nombre: 'Impuestos' },
{ origen: 'Estado', destino: 'Familias', tipo: 'monetario', nombre: 'Transferencias' }
],
dificultad: 'intermedio'
}
}
]
};
export default agentes;

View File

@@ -0,0 +1,404 @@
import type { Ejercicio } from './introduccion';
export interface EjercicioDetallado extends Ejercicio {
instrucciones: string;
pistas?: string[];
solucion?: string;
dificultad: 'facil' | 'medio' | 'dificil';
duracionEstimada: number; // en minutos
objetivosAprendizaje: string[];
}
export interface ModuloEjercicios {
titulo: string;
descripcion: string;
ejercicios: EjercicioDetallado[];
}
export const ejercicios: ModuloEjercicios = {
titulo: 'Ejercicios Prácticos - Módulo 1',
descripcion: 'Pon a prueba tus conocimientos con estos ejercicios interactivos sobre fundamentos de economía',
ejercicios: [
{
id: 'simulador-disyuntivas',
tipo: 'slider',
titulo: 'Simulador de Disyuntivas Económicas',
descripcion: 'Explora cómo una economía debe elegir entre producir diferentes bienes con recursos limitados',
instrucciones: `En este ejercicio, juegas el rol de un planificador económico que debe decidir cómo asignar los recursos de una economía entre dos bienes: Alimentos y Tecnología.
1. Usa los sliders para ajustar la producción de cada bien
2. Observa cómo la frontera de posibilidades de producción (FPP) muestra tus opciones
3. Identifica los costos de oportunidad de cada decisión
4. Experimenta con diferentes combinaciones y encuentra la asignación más eficiente
Recuerda: No puedes estar fuera de la frontera sin más recursos, y estar dentro significa ineficiencia.`,
dificultad: 'medio',
duracionEstimada: 15,
objetivosAprendizaje: [
'Comprender el concepto de escasez y elección',
'Visualizar la frontera de posibilidades de producción',
'Calcular costos de oportunidad',
'Identificar puntos eficientes, ineficientes e inalcanzables'
],
config: {
escenario: {
titulo: 'Economía Agrícola-Tecnológica',
descripcion: 'Una economía con recursos limitados debe decidir entre producir alimentos (bien de primera necesidad) o bienes tecnológicos (computadoras, smartphones)',
bienA: {
nombre: 'Alimentos',
unidad: 'millones de toneladas',
maxProduccion: 100,
color: '#4CAF50'
},
bienB: {
nombre: 'Tecnología',
unidad: 'millones de unidades',
maxProduccion: 80,
color: '#2196F3'
}
},
parametros: {
mostrarFPP: true,
mostrarCostoOportunidad: true,
mostrarPuntoActual: true,
tipoCurva: 'concava', // refleja costos crecientes
puntosDesplazamiento: [
{ causa: 'Mejora tecnológica en agricultura', efecto: 'fpp-externo-alimentos' },
{ causa: 'Innovación tecnológica general', efecto: 'fpp-externo-ambos' }
]
},
preguntasReflexion: [
'¿Qué representa la pendiente de la FPP?',
'¿Por qué la curva es cóncava y no una línea recta?',
'¿Qué pasaría si la economía está en un punto dentro de la FPP?',
'¿Cómo afectaría un terremoto a la FPP?'
]
},
pistas: [
'El costo de oportunidad es lo que sacrificas de un bien para obtener más del otro',
'La FPP es cóncava porque los recursos no son perfectamente sustituibles entre sectores',
'Un punto sobre la FPP es eficiente; dentro es ineficiente; fuera es inalcanzable'
],
solucion: `La FPP muestra que:
1. Existe un trade-off: más alimentos significan menos tecnología y viceversa
2. Los costos de oportunidad crecen conforme nos especializamos en un bien
3. La eficiencia requiere estar sobre la frontera
4. El crecimiento económico desplaza la FPP hacia afuera`
},
{
id: 'quiz-clasificacion-bienes',
tipo: 'quiz',
titulo: 'Quiz: Clasificación de Bienes y Servicios',
descripcion: 'Aprende a clasificar bienes según el comportamiento de la demanda ante cambios en el ingreso',
instrucciones: `Clasifica cada bien en la categoría correcta según cómo responde su demanda ante cambios en el ingreso de los consumidores:
- **Bien Normal**: La demanda aumenta cuando aumenta el ingreso (ej: ropa de calidad, restaurantes)
- **Bien Inferior**: La demanda disminuye cuando aumenta el ingreso (ej: fideos instantáneos, transporte público)
- **Bien de Lujo**: La demanda aumenta más que proporcionalmente al ingreso (ej: joyas, autos deportivos)
Lee cuidadosamente cada escenario y selecciona la respuesta correcta.`,
dificultad: 'facil',
duracionEstimada: 10,
objetivosAprendizaje: [
'Distinguir entre bienes normales, inferiores y de lujo',
'Comprender la elasticidad ingreso de la demanda',
'Analizar patrones de consumo según nivel de ingresos'
],
config: {
modo: 'clasificacion-multiple',
preguntas: [
{
id: 'p1',
bien: 'Carne de primera calidad',
descripcion: 'Carne de res premium vendida en supermercados de alta gama',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacionDetallada: 'La carne premium es considerada un bien de lujo porque cuando el ingreso aumenta significativamente, las familias aumentan su consumo de este tipo de carne sustituyendo carnes de menor calidad.',
categoriaElasticidad: 'Elasticidad ingreso > 1'
},
{
id: 'p2',
bien: 'Pan',
descripcion: 'Pan básico de consumo diario',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien normal',
explicacionDetallada: 'El pan es un bien normal porque su consumo aumenta moderadamente con el ingreso, aunque llega un punto donde se estabiliza (saturación).',
categoriaElasticidad: '0 < Elasticidad ingreso < 1'
},
{
id: 'p3',
bien: 'Transporte público (autobús)',
descripcion: 'Servicio de autobuses urbanos',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacionDetallada: 'El transporte público es un bien inferior porque cuando los ingresos aumentan, las personas tienden a comprar automóviles o usar taxis/Uber, reduciendo el uso del autobús.',
categoriaElasticidad: 'Elasticidad ingreso < 0'
},
{
id: 'p4',
bien: 'Fideos instantáneos',
descripcion: 'Comida rápida económica',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacionDetallada: 'Los fideos instantáneos son claramente un bien inferior. A medida que aumentan los ingresos, las personas prefieren alimentos más nutritivos y de mejor calidad.',
categoriaElasticidad: 'Elasticidad ingreso < 0'
},
{
id: 'p5',
bien: 'Vacaciones en el extranjero',
descripcion: 'Viajes turísticos internacionales',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacionDetallada: 'Las vacaciones internacionales son un bien de lujo porque su consumo aumenta significativamente cuando el ingreso crece, incluso más que proporcionalmente.',
categoriaElasticidad: 'Elasticidad ingreso > 1'
},
{
id: 'p6',
bien: 'Ropa de marca',
descripcion: 'Vestimenta de diseñador',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacionDetallada: 'La ropa de marca es un bien de lujo porque su demanda crece más rápido que el ingreso, especialmente en rangos de ingreso altos.',
categoriaElasticidad: 'Elasticidad ingreso > 1'
},
{
id: 'p7',
bien: 'Cine',
descripcion: 'Entradas a salas de cine',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien normal',
explicacionDetallada: 'El cine es un bien normal. Aunque con el auge del streaming podría debatirse, generalmente el consumo de entretenimiento aumenta con el ingreso de forma moderada.',
categoriaElasticidad: '0 < Elasticidad ingreso < 1'
},
{
id: 'p8',
bien: 'Productos de marca blanca',
descripcion: 'Productos genéricos de supermercado',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacionDetallada: 'Los productos de marca blanca son bienes inferiores porque son sustituidos por marcas reconocidas cuando el consumidor tiene mayores ingresos.',
categoriaElasticidad: 'Elasticidad ingreso < 0'
}
],
configuracionVisual: {
mostrarBarraProgreso: true,
mostrarPuntaje: true,
retroalimentacionInmediata: true,
tiempoLimite: 600, // segundos
permitirReintentar: true
},
nivelesDificultad: {
facil: ['p2', 'p4', 'p7'],
medio: ['p3', 'p5', 'p8'],
dificil: ['p1', 'p6']
}
},
pistas: [
'Pregúntate: ¿Qué compraría una persona si ganara el doble de dinero?',
'Los bienes de lujo son aquellos que dejarías de comprar primero si perdieras tu empleo',
'Un bien inferior no significa que sea de mala calidad, sino que tiene sustitutos mejores cuando aumenta el ingreso'
],
solucion: `Clasificación según elasticidad-ingreso:
- Bien Inferior: Elasticidad < 0 (ej: transporte público, fideos)
- Bien Normal: 0 < Elasticidad < 1 (ej: pan, cine)
- Bien de Lujo: Elasticidad > 1 (ej: carne premium, vacaciones)`
},
{
id: 'juego-flujo-circular',
tipo: 'juego',
titulo: 'Juego: El Flujo Circular de la Renta',
descripcion: 'Coloca cada elemento en su lugar correcto dentro del modelo del flujo circular',
instrucciones: `Completa el diagrama del flujo circular de la renta arrastrando cada elemento a su posición correcta.
El flujo circular muestra cómo interactúan los agentes económicos:
**Agentes principales:**
1. **Familias/Hogares**: Ofrecen factores productivos (trabajo, capital) y consumen bienes
2. **Empresas**: Producen bienes/servicios y demandan factores productivos
3. **Estado**: Recauda impuestos y realiza gastos públicos
4. **Sector Externo**: Intercambia bienes y servicios con el exterior
**Tipos de flujos:**
- **Flujos reales** (flechas azules): Bienes, servicios, factores productivos
- **Flujos monetarios** (flechas verdes): Dinero, pagos, transferencias
Instrucciones:
1. Observa los elementos en la parte inferior
2. Arrastra cada uno al círculo correspondiente o a las flechas correctas
3. Asegúrate de distinguir entre flujos reales y monetarios
4. Completa todos los elementos para ganar`,
dificultad: 'dificil',
duracionEstimada: 20,
objetivosAprendizaje: [
'Comprender la interdependencia entre agentes económicos',
'Diferenciar entre flujos reales y monetarios',
'Identificar los pagos correspondientes a cada factor productivo',
'Entender el papel del Estado y el sector externo'
],
config: {
tipoJuego: 'drag-and-drop',
diagrama: {
agentes: [
{
id: 'familias',
nombre: 'FAMILIAS',
posicion: 'izquierda',
icono: '👨‍👩‍👧‍👦',
color: '#4CAF50'
},
{
id: 'empresas',
nombre: 'EMPRESAS',
posicion: 'derecha',
icono: '🏭',
color: '#2196F3'
},
{
id: 'estado',
nombre: 'ESTADO',
posicion: 'arriba',
icono: '🏛️',
color: '#FF9800'
},
{
id: 'sector-externo',
nombre: 'SECTOR EXTERNO',
posicion: 'abajo',
icono: '🌍',
color: '#9C27B0'
}
],
flujos: [
// Flujo real superior (Familias → Empresas)
{
id: 'flujo1',
origen: 'familias',
destino: 'empresas',
tipo: 'real',
elementosCorrectos: ['trabajo', 'tierra', 'capital']
},
// Flujo monetario superior (Empresas → Familias)
{
id: 'flujo2',
origen: 'empresas',
destino: 'familias',
tipo: 'monetario',
elementosCorrectos: ['salarios', 'renta', 'intereses']
},
// Flujo real inferior (Empresas → Familias)
{
id: 'flujo3',
origen: 'empresas',
destino: 'familias',
tipo: 'real',
elementosCorrectos: ['bienes', 'servicios']
},
// Flujo monetario inferior (Familias → Empresas)
{
id: 'flujo4',
origen: 'familias',
destino: 'empresas',
tipo: 'monetario',
elementosCorrectos: ['gasto', 'consumo']
},
// Flujos del Estado
{
id: 'flujo5',
origen: 'familias',
destino: 'estado',
tipo: 'monetario',
elementosCorrectos: ['impuestos', 'impuestos-directos']
},
{
id: 'flujo6',
origen: 'estado',
destino: 'familias',
tipo: 'monetario',
elementosCorrectos: ['transferencias', 'subsidios']
},
{
id: 'flujo7',
origen: 'estado',
destino: 'empresas',
tipo: 'monetario',
elementosCorrectos: ['gasto-publico', 'compras-estado']
}
],
elementosArrastrables: [
{ id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', categoria: 'factor' },
{ id: 'tierra', texto: '🌾 Tierra', tipo: 'real', categoria: 'factor' },
{ id: 'capital', texto: '💰 Capital', tipo: 'real', categoria: 'factor' },
{ id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', categoria: 'pago' },
{ id: 'renta', texto: '🏠 Renta', tipo: 'monetario', categoria: 'pago' },
{ id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', categoria: 'pago' },
{ id: 'bienes', texto: '📦 Bienes', tipo: 'real', categoria: 'producto' },
{ id: 'servicios', texto: '🔧 Servicios', tipo: 'real', categoria: 'producto' },
{ id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', categoria: 'pago' },
{ id: 'consumo', texto: '🛒 Consumo', tipo: 'monetario', categoria: 'pago' },
{ id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', categoria: 'pago-estado' },
{ id: 'impuestos-directos', texto: '📋 Imp. Directos', tipo: 'monetario', categoria: 'pago-estado' },
{ id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', categoria: 'transferencia' },
{ id: 'subsidios', texto: '💸 Subsidios', tipo: 'monetario', categoria: 'transferencia' },
{ id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', categoria: 'gobierno' },
{ id: 'compras-estado', texto: '🛍️ Compras Estado', tipo: 'monetario', categoria: 'gobierno' }
]
},
niveles: [
{
nombre: 'Básico',
descripcion: 'Solo Familias y Empresas',
agentesActivos: ['familias', 'empresas'],
elementosDisponibles: ['trabajo', 'salarios', 'bienes', 'gasto', 'servicios', 'consumo'],
ayudaMaxima: true
},
{
nombre: 'Intermedio',
descripcion: 'Incluye al Estado',
agentesActivos: ['familias', 'empresas', 'estado'],
elementosDisponibles: ['trabajo', 'tierra', 'capital', 'salarios', 'renta', 'intereses', 'bienes', 'servicios', 'gasto', 'consumo', 'impuestos', 'transferencias', 'gasto-publico'],
ayudaMaxima: false
},
{
nombre: 'Avanzado',
descripcion: 'Todos los agentes incluyendo Sector Externo',
agentesActivos: ['familias', 'empresas', 'estado', 'sector-externo'],
elementosDisponibles: 'todos',
incluirExportacionesImportaciones: true,
ayudaMaxima: false
}
],
sistemaPuntuacion: {
acierto: 10,
error: -2,
bonusCompletitud: 50,
tiempoBonus: true
}
},
pistas: [
'Las familias venden sus factores productivos (trabajo, tierra, capital) a las empresas',
'Las empresas pagan salarios por trabajo, renta por tierra e intereses por capital',
'Las familias gastan dinero para comprar bienes y servicios de las empresas',
'El Estado recauda impuestos y redistribuye mediante transferencias y gasto público',
'Distingue flujos reales (cosas físicas) de flujos monetarios (dinero)'
],
solucion: `El flujo circular completo:
**Flujos reales:**
- Familias → Empresas: Trabajo, tierra, capital (factores productivos)
- Empresas → Familias: Bienes y servicios
**Flujos monetarios:**
- Empresas → Familias: Salarios, renta, intereses (pagos por factores)
- Familias → Empresas: Gasto de consumo
- Familias → Estado: Impuestos
- Estado → Familias/Empresas: Transferencias y gasto público`
}
]
};
// Exportar también los ejercicios individuales para facilitar importaciones selectivas
export const ejercicioDisyuntivas = ejercicios.ejercicios[0];
export const ejercicioClasificacion = ejercicios.ejercicios[1];
export const ejercicioFlujoCircular = ejercicios.ejercicios[2];
export default ejercicios;

View File

@@ -0,0 +1,192 @@
import type { ModuloContenido } from './introduccion';
export const factores: ModuloContenido = {
titulo: 'Factores de Producción',
contenido: [
{
titulo: 'La Tierra (Recursos Naturales)',
contenido: `La tierra como factor de producción incluye todos los recursos naturales proporcionados por la naturaleza:
**Características:**
- Tierra en sentido estricto (superficie territorial)
- Recursos minerales (petróleo, gas, minerales metálicos)
- Recursos hídricos (ríos, lagos, aguas subterráneas)
- Recursos forestales (madera, productos forestales)
- Recursos marinos (pesca)
- Recursos energéticos naturales (radiación solar, eólica)
**Remuneración:**
El factor tierra recibe la **renta** o **renta de la tierra** como pago por su uso.
**Importancia económica:**
- Los recursos naturales son la base de muchas industrias
- Países ricos en recursos naturales tienen ventajas comparativas
- La explotación sostenible garantiza recursos para futuras generaciones
- El agotamiento de recursos no renovables crea presión sobre la economía`
},
{
titulo: 'El Trabajo (Factor Humano)',
contenido: `El trabajo es el esfuerzo humano (físico y mental) aplicado a la producción de bienes y servicios:
**Clasificación del trabajo:**
**Por cualificación:**
- Trabajo no calificado: no requiere formación especial
- Trabajo semi-calificado: requiere entrenamiento básico
- Trabajo calificado: requiere educación especializada
- Trabajo altamente calificado: profesionales, técnicos especializados
**Por sector:**
- Trabajo primario: agricultura, pesca, minería
- Trabajo secundario: industria, manufactura, construcción
- Trabajo terciario: servicios, comercio, administración
**Remuneración:**
El trabajo recibe el **salario** como pago (puede ser por hora, pieza o mensualidad).
**Características del mercado laboral:**
- Oferta de trabajo: personas dispuestas a trabajar
- Demanda de trabajo: empresas que necesitan contratar
- Desempleo: diferencia entre oferta y demanda efectiva`
},
{
titulo: 'El Capital',
contenido: `El capital son los bienes de producción creados por el ser humano para producir otros bienes y servicios:
**Tipos de capital:**
**1. Capital físico o tangible:**
- Maquinaria y equipos
- Edificios e instalaciones
- Herramientas y vehículos
- Infraestructura (carreteras, puertos, redes)
**2. Capital financiero:**
- Dinero disponible para inversión
- Créditos y préstamos
- Acciones y bonos
**3. Capital humano:**
- Educación y formación de los trabajadores
- Experiencia y habilidades
- Salud de la población
**Formación de capital:**
El capital se forma mediante el **ahorro** e **inversión**. El ahorro diferido del consumo actual permite invertir en bienes de capital que aumentarán la producción futura.
**Remuneración:**
El capital recibe el **interés** como pago por su uso.
**Importancia:**
El capital aumenta la productividad del trabajo, permitiendo producir más con menos esfuerzo.`
},
{
titulo: 'Tecnología y Emprendimiento',
contenido: `Además de los tres factores clásicos, la economía moderna reconoce dos factores adicionales fundamentales:
**Tecnología:**
Es el conocimiento aplicado a la producción. No es solo máquinas, sino el "saber hacer":
- Procesos productivos más eficientes
- Innovaciones en productos y servicios
- Software y sistemas de información
- Metodologías de organización
**Impacto de la tecnología:**
- Aumenta la productividad total de los factores
- Reduce costos de producción
- Crea nuevos productos y mercados
- Transforma industrias enteras (disrupción digital)
**Emprendimiento:**
Es la capacidad de organizar y coordinar los otros factores de producción para crear valor:
**Funciones del empresario:**
- Identificar oportunidades de negocio
- Asumir riesgos económicos
- Innovar (nuevos productos, métodos, mercados)
- Tomar decisiones estratégicas
- Organizar los factores productivos
**Remuneración:**
El empresario recibe los **beneficios** (o pérdidas) como resultado de su actividad.
**Diferencia entre empresario y capitalista:**
- El capitalista aporta capital y recibe intereses
- El empresario organiza la producción y recibe beneficios (que incluyen compensación por su trabajo, riesgo asumido y habilidad empresarial)`
},
{
titulo: 'Productividad y Eficiencia',
contenido: `La combinación de factores de producción debe hacerse buscando la máxima eficiencia:
**Productividad:**
Relación entre la producción obtenida y los recursos utilizados:
Productividad = Producción / Factores utilizados
**Tipos de productividad:**
- Productividad del trabajo: producción por hora trabajada
- Productividad del capital: producción por unidad de capital
- Productividad total de los factores: eficiencia global
**Eficiencia técnica vs económica:**
**Eficiencia técnica:**
Producir la máxima cantidad posible con los recursos disponibles (no desperdiciar inputs).
**Eficiencia económica:**
Producir al menor costo posible, considerando los precios de los factores.
**Retornos a escala:**
- Crecientes: duplicar factores más que duplica la producción
- Constantes: duplicar factores duplica exactamente la producción
- Decrecientes: duplicar factores aumenta menos que el doble la producción`
}
],
ejercicios: [
{
id: 'quiz-bienes',
tipo: 'quiz',
titulo: 'Quiz de Clasificación de Bienes',
descripcion: 'Clasifica los siguientes bies según su tipo: normal, inferior o de lujo',
config: {
preguntas: [
{
bien: 'Un automóvil de lujo',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacion: 'Los automóviles de lujo aumentan su demanda cuando aumenta el ingreso más que proporcionalmente'
},
{
bien: 'Transporte público',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacion: 'Cuando los ingresos aumentan, las personas tienden a sustituir el transporte público por automóviles privados'
},
{
bien: 'Pan',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien normal',
explicacion: 'El pan es un bien básico cuya demanda aumenta moderadamente con el ingreso'
},
{
bien: 'Un yate',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien de lujo',
explicacion: 'Los yates son bienes exclusivos que solo son accesibles con altos ingresos'
},
{
bien: 'Productos de marca genérica',
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
respuestaCorrecta: 'Bien inferior',
explicacion: 'Las marcas genéricas son sustituidas por marcas premium cuando aumentan los ingresos'
}
],
tiempoLimite: 300,
mostrarRetroalimentacion: true
}
}
]
};
export default factores;

View File

@@ -0,0 +1,65 @@
/**
* Módulo 1: Fundamentos de Economía
*
* Este módulo cubre los conceptos básicos de economía, incluyendo:
* - Definición y ramas de la economía
* - Agentes económicos y su interacción
* - Factores de producción
* - Frontera de posibilidades de producción
* - Flujo circular de la renta
*/
// Importar valores para uso local
import { introduccion } from './introduccion';
import modulo1Introduccion from './introduccion';
import { agentes } from './agentes';
import modulo1Agentes from './agentes';
import { factores } from './factores';
import modulo1Factores from './factores';
import {
ejercicios,
ejercicioDisyuntivas,
ejercicioClasificacion,
ejercicioFlujoCircular
} from './ejercicios';
import modulo1Ejercicios from './ejercicios';
// Importar tipos para reexportar
import type {
EjercicioDetallado,
ModuloEjercicios
} from './ejercicios';
// Reexportar tipos
export type {
EjercicioDetallado,
ModuloEjercicios
};
// Exportar todo el módulo como un objeto consolidado
export const modulo1 = {
id: 'modulo-1',
titulo: 'Fundamentos de Economía',
descripcion: 'Introducción a los conceptos básicos de economía, agentes económicos, factores de producción y análisis de disyuntivas.',
duracionEstimada: '4-5 horas',
temas: [
introduccion,
agentes,
factores
],
ejercicios: ejercicios.ejercicios
};
// Reexportar contenidos para compatibilidad
export { introduccion, modulo1Introduccion };
export { agentes, modulo1Agentes };
export { factores, modulo1Factores };
export {
ejercicios,
ejercicioDisyuntivas,
ejercicioClasificacion,
ejercicioFlujoCircular,
modulo1Ejercicios
};
export default modulo1;

View File

@@ -0,0 +1,81 @@
export interface Seccion {
titulo: string;
contenido: string;
}
export interface Ejercicio {
id: string;
tipo: 'slider' | 'quiz' | 'juego';
titulo: string;
descripcion: string;
config: Record<string, unknown>;
}
export interface ModuloContenido {
titulo: string;
contenido: Seccion[];
ejercicios: Ejercicio[];
}
export const introduccion: ModuloContenido = {
titulo: 'Introducción a la Economía',
contenido: [
{
titulo: '¿Qué es la Economía?',
contenido: `La economía es la ciencia social que estudia cómo los individuos, empresas y gobiernos toman decisiones sobre la asignación de recursos escasos para satisfacer necesidades ilimitadas. El término proviene del griego "oikonomia" (gestión del hogar).
Los recursos disponibles (tierra, trabajo, capital) son limitados, mientras que las necesidades humanas son infinitas. Esta tensión entre lo que queremos y lo que podemos obtener constituye el problema fundamental de la economía.`
},
{
titulo: 'Microeconomía vs Macroeconomía',
contenido: `La economía se divide en dos grandes ramas:
**Microeconomía**: Estudia el comportamiento de agentes económicos individuales (hogares, empresas, mercados específicos). Analiza decisiones como: ¿Cuánto producirá una empresa? ¿Qué cantidad comprará un consumidor? ¿Cómo se determina el precio de un bien?
**Macroeconomía**: Examina el funcionamiento de la economía en su conjunto. Estudia variables agregadas como el Producto Interno Bruto (PIB), la inflación, el desempleo y el crecimiento económico. Busca entender los ciclos económicos y las políticas para estabilizar la economía.`
},
{
titulo: 'Las Tres Preguntas Fundamentales',
contenido: `Toda sociedad debe responder tres preguntas económicas básicas:
1. **¿Qué producir?**: Determinar qué bienes y servicios se fabricarán dados los recursos limitados. ¿Más comida o más ropa? ¿Más hospitales o más escuelas?
2. **¿Cómo producir?**: Elegir la combinación de factores de producción más eficiente. ¿Usar más trabajo manual o más maquinaria? ¿Tecnología intensiva o labor intensiva?
3. **¿Para quién producir?**: Distribuir los bienes y servicios entre la población. ¿Quién recibe qué? ¿Basado en la capacidad de pago o en la necesidad?`
},
{
titulo: 'La Frontera de Posibilidades de Producción (FPP)',
contenido: `La Frontera de Posibilidades de Producción (o Curva de Transformación) representa gráficamente las combinaciones máximas de dos bienes que una economía puede producir utilizando todos sus recursos y tecnología disponibles de manera eficiente.
**Características importantes:**
- **Pendiente negativa**: Para producir más de un bien, debemos sacrificar algo del otro (costo de oportunidad)
- **Forma cóncava**: Los costos de oportunidad crecientes reflejan que los recursos no son perfectamente adaptables
- **Puntos sobre la curva**: Producción eficiente (todos los recursos utilizados)
- **Puntos dentro de la curva**: Ineficiencia o desempleo de recursos
- **Puntos fuera de la curva**: Inalcanzables con los recursos actuales
**Desplazamientos de la FPP:**
- Hacia afuera: Crecimiento económico (más recursos o mejor tecnología)
- Hacia adentro: Destrucción de recursos o regresión tecnológica`
}
],
ejercicios: [
{
id: 'fpp-simulador',
tipo: 'slider',
titulo: 'Simulador de Disyuntivas',
descripcion: 'Ajusta los sliders para ver cómo la producción de dos bienes compite por recursos limitados',
config: {
bienes: [
{ nombre: 'Bien de Consumo', max: 100 },
{ nombre: 'Bien de Capital', max: 100 }
],
mostrarCostoOportunidad: true,
mostrarFPP: true
}
}
]
};
export default introduccion;

View File

@@ -0,0 +1,369 @@
/**
* Módulo 2: Ley de la Demanda
*
* Este módulo cubre los fundamentos de la demanda en economía,
* incluyendo la ley de la demanda, factores determinantes y
* tipos de curvas de demanda.
*/
// ============================================
// TIPOS Y ENUMERACIONES
// ============================================
export type TipoBien = 'normal' | 'inferior' | 'lujo' | 'necesidad';
export type TipoRelacionPrecio = 'sustituto' | 'complementario' | 'independiente';
export enum DireccionDesplazamiento {
IZQUIERDA = 'izquierda', // Disminución de demanda
DERECHA = 'derecha', // Aumento de demanda
NINGUNO = 'ninguno' // Sin cambio
}
// ============================================
// INTERFACES
// ============================================
export interface PuntoDemanda {
precio: number;
cantidad: number;
}
export interface CurvaDemanda {
id: string;
nombre: string;
puntos: PuntoDemanda[];
descripcion: string;
}
export interface FactorDesplazamiento {
nombre: string;
descripcion: string;
direccion: DireccionDesplazamiento;
ejemplo: string;
icono: string;
}
export interface EjemploDemanda {
titulo: string;
bien: string;
escenario: string;
explicacion: string;
graficoData: PuntoDemanda[];
}
// ============================================
// CONTENIDO TEÓRICO
// ============================================
export const definicionDemanda = {
titulo: 'Definición de Demanda',
definicion: 'La demanda es la cantidad de un bien o servicio que los consumidores están dispuestos y pueden comprar a diferentes precios durante un período específico, manteniendo constantes otros factores (ceteris paribus).',
elementosClave: [
{
elemento: 'Disposición a comprar',
descripcion: 'El consumidor debe querer adquirir el bien (preferencia)'
},
{
elemento: 'Capacidad de compra',
descripcion: 'El consumidor debe tener los recursos necesarios (ingreso)'
},
{
elemento: 'Precios variables',
descripcion: 'Se analiza la relación a diferentes niveles de precio'
},
{
elemento: 'Período de tiempo',
descripcion: 'La demanda siempre se refiere a un período específico'
}
],
diferenciaDeseo: {
deseo: 'Quiero un auto de lujo (sin capacidad de compra)',
demanda: 'Puedo y quiero comprar 2 litros de leche semanales a $2 cada uno'
}
};
export const leyDemanda = {
titulo: 'Ley de la Demanda',
enunciado: 'Existe una relación inversa entre el precio de un bien y la cantidad demandada: cuando el precio aumenta, la cantidad demandada disminuye, y viceversa.',
explicacion: 'Esta relación inversa ocurre por dos efectos principales:',
efectos: [
{
nombre: 'Efecto Sustitución',
descripcion: 'Cuando el precio de un bien aumenta, los consumidores sustituyen hacia bienes alternativos más baratos que satisfacen necesidades similares.',
ejemplo: 'Si el precio de la carne de res sube, los consumidores compran más pollo o pescado.'
},
{
nombre: 'Efecto Ingreso',
descripcion: 'Cuando el precio aumenta, el poder adquisitivo real del consumidor disminuye, permitiéndole comprar menos cantidad del bien.',
ejemplo: 'Si el precio de la gasolina sube, con el mismo presupuesto puedo comprar menos litros.'
}
],
representacionMatematica: {
funcion: 'Qd = f(P)',
donde: {
Qd: 'Cantidad demandada',
P: 'Precio del bien',
f: 'Función decreciente (pendiente negativa)'
},
ejemploLineal: 'Qd = 100 - 2P',
interpretacion: 'Por cada aumento de $1 en el precio, la cantidad demandada disminuye en 2 unidades.'
}
};
// ============================================
// FACTORES QUE DESPLAZAN LA CURVA DE DEMANDA
// ============================================
export const factoresDesplazamiento: FactorDesplazamiento[] = [
{
nombre: 'Ingreso del consumidor',
descripcion: 'Cambios en el ingreso disponible de los consumidores',
direccion: DireccionDesplazamiento.DERECHA,
ejemplo: 'Un aumento de sueldo permite comprar más restaurantes (bien normal) o menos fideos instantáneos (bien inferior)',
icono: '💰'
},
{
nombre: 'Gustos y preferencias',
descripcion: 'Cambios en los gustos de los consumidores por modas, publicidad o información',
direccion: DireccionDesplazamiento.DERECHA,
ejemplo: 'Una campaña de salud que promueve el consumo de agua aumenta la demanda de botellas',
icono: '❤️'
},
{
nombre: 'Precio de bienes relacionados',
descripcion: 'Cambios en el precio de sustitutos o complementarios',
direccion: DireccionDesplazamiento.DERECHA,
ejemplo: 'Si sube el precio del café, aumenta la demanda de té (sustituto)',
icono: '🔗'
},
{
nombre: 'Expectativas futuras',
descripcion: 'Expectativas sobre precios, ingresos o disponibilidad futura',
direccion: DireccionDesplazamiento.DERECHA,
ejemplo: 'Si se espera que suba el precio de la vivienda, la demanda actual aumenta',
icono: '🔮'
},
{
nombre: 'Número de compradores',
descripcion: 'Cambios en la población o demografía del mercado',
direccion: DireccionDesplazamiento.DERECHA,
ejemplo: 'Llegada de turistas aumenta la demanda de hospedaje en temporada alta',
icono: '👥'
}
];
export const tiposBienesDemanda = {
bienNormal: {
nombre: 'Bien Normal',
definicion: 'Demanda aumenta cuando aumenta el ingreso',
relacionIngreso: 'Directa',
ejemplos: ['Ropa de marca', 'Restaurantes', 'Viajes', 'Electrónicos'],
elasticidadIngreso: 'E_Y > 0'
},
bienInferior: {
nombre: 'Bien Inferior',
definicion: 'Demanda disminuye cuando aumenta el ingreso',
relacionIngreso: 'Inversa',
ejemplos: ['Fideos instantáneos', 'Transporte público', 'Marcas genéricas', 'Comida rápida económica'],
elasticidadIngreso: 'E_Y < 0'
},
bienLujo: {
nombre: 'Bien de Lujo',
definicion: 'Demanda aumenta proporcionalmente más que el ingreso',
relacionIngreso: 'Directa (elástica)',
ejemplos: ['Yates', 'Joyería', 'Autos deportivos', 'Viajes en primera clase'],
elasticidadIngreso: 'E_Y > 1'
},
bienNecesidad: {
nombre: 'Bien de Necesidad',
definicion: 'Demanda aumenta menos proporcionalmente que el ingreso',
relacionIngreso: 'Directa (inelástica)',
ejemplos: ['Sal', 'Agua', 'Pan básico', 'Medicinas esenciales'],
elasticidadIngreso: '0 < E_Y < 1'
}
};
export const bienesRelacionados = {
sustitutos: {
nombre: 'Bienes Sustitutos',
definicion: 'Bienes que pueden reemplazarse mutuamente para satisfacer la misma necesidad',
relacionPrecio: 'Directa: si sube el precio de A, aumenta la demanda de B',
coeficiente: 'E_AB > 0',
ejemplos: [
{ bienA: 'Coca-Cola', bienB: 'Pepsi' },
{ bienA: 'Mantequilla', bienB: 'Margarina' },
{ bienA: 'Carne de res', bienB: 'Pollo' }
]
},
complementarios: {
nombre: 'Bienes Complementarios',
definicion: 'Bienes que se consumen juntos para mayor satisfacción',
relacionPrecio: 'Inversa: si sube el precio de A, disminuye la demanda de B',
coeficiente: 'E_AB < 0',
ejemplos: [
{ bienA: 'Autos', bienB: 'Gasolina' },
{ bienA: 'Café', bienB: 'Azúcar' },
{ bienA: 'Impresora', bienB: 'Tinta' }
]
}
};
// ============================================
// CURVAS DE DEMANDA
// ============================================
export const curvaDemandaIndividual: CurvaDemanda = {
id: 'demanda-individual',
nombre: 'Curva de Demanda Individual',
descripcion: 'Muestra la relación entre precio y cantidad demandada por un solo consumidor',
puntos: [
{ precio: 10, cantidad: 1 },
{ precio: 8, cantidad: 3 },
{ precio: 6, cantidad: 5 },
{ precio: 4, cantidad: 7 },
{ precio: 2, cantidad: 9 }
]
};
export const curvaDemandaMercado: CurvaDemanda = {
id: 'demanda-mercado',
nombre: 'Curva de Demanda de Mercado',
descripcion: 'Suma horizontal de todas las demandas individuales en el mercado',
puntos: [
{ precio: 10, cantidad: 100 },
{ precio: 8, cantidad: 300 },
{ precio: 6, cantidad: 500 },
{ precio: 4, cantidad: 700 },
{ precio: 2, cantidad: 900 }
]
};
// ============================================
// EJEMPLOS PRÁCTICOS
// ============================================
export const ejemplosDemanda: EjemploDemanda[] = [
{
titulo: 'Demanda de Entradas de Cine',
bien: 'Entradas de cine',
escenario: 'El cine reduce sus precios de $12 a $8 durante los martes',
explicacion: 'Según la ley de la demanda, al disminuir el precio, más personas asistirán al cine. La cantidad demandada aumenta moviéndonos a lo largo de la curva.',
graficoData: [
{ precio: 12, cantidad: 100 },
{ precio: 10, cantidad: 150 },
{ precio: 8, cantidad: 220 },
{ precio: 6, cantidad: 300 },
{ precio: 4, cantidad: 400 }
]
},
{
titulo: 'Efecto de Ingreso en Restaurant',
bien: 'Comida en restaurantes',
escenario: 'Los habitantes de una ciudad reciben un aumento salarial del 20%',
explicacion: 'Los restaurantes son un bien normal. Al aumentar el ingreso, la demanda se desplaza a la derecha: a cada precio, se demanda más cantidad.',
graficoData: [
{ precio: 50, cantidad: 200 },
{ precio: 40, cantidad: 300 },
{ precio: 30, cantidad: 450 },
{ precio: 20, cantidad: 600 },
{ precio: 10, cantidad: 800 }
]
},
{
titulo: 'Sustitutos: Café vs Té',
bien: 'Té',
escenario: 'Una sequía en Brasil aumenta el precio del café en un 50%',
explicacion: 'Como café y té son sustitutos, al subir el precio del café, la demanda de té se desplaza a la derecha. Más consumidores optarán por té.',
graficoData: [
{ precio: 5, cantidad: 100 },
{ precio: 4, cantidad: 180 },
{ precio: 3, cantidad: 280 },
{ precio: 2, cantidad: 400 },
{ precio: 1, cantidad: 550 }
]
}
];
// ============================================
// MOVIMIENTO VS DESPLAZAMIENTO
// ============================================
export const diferenciaMovimientoDesplazamiento = {
titulo: 'Movimiento a lo largo vs Desplazamiento de la curva',
movimiento: {
nombre: 'Movimiento a lo largo de la curva',
causa: 'Cambio en el precio del propio bien',
efecto: 'Cambio en la cantidad demandada (no en la demanda)',
direccion: 'Subida o bajada por la misma curva',
ejemplo: 'El precio del pan sube de $2 a $3 → compramos menos pan'
},
desplazamiento: {
nombre: 'Desplazamiento de la curva',
causa: 'Cambio en factores distintos al precio (ingreso, gustos, precios relacionados)',
efecto: 'Cambio en la demanda (toda la curva se mueve)',
direccionDerecha: 'Aumento de demanda (más cantidad a cada precio)',
direccionIzquierda: 'Disminución de demanda (menos cantidad a cada precio)',
ejemplo: 'Aumento de ingreso → compramos más restaurantes a todos los precios'
},
tablaComparativa: [
{ concepto: 'Causa', movimiento: 'Precio del bien cambia', desplazamiento: 'Otros factores cambian' },
{ concepto: 'Gráfico', movimiento: 'Nos movemos sobre la curva', desplazamiento: 'Curva se desplaza' },
{ concepto: 'Terminología', movimiento: 'Cambio en cantidad demandada', desplazamiento: 'Cambio en demanda' },
{ concepto: 'Ejemplo', movimiento: 'Precio de manzanas ↓', desplazamiento: 'Ingreso ↑ (bien normal)' }
]
};
// ============================================
// RESUMEN Y PUNTOS CLAVE
// ============================================
export const resumenDemanda = {
titulo: 'Resumen: Demanda',
puntosClave: [
'La demanda requiere disposición Y capacidad de comprar',
'La ley de la demanda establece relación inversa precio-cantidad',
'La curva de demanda tiene pendiente negativa',
'El desplazamiento de la curva es causado por factores no-precio',
'Los bienes normales tienen demanda directa con el ingreso',
'Los bienes inferiores tienen demanda inversa con el ingreso',
'Sustitutos: precio de A ↑ → demanda de B ↑',
'Complementarios: precio de A ↑ → demanda de B ↓'
],
formulaRecordatorio: {
leyDemanda: 'P ↑ → Qd ↓ (ceteris paribus)',
demandaMercado: 'Qd_mercado = Σ Qd_individuales',
elasticidadPrecio: 'Ed = (%ΔQd) / (%ΔP) < 0'
}
};
// Exportación por defecto para facilitar importaciones
export default {
definicion: definicionDemanda,
ley: leyDemanda,
factores: factoresDesplazamiento,
tiposBienes: tiposBienesDemanda,
bienesRelacionados,
curvas: {
individual: curvaDemandaIndividual,
mercado: curvaDemandaMercado
},
ejemplos: ejemplosDemanda,
diferencia: diferenciaMovimientoDesplazamiento,
resumen: resumenDemanda
};

View File

@@ -0,0 +1,855 @@
/**
* Módulo 2: Ejercicios Interactivos
*
* Este módulo contiene la estructura de ejercicios para practicar
* los conceptos de oferta, demanda y equilibrio.
*/
import type { PuntoMercado } from './equilibrio';
// ============================================
// TIPOS Y ENUMERACIONES
// ============================================
export enum TipoEjercicio {
CONSTRUCTOR_CURVAS = 'constructor_curvas',
SIMULADOR_PRECIOS = 'simulador_precios',
IDENTIFICAR_SHOCKS = 'identificar_shocks'
}
export enum Dificultad {
FACIL = 'facil',
MEDIO = 'medio',
DIFICIL = 'dificil'
}
export enum TipoRespuesta {
MULTIPLE_CHOICE = 'multiple_choice',
ARRASTRAR_SOLTAR = 'arrastrar_soltar',
GRAFICO_INTERACTIVO = 'grafico_interactivo',
NUMERICO = 'numerico',
SELECCIONAR = 'seleccionar'
}
export enum TipoShock {
DEMANDA_AUMENTA = 'demanda_aumenta',
DEMANDA_DISMINUYE = 'demanda_disminuye',
OFERTA_AUMENTA = 'oferta_aumenta',
OFERTA_DISMINUYE = 'oferta_disminuye',
AMBAS_OFERTA_DEMANDA = 'ambas_oferta_demanda'
}
// ============================================
// INTERFACES
// ============================================
export interface PuntoGrafico {
x: number;
y: number;
etiqueta?: string;
tipo: 'demanda' | 'oferta' | 'equilibrio' | 'interseccion';
}
export interface CurvaEjercicio {
id: string;
tipo: 'demanda' | 'oferta';
puntos: PuntoGrafico[];
color: string;
etiqueta: string;
editable: boolean;
}
export interface OpcionRespuesta {
id: string;
texto: string;
correcta: boolean;
retroalimentacion?: string;
}
export interface Pregunta {
id: string;
enunciado: string;
tipo: TipoRespuesta;
opciones?: OpcionRespuesta[];
respuestaCorrecta?: number | string | string[];
ayuda?: string;
puntos: number;
}
export interface NivelEjercicio {
id: string;
nombre: string;
dificultad: Dificultad;
descripcion: string;
completado: boolean;
desbloqueado: boolean;
}
export interface Ejercicio {
id: string;
tipo: TipoEjercicio;
titulo: string;
descripcion: string;
instrucciones: string[];
dificultad: Dificultad;
niveles: NivelEjercicio[];
preguntas?: Pregunta[];
configuracionGrafico?: ConfiguracionGrafico;
datosSimulacion?: DatosSimulacion;
escenariosShock?: EscenarioShock[];
}
export interface ConfiguracionGrafico {
ancho: number;
alto: number;
escalaX: { min: number; max: number; etiqueta: string };
escalaY: { min: number; max: number; etiqueta: string };
curvasIniciales: CurvaEjercicio[];
puntosObjetivo: PuntoGrafico[];
toleranciaError: number;
}
export interface DatosSimulacion {
funcionDemanda: string;
funcionOferta: string;
precioEquilibrio: number;
cantidadEquilibrio: number;
rangoPrecios: { min: number; max: number };
controlesPrecio: {
precioMaximo: number | null;
precioMinimo: number | null;
};
}
export interface EscenarioShock {
id: string;
titulo: string;
descripcion: string;
mercado: string;
evento: string;
tipoShock: TipoShock;
magnitud: 'pequeña' | 'media' | 'grande';
graficoInicial: {
demanda: PuntoMercado[];
oferta: PuntoMercado[];
};
graficoFinal: {
demanda: PuntoMercado[];
oferta: PuntoMercado[];
};
resultadoEsperado: {
precioCambio: 'sube' | 'baja' | 'igual';
cantidadCambio: 'sube' | 'baja' | 'igual' | 'indeterminado';
explicacion: string;
};
opciones: OpcionRespuesta[];
}
export interface ProgresoEjercicio {
ejercicioId: string;
completado: boolean;
puntuacion: number;
tiempoSegundos: number;
intentos: number;
nivelesCompletados: string[];
}
// ============================================
// EJERCICIO 1: CONSTRUCTOR DE CURVAS
// ============================================
export const constructorCurvas: Ejercicio = {
id: 'ejercicio-1-constructor-curvas',
tipo: TipoEjercicio.CONSTRUCTOR_CURVAS,
titulo: 'Constructor de Curvas de Oferta y Demanda',
descripcion: 'Construye curvas de oferta y demanda arrastrando puntos para entender sus pendientes y movimientos.',
instrucciones: [
'Observa los puntos dados en el gráfico',
'Arrastra cada punto para formar una curva de demanda con pendiente negativa',
'Arrastra los puntos de oferta para formar una curva con pendiente positiva',
'Encuentra el punto de equilibrio donde se intersecan ambas curvas',
'Verifica tu respuesta con el botón "Comprobar"'
],
dificultad: Dificultad.FACIL,
niveles: [
{
id: 'nivel-1-basico',
nombre: 'Nivel 1: Trazado Básico',
dificultad: Dificultad.FACIL,
descripcion: 'Traza una curva de demanda simple con 3 puntos',
completado: false,
desbloqueado: true
},
{
id: 'nivel-2-oferta-demanda',
nombre: 'Nivel 2: Ambas Curvas',
dificultad: Dificultad.FACIL,
descripcion: 'Traza curvas de oferta y demanda y encuentra el equilibrio',
completado: false,
desbloqueado: false
},
{
id: 'nivel-3-desplazamientos',
nombre: 'Nivel 3: Desplazamientos',
dificultad: Dificultad.MEDIO,
descripcion: 'Muestra cómo cambian las curvas ante diferentes shocks',
completado: false,
desbloqueado: false
},
{
id: 'nivel-4-precision',
nombre: 'Nivel 4: Precisión',
dificultad: Dificultad.DIFICIL,
descripcion: 'Traza curvas con tolerancia mínima de error',
completado: false,
desbloqueado: false
}
],
configuracionGrafico: {
ancho: 600,
alto: 400,
escalaX: { min: 0, max: 100, etiqueta: 'Cantidad (Q)' },
escalaY: { min: 0, max: 50, etiqueta: 'Precio (P)' },
toleranciaError: 5,
curvasIniciales: [
{
id: 'demanda-nivel-1',
tipo: 'demanda',
etiqueta: 'Demanda',
color: '#e74c3c',
editable: true,
puntos: [
{ x: 20, y: 40, tipo: 'demanda', etiqueta: 'A' },
{ x: 50, y: 25, tipo: 'demanda', etiqueta: 'B' },
{ x: 80, y: 10, tipo: 'demanda', etiqueta: 'C' }
]
},
{
id: 'oferta-nivel-2',
tipo: 'oferta',
etiqueta: 'Oferta',
color: '#27ae60',
editable: true,
puntos: [
{ x: 20, y: 10, tipo: 'oferta', etiqueta: 'D' },
{ x: 50, y: 25, tipo: 'oferta', etiqueta: 'E' },
{ x: 80, y: 40, tipo: 'oferta', etiqueta: 'F' }
]
}
],
puntosObjetivo: [
{ x: 50, y: 25, tipo: 'equilibrio', etiqueta: 'E*' }
]
},
preguntas: [
{
id: 'pregunta-1-pendiente',
enunciado: '¿Qué tipo de pendiente tiene la curva de demanda?',
tipo: TipoRespuesta.MULTIPLE_CHOICE,
puntos: 10,
ayuda: 'Recuerda la ley de la demanda: cuando el precio sube, la cantidad demandada baja.',
opciones: [
{ id: 'a', texto: 'Pendiente positiva (sube de izquierda a derecha)', correcta: false, retroalimentacion: 'Incorrecto. La demanda tiene pendiente negativa.' },
{ id: 'b', texto: 'Pendiente negativa (baja de izquierda a derecha)', correcta: true, retroalimentacion: '¡Correcto! La curva de demanda tiene pendiente negativa por la ley de la demanda.' },
{ id: 'c', texto: 'Pendiente cero (línea horizontal)', correcta: false, retroalimentacion: 'Incorrecto. Eso sería oferta perfectamente elástica.' },
{ id: 'd', texto: 'Pendiente infinita (línea vertical)', correcta: false, retroalimentacion: 'Incorrecto. Eso sería oferta perfectamente inelástica.' }
]
},
{
id: 'pregunta-2-equilibrio',
enunciado: '¿Dónde ocurre el equilibrio de mercado?',
tipo: TipoRespuesta.MULTIPLE_CHOICE,
puntos: 15,
ayuda: 'El equilibrio es donde las decisiones de compradores y vendedores coinciden.',
opciones: [
{ id: 'a', texto: 'Donde la demanda es máxima', correcta: false },
{ id: 'b', texto: 'Donde la oferta es máxima', correcta: false },
{ id: 'c', texto: 'En la intersección de oferta y demanda', correcta: true, retroalimentacion: '¡Correcto! El equilibrio ocurre donde Qd = Qs.' },
{ id: 'd', texto: 'En el origen (0,0)', correcta: false }
]
}
]
};
// ============================================
// EJERCICIO 2: SIMULADOR DE PRECIOS INTERVENIDOS
// ============================================
export const simuladorPrecios: Ejercicio = {
id: 'ejercicio-2-simulador-precios',
tipo: TipoEjercicio.SIMULADOR_PRECIOS,
titulo: 'Simulador de Precios Intervenidos',
descripcion: 'Ajusta precios máximos y mínimos para observar sus efectos en el mercado: escasez, superávit, y pérdida de bienestar.',
instrucciones: [
'Observa el equilibrio inicial del mercado',
'Usa los controles deslizantes para establecer un precio máximo o mínimo',
'Observa cómo cambian las cantidades demandadas y ofrecidas',
'Identifica si se genera escasez o superávit',
'Analiza el área de pérdida de bienestar (triángulo)',
'Responde las preguntas sobre cada escenario'
],
dificultad: Dificultad.MEDIO,
niveles: [
{
id: 'nivel-1-techo',
nombre: 'Nivel 1: Precio Máximo',
dificultad: Dificultad.FACIL,
descripcion: 'Establece un precio máximo y observa la escasez generada',
completado: false,
desbloqueado: true
},
{
id: 'nivel-2-piso',
nombre: 'Nivel 2: Precio Mínimo',
dificultad: Dificultad.FACIL,
descripcion: 'Establece un precio mínimo y observa el superávit',
completado: false,
desbloqueado: false
},
{
id: 'nivel-3-ambos',
nombre: 'Nivel 3: Combinado',
dificultad: Dificultad.MEDIO,
descripcion: 'Analiza escenarios con precios mínimos y máximos simultáneos',
completado: false,
desbloqueado: false
},
{
id: 'nivel-4-calculo',
nombre: 'Nivel 4: Cálculo de Pérdida',
dificultad: Dificultad.DIFICIL,
descripcion: 'Calcula numéricamente la pérdida de bienestar',
completado: false,
desbloqueado: false
}
],
datosSimulacion: {
funcionDemanda: 'Qd = 100 - 2P',
funcionOferta: 'Qs = 20 + 2P',
precioEquilibrio: 20,
cantidadEquilibrio: 60,
rangoPrecios: { min: 0, max: 50 },
controlesPrecio: {
precioMaximo: 15,
precioMinimo: 25
}
},
preguntas: [
{
id: 'sim-pregunta-1',
enunciado: 'Si estableces un precio máximo de $15 (debajo del equilibrio de $20), ¿qué ocurre?',
tipo: TipoRespuesta.MULTIPLE_CHOICE,
puntos: 20,
opciones: [
{
id: 'a',
texto: 'Se genera un exceso de oferta (superávit)',
correcta: false,
retroalimentacion: 'Incorrecto. Un precio máximo por debajo del equilibrio genera escasez, no superávit.'
},
{
id: 'b',
texto: 'Se genera un exceso de demanda (escasez)',
correcta: true,
retroalimentacion: '¡Correcto! A $15, Qd = 70 y Qs = 50, hay escasez de 20 unidades.'
},
{
id: 'c',
texto: 'El mercado permanece en equilibrio',
correcta: false,
retroalimentacion: 'Incorrecto. El precio controlado impide alcanzar el equilibrio.'
},
{
id: 'd',
texto: 'La cantidad transada aumenta',
correcta: false,
retroalimentacion: 'Incorrecto. La cantidad transada disminuye al nivel de la oferta (50).'
}
]
},
{
id: 'sim-pregunta-2',
enunciado: '¿Cuál es la cantidad transada con un precio máximo de $15?',
tipo: TipoRespuesta.NUMERICO,
puntos: 25,
respuestaCorrecta: 50,
ayuda: 'Con precio máximo, la cantidad transada es el menor entre cantidad demandada y cantidad ofrecida.'
},
{
id: 'sim-pregunta-3',
enunciado: 'Si el gobierno establece un precio mínimo de $25, ¿qué cantidad demandarán los consumidores?',
tipo: TipoRespuesta.NUMERICO,
puntos: 25,
respuestaCorrecta: 50,
ayuda: 'Usa la función de demanda: Qd = 100 - 2P. Sustituye P = 25.'
},
{
id: 'sim-pregunta-4',
enunciado: 'Selecciona todos los efectos de un precio máximo efectivo:',
tipo: TipoRespuesta.SELECCIONAR,
puntos: 30,
opciones: [
{ id: 'a', texto: 'Escasez del bien', correcta: true },
{ id: 'b', texto: 'Colas y listas de espera', correcta: true },
{ id: 'c', texto: 'Aumento de calidad', correcta: false },
{ id: 'd', texto: 'Mercados negros', correcta: true },
{ id: 'e', texto: 'Reducción de oferta', correcta: true },
{ id: 'f', texto: 'Mayor bienestar total', correcta: false }
]
}
]
};
// ============================================
// EJERCICIO 3: IDENTIFICAR SHOCKS
// ============================================
export const identificarShocks: Ejercicio = {
id: 'ejercicio-3-identificar-shocks',
tipo: TipoEjercicio.IDENTIFICAR_SHOCKS,
titulo: 'Identificador de Shocks del Mercado',
descripcion: 'Analiza escenarios económicos reales e identifica si afectan la oferta, la demanda, ambas, y cómo cambian precio y cantidad de equilibrio.',
instrucciones: [
'Lee el escenario presentado cuidadosamente',
'Identifica qué factor económico ha cambiado',
'Determina si afecta a la oferta, demanda, o ambas',
'Predice la dirección del cambio (aumenta/disminuye)',
'Indica cómo cambiarán el precio y la cantidad de equilibrio',
'Verifica tu respuesta y lee la explicación'
],
dificultad: Dificultad.MEDIO,
niveles: [
{
id: 'nivel-1-shocks-simples',
nombre: 'Nivel 1: Shocks Simples',
dificultad: Dificultad.FACIL,
descripcion: 'Identifica cambios solo en oferta o solo en demanda',
completado: false,
desbloqueado: true
},
{
id: 'nivel-2-magnitud',
nombre: 'Nivel 2: Magnitud de Cambios',
dificultad: Dificultad.MEDIO,
descripcion: 'Determina qué curva se desplaza más y efecto neto',
completado: false,
desbloqueado: false
},
{
id: 'nivel-3-escenarios-reales',
nombre: 'Nivel 3: Escenarios Reales',
dificultad: Dificultad.MEDIO,
descripcion: 'Analiza noticias económicas reales',
completado: false,
desbloqueado: false
},
{
id: 'nivel-4-complejos',
nombre: 'Nivel 4: Casos Complejos',
dificultad: Dificultad.DIFICIL,
descripcion: 'Escenarios donde oferta y demanda cambian simultáneamente',
completado: false,
desbloqueado: false
}
],
escenariosShock: [
// Nivel 1 - Simples
{
id: 'shock-1-cafe',
titulo: 'Café y Clima',
descripcion: 'Una sequía severa afecta las principales zonas cafetaleras de Brasil.',
mercado: 'Café',
evento: 'Sequía en Brasil',
tipoShock: TipoShock.OFERTA_DISMINUYE,
magnitud: 'grande',
graficoInicial: {
demanda: [
{ precio: 10, cantidad: 100 },
{ precio: 8, cantidad: 120 },
{ precio: 6, cantidad: 140 }
],
oferta: [
{ precio: 6, cantidad: 100 },
{ precio: 8, cantidad: 120 },
{ precio: 10, cantidad: 140 }
]
},
graficoFinal: {
demanda: [
{ precio: 10, cantidad: 100 },
{ precio: 8, cantidad: 120 },
{ precio: 6, cantidad: 140 }
],
oferta: [
{ precio: 8, cantidad: 80 },
{ precio: 10, cantidad: 100 },
{ precio: 12, cantidad: 120 }
]
},
resultadoEsperado: {
precioCambio: 'sube',
cantidadCambio: 'baja',
explicacion: 'La sequía reduce la cosecha, desplazando la oferta a la izquierda. Menor cantidad disponible a cada precio.'
},
opciones: [
{ id: 'a', texto: 'Demanda aumenta → P sube, Q sube', correcta: false },
{ id: 'b', texto: 'Oferta disminuye → P sube, Q baja', correcta: true },
{ id: 'c', texto: 'Oferta aumenta → P baja, Q sube', correcta: false },
{ id: 'd', texto: 'Demanda disminuye → P baja, Q baja', correcta: false }
]
},
{
id: 'shock-2-ingreso',
titulo: 'Restaurant y Bonos',
descripcion: 'El gobierno entrega bonos de $500 a todos los ciudadanos.',
mercado: 'Comida en restaurantes',
evento: 'Aumento de ingreso',
tipoShock: TipoShock.DEMANDA_AUMENTA,
magnitud: 'media',
graficoInicial: {
demanda: [
{ precio: 20, cantidad: 100 },
{ precio: 16, cantidad: 140 },
{ precio: 12, cantidad: 180 }
],
oferta: [
{ precio: 12, cantidad: 100 },
{ precio: 16, cantidad: 140 },
{ precio: 20, cantidad: 180 }
]
},
graficoFinal: {
demanda: [
{ precio: 24, cantidad: 100 },
{ precio: 20, cantidad: 140 },
{ precio: 16, cantidad: 180 }
],
oferta: [
{ precio: 12, cantidad: 100 },
{ precio: 16, cantidad: 140 },
{ precio: 20, cantidad: 180 }
]
},
resultadoEsperado: {
precioCambio: 'sube',
cantidadCambio: 'sube',
explicacion: 'Los restaurantes son un bien normal. Al aumentar el ingreso, la demanda se desplaza a la derecha.'
},
opciones: [
{ id: 'a', texto: 'Demanda aumenta → P sube, Q sube', correcta: true },
{ id: 'b', texto: 'Demanda disminuye → P baja, Q baja', correcta: false },
{ id: 'c', texto: 'Oferta aumenta → P baja, Q sube', correcta: false },
{ id: 'd', texto: 'Oferta disminuye → P sube, Q baja', correcta: false }
]
},
// Nivel 2 - Magnitud
{
id: 'shock-3-tecnologia',
titulo: 'Autos Eléctricos',
descripcion: 'Nueva tecnología de baterías reduce costos de producción en 40%.',
mercado: 'Autos eléctricos',
evento: 'Avance tecnológico',
tipoShock: TipoShock.OFERTA_AUMENTA,
magnitud: 'grande',
graficoInicial: {
demanda: [
{ precio: 50000, cantidad: 10000 },
{ precio: 40000, cantidad: 20000 }
],
oferta: [
{ precio: 30000, cantidad: 10000 },
{ precio: 40000, cantidad: 20000 }
]
},
graficoFinal: {
demanda: [
{ precio: 50000, cantidad: 10000 },
{ precio: 40000, cantidad: 20000 }
],
oferta: [
{ precio: 20000, cantidad: 10000 },
{ precio: 28000, cantidad: 20000 }
]
},
resultadoEsperado: {
precioCambio: 'baja',
cantidadCambio: 'sube',
explicacion: 'La tecnología mejora la productividad, aumentando oferta. Precios más bajos y mayor cantidad.'
},
opciones: [
{ id: 'a', texto: 'Oferta aumenta significativamente → P baja mucho, Q sube mucho', correcta: true },
{ id: 'b', texto: 'Oferta disminuye → P sube, Q baja', correcta: false },
{ id: 'c', texto: 'Demanda aumenta → P sube, Q sube', correcta: false },
{ id: 'd', texto: 'Demanda disminuye → P baja, Q baja', correcta: false }
]
},
// Nivel 3 - Escenarios reales
{
id: 'shock-4-petroleo',
titulo: 'Crisis del Petróleo',
descripcion: 'Conflicto en Medio Oriente reduce exportaciones de petróleo. Simultáneamente, países invierten en energías renovables.',
mercado: 'Petróleo',
evento: 'Reducción oferta + cambio gustos',
tipoShock: TipoShock.AMBAS_OFERTA_DEMANDA,
magnitud: 'media',
graficoInicial: {
demanda: [
{ precio: 100, cantidad: 80 },
{ precio: 80, cantidad: 100 }
],
oferta: [
{ precio: 60, cantidad: 80 },
{ precio: 80, cantidad: 100 }
]
},
graficoFinal: {
demanda: [
{ precio: 100, cantidad: 70 },
{ precio: 80, cantidad: 90 }
],
oferta: [
{ precio: 70, cantidad: 70 },
{ precio: 90, cantidad: 90 }
]
},
resultadoEsperado: {
precioCambio: 'sube',
cantidadCambio: 'baja',
explicacion: 'Oferta disminuye (conflicto) y demanda disminuye (alternativas). El precio sube (oferta cae más), cantidad cae (ambas).'
},
opciones: [
{ id: 'a', texto: 'Oferta ↓ y Demanda ↓ → P indeterminado, Q baja', correcta: false },
{ id: 'b', texto: 'Oferta ↓ más que Demanda ↓ → P sube, Q baja', correcta: true },
{ id: 'c', texto: 'Oferta ↑ y Demanda ↑ → P indeterminado, Q sube', correcta: false },
{ id: 'd', texto: 'Solo oferta ↓ → P sube, Q baja', correcta: false }
]
},
{
id: 'shock-5-casa',
titulo: 'Mercado Inmobiliario',
descripcion: 'Tasas de interés bajan a mínimos históricos. Al mismo tiempo, regulaciones ambientales dificultan nueva construcción.',
mercado: 'Vivienda',
evento: 'Crédito barato + regulaciones',
tipoShock: TipoShock.AMBAS_OFERTA_DEMANDA,
magnitud: 'media',
graficoInicial: {
demanda: [
{ precio: 300000, cantidad: 1000 },
{ precio: 250000, cantidad: 1500 }
],
oferta: [
{ precio: 200000, cantidad: 1000 },
{ precio: 250000, cantidad: 1500 }
]
},
graficoFinal: {
demanda: [
{ precio: 350000, cantidad: 1000 },
{ precio: 300000, cantidad: 1500 }
],
oferta: [
{ precio: 250000, cantidad: 800 },
{ precio: 300000, cantidad: 1200 }
]
},
resultadoEsperado: {
precioCambio: 'sube',
cantidadCambio: 'indeterminado',
explicacion: 'Demanda aumenta (crédito barato) y oferta disminuye (regulaciones). El precio definitivamente sube, pero el efecto en cantidad depende de qué cambio sea mayor.'
},
opciones: [
{ id: 'a', texto: 'Demanda ↑ y Oferta ↓ → P sube, Q indeterminado', correcta: true },
{ id: 'b', texto: 'Demanda ↓ y Oferta ↑ → P baja, Q indeterminado', correcta: false },
{ id: 'c', texto: 'Demanda ↑ y Oferta ↑ → P indeterminado, Q sube', correcta: false },
{ id: 'd', texto: 'Demanda ↓ y Oferta ↓ → P indeterminado, Q baja', correcta: false }
]
},
// Nivel 4 - Complejos
{
id: 'shock-6-pandemia',
titulo: 'Efecto Pandemia',
descripcion: 'Durante COVID-19: cierres de fábricas reducen producción de laptops, pero trabajo remoto aumenta demanda dramáticamente.',
mercado: 'Laptops',
evento: 'Pandemia',
tipoShock: TipoShock.AMBAS_OFERTA_DEMANDA,
magnitud: 'grande',
graficoInicial: {
demanda: [
{ precio: 800, cantidad: 50000 },
{ precio: 600, cantidad: 80000 }
],
oferta: [
{ precio: 400, cantidad: 50000 },
{ precio: 600, cantidad: 80000 }
]
},
graficoFinal: {
demanda: [
{ precio: 1200, cantidad: 80000 },
{ precio: 1000, cantidad: 110000 }
],
oferta: [
{ precio: 600, cantidad: 40000 },
{ precio: 800, cantidad: 60000 }
]
},
resultadoEsperado: {
precioCambio: 'sube',
cantidadCambio: 'indeterminado',
explicacion: 'Oferta disminuyó (cierres) pero demanda aumentó mucho más (trabajo remoto). Precios subieron significativamente. La cantidad pudo subir o bajar según magnitudes relativas.'
},
opciones: [
{ id: 'a', texto: 'Demanda ↑↑↑ más que Oferta ↓ → P sube mucho, Q probablemente sube', correcta: true },
{ id: 'b', texto: 'Demanda y Oferta disminuyen → P indeterminado, Q baja', correcta: false },
{ id: 'c', texto: 'Solo demanda aumenta → P sube, Q sube', correcta: false },
{ id: 'd', texto: 'Solo oferta disminuye → P sube, Q baja', correcta: false }
]
}
]
};
// ============================================
// FUNCIÓN AUXILIAR: CALCULAR RESULTADO
// ============================================
export function calcularResultadoShock(
shockOferta: 'aumenta' | 'disminuye' | 'sin_cambio',
shockDemanda: 'aumenta' | 'disminuye' | 'sin_cambio',
_magnitudOferta: 'mayor' | 'menor' | 'igual',
_magnitudDemanda: 'mayor' | 'menor' | 'igual'
): { precio: 'sube' | 'baja' | 'igual' | 'indeterminado'; cantidad: 'sube' | 'baja' | 'igual' | 'indeterminado' } {
// Lógica simplificada para determinar resultado
let precio: 'sube' | 'baja' | 'igual' | 'indeterminado' = 'igual';
let cantidad: 'sube' | 'baja' | 'igual' | 'indeterminado' = 'igual';
// Análisis de precio
if (shockOferta === 'aumenta' && shockDemanda === 'aumenta') {
precio = 'indeterminado';
} else if (shockOferta === 'disminuye' && shockDemanda === 'disminuye') {
precio = 'indeterminado';
} else if (shockOferta === 'aumenta' && shockDemanda === 'disminuye') {
precio = 'baja';
} else if (shockOferta === 'disminuye' && shockDemanda === 'aumenta') {
precio = 'sube';
} else if (shockDemanda === 'aumenta') {
precio = 'sube';
} else if (shockDemanda === 'disminuye') {
precio = 'baja';
} else if (shockOferta === 'aumenta') {
precio = 'baja';
} else if (shockOferta === 'disminuye') {
precio = 'sube';
}
// Análisis de cantidad
if (shockOferta === 'aumenta' && shockDemanda === 'disminuye') {
cantidad = 'indeterminado';
} else if (shockOferta === 'disminuye' && shockDemanda === 'aumenta') {
cantidad = 'indeterminado';
} else if (shockOferta === 'aumenta' && shockDemanda === 'aumenta') {
cantidad = 'sube';
} else if (shockOferta === 'disminuye' && shockDemanda === 'disminuye') {
cantidad = 'baja';
} else if (shockOferta === 'aumenta') {
cantidad = 'sube';
} else if (shockOferta === 'disminuye') {
cantidad = 'baja';
} else if (shockDemanda === 'aumenta') {
cantidad = 'sube';
} else if (shockDemanda === 'disminuye') {
cantidad = 'baja';
}
return { precio, cantidad };
}
// ============================================
// DATOS DE PROGRESO EJEMPLO
// ============================================
export const progresoInicial: ProgresoEjercicio[] = [
{
ejercicioId: 'ejercicio-1-constructor-curvas',
completado: false,
puntuacion: 0,
tiempoSegundos: 0,
intentos: 0,
nivelesCompletados: []
},
{
ejercicioId: 'ejercicio-2-simulador-precios',
completado: false,
puntuacion: 0,
tiempoSegundos: 0,
intentos: 0,
nivelesCompletados: []
},
{
ejercicioId: 'ejercicio-3-identificar-shocks',
completado: false,
puntuacion: 0,
tiempoSegundos: 0,
intentos: 0,
nivelesCompletados: []
}
];
// ============================================
// RESUMEN DE EJERCICIOS
// ============================================
export const resumenEjercicios = {
titulo: 'Ejercicios del Módulo 2',
descripcion: 'Practica los conceptos de oferta, demanda y equilibrio con estos ejercicios interactivos.',
ejercicios: [
{
id: 'ejercicio-1',
nombre: 'Constructor de Curvas',
habilidades: ['Trazar curvas', 'Identificar pendientes', 'Encontrar equilibrio'],
tiempoEstimado: '10-15 minutos'
},
{
id: 'ejercicio-2',
nombre: 'Simulador de Precios',
habilidades: ['Controles de precio', 'Calcular desequilibrios', 'Pérdida de bienestar'],
tiempoEstimado: '15-20 minutos'
},
{
id: 'ejercicio-3',
nombre: 'Identificar Shocks',
habilidades: ['Análisis de escenarios', 'Desplazamientos de curvas', 'Predicción de cambios'],
tiempoEstimado: '20-25 minutos'
}
],
consejos: [
'Comienza con el Ejercicio 1 si eres principiante',
'Revisa la teoría antes de intentar los ejercicios',
'Usa papel y lápiz para hacer cálculos',
'No te preocupes por errores, son parte del aprendizaje',
'Repite los ejercicios hasta dominarlos'
]
};
// Exportación por defecto
export default {
constructorCurvas,
simuladorPrecios,
identificarShocks,
progresoInicial,
resumen: resumenEjercicios,
utilidades: {
calcularResultadoShock
}
};

View File

@@ -0,0 +1,608 @@
/**
* Módulo 2: Equilibrio de Mercado
*
* Este módulo cubre el concepto de equilibrio, desequilibrios,
* y controles de precios en los mercados.
*/
// ============================================
// TIPOS Y ENUMERACIONES
// ============================================
export enum TipoDesequilibrio {
EXCESO_OFERTA = 'exceso_oferta', // Superávit
EXCESO_DEMANDA = 'exceso_demanda', // Escasez
EQUILIBRIO = 'equilibrio'
}
export enum TipoControlPrecio {
PRECIO_MAXIMO = 'precio_maximo', // Techo
PRECIO_MINIMO = 'precio_minimo', // Piso
NINGUNO = 'ninguno'
}
export enum EfectoControlPrecio {
ESCASEZ = 'escasez',
SUPERAVIT = 'superavit',
MERCADO_NEGRO = 'mercado_negro',
DESEMPLEO = 'desempleo',
RACIONAMIENTO = 'racionamiento',
NINGUNO = 'ninguno'
}
// ============================================
// INTERFACES
// ============================================
export interface PuntoMercado {
precio: number;
cantidad: number;
}
export interface EquilibrioMercado {
precioEquilibrio: number;
cantidadEquilibrio: number;
punto: PuntoMercado;
excedenteConsumidor: number;
excedenteProductor: number;
bienestarTotal: number;
}
export interface CurvaMercado {
demanda: PuntoMercado[];
oferta: PuntoMercado[];
}
export interface Desequilibrio {
tipo: TipoDesequilibrio;
precioActual: number;
cantidadDemandada: number;
cantidadOfrecida: number;
diferencia: number;
magnitud: number;
presionPrecio: 'subir' | 'bajar' | 'ninguna';
}
export interface ControlPrecio {
tipo: TipoControlPrecio;
nivel: number;
precioEquilibrio: number;
efectivo: boolean;
efectos: EfectoControlPrecio[];
cantidadTransada: number;
perdidaBienestar: number;
}
export interface EjemploEquilibrio {
titulo: string;
mercado: string;
descripcion: string;
datos: CurvaMercado;
equilibrio: EquilibrioMercado;
escenarios: EscenarioDesequilibrio[];
}
export interface EscenarioDesequilibrio {
nombre: string;
precio: number;
tipo: TipoDesequilibrio;
explicacion: string;
resultado: string;
}
export interface HistoriaPrecio {
periodo: string;
descripcion: string;
precioControl: number;
precioEquilibrio: number;
consecuencias: string[];
lecciones: string;
}
// ============================================
// CONTENIDO TEÓRICO
// ============================================
export const definicionEquilibrio = {
titulo: 'Equilibrio de Mercado',
definicion: 'El equilibrio de mercado es una situación en la que la cantidad demandada de un bien es igual a la cantidad ofrecida. En este punto, no hay tendencia al cambio: ni compradores ni vendedores tienen incentivo para alterar sus decisiones.',
caracteristicas: [
{
caracteristica: 'Cantidad demandada = Cantidad ofrecida',
explicacion: 'No hay exceso de oferta ni de demanda'
},
{
caracteristica: 'Precio estable',
explicacion: 'No hay presiones para que el precio suba o baje'
},
{
caracteristica: 'Eficiencia',
explicacion: 'Se maximiza el bienestar social (excedente total)'
},
{
caracteristica: 'Voluntad de intercambio',
explicacion: 'Todos los intercambios mutuamente beneficiosos ocurren'
}
],
mecanismoAjuste: {
titulo: 'Mecanismo de Ajuste al Equilibrio',
proceso: [
{
paso: 1,
situacion: 'Precio por encima del equilibrio',
resultado: 'Exceso de oferta (superávit)',
ajuste: 'Vendedores compiten reduciendo precios para vender excedentes'
},
{
paso: 2,
situacion: 'Precio por debajo del equilibrio',
resultado: 'Exceso de demanda (escasez)',
ajuste: 'Compradores compiten ofreciendo precios más altos'
},
{
paso: 3,
situacion: 'Precio de equilibrio',
resultado: 'Cantidad demandada = Cantidad ofrecida',
ajuste: 'No hay presiones adicionales; mercado está en equilibrio'
}
]
},
representacionMatematica: {
condicion: 'Qd = Qs',
ejemplo: {
demanda: 'Qd = 100 - 2P',
oferta: 'Qs = 20 + 3P',
resolucion: [
'100 - 2P = 20 + 3P',
'80 = 5P',
'P* = 16 (precio de equilibrio)',
'Q* = 100 - 2(16) = 68 (cantidad de equilibrio)'
]
}
}
};
// ============================================
// EXCEDENTES DEL CONSUMIDOR Y PRODUCTOR
// ============================================
export const excedentesMercado = {
titulo: 'Bienestar en el Equilibrio',
excedenteConsumidor: {
nombre: 'Excedente del Consumidor (EC)',
definicion: 'Diferencia entre lo que los consumidores están dispuestos a pagar y lo que realmente pagan',
formula: 'EC = Valoración - Precio pagado',
calculo: 'Área bajo la curva de demanda y arriba del precio de equilibrio',
interpretacion: 'Beneficio neto que obtienen los consumidores del intercambio',
ejemplo: 'Dispuesto a pagar $50, pago $30 → Excedente = $20'
},
excedenteProductor: {
nombre: 'Excedente del Productor (EP)',
definicion: 'Diferencia entre el precio que reciben los productores y el costo mínimo al que estarían dispuestos a vender',
formula: 'EP = Precio recibido - Costo de producción',
calculo: 'Área arriba de la curva de oferta y debajo del precio de equilibrio',
interpretacion: 'Beneficio neto que obtienen los productores del intercambio',
ejemplo: 'Dispuesto a vender a $20, recibo $30 → Excedente = $10'
},
bienestarTotal: {
nombre: 'Bienestar Total (Excedente Total)',
definicion: 'Suma de excedentes del consumidor y del productor',
formula: 'BT = EC + EP',
propiedad: 'En equilibrio competitivo, el bienestar total se maximiza',
perdida: 'Cualquier desviación del equilibrio genera pérdida de bienestar'
}
};
// ============================================
// DESEQUILIBRIOS DE MERCADO
// ============================================
export const excesoOferta: Desequilibrio = {
tipo: TipoDesequilibrio.EXCESO_OFERTA,
precioActual: 25,
cantidadDemandada: 40,
cantidadOfrecida: 80,
diferencia: 40,
magnitud: 40,
presionPrecio: 'bajar'
};
export const excesoDemanda: Desequilibrio = {
tipo: TipoDesequilibrio.EXCESO_DEMANDA,
precioActual: 10,
cantidadDemandada: 90,
cantidadOfrecida: 30,
diferencia: 60,
magnitud: 60,
presionPrecio: 'subir'
};
export const tiposDesequilibrio = {
excesoOferta: {
nombre: 'Exceso de Oferta (Superávit)',
definicion: 'Cantidad ofrecida > Cantidad demandada',
causas: [
'Precio por encima del equilibrio',
'Aumento repentino de producción',
'Caída inesperada de la demanda'
],
consecuencias: [
'Acumulación de inventarios',
'Presión a la baja en precios',
'Posibles quiebras si persiste',
'Competencia agresiva entre vendedores'
],
ejemplos: [
'Viviendas sin vender después de una burbuja',
'Excedentes agrícolas después de buenas cosechas',
'Autos en concesionarias durante recesión'
],
grafico: {
descripcion: 'A P > P*, Qs > Qd',
areaSuperavit: 'Distancia horizontal entre curvas al precio dado'
}
},
excesoDemanda: {
nombre: 'Exceso de Demanda (Escasez)',
definicion: 'Cantidad demandada > Cantidad ofrecida',
causas: [
'Precio por debajo del equilibrio',
'Aumento repentino de la demanda',
'Caída inesperada de la oferta'
],
consecuencias: [
'Colas y listas de espera',
'Presión al alza en precios',
'Racionamiento de productos',
'Mercados negros',
'Malestar social'
],
ejemplos: [
'Gasolina en crisis (colas en bomba)',
'Entradas para conciertos populares',
'Vivienda asequible en ciudades caras',
'Productos básicos con precios controlados'
],
grafico: {
descripcion: 'A P < P*, Qd > Qs',
areaEscasez: 'Distancia horizontal entre curvas al precio dado'
}
}
};
// ============================================
// CONTROLES DE PRECIO
// ============================================
export const controlesPrecio = {
titulo: 'Controles de Precio',
introduccion: 'Los gobiernos a veces intervienen estableciendo precios máximos o mínimos que difieren del precio de equilibrio de mercado.',
precioMaximo: {
nombre: 'Precio Máximo (Techo)',
definicion: 'Precio legal más alto al que se puede vender un bien',
condicionEfectivo: 'Debe estar DEBAJO del precio de equilibrio',
efectos: {
cuandoEsEfectivo: [
'Escasez persistente (Qd > Qs)',
'Racionamiento del bien',
'Colas y esperas',
'Mercados negros',
'Reducción de calidad',
'Pérdida de bienestar'
],
cuandoNoEsEfectivo: [
'Precio máximo > precio de equilibrio',
'Mercado opera normalmente',
'Sin efectos sobre cantidad transada'
]
},
ejemplos: [
{ caso: 'Alquileres', ubicacion: 'Nueva York, San Francisco', resultado: 'Escasez de vivienda, subarriendos' },
{ caso: 'Gasolina', ubicacion: 'Estados Unidos 1970s', resultado: 'Largas colas, mercado negro' },
{ caso: 'Productos básicos', ubicacion: 'Venezuela', resultado: 'Desabastecimiento, contrabando' }
]
},
precioMinimo: {
nombre: 'Precio Mínimo (Piso)',
definicion: 'Precio legal más bajo al que se puede vender un bien',
condicionEfectivo: 'Debe estar ARRIBA del precio de equilibrio',
efectos: {
cuandoEsEfectivo: [
'Superávit persistente (Qs > Qd)',
'Acumulación de inventarios',
'Desperdicio de recursos',
'Mercados negros (venta a precio menor)',
'Pérdida de bienestar'
],
cuandoNoEsEfectivo: [
'Precio mínimo < precio de equilibrio',
'Mercado opera normalmente',
'Sin efectos sobre cantidad transada'
]
},
ejemplos: [
{ caso: 'Salario mínimo', ubicacion: 'Mayoría de países', resultado: 'Desempleo potencial en trabajadores no calificados' },
{ caso: 'Precios agrícolas', ubicacion: 'Unión Europea', resultado: 'Superávits, gasto gubernamental' },
{ caso: 'Alcohol/tabaco', ubicacion: 'Políticas de salud', resultado: 'Menor consumo, contrabando' }
]
}
};
// ============================================
// EJEMPLOS DE CONTROLES HISTÓRICOS
// ============================================
export const historiasControlesPrecio: HistoriaPrecio[] = [
{
periodo: '1971-1974',
descripcion: 'Control de precios de gasolina en EE.UU.',
precioControl: 0.36,
precioEquilibrio: 0.55,
consecuencias: [
'Largas colas en gasolineras (hasta 4 horas)',
'Racionamiento por día par/impar',
'Mercado negro de gasolina',
'Violencia en gasolineras',
'Desabastecimiento regional'
],
lecciones: 'Los controles de precios generan escasez cuando están por debajo del equilibrio'
},
{
periodo: '1946-1947',
descripcion: 'Control de alquileres en Nueva York',
precioControl: 75,
precioEquilibrio: 100,
consecuencias: [
'Reducción de mantenimiento de edificios',
'Conversión de apartamentos a condominios',
'Mercado negro (pagos bajo mesa)',
'Escasez crónica de vivienda',
'Subarriendos a precios más altos'
],
lecciones: 'Precios máximos reducen la calidad y cantidad de oferta a largo plazo'
},
{
periodo: 'Actual',
descripcion: 'Salario mínimo en diferentes países',
precioControl: 15,
precioEquilibrio: 12,
consecuencias: [
'Reducción de contratación de jóvenes',
'Automatización de trabajos (kioscos)',
'Reducción de horas trabajadas',
'Beneficio para trabajadores que mantienen empleo',
'Posible aumento de precios'
],
lecciones: 'Los precios mínimos crean desempleo cuando están por encima del equilibrio, pero benefician a quienes mantienen el empleo'
}
];
// ============================================
// EJEMPLOS PRÁCTICOS DE EQUILIBRIO
// ============================================
export const ejemplosEquilibrio: EjemploEquilibrio[] = [
{
titulo: 'Mercado de Manzanas',
mercado: 'Manzanas',
descripcion: 'Análisis del equilibrio en un mercado agrícola simple',
datos: {
demanda: [
{ precio: 1, cantidad: 90 },
{ precio: 2, cantidad: 80 },
{ precio: 3, cantidad: 70 },
{ precio: 4, cantidad: 60 },
{ precio: 5, cantidad: 50 }
],
oferta: [
{ precio: 1, cantidad: 10 },
{ precio: 2, cantidad: 30 },
{ precio: 3, cantidad: 50 },
{ precio: 4, cantidad: 70 },
{ precio: 5, cantidad: 90 }
]
},
equilibrio: {
precioEquilibrio: 3.5,
cantidadEquilibrio: 60,
punto: { precio: 3.5, cantidad: 60 },
excedenteConsumidor: 45,
excedenteProductor: 45,
bienestarTotal: 90
},
escenarios: [
{
nombre: 'Precio por encima del equilibrio',
precio: 5,
tipo: TipoDesequilibrio.EXCESO_OFERTA,
explicacion: 'A $5, los productores ofrecen 90 unidades pero solo se demandan 50',
resultado: 'Superávit de 40 unidades. Presión a la baja en precios.'
},
{
nombre: 'Precio por debajo del equilibrio',
precio: 2,
tipo: TipoDesequilibrio.EXCESO_DEMANDA,
explicacion: 'A $2, se demandan 80 unidades pero solo se ofrecen 30',
resultado: 'Escasez de 50 unidades. Colas y presión al alza.'
}
]
},
{
titulo: 'Mercado Laboral: Desempleo',
mercado: 'Trabajo no calificado',
descripcion: 'Efecto del salario mínimo en el empleo',
datos: {
demanda: [
{ precio: 4, cantidad: 100 },
{ precio: 6, cantidad: 80 },
{ precio: 8, cantidad: 60 },
{ precio: 10, cantidad: 40 },
{ precio: 12, cantidad: 20 }
],
oferta: [
{ precio: 4, cantidad: 20 },
{ precio: 6, cantidad: 40 },
{ precio: 8, cantidad: 60 },
{ precio: 10, cantidad: 80 },
{ precio: 12, cantidad: 100 }
]
},
equilibrio: {
precioEquilibrio: 8,
cantidadEquilibrio: 60,
punto: { precio: 8, cantidad: 60 },
excedenteConsumidor: 180,
excedenteProductor: 180,
bienestarTotal: 360
},
escenarios: [
{
nombre: 'Salario mínimo efectivo',
precio: 12,
tipo: TipoDesequilibrio.EXCESO_OFERTA,
explicacion: 'Salario mínimo de $12 está por encima del equilibrio de $8',
resultado: '100 personas buscan trabajo, pero solo 20 empleos. Desempleo de 80 personas.'
}
]
}
];
// ============================================
// TABLA COMPARATIVA: CONTROLES DE PRECIO
// ============================================
export const tablaComparativaControles = {
titulo: 'Comparación de Controles de Precio',
filas: [
{
caracteristica: 'Nombre',
precioMaximo: 'Techo, Precio Máximo',
precioMinimo: 'Piso, Precio Mínimo'
},
{
caracteristica: 'Ubicación',
precioMaximo: 'Debajo del equilibrio',
precioMinimo: 'Arriba del equilibrio'
},
{
caracteristica: 'Desequilibrio',
precioMaximo: 'Exceso de demanda (escasez)',
precioMinimo: 'Exceso de oferta (superávit)'
},
{
caracteristica: 'Cantidad transada',
precioMaximo: 'Determinada por oferta (Qs)',
precioMinimo: 'Determinada por demanda (Qd)'
},
{
caracteristica: 'Efectos secundarios',
precioMaximo: 'Colas, mercado negro, baja calidad',
precioMinimo: 'Inventarios, desperdicio, mercado negro'
},
{
caracteristica: 'Ejemplo común',
precioMaximo: 'Alquileres, gasolina',
precioMinimo: 'Salarios mínimos, precios agrícolas'
}
]
};
// ============================================
// PÉRDIDA DE BIENESTAR (DEADWEIGHT LOSS)
// ============================================
export const perdidaBienestar = {
titulo: 'Pérdida de Eficiencia por Controles',
definicion: 'La pérdida de peso muerto (deadweight loss) es la reducción en el bienestar total que ocurre cuando el mercado no alcanza el equilibrio. Representa transacciones mutuamente beneficiosas que no ocurren.',
calculoPrecioMaximo: {
pasos: [
'En equilibrio: BT = EC + EP',
'Con precio máximo: BT_control = EC_nuevo + EP_nuevo',
'Pérdida = BT_equilibrio - BT_control',
'Representada por el triángulo entre cantidad transada y cantidad de equilibrio'
],
areas: [
'Pérdida de excedente del consumidor (transacciones perdidas)',
'Pérdida de excedente del productor (transacciones perdidas)',
'Área total: triángulo entre curvas de oferta y demanda'
]
},
calculoPrecioMinimo: {
pasos: [
'En equilibrio: BT = EC + EP',
'Con precio mínimo: BT_control = EC_nuevo + EP_nuevo',
'Pérdida = BT_equilibrio - BT_control',
'Representada por el triángulo entre cantidad demandada y cantidad de equilibrio'
],
areas: [
'Pérdida por producción ineficiente (costos > beneficios)',
'Pérdida por consumo perdido (beneficios > costos)',
'Área total: triángulo entre curvas de oferta y demanda'
]
}
};
// ============================================
// RESUMEN
// ============================================
export const resumenEquilibrio = {
titulo: 'Resumen: Equilibrio de Mercado',
puntosClave: [
'Equilibrio: Qd = Qs, sin tendencia al cambio',
'Precio de equilibrio estable sin presiones',
'Exceso de oferta: P > P* → presión a la baja',
'Exceso de demanda: P < P* → presión al alza',
'Precio máximo efectivo: P_max < P* → escasez',
'Precio mínimo efectivo: P_min > P* → superávit',
'Controles de precio generan pérdida de bienestar',
'Mercados tienden al equilibrio mediante ajustes de precio'
],
formulasRecordatorio: {
condicionEquilibrio: 'Qd = Qs',
excesoOferta: 'Qs - Qd > 0 cuando P > P*',
excesoDemanda: 'Qd - Qs > 0 cuando P < P*',
bienestarTotal: 'BT = EC + EP',
perdidaBienestar: 'DWL = BT_equilibrio - BT_control'
},
mapaConceptual: {
centro: 'Equilibrio',
ramas: [
{ nombre: 'Condición', elementos: ['Qd = Qs', 'P estable', 'Eficiencia máxima'] },
{ nombre: 'Desequilibrios', elementos: ['Exceso oferta (superávit)', 'Exceso demanda (escasez)'] },
{ nombre: 'Controles', elementos: ['Precio máximo → escasez', 'Precio mínimo → superávit'] },
{ nombre: 'Bienestar', elementos: ['Excedente consumidor', 'Excedente productor', 'Pérdida peso muerto'] }
]
}
};
// Exportación por defecto
export default {
definicion: definicionEquilibrio,
excedentes: excedentesMercado,
desequilibrios: tiposDesequilibrio,
controles: controlesPrecio,
historias: historiasControlesPrecio,
ejemplos: ejemplosEquilibrio,
comparativa: tablaComparativaControles,
perdidaBienestar,
resumen: resumenEquilibrio
};

View File

@@ -0,0 +1,487 @@
/**
* Módulo 2: Ley de la Oferta
*
* Este módulo cubre los fundamentos de la oferta en economía,
* incluyendo la ley de la oferta, factores determinantes y
* comportamiento de los productores.
*/
// ============================================
// TIPOS Y ENUMERACIONES
// ============================================
export enum DireccionDesplazamientoOferta {
IZQUIERDA = 'izquierda', // Disminución de oferta
DERECHA = 'derecha', // Aumento de oferta
NINGUNO = 'ninguno' // Sin cambio
}
export enum HorizonteTemporal {
CORTO_PLAZO = 'corto_plazo',
LARGO_PLAZO = 'largo_plazo'
}
export enum TipoMercado {
COMPETENCIA_PERFECTA = 'competencia_perfecta',
MONOPOLIO = 'monopolio',
OLIGOPOLIO = 'oligopolio',
COMPETENCIA_MONOPOLISTICA = 'competencia_monopolistica'
}
// ============================================
// INTERFACES
// ============================================
export interface PuntoOferta {
precio: number;
cantidad: number;
}
export interface CurvaOferta {
id: string;
nombre: string;
puntos: PuntoOferta[];
descripcion: string;
horizonteTemporal: HorizonteTemporal;
}
export interface FactorDesplazamientoOferta {
nombre: string;
descripcion: string;
direccion: DireccionDesplazamientoOferta;
ejemplo: string;
icono: string;
mecanismo: string;
}
export interface CostoProduccion {
categoria: string;
componentes: string[];
impactoOferta: string;
}
export interface EjemploOferta {
titulo: string;
bien: string;
escenario: string;
explicacion: string;
graficoData: PuntoOferta[];
impactoEconomico: string;
}
// ============================================
// CONTENIDO TEÓRICO
// ============================================
export const definicionOferta = {
titulo: 'Definición de Oferta',
definicion: 'La oferta es la cantidad de un bien o servicio que los productores están dispuestos y pueden ofrecer al mercado a diferentes precios durante un período específico, manteniendo constantes otros factores.',
elementosClave: [
{
elemento: 'Disposición a vender',
descripcion: 'El productor debe querer ofrecer el bien (rentabilidad)'
},
{
elemento: 'Capacidad de producción',
descripcion: 'El productor debe tener los recursos para producir'
},
{
elemento: 'Precios variables',
descripcion: 'Se analiza la relación a diferentes niveles de precio'
},
{
elemento: 'Período de tiempo',
descripcion: 'La oferta siempre se refiere a un período específico'
}
],
diferenciaCapacidad: {
capacidad: 'Puedo producir 1000 unidades (capacidad técnica)',
oferta: 'Estoy dispuesto a ofrecer 800 unidades a $10 porque es rentable'
}
};
export const leyOferta = {
titulo: 'Ley de la Oferta',
enunciado: 'Existe una relación directa entre el precio de un bien y la cantidad ofrecida: cuando el precio aumenta, la cantidad ofrecida aumenta, y viceversa.',
explicacion: 'Esta relación directa se explica por:',
razones: [
{
nombre: 'Motivación de lucro',
descripcion: 'A precios más altos, la producción es más rentable, incentivando a los productores a aumentar la oferta.',
ejemplo: 'Si el precio de las manzanas sube a $5/kg, más agricultores querrán producir manzanas.'
},
{
nombre: 'Costos crecientes',
descripcion: 'Para producir más, las empresas deben usar recursos menos eficientes o pagar costos más altos por factores adicionales.',
ejemplo: 'Para cultivar más trigo, se deben usar tierras menos fértiles que requieren más insumos.'
},
{
nombre: 'Entrada de nuevos productores',
descripcion: 'Precios más altos atraen a nuevos productores al mercado, aumentando la oferta total.',
ejemplo: 'Si el café está caro, agricultores de otras zonas comienzan a cultivar café.'
}
],
representacionMatematica: {
funcion: 'Qs = f(P)',
donde: {
Qs: 'Cantidad ofrecida',
P: 'Precio del bien',
f: 'Función creciente (pendiente positiva)'
},
ejemploLineal: 'Qs = 20 + 3P',
interpretacion: 'Por cada aumento de $1 en el precio, la cantidad ofrecida aumenta en 3 unidades.'
},
excepciones: [
{
caso: 'Bienes de especulación',
descripcion: 'Si los productores esperan precios aún más altos en el futuro, pueden reducir la oferta actual.',
ejemplo: 'Productores de petróleo reducen oferta actual esperando precios más altos.'
},
{
caso: 'Bienes de lujo exclusivo',
descripcion: 'Para mantener exclusividad, productores pueden limitar cantidad aunque el precio sea alto.',
ejemplo: 'Relojes suizos de lujo mantienen producción limitada a pesar de alta demanda.'
},
{
caso: 'Trabajo (curva de oferta retrógrada)',
descripcion: 'Muy altos salarios pueden reducir horas trabajadas (preferencia por ocio).',
ejemplo: 'Médicos especialistas trabajan menos horas cuando ganan suficiente.'
}
]
};
// ============================================
// FACTORES QUE DESPLAZAN LA CURVA DE OFERTA
// ============================================
export const factoresDesplazamientoOferta: FactorDesplazamientoOferta[] = [
{
nombre: 'Tecnología',
descripcion: 'Avances tecnológicos que mejoran la productividad',
direccion: DireccionDesplazamientoOferta.DERECHA,
mecanismo: 'Reduce costos unitarios, permite producir más con mismos recursos',
ejemplo: 'Nuevas máquinas de coser automáticas duplican la producción de ropa',
icono: '⚙️'
},
{
nombre: 'Precio de insumos',
descripcion: 'Cambios en el costo de materias primas, mano de obra o capital',
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
mecanismo: 'Aumenta costos de producción, reduce rentabilidad',
ejemplo: 'Subida del precio del petróleo aumenta costos de transporte y plásticos',
icono: '⛽'
},
{
nombre: 'Número de vendedores',
descripcion: 'Entrada o salida de empresas del mercado',
direccion: DireccionDesplazamientoOferta.DERECHA,
mecanismo: 'Más productores = más oferta total en el mercado',
ejemplo: 'Eliminación de aranceles permite entrada de productores extranjeros',
icono: '🏭'
},
{
nombre: 'Expectativas de precios',
descripcion: 'Expectativas sobre precios futuros del bien',
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
mecanismo: 'Si esperan precios más altos futuros, reducen oferta actual',
ejemplo: 'Agricultores almacenan granos esperando precios más altos en invierno',
icono: '📈'
},
{
nombre: 'Impuestos y subsidios',
descripcion: 'Políticas gubernamentales que afectan costos',
direccion: DireccionDesplazamientoOferta.IZQUIERDA, // Para impuestos
mecanismo: 'Impuestos aumentan costos; subsidios reducen costos',
ejemplo: 'Nuevo impuesto al tabaco reduce oferta; subsidio a energías renovables aumenta oferta',
icono: '🏛️'
},
{
nombre: 'Condiciones naturales',
descripcion: 'Eventos climáticos, desastres naturales o condiciones ambientales',
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
mecanismo: 'Afecta capacidad productiva de sectores agrícolas o naturales',
ejemplo: 'Sequía reduce cosecha de trigo; huracán afecta producción petrolera',
icono: '🌪️'
},
{
nombre: 'Regulaciones gubernamentales',
descripcion: 'Normativas ambientales, laborales o de producción',
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
mecanismo: 'Mayores requisitos aumentan costos de cumplimiento',
ejemplo: 'Nuevas normas ambientales requieren filtros costosos en fábricas',
icono: '📋'
}
];
// ============================================
// COSTOS DE PRODUCCIÓN
// ============================================
export const costosProduccion = {
titulo: 'Costos de Producción y Oferta',
introduccion: 'Los costos son fundamentales para entender las decisiones de oferta. Los productores maximizan ganancias donde Ingreso Marginal = Costo Marginal.',
categorias: [
{
categoria: 'Costos Fijos (CF)',
definicion: 'Costos que no varían con la cantidad producida',
ejemplos: ['Alquiler de local', 'Seguros', 'Salarios administrativos', 'Licencias'],
ejemplosCantidad: 'Ej: $10,000 mensuales sin importar producción'
},
{
categoria: 'Costos Variables (CV)',
definicion: 'Costos que varían directamente con la producción',
ejemplos: ['Materias primas', 'Mano de obra directa', 'Energía productiva', 'Envases'],
ejemplosCantidad: 'Ej: $5 por unidad producida'
},
{
categoria: 'Costos Totales (CT)',
definicion: 'Suma de costos fijos y variables',
formula: 'CT = CF + CV',
ejemplosCantidad: 'Ej: CT = $10,000 + $5 × Q'
}
],
costosMarginales: {
nombre: 'Costo Marginal (CM)',
definicion: 'Costo adicional de producir una unidad más',
importancia: 'Determina la curva de oferta del productor',
relacionOferta: 'El productor ofrecerá cantidades donde P ≥ CM',
formula: 'CM = ΔCT / ΔQ'
},
tablaEjemplo: [
{ q: 0, cf: 100, cv: 0, ct: 100, cme: '-', cmg: '-' },
{ q: 1, cf: 100, cv: 50, ct: 150, cme: 150, cmg: 50 },
{ q: 2, cf: 100, cv: 90, ct: 190, cme: 95, cmg: 40 },
{ q: 3, cf: 100, cv: 140, ct: 240, cme: 80, cmg: 50 },
{ q: 4, cf: 100, cv: 220, ct: 320, cme: 80, cmg: 80 },
{ q: 5, cf: 100, cv: 340, ct: 440, cme: 88, cmg: 120 }
]
};
// ============================================
// CURVAS DE OFERTA
// ============================================
export const curvaOfertaIndividual: CurvaOferta = {
id: 'oferta-individual',
nombre: 'Curva de Oferta Individual',
descripcion: 'Muestra la relación entre precio y cantidad ofrecida por un solo productor',
horizonteTemporal: HorizonteTemporal.CORTO_PLAZO,
puntos: [
{ precio: 2, cantidad: 10 },
{ precio: 4, cantidad: 30 },
{ precio: 6, cantidad: 55 },
{ precio: 8, cantidad: 85 },
{ precio: 10, cantidad: 120 }
]
};
export const curvaOfertaMercado: CurvaOferta = {
id: 'oferta-mercado',
nombre: 'Curva de Oferta de Mercado',
descripcion: 'Suma horizontal de todas las ofertas individuales en el mercado',
horizonteTemporal: HorizonteTemporal.CORTO_PLAZO,
puntos: [
{ precio: 2, cantidad: 1000 },
{ precio: 4, cantidad: 3000 },
{ precio: 6, cantidad: 5500 },
{ precio: 8, cantidad: 8500 },
{ precio: 10, cantidad: 12000 }
]
};
export const curvaOfertaLargoPlazo: CurvaOferta = {
id: 'oferta-largo-plazo',
nombre: 'Curva de Oferta a Largo Plazo',
descripcion: 'A largo plazo, más elástica debido a la entrada/salida de empresas',
horizonteTemporal: HorizonteTemporal.LARGO_PLAZO,
puntos: [
{ precio: 2, cantidad: 500 },
{ precio: 3, cantidad: 2000 },
{ precio: 4, cantidad: 5000 },
{ precio: 5, cantidad: 10000 },
{ precio: 6, cantidad: 18000 }
]
};
// ============================================
// EJEMPLOS PRÁCTICOS
// ============================================
export const ejemplosOferta: EjemploOferta[] = [
{
titulo: 'Tecnología en Manufactura',
bien: 'Smartphones',
escenario: 'Implementación de robots en línea de ensamblaje reduce tiempo de producción en 40%',
explicacion: 'El avance tecnológico desplaza la curva de oferta a la derecha. A cada precio, los productores pueden ofrecer más unidades porque sus costos unitarios han disminuido.',
graficoData: [
{ precio: 400, cantidad: 5000 },
{ precio: 350, cantidad: 7000 },
{ precio: 300, cantidad: 9500 },
{ precio: 250, cantidad: 12000 },
{ precio: 200, cantidad: 15000 }
],
impactoEconomico: 'Precios más bajos para consumidores y mayor acceso tecnológico'
},
{
titulo: 'Shock de Insumos: Petróleo',
bien: 'Gasolina',
escenario: 'Conflicto geopolítico reduce exportaciones de petróleo crudo en 30%',
explicacion: 'El aumento del precio de la materia prima (petróleo) desplaza la curva de oferta de gasolina a la izquierda. Es más costoso producir gasolina.',
graficoData: [
{ precio: 5, cantidad: 8000 },
{ precio: 6, cantidad: 6500 },
{ precio: 7, cantidad: 5000 },
{ precio: 8, cantidad: 3500 },
{ precio: 9, cantidad: 2000 }
],
impactoEconomico: 'Aumento de precios en transporte y productos derivados'
},
{
titulo: 'Entrada de Nuevos Productores',
bien: 'Café de especialidad',
escenario: 'Eliminación de barreras comerciales permite importación de café de nuevos países',
explicacion: 'La entrada de más productores al mercado aumenta la oferta total. La curva se desplaza a la derecha, beneficiando a los consumidores con más opciones.',
graficoData: [
{ precio: 20, cantidad: 1000 },
{ precio: 18, cantidad: 1800 },
{ precio: 16, cantidad: 2800 },
{ precio: 14, cantidad: 4000 },
{ precio: 12, cantidad: 5500 }
],
impactoEconomico: 'Mayor diversidad de productos y presión a la baja en precios'
}
];
// ============================================
// OFERTA EN DIFERENTES HORIZONTES TEMPORALES
// ============================================
export const ofertaTemporal = {
titulo: 'Oferta: Corto vs Largo Plazo',
cortoPlazo: {
definicion: 'Período en el que al menos un factor de producción es fijo',
caracteristicas: [
'Capacidad productiva limitada',
'No puede entrar/salir de empresas',
'Curva de oferta más inclinada (inelástica)',
'Ajustes principalmente en intensidad de uso'
],
ejemplo: 'Una fábrica puede aumentar producción con turnos extra, pero no construir nuevas plantas'
},
largoPlazo: {
definicion: 'Período en el que todos los factores de producción son variables',
caracteristicas: [
'Capacidad productiva ajustable',
'Entrada y salida de empresas',
'Curva de oferta más plana (elástica)',
'Ajustes en escala y número de productores'
],
ejemplo: 'Nuevas fábricas se construyen, tecnologías cambian, empresas entran o salen del mercado'
},
comparacionElasticidad: {
cortoPlazo: 'Inelástica: dificultad para ajustar producción rápidamente',
largoPlazo: 'Elástica: tiempo suficiente para todos los ajustes',
ejemploAgricultura: 'Corto plazo: usar fertilizantes. Largo plazo: cultivar más tierra.'
}
};
// ============================================
// MOVIMIENTO VS DESPLAZAMIENTO
// ============================================
export const diferenciaMovimientoDesplazamientoOferta = {
titulo: 'Movimiento a lo largo vs Desplazamiento de la curva de oferta',
movimiento: {
nombre: 'Movimiento a lo largo de la curva',
causa: 'Cambio en el precio del propio bien',
efecto: 'Cambio en la cantidad ofrecida',
direccion: 'Subida o bajada por la misma curva',
ejemplo: 'El precio del trigo sube de $5 a $7 → agricultores ofrecen más trigo',
representacion: 'Movimiento de un punto a otro en la misma curva'
},
desplazamiento: {
nombre: 'Desplazamiento de la curva',
causa: 'Cambio en factores distintos al precio (tecnología, costos, regulaciones)',
efecto: 'Cambio en la oferta (toda la curva se mueve)',
direccionDerecha: 'Aumento de oferta (más cantidad a cada precio)',
direccionIzquierda: 'Disminución de oferta (menos cantidad a cada precio)',
ejemplo: 'Nueva tecnología reduce costos → más oferta a todos los precios',
representacion: 'Curva completa se desplaza'
},
tablaComparativa: [
{ concepto: 'Causa', movimiento: 'Precio del bien cambia', desplazamiento: 'Otros factores cambian' },
{ concepto: 'Gráfico', movimiento: 'Nos movemos sobre la curva', desplazamiento: 'Curva se desplaza' },
{ concepto: 'Terminología', movimiento: 'Cambio en cantidad ofrecida', desplazamiento: 'Cambio en oferta' },
{ concepto: 'Ejemplo', movimiento: 'Precio de manzanas ↑', desplazamiento: 'Tecnología mejora ↑' }
]
};
// ============================================
// RESUMEN Y PUNTOS CLAVE
// ============================================
export const resumenOferta = {
titulo: 'Resumen: Oferta',
puntosClave: [
'La oferta requiere disposición Y capacidad de producir',
'La ley de la oferta establece relación directa precio-cantidad',
'La curva de oferta tiene pendiente positiva',
'La oferta se desplaza por cambios en costos, tecnología, número de vendedores',
'Tecnología mejora → oferta aumenta (desplazamiento derecha)',
'Costos de insumos suben → oferta disminuye (desplazamiento izquierda)',
'A largo plazo, la oferta es más elástica que a corto plazo',
'Costo marginal determina la curva de oferta del productor'
],
formulaRecordatorio: {
leyOferta: 'P ↑ → Qs ↑ (ceteris paribus)',
ofertaMercado: 'Qs_mercado = Σ Qs_individuales',
decisionProductor: 'Producir donde P ≥ CMg'
},
mapaConceptual: {
centro: 'Oferta',
ramas: [
{ nombre: 'Ley', elementos: ['Relación directa P-Q', 'Pendiente positiva'] },
{ nombre: 'Determinantes', elementos: ['Tecnología', 'Costos', 'Vendedores', 'Expectativas'] },
{ nombre: 'Tipos', elementos: ['Individual', 'Mercado', 'Corto plazo', 'Largo plazo'] },
{ nombre: 'Costos', elementos: ['Fijos', 'Variables', 'Marginal', 'Total'] }
]
}
};
// Exportación por defecto
export default {
definicion: definicionOferta,
ley: leyOferta,
factores: factoresDesplazamientoOferta,
costos: costosProduccion,
curvas: {
individual: curvaOfertaIndividual,
mercado: curvaOfertaMercado,
largoPlazo: curvaOfertaLargoPlazo
},
ejemplos: ejemplosOferta,
temporal: ofertaTemporal,
diferencia: diferenciaMovimientoDesplazamientoOferta,
resumen: resumenOferta
};

View File

@@ -0,0 +1,450 @@
export const clasificacionBienes = {
id: "clasificacion-bienes-elasticidad",
titulo: "Clasificación de Bienes según Elasticidad",
introduccion: {
descripcion: `La elasticidad nos permite clasificar los bienes en diferentes categorías según
su comportamiento ante cambios en el ingreso (elasticidad ingreso) y ante cambios en el precio
de otros bienes (elasticidad cruzada). Esta clasificación es fundamental para entender las
relaciones de consumo y para la toma de decisiones empresariales y de política económica.`
},
clasificacionPorIngreso: {
titulo: "Clasificación según Elasticidad Ingreso (Ei)",
descripcion: "Los bienes se clasifican según cómo responde su demanda ante cambios en el ingreso de los consumidores",
formulaReferencia: "Ei = (% cambio en cantidad demandada) / (% cambio en ingreso)",
categorias: [
{
tipo: "Bienes Normales",
condicion: "Ei > 0",
descripcion: "La cantidad demandada aumenta cuando aumenta el ingreso. Son bienes que los consumidores desean más a medida que se vuelven más ricos.",
signo: "Positivo",
relacionIngreso: "Directa",
grafica: "Curva con pendiente positiva en plano Ingreso-Cantidad",
ejemplos: [
"Ropa de calidad",
"Electrodomésticos",
"Entretenimiento",
"Educación",
"Viajes"
],
comportamientoCicloEconomico: "Demanda aumenta en expansiones económicas",
subclasificacion: [
{
subtipo: "Bienes Necesarios",
condicion: "0 < Ei < 1",
descripcion: "La demanda aumenta con el ingreso, pero en menor proporción. Son bienes esenciales que todos consumen, pero los ricos no consumen proporcionalmente más.",
caracteristicas: [
"Demanda crece menos que proporcionalmente al ingreso",
"Son bienes básicos indispensables",
"La proporción del ingreso gastada disminuye al subir ingresos"
],
ejemplos: [
{ bien: "Alimentos básicos", eiAproximado: "0.2 - 0.5" },
{ bien: "Servicios médicos básicos", eiAproximado: "0.3 - 0.6" },
{ bien: "Vivienda básica", eiAproximado: "0.4 - 0.8" },
{ bien: "Transporte público", eiAproximado: "0.1 - 0.4" }
],
curvaEngel: "Pendiente positiva pero convexa (aplana al subir ingreso)"
},
{
subtipo: "Bienes de Lujo",
condicion: "Ei > 1",
descripcion: "La demanda aumenta más que proporcionalmente al ingreso. Cuando los ingresos crecen, el gasto en estos bienes crece más rápido.",
caracteristicas: [
"Demanda crece más que proporcionalmente al ingreso",
"Son deseables pero no esenciales",
"La proporción del ingreso gastada aumenta con el ingreso"
],
ejemplos: [
{ bien: "Viajes internacionales", eiAproximado: "2.0 - 3.5" },
{ bien: "Restaurantes de lujo", eiAproximado: "1.5 - 2.5" },
{ bien: "Joyas finas", eiAproximado: "2.0 - 4.0" },
{ bien: "Autos deportivos", eiAproximado: "2.5 - 3.5" },
{ bien: "Arte y antigüedades", eiAproximado: "1.8 - 3.0" }
],
curvaEngel: "Pendiente positiva y cóncava (se empinada al subir ingreso)"
}
]
},
{
tipo: "Bienes Inferiores",
condicion: "Ei < 0",
descripcion: "La cantidad demandada disminuye cuando aumenta el ingreso. Los consumidores sustituyen estos bienes por alternativas de mayor calidad a medida que pueden pagar más.",
signo: "Negativo",
relacionIngreso: "Inversa",
grafica: "Curva con pendiente negativa en plano Ingreso-Cantidad",
caracteristicas: [
"Demanda decrece al aumentar el ingreso",
"Sustituidos por bienes de mayor calidad",
"Mayor consumo en grupos de bajos ingresos",
"No son necesariamente de mala calidad, sino que hay mejores alternativas"
],
ejemplos: [
{
bien: "Transporte público",
explicacion: "Personas con más ingreso compran auto",
eiAproximado: "-0.3 a -0.6"
},
{
bien: "Fideos instantáneos",
explicacion: "Sustituidos por comida fresca",
eiAproximado: "-0.5 a -0.8"
},
{
bien: "Marcas genéricas",
explicacion: "Sustituidas por marcas reconocidas",
eiAproximado: "-0.4 a -0.7"
},
{
bien: "Carne de segunda",
explicacion: "Sustituida por cortes de primera",
eiAproximado: "-0.6 a -1.0"
},
{
bien: "Ropa de segunda mano",
explicacion: "Sustituida por ropa nueva",
eiAproximado: "-0.8 a -1.5"
},
{
bien: "Productos enlatados",
explicacion: "Sustituidos por productos frescos",
eiAproximado: "-0.3 a -0.5"
}
],
comportamientoCicloEconomico: "Demanda aumenta en recesiones",
empresasEjemplo: ["Dollar stores", "Marcas blancas", "Comida rápida económica"],
nota: "Un bien puede ser inferior para algunos grupos de ingreso y normal para otros"
}
],
ejemploNumerico: {
titulo: "Ejemplo Completo de Clasificación",
escenario: "Un consumidor tiene los siguientes cambios en su consumo cuando su ingreso mensual sube de $3000 a $3600 (20% de aumento):",
casos: [
{
bien: "Pan",
cantidadInicial: 20,
cantidadFinal: 21,
calculoEi: "%ΔQ = 5%, %ΔI = 20%, Ei = 5/20 = 0.25",
clasificacion: "Bien NORMAL NECESARIO",
justificacion: "0 < 0.25 < 1 → La demanda aumenta poco con el ingreso"
},
{
bien: "Restaurantes de lujo",
cantidadInicial: 2,
cantidadFinal: 5,
calculoEi: "%ΔQ = 150%, %ΔI = 20%, Ei = 150/20 = 7.5",
clasificacion: "Bien de LUJO",
justificacion: "Ei = 7.5 > 1 → La demanda crece mucho más que el ingreso"
},
{
bien: "Fideos instantáneos",
cantidadInicial: 15,
cantidadFinal: 10,
calculoEi: "%ΔQ = -33.3%, %ΔI = 20%, Ei = -33.3/20 = -1.67",
clasificacion: "Bien INFERIOR",
justificacion: "Ei = -1.67 < 0 → La demanda disminuye al subir el ingreso"
}
]
}
},
clasificacionPorElasticidadCruzada: {
titulo: "Clasificación según Elasticidad Cruzada (Exy)",
descripcion: "Los bienes se clasifican según cómo afecta el precio de un bien Y a la demanda del bien X",
formulaReferencia: "Exy = (% cambio en Qx) / (% cambio en Py)",
categorias: [
{
tipo: "Bienes Sustitutos",
condicion: "Exy > 0",
signo: "Positivo",
descripcion: "Cuando sube el precio del bien Y, aumenta la demanda del bien X. Los bienes pueden usarse en lugar uno del otro para satisfacer la misma necesidad.",
caracteristicas: [
"Satisfacen necesidades similares",
"Los consumidores pueden intercambiarlos",
"Compiten en el mismo mercado",
"A mayor diferencia de precio, mayor sustitución"
],
ejemplos: [
{
par: "Coca-Cola y Pepsi",
exyAproximado: "+0.8",
comentario: "Sustitutos cercanos"
},
{
par: "Café y té",
exyAproximado: "+0.5",
comentario: "Sustitutos moderados"
},
{
par: "Mantequilla y margarina",
exyAproximado: "+1.2",
comentario: "Muy buenos sustitutos"
},
{
par: "Carne de res y pollo",
exyAproximado: "+0.6",
comentario: "Sustitutos proteicos"
},
{
par: "Uber y taxi",
exyAproximado: "+1.5",
comentario: "Sustitutos cercanos en transporte"
}
],
relacionPrecioDemanda: "P↑ de Y → Q↑ de X",
curvaDemanda: "Se desplaza a la derecha cuando sube Py",
ejemploNumerico: {
titulo: "Ejemplo: Coca-Cola (X) y Pepsi (Y)",
datos: {
precioPepsiInicial: 3,
precioPepsiFinal: 3.6,
cantidadCocaInicial: 100,
cantidadCocaFinal: 125
},
calculo: [
"%ΔQx = (125-100)/100 × 100 = 25%",
"%ΔPy = (3.6-3)/3 × 100 = 20%",
"Exy = 25% / 20% = +1.25"
],
interpretacion: "Son sustitutos cercanos porque Exy > 0 y relativamente alto"
}
},
{
tipo: "Bienes Complementarios",
condicion: "Exy < 0",
signo: "Negativo",
descripcion: "Cuando sube el precio del bien Y, disminuye la demanda del bien X. Los bienes se consumen juntos o uno es necesario para usar el otro.",
caracteristicas: [
"Se consumen conjuntamente",
"Uno complementa al otro",
"El aumento de precio de uno reduce la demanda de ambos",
"A veces forman un 'sistema' de consumo"
],
tiposComplementariedad: [
{
tipo: "Complementos perfectos",
descripcion: "Se consumen en proporciones fijas",
ejemplos: ["Zapatos izquierdo y derecho", "Automóvil y gasolina (aprox)"]
},
{
tipo: "Complementos imperfectos",
descripcion: "Se consumen juntos pero no en proporción fija",
ejemplos: ["Cerveza y hamburguesas", "Celular y aplicaciones"]
}
],
ejemplos: [
{
par: "Autos y gasolina",
exyAproximado: "-0.4",
comentario: "Complementos esenciales"
},
{
par: "Computadores y software",
exyAproximado: "-0.8",
comentario: "Fuerte complementariedad"
},
{
par: "Tortillas y frijoles",
exyAproximado: "-0.3",
comentario: "Complementos dietarios"
},
{
par: "Impresoras y tinta",
exyAproximado: "-1.2",
comentario: "Complementos técnicos"
},
{
par: "Cámaras y rollos/memorias",
exyAproximado: "-0.9",
comentario: "Complementos fotográficos"
}
],
relacionPrecioDemanda: "P↑ de Y → Q↓ de X",
curvaDemanda: "Se desplaza a la izquierda cuando sube Py",
estrategiaEmpresas: "Las empresas a veces venden un bien barato (impresora) para ganar en el complemento (tinta)",
ejemploNumerico: {
titulo: "Ejemplo: Autos (X) y Gasolina (Y)",
datos: {
precioGasolinaInicial: 4,
precioGasolinaFinal: 5,
cantidadAutosInicial: 1000,
cantidadAutosFinal: 850
},
calculo: [
"%ΔQx = (850-1000)/1000 × 100 = -15%",
"%ΔPy = (5-4)/4 × 100 = 25%",
"Exy = -15% / 25% = -0.6"
],
interpretacion: "Son complementarios porque Exy < 0"
}
},
{
tipo: "Bienes Independientes",
condicion: "Exy = 0",
signo: "Cero",
descripcion: "El precio del bien Y no afecta la demanda del bien X. No existe relación de consumo entre ellos.",
caracteristicas: [
"No se relacionan en el consumo",
"Pertenecen a categorías completamente diferentes",
"El cambio de precio de uno no afecta al otro"
],
ejemplos: [
{ par: "Libros y tomates", explicacion: "Sin relación de consumo" },
{ par: "Zapatos y sillas", explicacion: "Bienes de categorías distintas" },
{ par: "Computadores y sal", explicacion: "Sin relación de consumo" },
{ par: "Viajes y papel higiénico", explicacion: "Necesidades independientes" }
]
}
]
},
matrizClasificacionCompleta: {
titulo: "Matriz de Clasificación Completa",
descripcion: "Un bien puede clasificarse usando ambos criterios simultáneamente",
matriz: [
{
combinacion: "Bien Normal + Sustituto",
ejemplo: "Restaurantes de lujo vs. restaurantes medianos",
caracteristicas: "Demanda crece con ingreso, compite con similares"
},
{
combinacion: "Bien Normal + Complemento",
ejemplo: "Autos eléctricos (complemento: estaciones de carga)",
caracteristicas: "Demanda crece con ingreso, depende de bien relacionado"
},
{
combinacion: "Bien Inferior + Sustituto",
ejemplo: "Transporte público vs. taxis",
caracteristicas: "Demanda cae con ingreso, compite con alternativas"
},
{
combinacion: "Bien Inferior + Complemento",
ejemplo: "Fideos instantáneos + salsa instantánea",
caracteristicas: "Ambos tienen demanda decreciente con ingreso"
}
]
},
aplicacionesPracticas: {
titulo: "Aplicaciones Prácticas de la Clasificación",
aplicaciones: [
{
area: "Marketing y Estrategia Empresarial",
usos: [
"Identificar mercados objetivo según nivel de ingreso",
"Desarrollar estrategias de precios basadas en elasticidad",
"Diseñar campañas para bienes de lujo vs. necesarios"
]
},
{
area: "Política Económica",
usos: [
"Diseñar impuestos sobre bienes inelásticos (generan más recaudación)",
"Subvencionar bienes necesarios para grupos de bajos ingresos",
"Predecir efectos de políticas redistributivas"
]
},
{
area: "Análisis de Mercado",
usos: [
"Identificar oportunidades de negocio en diferentes segmentos",
"Predecir demanda en ciclos económicos",
"Analizar competencia entre productos sustitutos"
]
},
{
area: "Planificación Financiera",
usos: [
"Sectores defensivos (bienes necesarios) vs. cíclicos (lujos)",
"Diversificación de inversiones",
"Evaluación de riesgos en recesiones"
]
}
]
},
ejerciciosResueltos: [
{
id: 1,
enunciado: "Clasifica los siguientes bienes según su elasticidad ingreso esperada: a) Arroz, b) Yates, c) Autobuses, d) Medicinas",
respuestas: [
{
bien: "Arroz",
eiEstimado: "0.2 - 0.4",
clasificacion: "Bien NORMAL NECESARIO",
justificacion: "Es un alimento básico. La demanda aumenta con el ingreso pero poco."
},
{
bien: "Yates",
eiEstimado: "3.0 - 5.0",
clasificacion: "Bien de LUJO",
justificacion: "Solo los muy ricos los compran. Demanda muy sensible al ingreso."
},
{
bien: "Autobuses",
eiEstimado: "-0.5 - -0.3",
clasificacion: "Bien INFERIOR",
justificacion: "Con más ingreso la gente prefiere auto o taxi."
},
{
bien: "Medicinas esenciales",
eiEstimado: "0.0 - 0.1",
clasificacion: "Bien NORMAL NECESARIO (casi inelástico)",
justificacion: "Todos las necesitan sin importar el ingreso."
}
]
},
{
id: 2,
enunciado: "¿Son sustitutos o complementos los siguientes pares? a) Netflix y cines, b) Lápices y papel, c) iPhone y Samsung",
respuestas: [
{
par: "Netflix y cines",
exyEsperado: "+0.6",
clasificacion: "SUSTITUTOS",
explicacion: "Compiten por el tiempo de entretenimiento del consumidor. Si suben las entradas de cine, más gente se queda en casa con Netflix."
},
{
par: "Lápices y papel",
exyEsperado: "-0.4",
clasificacion: "COMPLEMENTOS",
explicacion: "Se usan juntos. Si sube el precio del papel, se demandan menos lápices."
},
{
par: "iPhone y Samsung",
exyEsperado: "+1.2",
clasificacion: "SUSTITUTOS CERCANOS",
explicacion: "Son competidores directos en smartphones. Alta sustituibilidad."
}
]
}
]
};
export default clasificacionBienes;

View File

@@ -0,0 +1,242 @@
export const conceptosElasticidad = {
id: "conceptos-elasticidad",
titulo: "Conceptos Fundamentales de Elasticidad",
introduccion: {
descripcion: `La elasticidad mide la sensibilidad o respuesta de la cantidad demandada u ofrecida
de un bien ante cambios en variables económicas como el precio, el ingreso o el precio de otros bienes.
Es una herramienta fundamental para analizar cómo reaccionan los consumidores y productores ante
cambios en el mercado.`,
importancia: [
"Permite predecir cambios en la cantidad demandada ante variaciones de precio",
"Ayuda a las empresas a fijar estrategias de precios óptimas",
"Permite clasificar bienes según su comportamiento ante cambios económicos",
"Es esencial para la formulación de políticas fiscales y de ingresos"
]
},
definicionElasticidad: {
titulo: "¿Qué es la Elasticidad?",
definicion: "La elasticidad mide el grado de respuesta de la cantidad demandada (u ofrecida) ante cambios porcentuales en variables económicas relevantes.",
interpretacionIntuitiva: "Una elasticidad alta significa que la cantidad es muy sensible a cambios en la variable. Una elasticidad baja indica poca sensibilidad.",
formulaGeneral: {
simbolo: "E",
ecuacion: "E = (% cambio en la cantidad) / (% cambio en la variable)",
latex: "E = \\frac{\\% \\Delta Q}{\\% \\Delta X}",
donde: [
{ variable: "E", significado: "Coeficiente de elasticidad (número puro)" },
{ variable: "% ΔQ", significado: "Porcentaje de cambio en la cantidad" },
{ variable: "% ΔX", significado: "Porcentaje de cambio en la variable (precio, ingreso, etc.)" }
]
}
},
elasticidadPrecioDemanda: {
titulo: "Elasticidad Precio de la Demanda (Ed)",
definicion: "Mide el porcentaje de cambio en la cantidad demandada como respuesta a un cambio porcentual en el precio del bien.",
formula: {
ecuacion: "Ed = (% cambio en cantidad demandada) / (% cambio en precio)",
latex: "E_d = \\frac{\\% \\Delta Q_d}{\\% \\Delta P} = \\frac{\\frac{Q_2 - Q_1}{Q_1} \\times 100}{\\frac{P_2 - P_1}{P_1} \\times 100}",
simplificada: "Ed = (ΔQ/Q) / (ΔP/P) = (ΔQ/ΔP) × (P/Q)"
},
metodoPuntoMedio: {
nombre: "Método del Punto Medio (Arc Elasticity)",
descripcion: "Método más preciso que usa el promedio de los valores inicial y final",
formula: {
latex: "E_d = \\frac{\\frac{Q_2 - Q_1}{(Q_1 + Q_2)/2}}{\\frac{P_2 - P_1}{(P_1 + P_2)/2}}",
descripcion: "Usa los valores promedio como base para calcular los porcentajes"
}
},
interpretacion: [
{
rango: "|Ed| > 1",
clasificacion: "Demanda ELÁSTICA",
significado: "La cantidad cambia en mayor proporción que el precio",
ejemplo: "Si el precio sube 10%, la cantidad demandada baja más de 10%",
bienesTipicos: ["Bienes de lujo", "Productos con muchos sustitutos", "Bienes no esenciales"]
},
{
rango: "|Ed| < 1",
clasificacion: "Demanda INELÁSTICA",
significado: "La cantidad cambia en menor proporción que el precio",
ejemplo: "Si el precio sube 10%, la cantidad demandada baja menos de 10%",
bienesTipicos: ["Bienes necesarios", "Medicinas", "Combustible", "Sal"]
},
{
rango: "|Ed| = 1",
clasificacion: "Demanda UNITARIA",
significado: "La cantidad cambia en la misma proporción que el precio",
ejemplo: "Si el precio sube 10%, la cantidad demandada baja exactamente 10%",
bienesTipicos: ["Raramente ocurre en la realidad", "Curva teórica de demanda rectangular de hiperbola"]
},
{
rango: "|Ed| = 0",
clasificacion: "Demanda PERFECTAMENTE INELÁSTICA",
significado: "La cantidad no cambia ante cualquier cambio de precio",
ejemplo: "Medicinas indispensables para la vida",
representacionGrafica: "Línea vertical"
},
{
rango: "|Ed| = ∞",
clasificacion: "Demanda PERFECTAMENTE ELÁSTICA",
significado: "Cualquier cambio de precio elimina toda la demanda",
ejemplo: "Bienes con sustitutos perfectos en mercados competitivos",
representacionGrafica: "Línea horizontal"
}
]
},
determinantesElasticidad: {
titulo: "Factores que Determinan la Elasticidad",
factores: [
{
factor: "Disponibilidad de sustitutos",
efecto: "Más sustitutos → Mayor elasticidad",
explicacion: "Si existen muchos bienes similares, los consumidores pueden cambiar fácilmente cuando sube el precio"
},
{
factor: "Necesidad vs. Lujo",
efecto: "Necesidades → Menor elasticidad | Lujos → Mayor elasticidad",
explicacion: "Los bienes necesarios se siguen consumiendo aunque suban de precio"
},
{
factor: "Proporción del ingreso",
efecto: "Mayor proporción → Mayor elasticidad",
explicacion: "Bienes caros (autos, casas) tienen elasticidad mayor que bienes baratos (fósforos)"
},
{
factor: "Horizonte temporal",
efecto: "Largo plazo → Mayor elasticidad",
explicacion: "A largo plazo los consumidores pueden ajustar hábitos y encontrar alternativas"
},
{
factor: "Definición del mercado",
efecto: "Mercado amplio → Menor elasticidad | Mercado específico → Mayor elasticidad",
explicacion: "La demanda de 'alimentos' es inelástica, pero la de 'manzanas' es más elástica"
}
]
},
ejemplosNumericos: [
{
titulo: "Ejemplo 1: Cálculo básico de elasticidad",
datos: {
precioInicial: 10,
precioFinal: 12,
cantidadInicial: 100,
cantidadFinal: 80
},
pasos: [
{
paso: 1,
descripcion: "Calcular % cambio en cantidad",
calculo: "(80 - 100) / 100 × 100 = -20%"
},
{
paso: 2,
descripcion: "Calcular % cambio en precio",
calculo: "(12 - 10) / 10 × 100 = 20%"
},
{
paso: 3,
descripcion: "Calcular elasticidad",
calculo: "Ed = -20% / 20% = -1.0",
nota: "En valor absoluto: |Ed| = 1.0"
},
{
paso: 4,
descripcion: "Interpretación",
resultado: "Demanda UNITARIA - la cantidad disminuye en la misma proporción que aumenta el precio"
}
]
},
{
titulo: "Ejemplo 2: Método del punto medio",
datos: {
precioInicial: 8,
precioFinal: 10,
cantidadInicial: 120,
cantidadFinal: 90
},
pasos: [
{
paso: 1,
descripcion: "Calcular cambio en cantidad usando promedio",
calculo: "(90 - 120) / ((120 + 90)/2) = -30 / 105 = -0.2857 = -28.57%"
},
{
paso: 2,
descripcion: "Calcular cambio en precio usando promedio",
calculo: "(10 - 8) / ((8 + 10)/2) = 2 / 9 = 0.2222 = 22.22%"
},
{
paso: 3,
descripcion: "Calcular elasticidad",
calculo: "Ed = -28.57% / 22.22% = -1.29",
nota: "En valor absoluto: |Ed| = 1.29"
},
{
paso: 4,
descripcion: "Interpretación",
resultado: "Demanda ELÁSTICA - la cantidad es muy sensible al precio"
}
]
}
],
relacionIngresoTotal: {
titulo: "Relación entre Elasticidad e Ingreso Total",
definicion: "El ingreso total (IT) es el precio multiplicado por la cantidad vendida: IT = P × Q",
reglas: [
{
elasticidad: "Elástica (|Ed| > 1)",
efectoPrecioArriba: "El ingreso total DISMINUYE",
efectoPrecioAbajo: "El ingreso total AUMENTA",
explicacion: "La cantidad cambia más que proporcionalmente al precio"
},
{
elasticidad: "Inelástica (|Ed| < 1)",
efectoPrecioArriba: "El ingreso total AUMENTA",
efectoPrecioAbajo: "El ingreso total DISMINUYE",
explicacion: "La cantidad cambia menos que proporcionalmente al precio"
},
{
elasticidad: "Unitaria (|Ed| = 1)",
efectoPrecioArriba: "El ingreso total se MANTIENE CONSTANTE",
efectoPrecioAbajo: "El ingreso total se MANTIENE CONSTANTE",
explicacion: "Los cambios en precio y cantidad se compensan exactamente"
}
],
ejemploNumerico: {
descripcion: "Ejemplo: Producto con demanda elástica (|Ed| = 2)",
escenarioBase: { precio: 100, cantidad: 1000, ingresoTotal: 100000 },
escenarioPrecioSube: { precio: 110, cantidad: 800, ingresoTotal: 88000, cambio: "-12%" },
escenarioPrecioBaja: { precio: 90, cantidad: 1200, ingresoTotal: 108000, cambio: "+8%" },
conclusion: "Al ser elástica, subir el precio reduce los ingresos, y bajar el precio aumenta los ingresos"
}
},
resumenVisual: {
titulo: "Resumen Visual de Elasticidad",
tablaInterpretacion: {
columnas: ["|Ed|", "Clasificación", "Respuesta de Q", "Ejemplo"],
filas: [
["0", "Perfectamente inelástica", "Sin cambio", "Insulina"],
["0 - 0.5", "Muy inelástica", "Cambia poco", "Sal"],
["0.5 - 1", "Inelástica", "Cambia menos proporcional", "Gasolina"],
["1", "Unitaria", "Cambia igual proporción", "Teórico"],
["1 - 2", "Elástica", "Cambia más proporcional", "Restaurantes"],
["2 - 5", "Muy elástica", "Cambia mucho", "Cine"],
["∞", "Perfectamente elástica", "Q → 0 con cualquier ΔP", "Trigo en mercado mundial"]
]
}
}
};
export default conceptosElasticidad;

View File

@@ -0,0 +1,677 @@
export interface PasoEjercicio {
paso: number;
descripcion: string;
formula?: string;
latex?: string;
calculo: string;
resultado?: string;
explicacion?: string;
}
export interface Ejercicio {
id: string;
tipo: "calculadora" | "clasificacion" | "examen";
titulo: string;
dificultad: "basico" | "intermedio" | "avanzado";
tiempoEstimado: number;
enunciado: string;
datos?: Record<string, number>;
pasos: PasoEjercicio[];
respuestaCorrecta: string | number;
interpretacion: string;
pistas?: string[];
}
export const ejerciciosElasticidad: Ejercicio[] = [
{
id: "ejercicio-1-calculadora",
tipo: "calculadora",
titulo: "Calculadora de Elasticidad Precio - Paso a Paso",
dificultad: "basico",
tiempoEstimado: 10,
enunciado: `Una tienda vende café gourmet. Cuando el precio es de $10 por libra,
venden 200 libras al mes. Cuando suben el precio a $12, las ventas bajan a 150 libras.
Calcula la elasticidad precio de la demanda usando el método del punto medio y clasifica el resultado.`,
datos: {
precioInicial: 10,
precioFinal: 12,
cantidadInicial: 200,
cantidadFinal: 150
},
pasos: [
{
paso: 1,
descripcion: "Identificar los datos del problema",
calculo: "P₁ = $10, P₂ = $12, Q₁ = 200, Q₂ = 150"
},
{
paso: 2,
descripcion: "Calcular el cambio en cantidad (ΔQ)",
formula: "ΔQ = Q₂ - Q₁",
calculo: "ΔQ = 150 - 200 = -50 libras"
},
{
paso: 3,
descripcion: "Calcular el cambio en precio (ΔP)",
formula: "ΔP = P₂ - P₁",
calculo: "ΔP = $12 - $10 = $2"
},
{
paso: 4,
descripcion: "Calcular el promedio de cantidades",
formula: "Q̄ = (Q₁ + Q₂) / 2",
latex: "\\bar{Q} = \\frac{Q_1 + Q_2}{2}",
calculo: "Q̄ = (200 + 150) / 2 = 175 libras"
},
{
paso: 5,
descripcion: "Calcular el promedio de precios",
formula: "P̄ = (P₁ + P₂) / 2",
latex: "\\bar{P} = \\frac{P_1 + P_2}{2}",
calculo: "P̄ = ($10 + $12) / 2 = $11"
},
{
paso: 6,
descripcion: "Calcular el % cambio en cantidad",
formula: "%ΔQ = (ΔQ / Q̄) × 100",
latex: "\\% \\Delta Q = \\frac{\\Delta Q}{\\bar{Q}} \\times 100",
calculo: "%ΔQ = (-50 / 175) × 100 = -28.57%"
},
{
paso: 7,
descripcion: "Calcular el % cambio en precio",
formula: "%ΔP = (ΔP / P̄) × 100",
latex: "\\% \\Delta P = \\frac{\\Delta P}{\\bar{P}} \\times 100",
calculo: "%ΔP = (2 / 11) × 100 = 18.18%"
},
{
paso: 8,
descripcion: "Calcular la elasticidad precio de la demanda",
formula: "Ed = %ΔQ / %ΔP",
latex: "E_d = \\frac{\\% \\Delta Q}{\\% \\Delta P}",
calculo: "Ed = -28.57% / 18.18% = -1.57",
resultado: "Ed = -1.57",
explicacion: "El signo negativo indica la relación inversa entre precio y cantidad (Ley de la Demanda)"
},
{
paso: 9,
descripcion: "Tomar valor absoluto para clasificar",
calculo: "|Ed| = |-1.57| = 1.57"
},
{
paso: 10,
descripcion: "Clasificar según el valor de elasticidad",
calculo: "|Ed| = 1.57 > 1",
resultado: "DEMANDA ELÁSTICA",
explicacion: "La cantidad demandada cambia en mayor proporción que el precio"
}
],
respuestaCorrecta: -1.57,
interpretacion: `La elasticidad es -1.57 (elástica). Esto significa que por cada 1% que aumenta
el precio del café, la cantidad demandada disminuye 1.57%. Como |Ed| > 1, la demanda es elástica:
los consumidores son sensibles al precio. Esto tiene sentido porque el café gourmet tiene
muchos sustitutos (café regular, té, otras marcas). Si la tienda sube precios, perderá
muchas ventas. Para maximizar ingresos, debería considerar BAJAR el precio.`,
pistas: [
"Recuerda usar el método del punto medio: divide por el promedio de valores inicial y final",
"La elasticidad es (% cambio Q) / (% cambio P)",
"Si |Ed| > 1, la demanda es elástica"
]
},
{
id: "ejercicio-2-calculadora-ingreso",
tipo: "calculadora",
titulo: "Calculadora de Elasticidad Ingreso",
dificultad: "intermedio",
tiempoEstimado: 12,
enunciado: `En un país, cuando el ingreso promedio mensual es de $2000, los hogares consumen
4 kg de carne de res al mes. Cuando el ingreso sube a $2500, el consumo aumenta a 6 kg.
Calcula la elasticidad ingreso de la demanda y clasifica la carne de res.`,
datos: {
ingresoInicial: 2000,
ingresoFinal: 2500,
cantidadInicial: 4,
cantidadFinal: 6
},
pasos: [
{
paso: 1,
descripcion: "Identificar los datos",
calculo: "I₁ = $2000, I₂ = $2500, Q₁ = 4 kg, Q₂ = 6 kg"
},
{
paso: 2,
descripcion: "Fórmula de elasticidad ingreso",
formula: "Ei = (% cambio Q) / (% cambio I)",
latex: "E_i = \\frac{\\% \\Delta Q}{\\% \\Delta I} = \\frac{\\frac{Q_2 - Q_1}{(Q_1 + Q_2)/2}}{\\frac{I_2 - I_1}{(I_1 + I_2)/2}}",
calculo: "Método del punto medio"
},
{
paso: 3,
descripcion: "Calcular % cambio en cantidad",
formula: "%ΔQ = (Q₂ - Q₁) / ((Q₁ + Q₂)/2)",
calculo: "%ΔQ = (6 - 4) / ((4 + 6)/2) = 2 / 5 = 0.40 = 40%"
},
{
paso: 4,
descripcion: "Calcular % cambio en ingreso",
formula: "%ΔI = (I₂ - I₁) / ((I₁ + I₂)/2)",
calculo: "%ΔI = (2500 - 2000) / ((2000 + 2500)/2) = 500 / 2250 = 0.222 = 22.22%"
},
{
paso: 5,
descripcion: "Calcular elasticidad ingreso",
formula: "Ei = %ΔQ / %ΔI",
calculo: "Ei = 40% / 22.22% = 1.80"
},
{
paso: 6,
descripcion: "Clasificar el bien",
calculo: "Ei = 1.80 > 1",
resultado: "BIEN DE LUJO",
explicacion: "El consumo de carne crece más que proporcionalmente al ingreso"
}
],
respuestaCorrecta: 1.80,
interpretacion: `La elasticidad ingreso es 1.80, indicando que la carne de res es un BIEN DE LUJO
en este contexto. Esto significa que cuando el ingreso aumenta 1%, el consumo de carne aumenta 1.8%.
Esto es típico en economías donde la carne es un símbolo de estatus o donde existe una dieta
base de alimentos más baratos (granos, vegetales) que los consumidores mejoran al subir de ingreso.`
},
{
id: "ejercicio-3-calculadora-cruzada",
tipo: "calculadora",
titulo: "Calculadora de Elasticidad Cruzada",
dificultad: "intermedio",
tiempoEstimado: 12,
enunciado: `El precio del té (bien Y) sube de $3 a $4 por caja. Como resultado, la cantidad
demandada de café (bien X) aumenta de 100 a 130 libras al mes. Calcula la elasticidad cruzada
y determina si son sustitutos o complementos.`,
datos: {
precioYInicial: 3,
precioYFinal: 4,
cantidadXInicial: 100,
cantidadXFinal: 130
},
pasos: [
{
paso: 1,
descripcion: "Identificar los datos",
calculo: "Py₁ = $3, Py₂ = $4, Qx₁ = 100, Qx₂ = 130"
},
{
paso: 2,
descripcion: "Fórmula de elasticidad cruzada",
formula: "Exy = (% cambio Qx) / (% cambio Py)",
latex: "E_{xy} = \\frac{\\% \\Delta Q_x}{\\% \\Delta P_y}",
calculo: "Usar método del punto medio"
},
{
paso: 3,
descripcion: "Calcular % cambio en Qx",
formula: "%ΔQx = (Qx₂ - Qx₁) / ((Qx₁ + Qx₂)/2)",
calculo: "%ΔQx = (130 - 100) / ((100 + 130)/2) = 30 / 115 = 26.09%"
},
{
paso: 4,
descripcion: "Calcular % cambio en Py",
formula: "%ΔPy = (Py₂ - Py₁) / ((Py₁ + Py₂)/2)",
calculo: "%ΔPy = (4 - 3) / ((3 + 4)/2) = 1 / 3.5 = 28.57%"
},
{
paso: 5,
descripcion: "Calcular elasticidad cruzada",
formula: "Exy = %ΔQx / %ΔPy",
calculo: "Exy = 26.09% / 28.57% = 0.91"
},
{
paso: 6,
descripcion: "Determinar relación entre bienes",
calculo: "Exy = 0.91 > 0",
resultado: "BIENES SUSTITUTOS",
explicacion: "Signo positivo indica que al subir precio de Y aumenta demanda de X"
}
],
respuestaCorrecta: 0.91,
interpretacion: `La elasticidad cruzada es 0.91 (positiva), confirmando que café y té son SUSTITUTOS.
Cuando el té se encarece, los consumidores sustituyen parcialmente su consumo por café.
El valor menor a 1 indica que son sustitutos moderados, no perfectos. Los consumidores
tienen cierta preferencia por uno u otro, pero sí responden a diferencias de precio.`
}
];
export const ejerciciosClasificacion = [
{
id: "clasificacion-1",
tipo: "clasificacion",
titulo: "Clasificar Bienes según Elasticidad Ingreso",
dificultad: "intermedio",
tiempoEstimado: 15,
enunciado: `Analiza los siguientes casos y clasifica cada bien como: Normal Necesario,
de Lujo, o Inferior. Justifica tu respuesta con el valor calculado de Ei.`,
casos: [
{
id: "caso-a",
bien: "Arroz",
datos: {
ingresoInicial: 1000,
ingresoFinal: 1500,
cantidadInicial: 20,
cantidadFinal: 22
},
pasos: [
"%ΔQ = (22-20)/((20+22)/2) = 2/21 = 9.52%",
"%ΔI = (1500-1000)/((1000+1500)/2) = 500/1250 = 40%",
"Ei = 9.52% / 40% = 0.24"
],
respuesta: "NORMAL NECESARIO",
justificacion: "0 < 0.24 < 1: El consumo aumenta poco respecto al ingreso"
},
{
id: "caso-b",
bien: "Viajes internacionales",
datos: {
ingresoInicial: 3000,
ingresoFinal: 4500,
cantidadInicial: 1,
cantidadFinal: 4
},
pasos: [
"%ΔQ = (4-1)/((1+4)/2) = 3/2.5 = 120%",
"%ΔI = (4500-3000)/((3000+4500)/2) = 1500/3750 = 40%",
"Ei = 120% / 40% = 3.0"
],
respuesta: "BIEN DE LUJO",
justificacion: "Ei = 3.0 > 1: El consumo crece más que proporcionalmente al ingreso"
},
{
id: "caso-c",
bien: "Autobuses urbanos",
datos: {
ingresoInicial: 2000,
ingresoFinal: 3500,
cantidadInicial: 40,
cantidadFinal: 20
},
pasos: [
"%ΔQ = (20-40)/((40+20)/2) = -20/30 = -66.67%",
"%ΔI = (3500-2000)/((2000+3500)/2) = 1500/2750 = 54.55%",
"Ei = -66.67% / 54.55% = -1.22"
],
respuesta: "BIEN INFERIOR",
justificacion: "Ei = -1.22 < 0: El consumo disminuye al aumentar el ingreso (la gente compra auto o usa taxi)"
}
]
},
{
id: "clasificacion-2",
tipo: "clasificacion",
titulo: "Identificar Sustitutos y Complementos",
dificultad: "intermedio",
tiempoEstimado: 15,
enunciado: `Para cada par de bienes, calcula la elasticidad cruzada y determina si son
sustitutos, complementos o independientes.`,
casos: [
{
id: "caso-a",
bienX: "Coca-Cola",
bienY: "Pepsi",
datos: {
precioYInicial: 2,
precioYFinal: 2.5,
cantidadXInicial: 100,
cantidadXFinal: 130
},
pasos: [
"%ΔQx = (130-100)/((100+130)/2) = 30/115 = 26.09%",
"%ΔPy = (2.5-2)/((2+2.5)/2) = 0.5/2.25 = 22.22%",
"Exy = 26.09% / 22.22% = 1.17"
],
respuesta: "SUSTITUTOS",
justificacion: "Exy = 1.17 > 0: Al subir Pepsi, aumenta demanda de Coca-Cola"
},
{
id: "caso-b",
bienX: "Autos",
bienY: "Gasolina",
datos: {
precioYInicial: 4,
precioYFinal: 6,
cantidadXInicial: 1000,
cantidadXFinal: 700
},
pasos: [
"%ΔQx = (700-1000)/((1000+700)/2) = -300/850 = -35.29%",
"%ΔPy = (6-4)/((4+6)/2) = 2/5 = 40%",
"Exy = -35.29% / 40% = -0.88"
],
respuesta: "COMPLEMENTOS",
justificacion: "Exy = -0.88 < 0: Al subir gasolina, disminuye demanda de autos"
},
{
id: "caso-c",
bienX: "Libros",
bienY: "Manzanas",
datos: {
precioYInicial: 2,
precioYFinal: 3,
cantidadXInicial: 50,
cantidadXFinal: 50
},
pasos: [
"%ΔQx = (50-50)/((50+50)/2) = 0%",
"%ΔPy = (3-2)/((2+3)/2) = 1/2.5 = 40%",
"Exy = 0% / 40% = 0"
],
respuesta: "INDEPENDIENTES",
justificacion: "Exy = 0: El precio de las manzanas no afecta demanda de libros"
}
]
}
];
export const ejerciciosExamen = [
{
id: "examen-1",
tipo: "examen",
titulo: "Problema Tipo Examen - Análisis de Mercado",
dificultad: "avanzado",
tiempoEstimado: 25,
enunciado: `La empresa "TechPhone" vende smartphones. El año pasado, con un precio de $800,
vendieron 50,000 unidades. Este año, debido a la competencia, bajaron el precio a $720
y vendieron 65,000 unidades.
a) Calcule la elasticidad precio de la demanda usando el método del punto medio.
b) Clasifique la demanda y explique qué significa para la empresa.
c) ¿Qué pasaría con los ingresos totales si TechPhone subiera el precio a $850?
(Calcule los ingresos en ambos escenarios y compare)`,
solucion: {
parteA: {
pasos: [
{
descripcion: "Datos",
calculo: "P₁ = $800, P₂ = $720, Q₁ = 50,000, Q₂ = 65,000"
},
{
descripcion: "Calcular %ΔQ",
calculo: "%ΔQ = (65,000-50,000)/((50,000+65,000)/2) = 15,000/57,500 = 26.09%"
},
{
descripcion: "Calcular %ΔP",
calculo: "%ΔP = (720-800)/((800+720)/2) = -80/760 = -10.53%"
},
{
descripcion: "Calcular Ed",
calculo: "Ed = 26.09% / -10.53% = -2.48",
resultado: "|Ed| = 2.48"
}
],
respuesta: "Ed = -2.48 (elástica)"
},
parteB: {
clasificacion: "Demanda ELÁSTICA (|Ed| = 2.48 > 1)",
interpretacion: `La demanda es muy sensible al precio. Un cambio de 1% en el precio
produce un cambio de 2.48% en la cantidad demandada (en sentido opuesto). Esto indica
que existen muchos competidores y sustitutos en el mercado de smartphones.`,
implicacionEmpresa: `TechPhone tiene poco poder de fijación de precios. Si sube precios,
perderá muchos clientes a la competencia.`
},
parteC: {
escenarioActual: {
precio: 720,
cantidad: 65000,
ingresoTotal: 720 * 65000
},
escenarioPropuesto: {
precio: 850,
cantidadEstimada: "Usar elasticidad para estimar",
calculoCantidad: [
"%ΔP = (850-720)/720 × 100 = 18.06%",
"Como Ed = -2.48, %ΔQ = -2.48 × 18.06% = -44.79%",
"Q nueva = 65,000 × (1 - 0.4479) = 35,887 unidades"
],
ingresoTotal: 850 * 35887
},
comparacion: {
ingresoActual: 46800000,
ingresoConSubida: 30503950,
diferencia: -16296050,
porcentaje: -34.8
},
conclusion: `Si TechPhone sube el precio a $850, sus ingresos caerían aproximadamente
$16.3 millones (35% menos). Como la demanda es elástica, subir precios reduce los ingresos
totales. La estrategia correcta sería BAJAR precios para aumentar ingresos.`
}
}
},
{
id: "examen-2",
tipo: "examen",
titulo: "Caso Real - Bienes Inferiores en Recesión",
dificultad: "avanzado",
tiempoEstimado: 20,
enunciado: `Durante una recesión económica, el ingreso promedio familiar cayó de $4000 a $3200
mensuales. Como resultado:
- Las ventas de carne de res cayeron de 8 kg a 5 kg por familia
- Las ventas de fideos instantáneos subieron de 10 paquetes a 18 paquetes
a) Calcule la elasticidad ingreso para cada bien.
b) Clasifique cada bien y explique el comportamiento observado.
c) ¿Qué tipo de negocios prosperarían en una recesión según estos datos?`,
solucion: {
parteA: {
carneRes: {
pasos: [
"%ΔQ = (5-8)/((8+5)/2) = -3/6.5 = -46.15%",
"%ΔI = (3200-4000)/((4000+3200)/2) = -800/3600 = -22.22%",
"Ei = -46.15% / -22.22% = 2.08"
],
resultado: "Ei = 2.08"
},
fideos: {
pasos: [
"%ΔQ = (18-10)/((10+18)/2) = 8/14 = 57.14%",
"%ΔI = (3200-4000)/((4000+3200)/2) = -800/3600 = -22.22%",
"Ei = 57.14% / -22.22% = -2.57"
],
resultado: "Ei = -2.57"
}
},
parteB: {
carneRes: {
clasificacion: "BIEN DE LUJO",
explicacion: `Ei = 2.08 > 1. Cuando el ingreso cayó 22%, el consumo de carne cayó 46%.
El consumo es muy sensible al ingreso, cayendo más que proporcionalmente.`
},
fideos: {
clasificacion: "BIEN INFERIOR",
explicacion: `Ei = -2.57 < 0. Cuando el ingreso cayó 22%, el consumo de fideos subió 57%.
Las familias sustituyeron carne por fideos al empobrecerse.`
}
},
parteC: {
negociosProsperos: [
"Tiendas de descuento y marcas genéricas",
"Comida rápida económica",
"Transporte público",
"Productos de segunda mano",
"Entretenimiento en casa (streaming vs cine)"
],
justificacion: `Los bienes inferiores ven aumentar su demanda en recesiones. Las empresas
que venden estos bienes tienden a tener ventas estables o crecientes durante crisis económicas,
mientras que las de bienes de lujo sufren.`
}
}
},
{
id: "examen-3",
tipo: "examen",
titulo: "Problema Integrador - Todas las Elasticidades",
dificultad: "avanzado",
tiempoEstimado: 30,
enunciado: `Una cadena de supermercados analiza el mercado de bebidas. Recopilan los siguientes datos:
CASO 1: Cuando el precio del jugo de naranja bajó de $5 a $4:
- Ventas de jugo de naranja: de 1000 a 1400 litros
- Ventas de jugo de manzana: de 800 a 600 litros
CASO 2: Cuando el ingreso promedio de clientes subió de $3000 a $3600:
- Ventas de jugo de naranja: de 1000 a 1300 litros
- Ventas de soda: de 2000 a 1600 litros
Resuelva:
a) Elasticidad precio del jugo de naranja. ¿Es elástica o inelástica?
b) Elasticidad cruzada entre jugo de naranja y manzana. ¿Qué relación tienen?
c) Elasticidad ingreso del jugo de naranja. ¿Qué tipo de bien es?
d) Elasticidad ingreso de la soda. ¿Qué tipo de bien es?
e) Si el supermercado quiere maximizar ingresos por ventas de jugo de naranja,
¿debería subir o bajar el precio? Justifique con números.`,
solucion: {
parteA: {
descripcion: "Elasticidad precio del jugo de naranja",
pasos: [
"P₁=$5, P₂=$4, Q₁=1000, Q₂=1400",
"%ΔQ = (1400-1000)/((1000+1400)/2) = 400/1200 = 33.33%",
"%ΔP = (4-5)/((5+4)/2) = -1/4.5 = -22.22%",
"Ed = 33.33% / -22.22% = -1.5",
"|Ed| = 1.5"
],
respuesta: "Ed = -1.5 → ELÁSTICA",
interpretacion: "Por cada 1% que baja el precio, la cantidad demandada aumenta 1.5%"
},
parteB: {
descripcion: "Elasticidad cruzada (naranja X, manzana Y)",
pasos: [
"Py₁=$4 (asumiendo precio inicial de manzana), pero mejor usar %ΔPy del naranja",
"Exy = (%ΔQx manzana) / (%ΔPy naranja)",
"%ΔQx manzana = (600-800)/((800+600)/2) = -200/700 = -28.57%",
"%ΔPy naranja = -22.22% (de la parte a)",
"Exy = -28.57% / -22.22% = +1.29"
],
respuesta: "Exy = +1.29 → SUSTITUTOS",
interpretacion: "Signo positivo indica sustitutos. Al bajar el precio del jugo de naranja, la gente compra menos manzana"
},
parteC: {
descripcion: "Elasticidad ingreso del jugo de naranja",
pasos: [
"I₁=$3000, I₂=$3600, Q₁=1000, Q₂=1300",
"%ΔQ = (1300-1000)/((1000+1300)/2) = 300/1150 = 26.09%",
"%ΔI = (3600-3000)/((3000+3600)/2) = 600/3300 = 18.18%",
"Ei = 26.09% / 18.18% = 1.44"
],
respuesta: "Ei = 1.44 → BIEN DE LUJO",
interpretacion: "Ei > 1 indica que el jugo de naranja es un bien de lujo. El consumo crece más que proporcionalmente al ingreso"
},
parteD: {
descripcion: "Elasticidad ingreso de la soda",
pasos: [
"I₁=$3000, I₂=$3600, Q₁=2000, Q₂=1600",
"%ΔQ = (1600-2000)/((2000+1600)/2) = -400/1800 = -22.22%",
"%ΔI = 18.18% (igual que arriba)",
"Ei = -22.22% / 18.18% = -1.22"
],
respuesta: "Ei = -1.22 → BIEN INFERIOR",
interpretacion: "Ei < 0 indica bien inferior. Al subir el ingreso, la gente compra menos soda (prefiere jugos naturales)"
},
parteE: {
descripcion: "Estrategia de precios para maximizar ingresos",
analisis: {
elasticidad: "Ed = -1.5 (elástica)",
regla: "Cuando |Ed| > 1, subir precio reduce ingresos; bajar precio aumenta ingresos"
},
calculoComparativo: {
escenario1: { precio: 5, cantidad: 1000, ingreso: 5000 },
escenario2: { precio: 4, cantidad: 1400, ingreso: 5600 }
},
diferencia: 600,
porcentaje: "+12%",
respuesta: "BAJAR EL PRECIO",
justificacion: `Al bajar el precio de $5 a $4, los ingresos aumentaron de $5000 a $5600 (+12%).
Como la demanda es elástica, el aumento porcentual en cantidad (33%) supera la caída porcentual
en precio (22%), resultando en mayores ingresos totales.`
}
}
}
];
export const datosPractica = {
bienesEjemplo: [
{ nombre: "Gasolina", ed: 0.2, ei: 0.8, tipo: "Necesidad inelástica" },
{ nombre: "Restaurantes", ed: 1.6, ei: 2.2, tipo: "Lujo elástico" },
{ nombre: "Cine", ed: 3.0, ei: 1.8, tipo: "Entretenimiento elástico" },
{ nombre: "Medicinas", ed: 0.1, ei: 0.2, tipo: "Necesidad muy inelástica" },
{ nombre: "Viajes internacionales", ed: 4.0, ei: 3.5, tipo: "Lujo muy elástico" },
{ nombre: "Sal", ed: 0.05, ei: 0.1, tipo: "Necesidad casi perfectamente inelástica" },
{ nombre: "Cerveza", ed: 1.2, ei: 0.9, tipo: "Bien normal elástico" },
{ nombre: "Transporte público", ed: 0.4, ei: -0.6, tipo: "Inferior inelástico" },
{ nombre: "Marca genérica", ed: 2.5, ei: -1.2, tipo: "Inferior elástico" },
{ nombre: "Vivienda", ed: 0.8, ei: 1.1, tipo: "Lujo/Necesidad borde" }
],
formulasRapidas: {
precioDemanda: {
nombre: "Elasticidad Precio Demanda",
latex: "E_d = \\frac{\\% \\Delta Q_d}{\\% \\Delta P}",
signo: "Negativo (usar |Ed|)",
interpretacion: {
mayor1: "Elástica - sensible al precio",
menor1: "Inelástica - poco sensible al precio",
igual1: "Unitaria - cambio proporcional"
}
},
ingreso: {
nombre: "Elasticidad Ingreso",
latex: "E_i = \\frac{\\% \\Delta Q}{\\% \\Delta I}",
clasificacion: {
mayor0: "Bien Normal",
entre0y1: "Necesidad",
mayor1: "Lujo",
menor0: "Inferior"
}
},
cruzada: {
nombre: "Elasticidad Cruzada",
latex: "E_{xy} = \\frac{\\% \\Delta Q_x}{\\% \\Delta P_y}",
clasificacion: {
mayor0: "Sustitutos",
menor0: "Complementos",
igual0: "Independientes"
}
}
}
};
export default {
ejerciciosCalculadora: ejerciciosElasticidad,
ejerciciosClasificacion: ejerciciosClasificacion,
ejerciciosExamen: ejerciciosExamen,
datosPractica
};

View File

@@ -0,0 +1,328 @@
export const tiposElasticidad = {
id: "tipos-elasticidad",
titulo: "Tipos de Elasticidad en Economía",
introduccion: {
descripcion: `Además de la elasticidad precio de la demanda, existen otros tipos de elasticidad
que miden la respuesta de la cantidad ante diferentes variables económicas. Cada tipo de elasticidad
proporciona información valiosa sobre el comportamiento de consumidores y productores.`
},
tipos: [
{
id: "elasticidad-precio-demanda",
nombre: "Elasticidad Precio de la Demanda (Ed)",
abreviatura: "Ed o Ep",
descripcion: "Mide la sensibilidad de la cantidad demandada ante cambios en el precio del propio bien",
formula: {
latex: "E_d = \\frac{\\% \\Delta Q_d}{\\% \\Delta P} = \\frac{\\Delta Q_d / Q_d}{\\Delta P / P}",
verbal: "Porcentaje de cambio en cantidad demandada dividido por porcentaje de cambio en precio",
nota: "Siempre es negativa (ley de la demanda), pero se usa valor absoluto para clasificar"
},
interpretacion: {
negativa: "Por convención, se reporta en valor absoluto (positivo)",
elastico: "|Ed| > 1: La cantidad es muy sensible al precio",
inelastico: "|Ed| < 1: La cantidad es poco sensible al precio",
unitario: "|Ed| = 1: Cambio proporcional"
},
ejemploNumerico: {
titulo: "Ejemplo: Gasolina",
datos: {
precioInicial: 4.0,
precioFinal: 4.4,
cantidadInicial: 1000,
cantidadFinal: 950
},
calculo: [
"%ΔQ = (950 - 1000) / 1000 × 100 = -5%",
"%ΔP = (4.4 - 4.0) / 4.0 × 100 = 10%",
"Ed = -5% / 10% = -0.5",
"|Ed| = 0.5 (INELÁSTICA)"
],
conclusion: "La gasolina tiene demanda inelástica a corto plazo porque es una necesidad"
},
determinantes: [
"Disponibilidad de sustitutos cercanos",
"Naturaleza del bien (necesidad vs. lujo)",
"Proporción del ingreso gastada en el bien",
"Horizonte temporal (corto vs. largo plazo)",
"Definición del mercado (amplio vs. específico)"
]
},
{
id: "elasticidad-ingreso-demanda",
nombre: "Elasticidad Ingreso de la Demanda (Ei)",
abreviatura: "Ei o Ey",
descripcion: "Mide la sensibilidad de la cantidad demandada ante cambios en el ingreso del consumidor",
formula: {
latex: "E_i = \\frac{\\% \\Delta Q_d}{\\% \\Delta I} = \\frac{\\Delta Q_d / Q_d}{\\Delta I / I}",
verbal: "Porcentaje de cambio en cantidad demandada dividido por porcentaje de cambio en ingreso",
donde: [
{ variable: "Qd", significado: "Cantidad demandada" },
{ variable: "I", significado: "Ingreso del consumidor" }
]
},
clasificacionBienes: [
{
tipo: "Bien Normal",
condicion: "Ei > 0",
descripcion: "La cantidad demandada aumenta cuando aumenta el ingreso",
subtipos: [
{ tipo: "Bien Necesario", rango: "0 < Ei < 1", ejemplo: "Alimentos básicos" },
{ tipo: "Bien de Lujo", rango: "Ei > 1", ejemplo: "Viajes, joyas, autos deportivos" }
]
},
{
tipo: "Bien Inferior",
condicion: "Ei < 0",
descripcion: "La cantidad demandada disminuye cuando aumenta el ingreso",
ejemplo: "Transporte público, fideos instantáneos, marca genérica"
}
],
ejemploNumerico: {
titulo: "Ejemplo: Restaurantes de lujo",
datos: {
ingresoInicial: 50000,
ingresoFinal: 60000,
cantidadInicial: 12,
cantidadFinal: 20
},
calculo: [
"%ΔQ = (20 - 12) / 12 × 100 = 66.67%",
"%ΔI = (60000 - 50000) / 50000 × 100 = 20%",
"Ei = 66.67% / 20% = 3.33",
"Ei > 1 → Bien de LUJO"
],
conclusion: "Los restaurantes de lujo son un bien de lujo porque su demanda crece más que proporcionalmente al ingreso"
},
aplicacion: "Ayuda a predecir cómo cambiará la demanda en ciclos económicos (expansión/recesión)"
},
{
id: "elasticidad-cruzada",
nombre: "Elasticidad Cruzada de la Demanda (Exy)",
abreviatura: "Exy o Ec",
descripcion: "Mide la sensibilidad de la cantidad demandada de un bien X ante cambios en el precio de otro bien Y",
formula: {
latex: "E_{xy} = \\frac{\\% \\Delta Q_x}{\\% \\Delta P_y} = \\frac{\\Delta Q_x / Q_x}{\\Delta P_y / P_y}",
verbal: "Porcentaje de cambio en cantidad demandada del bien X dividido por porcentaje de cambio en precio del bien Y",
donde: [
{ variable: "Qx", significado: "Cantidad demandada del bien X" },
{ variable: "Py", significado: "Precio del bien Y" }
]
},
clasificacionBienes: [
{
tipo: "Bienes Sustitutos",
condicion: "Exy > 0",
signo: "Positiva",
descripcion: "Si sube el precio de Y, aumenta la demanda de X",
ejemplo: "Coca-Cola y Pepsi, café y té, mantequilla y margarina",
logica: "Cuando el café sube de precio, la gente consume más té"
},
{
tipo: "Bienes Complementarios",
condicion: "Exy < 0",
signo: "Negativa",
descripcion: "Si sube el precio de Y, disminuye la demanda de X",
ejemplo: "Autos y gasolina, computadores y software, pan y mantequilla",
logica: "Si sube el precio de la gasolina, se demandan menos autos"
},
{
tipo: "Bienes Independientes",
condicion: "Exy = 0",
signo: "Cero",
descripcion: "El precio de Y no afecta la demanda de X",
ejemplo: "Zapatos y tomates, libros y sillas",
logica: "No existe relación de consumo entre ambos bienes"
}
],
ejemploNumerico: {
titulo: "Ejemplo: Café (X) y Té (Y) - Sustitutos",
datos: {
precioTeInicial: 3,
precioTeFinal: 3.6,
cantidadCafeInicial: 100,
cantidadCafeFinal: 120
},
calculo: [
"%ΔQx = (120 - 100) / 100 × 100 = 20%",
"%ΔPy = (3.6 - 3) / 3 × 100 = 20%",
"Exy = 20% / 20% = 1.0",
"Exy > 0 → BIENES SUSTITUTOS"
],
conclusion: "El café y el té son sustitutos porque al subir el precio del té, aumenta la demanda de café"
},
magnitud: "Entre más grande sea el valor absoluto de Exy, más fuerte es la relación entre los bienes"
},
{
id: "elasticidad-precio-oferta",
nombre: "Elasticidad Precio de la Oferta (Es o Eo)",
abreviatura: "Es",
descripcion: "Mide la sensibilidad de la cantidad ofrecida ante cambios en el precio del bien",
formula: {
latex: "E_s = \\frac{\\% \\Delta Q_s}{\\% \\Delta P} = \\frac{\\Delta Q_s / Q_s}{\\Delta P / P}",
verbal: "Porcentaje de cambio en cantidad ofrecida dividido por porcentaje de cambio en precio",
nota: "Siempre es positiva (ley de la oferta)"
},
interpretacion: [
{
rango: "Es > 1",
clasificacion: "Oferta ELÁSTICA",
significado: "La cantidad ofrecida es muy sensible al precio",
ejemplo: "Bienes manufacturados que se pueden producir rápidamente"
},
{
rango: "Es < 1",
clasificacion: "Oferta INELÁSTICA",
significado: "La cantidad ofrecida es poco sensible al precio",
ejemplo: "Bienes agrícolas a corto plazo, bienes con capacidad limitada"
},
{
rango: "Es = 1",
clasificacion: "Oferta UNITARIA",
significado: "Cambio proporcional en cantidad ofrecida"
},
{
rango: "Es = 0",
clasificacion: "Oferta PERFECTAMENTE INELÁSTICA",
significado: "Cantidad fija sin importar el precio",
ejemplo: "Obras de arte únicas, terrenos en una ubicación específica"
},
{
rango: "Es = ∞",
clasificacion: "Oferta PERFECTAMENTE ELÁSTICA",
significado: "Los productores ofrecen cualquier cantidad al precio de mercado",
ejemplo: "Industria con capacidad ilimitada y costos constantes"
}
],
horizonteTemporal: {
titulo: "Elasticidad en Diferentes Horizontes Temporales",
descripcion: "La elasticidad de la oferta varía según el tiempo disponible para ajustar la producción",
periodos: [
{
periodo: "Mercado Momentáneo o Very Short Run",
tiempo: "Horas o días",
caracteristicas: "Cantidad fija, Es = 0",
ejemplo: "Pescado fresco del día, flores cortadas",
curva: "Línea vertical"
},
{
periodo: "Corto Plazo (Short Run)",
tiempo: "Meses",
caracteristicas: "Es inelástica pero > 0, algunos factores son fijos",
ejemplo: "Agricultura (tierra fija), manufactura (planta fija)",
curva: "Pendiente positiva empinada"
},
{
periodo: "Largo Plazo (Long Run)",
tiempo: "Años",
caracteristicas: "Es más elástica, todos los factores son variables",
ejemplo: "Pueden construirse nuevas fábricas, comprarse más tierras",
curva: "Pendiente positiva más plana"
}
]
},
ejemploNumerico: {
titulo: "Ejemplo: Tomates (corto plazo vs largo plazo)",
cortoPlazo: {
datos: {
precioInicial: 2,
precioFinal: 3,
cantidadInicial: 1000,
cantidadFinal: 1100
},
calculo: [
"%ΔQs = (1100 - 1000) / 1000 × 100 = 10%",
"%ΔP = (3 - 2) / 2 × 100 = 50%",
"Es = 10% / 50% = 0.2 (INELÁSTICA)"
],
explicacion: "En el corto plazo no se pueden plantar más tomates, la oferta es rígida"
},
largoPlazo: {
datos: {
precioInicial: 2,
precioFinal: 3,
cantidadInicial: 1000,
cantidadFinal: 2000
},
calculo: [
"%ΔQs = (2000 - 1000) / 1000 × 100 = 100%",
"%ΔP = (3 - 2) / 2 × 100 = 50%",
"Es = 100% / 50% = 2.0 (ELÁSTICA)"
],
explicacion: "En el largo plazo se pueden ampliar los cultivos, la oferta es flexible"
}
},
determinantes: [
"Flexibilidad de los factores de producción",
"Tiempo necesario para ajustar la producción",
"Costos de almacenamiento",
"Capacidad ociosa disponible",
"Movilidad de los factores productivos"
]
}
],
tablaComparativa: {
titulo: "Tabla Comparativa de Tipos de Elasticidad",
columnas: ["Tipo", "Fórmula", "Signo", "Interpretación Principal"],
filas: [
["Precio Demanda (Ed)", "%ΔQd / %ΔP", "Negativo (|Ed|)", "Sensibilidad al precio propio"],
["Ingreso (Ei)", "%ΔQd / %ΔI", "Positivo/Negativo", "Clasifica bienes normales/inferiores"],
["Cruzada (Exy)", "%ΔQx / %ΔPy", "Positivo/Negativo/Cero", "Identifica sustitutos/complementos"],
["Precio Oferta (Es)", "%ΔQs / %ΔP", "Positivo", "Capacidad de respuesta de productores"]
]
},
ejercicioIntegrador: {
titulo: "Ejercicio Integrador: Análisis Completo",
escenario: `Una empresa vende smartphones. Observa que cuando el precio baja de $800 a $720,
la cantidad demandada aumenta de 1000 a 1200 unidades. Además, cuando el ingreso promedio
de los consumidores sube de $3000 a $3300, la cantidad demandada aumenta de 1000 a 1150 unidades.
Finalmente, cuando el precio de los tablets (bien relacionado) sube de $500 a $600,
la cantidad demandada de smartphones aumenta de 1000 a 1100 unidades.`,
preguntas: [
{
pregunta: "Calcular Ed (elasticidad precio)",
respuesta: "|Ed| = 1.76 → Demanda ELÁSTICA",
interpretacion: "Los smartphones son sensibles al precio"
},
{
pregunta: "Calcular Ei (elasticidad ingreso)",
respuesta: "Ei = 1.5 → Bien de LUJO",
interpretacion: "La demanda crece más que proporcionalmente al ingreso"
},
{
pregunta: "Calcular Exy (elasticidad cruzada con tablets)",
respuesta: "Exy = 0.45 → BIENES SUSTITUTOS",
interpretacion: "Tablets y smartphones son sustitutos débiles"
}
]
}
};
export default tiposElasticidad;

View File

@@ -0,0 +1,237 @@
export interface Seccion {
titulo: string;
contenido: string;
}
export interface Ejercicio {
id: string;
tipo: 'slider' | 'quiz' | 'juego' | 'tabla' | 'calculadora';
titulo: string;
descripcion: string;
config: Record<string, unknown>;
}
export interface ModuloContenido {
titulo: string;
contenido: Seccion[];
ejercicios: Ejercicio[];
}
export const costos: ModuloContenido = {
titulo: 'Costos de Producción',
contenido: [
{
titulo: 'Costos Fijos y Variables',
contenido: `Los costos totales se componen de dos categorías fundamentales:
**Costos Fijos (CF)**
Son costos que no varían con la cantidad producida. Se incurren incluso si Q = 0.
Ejemplos:
- Alquiler de la planta
- Seguros
- Salarios de administración
- Depreciación (método lineal)
**Costos Variables (CV)**
Varían directamente con el nivel de producción. Si Q = 0, CV = 0.
Ejemplos:
- Materias primas
- Mano de obra directa
- Energía consumida
- Envases y embalajes
**Costo Total (CT)**
$$CT = CF + CV$$
**Ejemplo numérico:**
Una panadería tiene:
- CF = $1,000/mes (alquiler, seguros)
- CV = $5 por pan (harina, salario panadero)
| Q (panes) | CF | CV | CT |
|-----------|----|----|----|
| 0 | 1,000 | 0 | 1,000 |
| 100 | 1,000 | 500 | 1,500 |
| 200 | 1,000 | 1,000 | 2,000 |
| 300 | 1,000 | 1,500 | 2,500 |
| 400 | 1,000 | 2,000 | 3,000 |`
},
{
titulo: 'Costos Medios',
contenido: `Los costos medios (o unitarios) representan el costo por unidad producida:
**Costo Fijo Medio (CFMe)**
$$CFMe = \frac{CF}{Q}$$
Característica: Siempre decreciente conforme aumenta Q (se "diluye" el costo fijo).
**Costo Variable Medio (CVMe)**
$$CVMe = \frac{CV}{Q}$$
Forma típica: U invertida (primero decrece por economías de escala, luego crece por deseconomías).
**Costo Total Medio (CMe o CTM)**
$$CMe = \frac{CT}{Q} = CFMe + CVMe$$
Forma típica: U invertida, con un mínimo donde CMe = CMg.
**Tabla de ejemplo:**
| Q | CF | CV | CT | CFMe | CVMe | CMe |
|---|----|----|----|------|------|-----|
| 0 | 100 | 0 | 100 | - | - | - |
| 1 | 100 | 50 | 150 | 100.0 | 50.0 | 150.0 |
| 2 | 100 | 90 | 190 | 50.0 | 45.0 | 95.0 |
| 3 | 100 | 120 | 220 | 33.3 | 40.0 | 73.3 |
| 4 | 100 | 160 | 260 | 25.0 | 40.0 | 65.0 |
| 5 | 100 | 250 | 350 | 20.0 | 50.0 | 70.0 |
Observa que CMe es mínimo (65.0) cuando está en su punto más bajo entre CFMe y CVMe.`
},
{
titulo: 'Costo Marginal',
contenido: `El **costo marginal (CMg)** es el incremento en el costo total al producir una unidad adicional:
$$CMg = \frac{\Delta CT}{\Delta Q} = \frac{dCT}{dQ}$$
**Importancia:**
- Determina la decisión de producción óptima
- Representa el costo de la última unidad producida
- Es la derivada del costo total
**Relación fundamental:**
$$CMg = \frac{\Delta CV}{\Delta Q}$$
(Dado que CF no varía con Q, solo CV afecta CMg)
**Ejemplo de cálculo:**
| Q | CT | CMg |
|---|----|-----|
| 0 | 100 | - |
| 1 | 150 | 50 |
| 2 | 190 | 40 |
| 3 | 220 | 30 |
| 4 | 260 | 40 |
| 5 | 350 | 90 |
**Propiedades matemáticas:**
1. CMg intercepta CVMe y CMe en sus puntos mínimos
2. Cuando CMg < CMe, CMe está decreciendo
3. Cuando CMg > CMe, CMe está creciendo
4. Cuando CMg = CMe, CMe está en su mínimo
**Intuición:** Si el costo de la siguiente unidad (CMg) es menor que el costo promedio actual, producirla reduce el costo medio.`
},
{
titulo: 'Relación entre Curvas de Costos',
contenido: `Las curvas de costos tienen relaciones matemáticas y económicas fundamentales:
**Gráfico conceptual:**
Las curvas de costos se relacionan de la siguiente manera:
| Elemento | Descripción |
|----------|-------------|
| **Eje vertical** | Costos |
| **Eje horizontal** | Cantidad (Q) |
| **Curva CMe** | Forma de U invertida |
| **Curva CMg** | U invertida que intersecta a CMe en su punto más bajo |
| **Punto de eficiencia** | Donde CMg = CMe (mínimo del costo medio) |
**Relaciones clave entre las curvas:**
1. **CFMe siempre decreciente:** A medida que aumenta Q, el costo fijo se distribuye en más unidades
2. **CMg corta a CVMe en su mínimo:**
- Antes del punto de intersección: CMg < CVMe → CVMe decrece
- Después del punto de intersección: CMg > CVMe → CVMe crece
3. **CMg corta a CMe en su mínimo:**
- Cuando CMg < CMe: CMe está decreciendo
- Cuando CMg > CMe: CMe está creciendo
- Cuando CMg = CMe: CMe está en su punto mínimo (producción técnicamente más eficiente)
**Relaciones clave:**
1. **CFMe siempre decreciente**
- Forma de hipérbola rectangular
- Nunca intersecta a ninguna otra curva
2. **CMg corta a CVMe en su mínimo**
- Antes: CMg < CVMe → CVMe decrece
- Después: CMg > CVMe → CVMe crece
3. **CMg corta a CMe en su mínimo**
- Punto de mínimo costo medio de producción
- Producto técnicamente más eficiente
4. **Forma de las curvas:**
- **CT**: Siempre creciente, convexa luego cóncava
- **CMg**: U invertida, corta mínimos
- **CMe**: U invertida, por encima de CVMe
- **CVMe**: U invertida, por debajo de CMe
**Relación con producción:**
El CMg mínimo corresponde al PMg máximo (ley de rendimientos decrecientes en acción). Cuando PMg decrece, CMg crece.`
}
],
ejercicios: [
{
id: 'costos-calculadora',
tipo: 'calculadora',
titulo: 'Calculadora de Costos',
descripcion: 'Ingresa CF, CV para cada nivel de producción y calcula automáticamente todos los costos medios y marginales',
config: {
columnas: ['Q', 'CF', 'CV', 'CT', 'CFMe', 'CVMe', 'CMe', 'CMg'],
datosEditables: ['CF', 'CV'],
calcularAutomatico: true,
nivelMaximo: 10,
mostrarGrafico: true,
destacarMinimos: true
}
},
{
id: 'costos-relaciones',
tipo: 'quiz',
titulo: 'Relaciones entre Costos',
descripcion: 'Identifica las relaciones correctas entre las curvas de costo',
config: {
preguntas: [
{
pregunta: '¿Dónde se intersectan CMg y CMe?',
opciones: [
'En el origen',
'En el mínimo de CMe',
'En el máximo de producción',
'Nunca se intersectan'
],
respuestaCorrecta: 1
},
{
pregunta: '¿Qué pasa con CMe cuando CMg < CMe?',
opciones: [
'CMe aumenta',
'CMe disminuye',
'CMe se mantiene constante',
'CMe se vuelve negativo'
],
respuestaCorrecta: 1
},
{
pregunta: '¿Por qué CFMe siempre decrece?',
opciones: [
'Porque CF aumenta',
'Porque el costo fijo se distribuye en más unidades',
'Porque CV disminuye',
'Porque CT es constante'
],
respuestaCorrecta: 1
}
]
}
}
]
};
export default costos;

View File

@@ -0,0 +1,249 @@
export interface Seccion {
titulo: string;
contenido: string;
}
export interface Ejercicio {
id: string;
titulo: string;
tipo: 'calculadora' | 'simulador' | 'visualizacion' | 'tabla';
descripcion: string;
datos: Record<string, unknown>;
solucion?: Record<string, unknown>;
}
export interface ModuloContenido {
titulo: string;
contenido: Seccion[];
ejercicios: Ejercicio[];
}
export const ejercicios: ModuloContenido = {
titulo: 'Ejercicios Prácticos - Teoría del Productor',
contenido: [
{
titulo: 'Guía de Ejercicios',
contenido: `Esta sección contiene ejercicios prácticos para aplicar los conceptos de:
- Funciones de producción
- Cálculo de costos
- Decisión óptima de producción
- Análisis de excedentes
Cada ejercicio incluye:
- Datos del problema
- Paso a paso para resolver
- Tablas interactivas
- Visualizaciones gráficas
- Respuestas y explicaciones`
}
],
ejercicios: [
{
id: 'ejercicio-1-costos',
titulo: 'Ejercicio 1: Calculadora de Costos',
tipo: 'tabla',
descripcion: 'Completa la tabla de costos a partir de los costos fijos y variables. Identifica el costo total medio mínimo y el costo marginal.',
datos: {
instrucciones: 'Completa la tabla calculando CT, CFMe, CVMe, CMe y CMg',
costoFijo: 200,
datosTabla: [
{ Q: 0, CV: 0 },
{ Q: 1, CV: 50 },
{ Q: 2, CV: 90 },
{ Q: 3, CV: 120 },
{ Q: 4, CV: 160 },
{ Q: 5, CV: 220 },
{ Q: 6, CV: 300 },
{ Q: 7, CV: 400 },
{ Q: 8, CV: 520 }
],
columnasSolucion: ['Q', 'CF', 'CV', 'CT', 'CFMe', 'CVMe', 'CMe', 'CMg']
},
solucion: {
tablaCompleta: [
{ Q: 0, CF: 200, CV: 0, CT: 200, CFMe: '-', CVMe: '-', CMe: '-', CMg: '-' },
{ Q: 1, CF: 200, CV: 50, CT: 250, CFMe: 200.0, CVMe: 50.0, CMe: 250.0, CMg: 50 },
{ Q: 2, CF: 200, CV: 90, CT: 290, CFMe: 100.0, CVMe: 45.0, CMe: 145.0, CMg: 40 },
{ Q: 3, CF: 200, CV: 120, CT: 320, CFMe: 66.7, CVMe: 40.0, CMe: 106.7, CMg: 30 },
{ Q: 4, CF: 200, CV: 160, CT: 360, CFMe: 50.0, CVMe: 40.0, CMe: 90.0, CMg: 40 },
{ Q: 5, CF: 200, CV: 220, CT: 420, CFMe: 40.0, CVMe: 44.0, CMe: 84.0, CMg: 60 },
{ Q: 6, CF: 200, CV: 300, CT: 500, CFMe: 33.3, CVMe: 50.0, CMe: 83.3, CMg: 80 },
{ Q: 7, CF: 200, CV: 400, CT: 600, CFMe: 28.6, CVMe: 57.1, CMe: 85.7, CMg: 100 },
{ Q: 8, CF: 200, CV: 520, CT: 720, CFMe: 25.0, CVMe: 65.0, CMe: 90.0, CMg: 120 }
],
respuestasClave: {
cmeMinimo: { Q: 6, valor: 83.3 },
cmgEnQ4: 40,
cmgEnQ6: 80,
observacion: 'CMe es mínimo cuando CMg pasa de ser menor a mayor que CMe'
},
pasos: [
'Paso 1: CT = CF + CV (CF siempre es 200)',
'Paso 2: CFMe = CF/Q',
'Paso 3: CVMe = CV/Q',
'Paso 4: CMe = CT/Q (o CFMe + CVMe)',
'Paso 5: CMg = ΔCT/ΔQ = CT(Q) - CT(Q-1)'
]
}
},
{
id: 'ejercicio-2-produccion-optima',
titulo: 'Ejercicio 2: Simulador de Decisión de Producción',
tipo: 'simulador',
descripcion: 'Determina la cantidad óptima de producción dado un precio de mercado y decide si la empresa debe producir, cerrar temporalmente o salir del mercado.',
datos: {
escenario: {
nombre: 'Panadería El Trigo de Oro',
precioMercado: 70,
costoFijo: 200,
funcionCostos: [
{ Q: 0, CT: 200 },
{ Q: 1, CT: 250 },
{ Q: 2, CT: 290 },
{ Q: 3, CT: 320 },
{ Q: 4, CT: 360 },
{ Q: 5, CT: 420 },
{ Q: 6, CT: 500 },
{ Q: 7, CT: 600 },
{ Q: 8, CT: 720 }
]
},
preguntas: [
'¿Cuál es la cantidad óptima de producción (Q*)?',
'¿Cuál es el beneficio máximo?',
'¿Debe producir la empresa o cerrar temporalmente?',
'¿Qué sucedería si el precio baja a $40?'
],
opcionesPrecio: [40, 50, 60, 70, 80, 90]
},
solucion: {
qOptima: 6,
beneficioMaximo: -80,
decision: 'Producir con pérdidas (menor que CF)',
razonamiento: 'P ($70) > CVMe en Q=6 ($50), por lo que cubre costos variables. La pérdida de $80 es menor que CF ($200).',
analisisPorPrecio: {
'40': { qOptima: 3, beneficio: -200, decision: 'Indiferente (P = CVMe mínimo)', detalle: 'Pérdida = CF. Puede producir o cerrar.' },
'50': { qOptima: 4, beneficio: -160, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($160) < CF ($200)' },
'60': { qOptima: 5, beneficio: -120, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($120) < CF ($200)' },
'70': { qOptima: 6, beneficio: -80, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($80) < CF ($200)' },
'80': { qOptima: 6, beneficio: -20, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($20) < CF ($200)' },
'90': { qOptima: 7, beneficio: 30, decision: 'Producir con beneficios', detalle: 'P > CMe, beneficio económico positivo' }
},
reglaDecision: {
paso1: 'Encontrar Q donde P = CMg (o aproximadamente igual)',
paso2: 'Calcular CVMe en esa Q',
paso3: 'Si P >= CVMe: Producir. Si P < CVMe: Cerrar',
paso4: 'Calcular beneficio: π = IT - CT = (P × Q) - CT'
}
}
},
{
id: 'ejercicio-3-excedentes',
titulo: 'Ejercicio 3: Visualización de Excedentes',
tipo: 'visualizacion',
descripcion: 'Calcula y visualiza el excedente del productor bajo diferentes escenarios de precio. Comprende la relación entre excedente, costos variables y beneficios.',
datos: {
escenario: {
curvaCMg: [
{ Q: 0, CMg: 0 },
{ Q: 1, CMg: 10 },
{ Q: 2, CMg: 15 },
{ Q: 3, CMg: 20 },
{ Q: 4, CMg: 25 },
{ Q: 5, CMg: 35 },
{ Q: 6, CMg: 50 },
{ Q: 7, CMg: 70 },
{ Q: 8, CMg: 95 }
],
costoFijo: 100,
precioEjemplo: 50
},
tareas: [
'Calcular el excedente del productor a P = $50',
'Calcular el costo variable total',
'Calcular el beneficio económico',
'Visualizar las áreas correspondientes en el gráfico'
],
escenariosAdicionales: [
{ precio: 25, descripcion: 'Punto de cierre' },
{ precio: 50, descripcion: 'Producción con pérdidas' },
{ precio: 70, descripcion: 'Beneficio positivo' }
]
},
solucion: {
escenarioPrincipal: {
precio: 50,
qOptima: 6,
calculoExcedente: [
{ unidad: 1, precio: 50, cmg: 10, excedente: 40 },
{ unidad: 2, precio: 50, cmg: 15, excedente: 35 },
{ unidad: 3, precio: 50, cmg: 20, excedente: 30 },
{ unidad: 4, precio: 50, cmg: 25, excedente: 25 },
{ unidad: 5, precio: 50, cmg: 35, excedente: 15 },
{ unidad: 6, precio: 50, cmg: 50, excedente: 0 }
],
excedenteTotal: 145,
ingresoTotal: 300,
costoVariable: 155,
costoTotal: 255,
beneficio: 45
},
formulaVerificacion: {
metodo1: 'EP = IT - CV = 300 - 155 = 145',
metodo2: 'EP = Suma de excedentes = 40+35+30+25+15+0 = 145',
metodo3: 'π = EP - CF = 145 - 100 = 45 (Beneficio)'
},
interpretacionAreas: {
areaTotal: 'Rectángulo P × Q = 50 × 6 = 300 (IT)',
areaCV: 'Área bajo CMg = 155 (CV)',
areaEP: 'Área entre P y CMg = 145 (EP)',
areaBeneficio: 'EP - CF = 145 - 100 = 45 (π)'
},
comparacionEscenarios: {
'25': {
qOptima: 4,
excedente: 25,
beneficio: -75,
observacion: 'P = CVMe mínimo. EP mínimo positivo, pero π negativo. Indiferente entre producir o cerrar.'
},
'50': {
qOptima: 6,
excedente: 145,
beneficio: 45,
observacion: 'P > CVMe. EP cubre CF parcialmente, quedando beneficio positivo.'
},
'70': {
qOptima: 7,
excedente: 265,
beneficio: 165,
observacion: 'P >> CMe. EP cubre completamente CF y genera beneficio económico significativo.'
}
},
graficoConceptual: `
**Gráfico Conceptual del Excedente del Productor**
El excedente del productor se representa como el área triangular entre el precio de mercado y la curva de costo marginal.
| Elemento | Descripción |
|----------|-------------|
| Eje vertical | Precio ($) |
| Eje horizontal | Cantidad (Q) |
| Línea P = 50 | Precio de mercado horizontal |
| Curva CMg | Costo marginal creciente |
| Área EP | Excedente del productor (entre P y CMg) |
| Punto óptimo (Q=6) | Donde P = CMg |
**Descripción del gráfico:**
- A precio P = $50, la empresa produce Q = 6 unidades
- La curva CMg representa el costo de cada unidad adicional
- El área sombreada entre P = $50 y la curva CMg representa el excedente del productor
- Este excedente es la ganancia total sobre el costo variable de producción
Área sombreada = Excedente del Productor
Área bajo CMg = Costo Variable`
}
}
]
};
export default ejercicios;

View File

@@ -0,0 +1,301 @@
export interface Seccion {
titulo: string;
contenido: string;
}
export interface Ejercicio {
id: string;
tipo: 'slider' | 'quiz' | 'juego' | 'tabla' | 'calculadora';
titulo: string;
descripcion: string;
config: Record<string, unknown>;
}
export interface ModuloContenido {
titulo: string;
contenido: Seccion[];
ejercicios: Ejercicio[];
}
export const mercado: ModuloContenido = {
titulo: 'Competencia Perfecta',
contenido: [
{
titulo: 'Características de Competencia Perfecta',
contenido: `La **competencia perfecta** es una estructura de mercado teórica con cinco características fundamentales:
**1. Muchos compradores y vendedores**
- Ningún agente individual puede influir en el precio
- El mercado determina el precio (precio-aceptante)
**2. Producto homogéneo**
- Los bienes son perfectamente sustituibles
- No hay diferenciación de marca o calidad
- Ejemplo: trigo, acciones, divisas
**3. Información perfecta**
- Todos conocen precios, costos y tecnologías
- No hay ventajas informativas
- Transparencia total
**4. Libre entrada y salida**
- Sin barreras legales o económicas
- Empresas entran si hay beneficios
- Empresas salen si hay pérdidas
**5. Movilidad perfecta de factores**
- Los recursos pueden reasignarse sin fricción
- Trabajo y capital fluyen hacia los mejores usos
**Implicaciones:**
- La demanda percibida por cada empresa es perfectamente elástica (horizontal)
- Precio = Ingreso Medio = Ingreso Marginal
- $$P = IM = IMg$$
**Ejemplo real aproximado:**
Mercados agrícolas, mercados de valores, mercado de cambio de divisas.`
},
{
titulo: 'Maximización de Beneficios',
contenido: `El objetivo de la empresa es maximizar el **beneficio económico (π)**:
$$\\pi = IT - CT$$
Donde:
- **IT** = Ingreso Total = $P \\times Q$
- **CT** = Costo Total (CF + CV)
**Condición de primer orden:**
Para maximizar, la empresa produce donde:
$$\\frac{d\\pi}{dQ} = 0 \\Rightarrow IMg = CMg$$
**En competencia perfecta:**
- $IMg = P$ (precio constante)
- Por lo tanto: **$P = CMg$**
**Interpretación:**
La empresa produce hasta donde el ingreso de la última unidad (precio) iguala su costo (CMg).
**Ejemplo numérico:**
Precio de mercado: $P = $50
| Q | CT | CMg | IT | π |
|---|---|----|-----|----|---|
| 0 | 100 | - | 0 | -100 |
| 1 | 140 | 40 | 50 | -90 |
| 2 | 180 | 40 | 100 | -80 |
| 3 | 220 | 40 | 150 | -70 |
| 4 | 270 | 50 | 200 | -70 |
| 5 | 330 | 60 | 250 | -80 |
| 6 | 400 | 70 | 300 | -100 |
La cantidad óptima es **Q = 4** (o Q = 3, ambas dan π = -70, máximo menos negativo).
**Nota importante:** Maximizar beneficios no siempre significa beneficios positivos. Puede significar "minimizar pérdidas".`
},
{
titulo: 'Regla IMg = CMg',
contenido: `La regla fundamental de producción establece que la empresa maximiza beneficios cuando:
$$IMg = CMg$$
**Justificación matemática:**
Si $IMg > CMg$:
- Producir una unidad más genera más ingreso que costo
- Convendría aumentar Q
Si $IMg < CMg$:
- La última unidad cuesta más de lo que genera
- Convendría disminuir Q
**En competencia perfecta:**
$$P = CMg$$
**Condición de segundo orden:**
Para asegurar que es un máximo (no un mínimo):
$$\\frac{d^2\\pi}{dQ^2} < 0 \\Rightarrow \\text{pendiente CMg} > \\text{pendiente IMg}$$
**Ejemplo gráfico conceptual:**
El gráfico muestra la maximización de beneficios en competencia perfecta:
| Elemento | Descripción |
|----------|-------------|
| **Eje vertical** | Precio ($) y Costos |
| **Eje horizontal** | Cantidad (Q) |
| **Curva CMg** | Forma de U invertida (primero decrece, luego crece) |
| **Curva CMe** | Forma de U invertida, por encima de CMg en su mínimo |
| **Línea P = IMg** | Línea horizontal a $50 (perfectamente elástica) |
| **Punto óptimo (Q*)** | Intersección de CMg con P = IMg |
La empresa produce en el punto donde la curva CMg ascendente corta al precio.`
},
{
titulo: 'Punto de Cierre a Corto Plazo',
contenido: `A corto plazo, la empresa debe decidir si produce o cierra temporalmente:
**Decisión de cierre:**
La empresa cierra si:
$$P < CVMe_{min}$$
O equivalentemente:
$$IT < CV$$
**Razón:**
- Si produce: Pierde CF + pérdida variable
- Si cierra: Pierde solo CF
- Mejor cerrar si no cubre al menos los costos variables
**Punto de cierre:**
$$P = CVMe_{min}$$
A este precio, la empresa es indiferente entre producir o cerrar. Pérdida = CF en ambos casos.
**Ejemplo:**
Si $CVMe_{min} = $30 y $CF = $100$:
| Precio | Decisión | Pérdida si produce | Pérdida si cierra |
|--------|----------|-------------------|-------------------|
| $50 | Producir | Menor que $100 | $100 |
| $30 | Indiferente | $100 | $100 |
| $20 | Cerrar | Mayor que $100 | $100 |
**Importante:**
Cerrar ≠ Salir del mercado. A corto plazo, la empresa mantiene sus activos (CF) pero no opera. La salida es decisión a largo plazo.`
},
{
titulo: 'Punto de Equilibrio a Largo Plazo',
contenido: `A largo plazo, todas las empresas pueden entrar o salir del mercado:
**Condición de equilibrio:**
En el largo plazo, las empresas entran si hay beneficios económicos positivos y salen si hay pérdidas.
**Equilibrio de largo plazo:**
$$P = CMe_{min}$$
En este punto:
- $P = CMg = CMe_{min}$
- Beneficio económico = 0 (beneficio contable normal)
- No hay incentivos para entrar ni salir
**Proceso de ajuste:**
1. **Si P > CMe** (beneficios):
- Entran nuevas empresas
- Aumenta oferta del mercado
- Baja el precio
- Hasta P = CMe
2. **Si P < CMe** (pérdidas):
- Salen empresas
- Disminuye oferta del mercado
- Sube el precio
- Hasta P = CMe
**Ejemplo:**
El gráfico del equilibrio de largo plazo muestra:
| Elemento | Descripción |
|----------|-------------|
| **Eje vertical** | Costos |
| **Eje horizontal** | Cantidad (Q) |
| **Curva CMe** | Forma de U invertida |
| **Curva CMg** | U invertida que corta a CMe en su punto mínimo |
| **Precio de equilibrio** | Línea horizontal a $40 que pasa por el mínimo de CMe |
| **Cantidad de equilibrio** | Punto donde P = CMg = CMe (mínimo de CMe) |
**Proceso de ajuste hacia el equilibrio:**
1. Si P > CMe: Entran empresas, aumenta la oferta, baja el precio
2. Si P < CMe: Salen empresas, disminuye la oferta, sube el precio
3. Equilibrio: P = CMe_minimo, beneficio económico = 0
**Nota:** Beneficio económico cero no significa que la empresa no gana nada. Significa que gana exactamente su costo de oportunidad (lo que podría ganar en su mejor alternativa).`
},
{
titulo: 'Excedente del Productor',
contenido: `El **excedente del productor** es la diferencia entre lo que un productor recibe y el costo mínimo al que estaría dispuesto a vender.
**Definición:**
$$EP = IT - CV = P \\times Q - CV$$
O equivalentemente:
$$EP = \\sum (P - CMg) \\text{ para todas las unidades producidas}$$
**Interpretación:**
Representa el beneficio sobre los costos variables, o el "alquiler económico" que obtiene el productor.
**Relación con beneficios:**
$$\\pi = EP - CF$$
**Gráfico conceptual:**
El excedente del productor se representa gráficamente como:
| Elemento | Descripción |
|----------|-------------|
| **Eje vertical** | Precio |
| **Eje horizontal** | Cantidad (Q) |
| **Curva CMg** | Curva ascendente (costo marginal creciente) |
| **Precio de mercado (P*)** | Línea horizontal |
| **Excedente del productor (EP)** | Área entre P* y la curva CMg, desde 0 hasta Q* |
| **Cantidad óptima (Q*)** | Punto donde P* = CMg |
**Cálculo del excedente:**
El excedente es el área entre el precio de mercado y la curva de costo marginal, desde cero hasta la cantidad producida. Representa la ganancia sobre el costo variable mínimo necesario para producir cada unidad.
**Ejemplo numérico:**
P = $50, Q = 10 unidades
| Unidad | CMg | Excedente unitario |
|--------|-----|-------------------|
| 1 | $10 | $40 |
| 2 | $15 | $35 |
| 3 | $20 | $30 |
| 4 | $25 | $25 |
| 5 | $30 | $20 |
| 6 | $35 | $15 |
| 7 | $40 | $10 |
| 8 | $45 | $5 |
| 9 | $50 | $0 |
| 10 | $55 | -$5 (no produce) |
EP total = $180 (suma de excedentes de unidades 1-9)`
}
],
ejercicios: [
{
id: 'competencia-decision',
tipo: 'calculadora',
titulo: 'Simulador de Decisión de Producción',
descripcion: 'Dado un precio de mercado y curva de costos, encuentra la cantidad óptima y determina si debes producir o cerrar',
config: {
inputs: ['precioMercado', 'CF', 'funcionCosto'],
outputs: ['QOptima', 'IT', 'CT', 'Beneficio', 'Decision'],
criterios: [
'Si P >= CMe: Beneficios positivos',
'Si CVMe < P < CMe: Producir con pérdidas (menor que CF)',
'Si P < CVMe: Cerrar temporalmente'
],
mostrarGrafico: true,
destacarZona: true
}
},
{
id: 'excedente-visualizacion',
tipo: 'juego',
titulo: 'Visualización de Excedentes',
descripcion: 'Interactúa con el gráfico para ver cómo cambia el excedente del productor al variar el precio y la cantidad',
config: {
tipoGrafico: 'area',
mostrarAreas: ['excedenteProductor', 'costoVariable', 'beneficio'],
interactivo: true,
sliders: ['precio', 'cantidad'],
calcularAutomatico: true,
mostrarTabla: true
}
}
]
};
export default mercado;

View File

@@ -0,0 +1,157 @@
export interface Seccion {
titulo: string;
contenido: string;
}
export interface Ejercicio {
id: string;
tipo: 'slider' | 'quiz' | 'juego' | 'tabla' | 'calculadora';
titulo: string;
descripcion: string;
config: Record<string, unknown>;
}
export interface ModuloContenido {
titulo: string;
contenido: Seccion[];
ejercicios: Ejercicio[];
}
export const produccion: ModuloContenido = {
titulo: 'Producción',
contenido: [
{
titulo: 'Función de Producción',
contenido: `La **función de producción** describe la relación técnica entre los factores de producción utilizados y la cantidad máxima de producto obtenida.
**Fórmula general:**
$$Q = f(K, L)$$
Donde:
- **Q** = Cantidad producida (output)
- **K** = Capital (maquinaria, equipos, instalaciones)
- **L** = Trabajo (horas-hombre, número de trabajadores)
- **f** = Función de producción (tecnología)
**Ejemplo:** Una fábrica de pan utiliza hornos (K) y panaderos (L) para producir pan (Q).
**Formas comunes:**
- **Lineal**: $Q = aK + bL$ (sustitutos perfectos)
- **Cobb-Douglas**: $Q = A \cdot K^\alpha \cdot L^\beta$ (sustituibles)
- **Leontief**: $Q = \min(aK, bL)$ (complementarios perfectos)`
},
{
titulo: 'Producto Total, Marginal y Medio',
contenido: `El análisis de producción distingue tres conceptos fundamentales:
**Producto Total (PT)**
Cantidad total producida con una cantidad dada de factores.
$$PT = f(L)$$ (manteniendo K constante)
**Producto Marginal (PMg)**
Incremento en el producto total al aumentar en una unidad el factor variable.
$$PMg_L = \frac{\Delta PT}{\Delta L}$$
**Producto Medio (PMe)**
Producto por unidad de factor.
$$PMe_L = \frac{PT}{L}$$
**Ejemplo numérico:**
| L (trabajadores) | PT (panes) | PMg | PMe |
|------------------|------------|-----|-----|
| 0 | 0 | - | - |
| 1 | 10 | 10 | 10.0 |
| 2 | 24 | 14 | 12.0 |
| 3 | 36 | 12 | 12.0 |
| 4 | 44 | 8 | 11.0 |
| 5 | 48 | 4 | 9.6 |
| 6 | 48 | 0 | 8.0 |
| 7 | 42 | -6 | 6.0 |
Observa que el PMg máximo (14) ocurre antes que el PMe máximo (12.0), y ambos antes del PT máximo (48).`
},
{
titulo: 'Ley de Rendimientos Decrecientes',
contenido: `La **ley de rendimientos decrecientes** (o ley de productividad marginal decreciente) establece que:
> *"Al mantener constantes todos los demás factores, si se va aumentando la cantidad de un factor variable, llega un punto a partir del cual los incrementos de producto son cada vez menores."*
**Condiciones:**
- Tecnología constante
- Al menos un factor fijo
- Factores variables homogéneos
**Interpretación:**
Inicialmente, al añadir trabajadores a una fábrica fija, el PMg aumenta (especialización). Pero una vez alcanzado el óptimo, cada trabajador adicional tiene menos capital y espacio, reduciendo su contribución marginal.
**Ejemplo gráfico conceptual:**
El gráfico muestra la relación entre PMg y PMe:
- PMg alcanza su máximo primero
- Luego PMe alcanza su máximo (cuando PMg = PMe)
- Finalmente PT alcanza su máximo (cuando PMg = 0)
- Después PMg se vuelve negativo (Etapa III)
**Importancia económica:**
Esta ley explica por qué las empresas no crecen indefinidamente y por qué existen costos crecientes a largo plazo.`
},
{
titulo: 'Etapas de Producción',
contenido: `El análisis del producto marginal y medio permite dividir la producción en tres etapas:
**Etapa I: Crecientes**
- PMg > PMe (ambos creciendo inicialmente)
- PMe está aumentando
- La empresa no opera aquí: está desperdiciando capacidad fija
- Fin: Cuando PMg = PMe (PMe máximo)
**Etapa II: Decrecientes**
- 0 < PMg < PMe (ambos decrecientes)
- PMe decreciente pero positivo
- PMg positivo pero decreciente
- **Zona racional de producción**
- Fin: Cuando PMg = 0 (PT máximo)
**Etapa III: Negativos**
- PMg < 0
- PT decreciente
- La empresa nunca opera aquí: tiene "demasiado" factor variable
- Agregar más trabajo reduce la producción total
**Resumen de etapas:**
| Etapa | Características | Decisión |
|-------|----------------|----------|
| **I** | PMg > PMe, PMe creciente | No operar - desperdicio de capacidad |
| **II** | 0 < PMg < PMe, ambos decrecientes | **Operar aquí** - zona racional |
| **III** | PMg < 0, PT decreciente | No operar - demasiado factor variable |
**Decisión del productor:**
La empresa racional operará en la **Etapa II**, donde PMg es positivo pero decreciente. La ubicación exacta depende de los precios de los factores y del producto.`
}
],
ejercicios: [
{
id: 'produccion-tabla',
tipo: 'tabla',
titulo: 'Análisis de Productividad',
descripcion: 'Completa la tabla de producción calculando PMg y PMe, identificando las tres etapas',
config: {
columnas: ['L', 'PT', 'PMg', 'PMe', 'Etapa'],
datosIniciales: [
{ L: 0, PT: 0, PMg: null, PMe: null, Etapa: '-' },
{ L: 1, PT: 8, PMg: null, PMe: null, Etapa: '?' },
{ L: 2, PT: 20, PMg: null, PMe: null, Etapa: '?' },
{ L: 3, PT: 36, PMg: null, PMe: null, Etapa: '?' },
{ L: 4, PT: 48, PMg: null, PMe: null, Etapa: '?' },
{ L: 5, PT: 55, PMg: null, PMe: null, Etapa: '?' },
{ L: 6, PT: 60, PMg: null, PMe: null, Etapa: '?' },
{ L: 7, PT: 56, PMg: null, PMe: null, Etapa: '?' }
],
mostrarGrafico: true,
identificarEtapas: true
}
}
]
};
export default produccion;

View File

@@ -0,0 +1,85 @@
import { useProgressStore } from '../stores/progressStore';
import { useState, useCallback, useEffect } from 'react';
interface UseEjercicioProgresoOptions {
moduloId: string;
ejercicioId: string;
onComplete?: (puntuacion?: number) => void;
}
interface UseEjercicioProgresoReturn {
guardarProgreso: (puntuacion: number) => Promise<void>;
progresoGuardado: boolean;
puntuacionAnterior: number | undefined;
intentos: number;
isLoading: boolean;
error: string | null;
}
export function useEjercicioProgreso({
moduloId,
ejercicioId,
onComplete,
}: UseEjercicioProgresoOptions): UseEjercicioProgresoReturn {
const { saveProgreso, getProgresoEjercicio, modulos } = useProgressStore();
const [progresoGuardado, setProgresoGuardado] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [puntuacionAnterior, setPuntuacionAnterior] = useState<number | undefined>(undefined);
const [intentos, setIntentos] = useState(0);
// Cargar progreso existente de manera reactiva
useEffect(() => {
const progreso = getProgresoEjercicio(moduloId, ejercicioId);
if (progreso) {
setPuntuacionAnterior(progreso.puntuacion);
setIntentos(progreso.intentos);
} else {
setPuntuacionAnterior(undefined);
setIntentos(0);
}
}, [moduloId, ejercicioId, getProgresoEjercicio, modulos]);
const guardarProgreso = useCallback(async (puntuacion: number) => {
setIsLoading(true);
setError(null);
try {
// Guardar en el store (que ahora usa la API)
await saveProgreso(moduloId, ejercicioId, puntuacion);
setProgresoGuardado(true);
// Actualizar estado local
const progresoAnterior = getProgresoEjercicio(moduloId, ejercicioId);
if (progresoAnterior) {
setPuntuacionAnterior(progresoAnterior.puntuacion);
setIntentos(progresoAnterior.intentos);
} else {
setPuntuacionAnterior(puntuacion);
setIntentos(1);
}
// Llamar callback si existe
if (onComplete) {
onComplete(puntuacion);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al guardar el progreso');
console.error('Error saving progress:', err);
} finally {
setIsLoading(false);
}
}, [moduloId, ejercicioId, saveProgreso, onComplete, getProgresoEjercicio]);
return {
guardarProgreso,
progresoGuardado,
puntuacionAnterior,
intentos,
isLoading,
error,
};
}
export default useEjercicioProgreso;

View File

@@ -1,82 +1,109 @@
import { useState, useEffect } from 'react';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
import { useProgressStore } from '../stores/progressStore';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { progresoService } from '../services/api';
import type { ModuloProgreso } from '../types';
import { BookOpen, TrendingUp, User, LogOut, LayoutGrid } from 'lucide-react';
import { ProgressBar } from '../components/progress/ProgressBar';
import { ScoreDisplay } from '../components/progress/ScoreDisplay';
import { BadgesSection } from '../components/progress/Badges';
import { Loader } from '../components/ui/Loader';
import { BookOpen, User, LogOut, LayoutGrid, Award, Star, Target, CheckCircle, FileText } from 'lucide-react';
const MODULOS_DEFAULT = [
{ numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos' },
{ numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado' },
{ numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor' },
{ numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción' },
const MODULOS_CONFIG = [
{ id: 'modulo1', numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos', totalEjercicios: 3 },
{ id: 'modulo2', numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado', totalEjercicios: 3 },
{ id: 'modulo3', numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor', totalEjercicios: 3 },
{ id: 'modulo4', numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción', totalEjercicios: 3 },
];
export function Dashboard() {
const { usuario, logout } = useAuthStore();
const [modulosProgreso, setModulosProgreso] = useState<ModuloProgreso[]>([]);
const {
puntuacionTotal,
nivel,
calcularPorcentajeModulo,
getBadgesDesbloqueados,
getBadgesBloqueados,
modulos,
loadProgreso,
isLoading,
error,
} = useProgressStore();
useEffect(() => {
loadProgreso();
}, []);
const loadProgreso = async () => {
try {
const progresos = await progresoService.getProgreso();
const modulos = MODULOS_DEFAULT.map((mod) => {
const modProgresos = progresos.filter((p) => p.modulo_numero === mod.numero);
const completados = modProgresos.filter((p) => p.completado).length;
const total = 5; // Asumiendo 5 ejercicios por módulo
return {
numero: mod.numero,
titulo: mod.titulo,
porcentaje: Math.round((completados / total) * 100),
ejerciciosCompletados: completados,
totalEjercicios: total,
};
});
setModulosProgreso(modulos);
} catch {
// Si hay error, mostrar progreso vacío
setModulosProgreso(
MODULOS_DEFAULT.map((mod) => ({
numero: mod.numero,
titulo: mod.titulo,
porcentaje: 0,
ejerciciosCompletados: 0,
totalEjercicios: 5,
}))
);
}
};
}, [loadProgreso]);
const handleLogout = async () => {
await logout();
};
if (isLoading && Object.keys(modulos).length === 0) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader size="lg" className="mx-auto mb-4" />
<p className="text-gray-600">Cargando tu progreso...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center max-w-md mx-auto px-4">
<div className="text-red-500 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h2 className="text-xl font-bold text-gray-900 mb-2">Error al cargar el progreso</h2>
<p className="text-gray-600 mb-4">{error}</p>
<Button onClick={loadProgreso}>Reintentar</Button>
</div>
</div>
);
}
// Calcular progreso total
const totalProgreso = Math.round(
modulosProgreso.reduce((acc, mod) => acc + mod.porcentaje, 0) / modulosProgreso.length
MODULOS_CONFIG.reduce((acc, mod) => {
return acc + calcularPorcentajeModulo(mod.id, mod.totalEjercicios);
}, 0) / MODULOS_CONFIG.length
);
const badgesDesbloqueados = getBadgesDesbloqueados();
const badgesBloqueados = getBadgesBloqueados();
// Calcular ejercicios completados por módulo
const getEjerciciosCompletados = (moduloId: string) => {
const modulo = modulos[moduloId];
if (!modulo) return 0;
return Object.values(modulo.ejercicios).filter(ej => ej.completado).length;
};
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<BookOpen className="w-5 h-5 text-white" />
</div>
<h1 className="text-xl font-bold text-gray-900">Economía</h1>
<h1 className="text-xl font-bold text-gray-900">Economía Interactiva</h1>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-gray-600">
<User className="w-5 h-5" />
<span className="font-medium">{usuario?.nombre}</span>
<span className="font-medium">{usuario?.nombre || 'Usuario'}</span>
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full font-semibold">
{nivel}
</span>
{usuario?.rol === 'admin' && (
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full">
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full font-semibold">
Admin
</span>
)}
@@ -91,86 +118,154 @@ export function Dashboard() {
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900">Tu progreso</h2>
<p className="text-gray-600">Continúa donde lo dejaste</p>
<p className="text-gray-600">Continúa donde lo dejaste y desbloquea nuevos logros</p>
</div>
<Card className="mb-8">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold text-gray-900">Progreso total</h3>
<p className="text-sm text-gray-500">{totalProgreso}% completado</p>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 text-white border-none">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Progreso total</p>
<p className="text-3xl font-bold mt-1">{totalProgreso}%</p>
</div>
<Target className="w-12 h-12 text-blue-100 opacity-80" />
</div>
<div className="mt-4 w-full bg-white/20 rounded-full h-2">
<div
className="bg-white h-2 rounded-full transition-all duration-500"
style={{ width: `${totalProgreso}%` }}
/>
</div>
<p className="mt-2 text-xs text-blue-100">
{totalProgreso === 100 ? '¡Has completado todos los módulos!' : 'Sigue así, vas por buen camino'}
</p>
</Card>
<Card className="bg-gradient-to-br from-amber-500 to-orange-500 text-white border-none">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm">Puntuación total</p>
<p className="text-3xl font-bold mt-1">{puntuacionTotal.toLocaleString()}</p>
</div>
<Star className="w-12 h-12 text-orange-100 opacity-80" />
</div>
<p className="mt-4 text-sm text-orange-100">
Acumula puntos completando ejercicios para subir de nivel
</p>
</Card>
<Card className="bg-gradient-to-br from-purple-500 to-pink-500 text-white border-none">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm">Logros</p>
<p className="text-3xl font-bold mt-1">
{badgesDesbloqueados.length}/{badgesDesbloqueados.length + badgesBloqueados.length}
</p>
</div>
<Award className="w-12 h-12 text-purple-100 opacity-80" />
</div>
<p className="mt-4 text-sm text-purple-100">
{badgesBloqueados.length === 0
? '¡Todos los logros desbloqueados!'
: `${badgesBloqueados.length} logros por desbloquear`}
</p>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Columna izquierda - Módulos */}
<div className="lg:col-span-2 space-y-6">
{/* Puntuación y Nivel */}
<ScoreDisplay
puntos={puntuacionTotal}
animar={false}
showNivel={true}
size="md"
/>
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">Módulos</h2>
{usuario?.rol === 'admin' && (
<Link to="/admin">
<Button variant="outline" size="sm">
Panel de Admin
</Button>
</Link>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{MODULOS_CONFIG.map((modulo) => {
const porcentaje = calcularPorcentajeModulo(modulo.id, modulo.totalEjercicios);
const completados = getEjerciciosCompletados(modulo.id);
return (
<Link key={modulo.id} to={`/modulo/${modulo.numero}`}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-blue-600 font-bold text-lg">{modulo.numero}</span>
</div>
<div>
<h3 className="font-semibold text-gray-900">{modulo.titulo}</h3>
<p className="text-sm text-gray-500">
{completados}/{modulo.totalEjercicios} ejercicios
</p>
</div>
</div>
</div>
<ProgressBar
porcentaje={porcentaje}
moduloNumero={modulo.numero}
showLabel={false}
size="sm"
/>
<div className="flex items-center justify-between text-sm mt-3">
<span className="text-gray-500">{porcentaje}% completado</span>
{porcentaje === 100 && (
<span className="text-green-600 flex items-center gap-1 font-medium">
<CheckCircle className="w-4 h-4" />
Completado
</span>
)}
</div>
</Card>
</Link>
);
})}
</div>
<div className="mt-8 flex justify-center gap-4">
<Link to="/modulos">
<Button variant="outline" size="lg">
<LayoutGrid className="w-5 h-5 mr-2" />
Ver todos los módulos
</Button>
</Link>
<Link to="/recursos">
<Button variant="outline" size="lg">
<FileText className="w-5 h-5 mr-2" />
Material PDF
</Button>
</Link>
</div>
<div className="text-3xl font-bold text-primary">{totalProgreso}%</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-primary h-3 rounded-full transition-all duration-500"
style={{ width: `${totalProgreso}%` }}
{/* Columna derecha - Logros */}
<div>
<BadgesSection
badgesDesbloqueados={badgesDesbloqueados}
badgesBloqueados={badgesBloqueados}
/>
</div>
</Card>
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">Módulos</h2>
{usuario?.rol === 'admin' && (
<Link to="/admin">
<Button variant="outline" size="sm">
Panel de Admin
</Button>
</Link>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{modulosProgreso.map((modulo) => (
<Link key={modulo.numero} to={`/modulo/${modulo.numero}`}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-primary font-bold">{modulo.numero}</span>
</div>
<div>
<h3 className="font-semibold text-gray-900">{modulo.titulo}</h3>
<p className="text-sm text-gray-500">
{modulo.ejerciciosCompletados}/{modulo.totalEjercicios} ejercicios
</p>
</div>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
<div
className={`h-2 rounded-full transition-all ${
modulo.porcentaje === 100 ? 'bg-success' : 'bg-primary'
}`}
style={{ width: `${modulo.porcentaje}%` }}
/>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500">{modulo.porcentaje}% completado</span>
{modulo.porcentaje === 100 && (
<span className="text-success flex items-center gap-1">
<TrendingUp className="w-4 h-4" />
Completado
</span>
)}
</div>
</Card>
</Link>
))}
</div>
<div className="mt-8 text-center">
<Link to="/modulos">
<Button variant="outline" size="lg">
<LayoutGrid className="w-5 h-5 mr-2" />
Ver todos los módulos
</Button>
</Link>
</div>
</main>
</div>
);
}
export default Dashboard;

View File

@@ -1,63 +1,213 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { progresoService } from '../services/api';
import type { Progreso } from '../types';
import { ArrowLeft, CheckCircle, Play } from 'lucide-react';
import { Loader } from '../components/ui/Loader';
import { useProgressStore } from '../stores/progressStore';
import { ScoreDisplay } from '../components/progress/ScoreDisplay';
import {
ArrowLeft,
CheckCircle,
Play,
Lock,
Trophy,
TrendingUp,
RotateCcw
} from 'lucide-react';
import type { EjercicioProgreso } from '../stores/progressStore';
const MODULOS_INFO: Record<number, { titulo: string; descripcion: string }> = {
1: { titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos de economía' },
2: { titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de oferta y demanda en el mercado' },
3: { titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor y elasticidades' },
4: { titulo: 'Teoría del Productor', descripcion: 'Costos de producción y competencia perfecta' },
// Importar ejercicios reales
import { FlujoCircular } from '../components/exercises/modulo1/FlujoCircular';
import { QuizBienes } from '../components/exercises/modulo1/QuizBienes';
import { SimuladorDisyuntivas } from '../components/exercises/modulo1/SimuladorDisyuntivas';
import { ConstructorCurvas } from '../components/exercises/modulo2/ConstructorCurvas';
import { IdentificarShocks } from '../components/exercises/modulo2/IdentificarShocks';
import { SimuladorPrecios } from '../components/exercises/modulo2/SimuladorPrecios';
import { ClasificadorBienes } from '../components/exercises/modulo3/ClasificadorBienes';
import { CalculadoraElasticidad } from '../components/exercises/modulo3/CalculadoraElasticidad';
import { EjerciciosExamen } from '../components/exercises/modulo3/EjerciciosExamen';
import { CalculadoraCostos } from '../components/exercises/modulo4/CalculadoraCostos';
import { SimuladorProduccion } from '../components/exercises/modulo4/SimuladorProduccion';
import { VisualizadorExcedentes } from '../components/exercises/modulo4/VisualizadorExcedentes';
const MODULOS_INFO: Record<number, {
id: string;
titulo: string;
descripcion: string;
color: string;
}> = {
1: {
id: 'modulo1',
titulo: 'Fundamentos de Economía',
descripcion: 'Introducción a los conceptos básicos de economía',
color: 'from-blue-500 to-blue-600'
},
2: {
id: 'modulo2',
titulo: 'Oferta, Demanda y Equilibrio',
descripcion: 'Curvas de oferta y demanda en el mercado',
color: 'from-green-500 to-green-600'
},
3: {
id: 'modulo3',
titulo: 'Utilidad y Elasticidad',
descripcion: 'Teoría del consumidor y elasticidades',
color: 'from-purple-500 to-purple-600'
},
4: {
id: 'modulo4',
titulo: 'Teoría del Productor',
descripcion: 'Costos de producción y competencia perfecta',
color: 'from-orange-500 to-orange-600'
},
};
const EJERCICIOS_MOCK = [
{ id: 'e1', titulo: 'Conceptos básicos', descripcion: 'Repasa los fundamentos de la economía' },
{ id: 'e2', titulo: 'Agentes económicos', descripcion: 'Identifica los diferentes agentes en la economía' },
{ id: 'e3', titulo: 'Factores de producción', descripcion: 'Aprende sobre tierra, trabajo y capital' },
{ id: 'e4', titulo: 'Flujo circular', descripcion: 'Comprende el flujo de bienes y dinero' },
{ id: 'e5', titulo: 'Evaluación final', descripcion: 'Pon a prueba todo lo aprendido' },
];
const EJERCICIOS_POR_MODULO: Record<number, Array<{
id: string;
titulo: string;
descripcion: string;
componente: React.ComponentType<{ ejercicioId: string; onComplete?: (puntuacion: number) => void }>;
}>> = {
1: [
{ id: 'simulador-disyuntivas', titulo: 'Simulador de Disyuntivas', descripcion: 'Explora las decisiones económicas fundamentales', componente: SimuladorDisyuntivas },
{ id: 'quiz-bienes', titulo: 'Quiz de Bienes', descripcion: 'Identifica diferentes tipos de bienes', componente: QuizBienes },
{ id: 'flujo-circular', titulo: 'Flujo Circular', descripcion: 'Comprende el flujo de bienes y dinero en la economía', componente: FlujoCircular },
],
2: [
{ id: 'constructor-curvas', titulo: 'Constructor de Curvas', descripcion: 'Construye curvas de oferta y demanda', componente: ConstructorCurvas },
{ id: 'identificar-shocks', titulo: 'Identificar Shocks', descripcion: 'Reconoce cambios en el mercado', componente: IdentificarShocks },
{ id: 'simulador-precios', titulo: 'Simulador de Precios', descripcion: 'Simula el equilibrio de precios', componente: SimuladorPrecios },
],
3: [
{ id: 'clasificador-bienes', titulo: 'Clasificador de Bienes', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificadorBienes },
{ id: 'calculadora-elasticidad', titulo: 'Calculadora de Elasticidad', descripcion: 'Calcula elasticidades de demanda', componente: CalculadoraElasticidad },
{ id: 'ejercicios-examen', titulo: 'Ejercicios de Examen', descripcion: 'Pon a prueba tus conocimientos', componente: EjerciciosExamen },
],
4: [
{ id: 'calculadora-costos', titulo: 'Calculadora de Costos', descripcion: 'Calcula costos de producción', componente: CalculadoraCostos },
{ id: 'simulador-produccion', titulo: 'Simulador de Producción', descripcion: 'Simula la producción óptima', componente: SimuladorProduccion },
{ id: 'visualizador-excedentes', titulo: 'Visualizador de Excedentes', descripcion: 'Visualiza excedentes del consumidor y productor', componente: VisualizadorExcedentes },
],
};
export function Modulo() {
const { numero } = useParams<{ numero: string }>();
const num = parseInt(numero || '1', 10);
const [progresos, setProgresos] = useState<Progreso[]>([]);
const {
puntuacionTotal,
getProgresoEjercicio,
saveProgreso,
calcularPorcentajeModulo,
loadProgreso,
isLoading,
error,
} = useProgressStore();
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
const ejercicios = EJERCICIOS_MOCK;
const [ejercicioActivo, setEjercicioActivo] = useState<string | null>(null);
useEffect(() => {
loadProgreso();
}, [num]);
}, [loadProgreso]);
const loadProgreso = async () => {
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
const ejercicios = EJERCICIOS_POR_MODULO[num] || [];
const porcentaje = calcularPorcentajeModulo(moduloInfo.id, ejercicios.length);
const getProgresoEjercicioLocal = (ejercicioId: string): EjercicioProgreso | undefined => {
return getProgresoEjercicio(moduloInfo.id, ejercicioId);
};
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
try {
const data = await progresoService.getProgreso();
setProgresos(data);
} catch {
// Silencio
await saveProgreso(moduloInfo.id, ejercicioId, puntuacion);
setEjercicioActivo(null);
} catch (err) {
console.error('Error al guardar progreso:', err);
}
};
const getProgresoForEjercicio = (ejercicioId: string) => {
return progresos.find(
(p) => p.modulo_numero === num && p.ejercicio_id === ejercicioId
);
const completados = ejercicios.filter(
(e) => getProgresoEjercicioLocal(e.id)?.completado
).length;
// Determinar si un ejercicio está bloqueado (el primero siempre desbloqueado)
const isEjercicioBloqueado = (index: number): boolean => {
if (index === 0) return false;
// Ejercicio anterior completado?
const ejercicioAnterior = ejercicios[index - 1];
return !getProgresoEjercicioLocal(ejercicioAnterior.id)?.completado;
};
const completados = ejercicios.filter(
(e) => getProgresoForEjercicio(e.id)?.completado
).length;
const porcentaje = Math.round((completados / ejercicios.length) * 100);
if (ejercicioActivo) {
const ejercicio = ejercicios.find(e => e.id === ejercicioActivo);
if (!ejercicio) return null;
const EjercicioComponent = ejercicio.componente;
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<button
onClick={() => setEjercicioActivo(null)}
className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Volver al módulo
</button>
</div>
</header>
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">{ejercicio.titulo}</h1>
<p className="text-gray-600">{ejercicio.descripcion}</p>
</div>
<EjercicioComponent
ejercicioId={ejercicio.id}
onComplete={(puntuacion: number) => handleCompleteEjercicio(ejercicio.id, puntuacion)}
/>
</main>
</div>
);
}
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader size="lg" className="mx-auto mb-4" />
<p className="text-gray-600">Cargando ejercicios...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center max-w-md mx-auto px-4">
<div className="text-red-500 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h2 className="text-xl font-bold text-gray-900 mb-2">Error al cargar el progreso</h2>
<p className="text-gray-600 mb-4">{error}</p>
<Button onClick={loadProgreso}>Reintentar</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<Link to="/" className="inline-flex items-center text-primary hover:underline">
<Link to="/" className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver al Dashboard
</Link>
@@ -67,7 +217,7 @@ export function Modulo() {
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<div className="flex items-center gap-4 mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-primary to-blue-600 rounded-xl flex items-center justify-center text-white text-2xl font-bold shadow-lg">
<div className={`w-16 h-16 bg-gradient-to-br ${moduloInfo.color} rounded-xl flex items-center justify-center text-white text-2xl font-bold shadow-lg`}>
{num}
</div>
<div>
@@ -76,76 +226,153 @@ export function Modulo() {
</div>
</div>
<Card className="bg-gradient-to-r from-primary to-blue-600 text-white">
<div className="flex items-center justify-between">
<Card className={`bg-gradient-to-r ${moduloInfo.color} text-white border-none`}>
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-blue-100">Tu progreso en este módulo</p>
<p className="text-white/80 text-sm">Tu progreso en este módulo</p>
<p className="text-3xl font-bold mt-1">{porcentaje}%</p>
</div>
<div className="text-right">
<p className="text-blue-100">{completados}/{ejercicios.length} ejercicios</p>
<p className="text-white/80 text-sm">Ejercicios</p>
<p className="text-xl font-bold">{completados}/{ejercicios.length}</p>
</div>
</div>
<div className="mt-4 w-full bg-white/20 rounded-full h-2">
<div
className="bg-white h-2 rounded-full transition-all"
style={{ width: `${porcentaje}%` }}
<div className="w-full bg-white/20 rounded-full h-3 overflow-hidden">
<motion.div
className="bg-white h-full rounded-full"
initial={{ width: 0 }}
animate={{ width: `${porcentaje}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
/>
</div>
</Card>
</div>
<h2 className="text-xl font-bold text-gray-900 mb-4">Ejercicios</h2>
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">Ejercicios</h2>
<ScoreDisplay puntos={puntuacionTotal} animar={false} showNivel={false} size="sm" />
</div>
<div className="space-y-3">
<div className="space-y-4">
{ejercicios.map((ejercicio, index) => {
const progreso = getProgresoForEjercicio(ejercicio.id);
const progreso = getProgresoEjercicioLocal(ejercicio.id);
const completado = progreso?.completado || false;
const bloqueado = isEjercicioBloqueado(index);
return (
<Card key={ejercicio.id} className="hover:shadow-md transition-shadow">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
completado ? 'bg-success text-white' : 'bg-gray-100 text-gray-500'
}`}>
{completado ? (
<CheckCircle className="w-5 h-5" />
) : (
<span className="font-medium">{index + 1}</span>
)}
</div>
<motion.div
key={ejercicio.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
>
<Card className={`transition-all ${
bloqueado
? 'opacity-60 bg-gray-50'
: 'hover:shadow-md cursor-pointer'
}`}
onClick={() => !bloqueado && setEjercicioActivo(ejercicio.id)}
>
<div className="flex items-center gap-4">
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
completado
? 'bg-green-500 text-white'
: bloqueado
? 'bg-gray-200 text-gray-400'
: 'bg-blue-100 text-blue-600'
}`}>
{completado ? (
<CheckCircle className="w-6 h-6" />
) : bloqueado ? (
<Lock className="w-5 h-5" />
) : (
<span className="font-bold">{index + 1}</span>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
<p className="text-sm text-gray-500">{ejercicio.descripcion}</p>
</div>
<div className="flex-1">
<h3 className={`font-semibold ${
completado ? 'text-gray-900' : bloqueado ? 'text-gray-500' : 'text-gray-900'
}`}>
{ejercicio.titulo}
</h3>
<p className="text-sm text-gray-500">{ejercicio.descripcion}</p>
{completado && progreso && progreso.puntuacion > 0 && (
<div className="flex items-center gap-2 mt-1">
<span className="text-xs font-medium text-green-600">
Mejor puntuación: {progreso.puntuacion} pts
</span>
<span className="text-xs text-gray-400">
({progreso.intentos} {progreso.intentos === 1 ? 'intento' : 'intentos'})
</span>
</div>
)}
</div>
<Button size="sm">
<Play className="w-4 h-4 mr-2" />
{completado ? 'Repetir' : 'Comenzar'}
</Button>
</div>
</Card>
<Button
size="sm"
disabled={bloqueado}
variant={completado ? 'outline' : 'primary'}
onClick={(e) => {
e.stopPropagation();
!bloqueado && setEjercicioActivo(ejercicio.id);
}}
>
{completado ? (
<>
<RotateCcw className="w-4 h-4 mr-2" />
Repetir
</>
) : bloqueado ? (
'Bloqueado'
) : (
<>
<Play className="w-4 h-4 mr-2" />
Comenzar
</>
)}
</Button>
</div>
</Card>
</motion.div>
);
})}
</div>
{porcentaje === 100 && (
<Card className="mt-6 bg-success/10 border border-success">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-success rounded-full flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-white" />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="mt-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white border-none">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-white/20 rounded-full flex items-center justify-center">
<Trophy className="w-7 h-7" />
</div>
<div className="flex-1">
<h3 className="font-bold text-lg">¡Felicitaciones!</h3>
<p className="text-green-100">
Has completado todos los ejercicios de este módulo.
{num < 4 ? ' ¡Continúa con el siguiente módulo!' : ' ¡Has completado todos los módulos!'}
</p>
</div>
{num < 4 && (
<Link to={`/modulo/${num + 1}`}>
<Button variant="primary" className="bg-white text-green-600 hover:bg-green-50">
Siguiente módulo
<TrendingUp className="w-4 h-4 ml-2" />
</Button>
</Link>
)}
</div>
<div>
<h3 className="font-semibold text-success">¡Felicitaciones!</h3>
<p className="text-sm text-gray-600">
Has completado todos los ejercicios de este módulo.
</p>
</div>
</div>
</Card>
</Card>
</motion.div>
)}
</main>
</div>
);
}
export default Modulo;

View File

@@ -1,7 +1,8 @@
import { Link } from 'react-router-dom';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { ArrowRight, ArrowLeft } from 'lucide-react';
import { useProgresoStore } from '../stores/progresoStore';
import { ArrowRight, ArrowLeft, CheckCircle, Lock, Play } from 'lucide-react';
const MODULOS = [
{
@@ -9,28 +10,45 @@ const MODULOS = [
titulo: 'Fundamentos de Economía',
descripcion: 'Aprende los conceptos básicos: definición de economía, agentes económicos, factores de producción y el flujo circular de la economía.',
temas: ['Definición de economía', 'Agentes económicos', 'Factores de producción', 'Flujo circular'],
totalEjercicios: 5,
bloqueado: false,
},
{
numero: 2,
titulo: 'Oferta, Demanda y Equilibrio',
descripcion: 'Domina las curvas de oferta y demanda, aprende cómo se determinan los precios y entiende los controles de mercado.',
temas: ['Curva de demanda', 'Curva de oferta', 'Equilibrio de mercado', 'Controles de precios'],
totalEjercicios: 5,
bloqueado: false,
},
{
numero: 3,
titulo: 'Utilidad y Elasticidad',
descripcion: 'Explora la teoría del consumidor, aprende a calcular elasticidades y clasifica diferentes tipos de bienes.',
temas: ['Utilidad marginal', 'Elasticidad precio', 'Elasticidad ingreso', 'Clasificación de bienes'],
totalEjercicios: 5,
bloqueado: false,
},
{
numero: 4,
titulo: 'Teoría del Productor',
descripcion: 'Comprende los costos de producción, la toma de decisiones del productor y los fundamentos de la competencia perfecta.',
temas: ['Costos de producción', 'Producción y costos', 'Competencia perfecta', 'Maximización de beneficios'],
totalEjercicios: 5,
bloqueado: false,
},
];
export function Modulos() {
const { progresoModulos } = useProgresoStore();
const getModuloProgress = (moduloNumero: number, totalEjercicios: number) => {
const progreso = progresoModulos.find(p => p.moduloNumero === moduloNumero);
const completados = progreso?.ejercicios.filter(e => e.completado).length || 0;
const porcentaje = Math.round((completados / totalEjercicios) * 100);
return { completados, porcentaje };
};
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
@@ -52,42 +70,90 @@ export function Modulos() {
</div>
<div className="space-y-6">
{MODULOS.map((modulo) => (
<Card key={modulo.numero} className="hover:shadow-lg transition-shadow">
<div className="flex flex-col md:flex-row md:items-center gap-6">
<div className="flex items-center gap-4 md:w-32">
<div className="w-16 h-16 bg-gradient-to-br from-primary to-blue-600 rounded-2xl flex items-center justify-center text-white text-2xl font-bold shadow-lg">
{modulo.numero}
{MODULOS.map((modulo) => {
const { completados, porcentaje } = getModuloProgress(modulo.numero, modulo.totalEjercicios);
const estaCompletado = porcentaje === 100;
return (
<Card key={modulo.numero} className={`hover:shadow-lg transition-shadow ${
modulo.bloqueado ? 'opacity-75' : ''
}`}>
<div className="flex flex-col md:flex-row md:items-center gap-6">
<div className="flex items-center gap-4 md:w-32">
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center text-white text-2xl font-bold shadow-lg ${
estaCompletado
? 'bg-gradient-to-br from-success to-green-600'
: 'bg-gradient-to-br from-primary to-blue-600'
}`}>
{modulo.numero}
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-xl font-bold text-gray-900">{modulo.titulo}</h2>
{estaCompletado && (
<span className="flex items-center gap-1 text-success text-sm">
<CheckCircle className="w-4 h-4" />
Completado
</span>
)}
</div>
<p className="text-gray-600 mb-4">{modulo.descripcion}</p>
<div className="flex flex-wrap gap-2 mb-4">
{modulo.temas.map((tema) => (
<span
key={tema}
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
>
{tema}
</span>
))}
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
estaCompletado ? 'bg-success' : 'bg-primary'
}`}
style={{ width: `${porcentaje}%` }}
/>
</div>
<p className="text-sm text-gray-500 mt-1">
{completados}/{modulo.totalEjercicios} ejercicios completados ({porcentaje}%)
</p>
</div>
<div className="md:text-right">
{modulo.bloqueado ? (
<Button disabled>
<Lock className="w-4 h-4 mr-2" />
Bloqueado
</Button>
) : (
<Link to={`/modulo/${modulo.numero}`}>
<Button>
{estaCompletado ? (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Revisar
</>
) : (
<>
<Play className="w-4 h-4 mr-2" />
Entrar
</>
)}
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
)}
</div>
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-gray-900 mb-2">{modulo.titulo}</h2>
<p className="text-gray-600 mb-4">{modulo.descripcion}</p>
<div className="flex flex-wrap gap-2 mb-4">
{modulo.temas.map((tema) => (
<span
key={tema}
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
>
{tema}
</span>
))}
</div>
</div>
<div className="md:text-right">
<Link to={`/modulo/${modulo.numero}`}>
<Button>
Entrar
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
</div>
</div>
</Card>
))}
</Card>
);
})}
</div>
</main>
</div>

View File

@@ -0,0 +1,134 @@
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { FileText, Download, BookOpen, ArrowLeft } from 'lucide-react';
import { Link } from 'react-router-dom';
const recursos = [
{
id: 1,
titulo: 'Resumen Clase 1 - Fundamentos de Economía',
descripcion: 'Definición de economía, agentes económicos, factores de producción y flujo circular',
archivo: '/pdfs/resumen_clase_1.pdf',
modulo: 'Módulo 1',
icono: FileText
},
{
id: 2,
titulo: 'Resumen Clase 2 - Oferta, Demanda y Equilibrio',
descripcion: 'Ley de la demanda, ley de la oferta, equilibrio de mercado y controles de precios',
archivo: '/pdfs/resumen_clase_2.pdf',
modulo: 'Módulo 2',
icono: FileText
},
{
id: 3,
titulo: 'Resumen Clase 3 - Elasticidad',
descripcion: 'Tipos de elasticidad, cálculos y clasificación de bienes según elasticidad',
archivo: '/pdfs/resumen_clase_3.pdf',
modulo: 'Módulo 3',
icono: FileText
},
{
id: 4,
titulo: 'Resumen Clase 4 - Teoría del Productor',
descripcion: 'Costos, producción, competencia perfecta y maximización de beneficios',
archivo: '/pdfs/resumen_clase_4.pdf',
modulo: 'Módulo 4',
icono: FileText
}
];
export function RecursosPage() {
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center text-gray-600 hover:text-blue-600 mb-4"
>
<ArrowLeft size={20} className="mr-2" />
Volver al Dashboard
</Link>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Recursos de Estudio</h1>
<p className="text-gray-600">
Material académico en PDF para consultar offline
</p>
</div>
{/* Info Card */}
<Card className="mb-8 bg-blue-50 border-blue-200">
<div className="flex items-start gap-4">
<div className="p-3 bg-blue-100 rounded-lg">
<BookOpen className="w-6 h-6 text-blue-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-blue-900 mb-1">Material de Apoyo</h2>
<p className="text-blue-800 text-sm">
Estos documentos PDF contienen el contenido teórico de cada módulo.
Úsalos como referencia mientras realizas los ejercicios interactivos.
</p>
</div>
</div>
</Card>
{/* Recursos Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{recursos.map((recurso) => (
<Card key={recurso.id} className="hover:shadow-lg transition-shadow">
<div className="flex items-start gap-4">
<div className="p-3 bg-gray-100 rounded-lg">
<recurso.icono className="w-8 h-8 text-gray-600" />
</div>
<div className="flex-1">
<div className="mb-2">
<span className="inline-block px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 rounded">
{recurso.modulo}
</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{recurso.titulo}
</h3>
<p className="text-gray-600 text-sm mb-4">
{recurso.descripcion}
</p>
<a
href={recurso.archivo}
download
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline" className="w-full">
<Download size={18} className="mr-2" />
Descargar PDF
</Button>
</a>
</div>
</div>
</Card>
))}
</div>
{/* Footer */}
<div className="mt-12 text-center">
<p className="text-gray-500 text-sm">
¿Tienes dudas sobre el contenido? Revisa los ejercicios interactivos en cada módulo.
</p>
<Link to="/modulos">
<Button className="mt-4">
Ver Módulos
</Button>
</Link>
</div>
</div>
</div>
);
}
export default RecursosPage;

View File

@@ -0,0 +1,364 @@
// @ts-nocheck
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/ui/Card';
import { Button } from '../../components/ui/Button';
import { progresoService } from '../../services/api';
import type { Progreso } from '../../types';
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
// Importar contenido del módulo 1
import { introduccion, agentes, factores } from '../../content/modulo1';
import { ejercicios as modulo1Ejercicios } from '../../content/modulo1/ejercicios';
// Importar componentes de ejercicios
import { SimuladorDisyuntivas, QuizBienes, FlujoCircular } from '../../components/exercises/modulo1';
const TABS = ['Contenido', 'Ejercicios'] as const;
type Tab = typeof TABS[number];
interface EjercicioConfig {
id: string;
titulo: string;
descripcion: string;
componente: React.ReactNode;
}
export function Modulo1Page() {
const { numero } = useParams<{ numero: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
const [activeSeccion, setActiveSeccion] = useState<'introduccion' | 'agentes' | 'factores'>('introduccion');
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
const [progresos, setProgresos] = useState<Progreso[]>([]);
const [loading, setLoading] = useState(false);
// Cargar progreso al montar
useEffect(() => {
loadProgreso();
}, []);
const loadProgreso = async () => {
try {
const data = await progresoService.getProgreso();
setProgresos(data);
} catch {
// Silenciar error
}
};
const getProgresoForEjercicio = (ejercicioId: string) => {
return progresos.find(
(p) => p.modulo_numero === 1 && p.ejercicio_id === ejercicioId
);
};
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
setLoading(true);
try {
await progresoService.saveProgreso(ejercicioId, puntuacion);
await loadProgreso();
} catch {
// Silenciar error
} finally {
setLoading(false);
}
};
// Configuración de ejercicios con sus componentes
const ejerciciosConfig: EjercicioConfig[] = [
{
id: 'simulador-disyuntivas',
titulo: modulo1Ejercicios.ejercicios[0].titulo,
descripcion: modulo1Ejercicios.ejercicios[0].descripcion,
componente: (
<SimuladorDisyuntivas
ejercicioId="simulador-disyuntivas"
onComplete={(puntuacion) => handleCompleteEjercicio('simulador-disyuntivas', puntuacion)}
/>
),
},
{
id: 'quiz-clasificacion-bienes',
titulo: modulo1Ejercicios.ejercicios[1].titulo,
descripcion: modulo1Ejercicios.ejercicios[1].descripcion,
componente: (
<QuizBienes
ejercicioId="quiz-clasificacion-bienes"
onComplete={(puntuacion) => handleCompleteEjercicio('quiz-clasificacion-bienes', puntuacion)}
/>
),
},
{
id: 'juego-flujo-circular',
titulo: modulo1Ejercicios.ejercicios[2].titulo,
descripcion: modulo1Ejercicios.ejercicios[2].descripcion,
componente: (
<FlujoCircular
ejercicioId="juego-flujo-circular"
onComplete={(puntuacion) => handleCompleteEjercicio('juego-flujo-circular', puntuacion)}
/>
),
},
];
const seccionesContenido = {
introduccion: {
titulo: introduccion.titulo,
data: introduccion,
},
agentes: {
titulo: agentes.titulo,
data: agentes,
},
factores: {
titulo: factores.titulo,
data: factores,
},
};
const currentSeccion = seccionesContenido[activeSeccion];
// Calcular progreso del módulo
const ejerciciosCompletados = ejerciciosConfig.filter(
(e) => getProgresoForEjercicio(e.id)?.completado
).length;
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a Módulos
</Link>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Progreso:</span>
<div className="w-32 bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${porcentajeProgreso}%` }}
/>
</div>
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
</div>
</div>
</div>
</header>
{/* Título del módulo */}
<div className="bg-gradient-to-r from-blue-600 to-blue-800 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
1
</div>
<div>
<h1 className="text-3xl font-bold">Módulo 1: Fundamentos de Economía</h1>
<p className="text-blue-100 mt-1">
Introducción a los conceptos básicos, agentes económicos y factores de producción
</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex gap-2 border-b border-gray-200">
{TABS.map((tab) => (
<button
key={tab}
onClick={() => {
setActiveTab(tab);
setActiveEjercicio(null);
}}
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
activeTab === tab
? 'text-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
{tab}
{activeTab === tab && (
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
/>
)}
</button>
))}
</div>
{/* Contenido según tab activo */}
<AnimatePresence mode="wait">
{activeTab === 'Contenido' ? (
<motion.div
key="contenido"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
>
{/* Navegación de secciones */}
<div className="lg:col-span-1">
<Card className="sticky top-24">
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
<nav className="space-y-2">
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
<button
key={key}
onClick={() => setActiveSeccion(key)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
activeSeccion === key
? 'bg-blue-50 text-blue-700 font-medium'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
{seccionesContenido[key].titulo}
<ChevronRight className="w-4 h-4" />
</button>
))}
</nav>
</Card>
</div>
{/* Contenido de la sección */}
<div className="lg:col-span-3 space-y-6">
<Card>
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
<div className="space-y-6">
{currentSeccion.data.contenido.map((seccion, index) => (
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
<h3 className="text-lg font-semibold text-gray-800 mb-3">{seccion.titulo}</h3>
<div className="prose prose-blue max-w-none">
{seccion.contenido.split('\n\n').map((parrafo, pIndex) => (
<p key={pIndex} className="text-gray-600 mb-4 leading-relaxed whitespace-pre-line">
{parrafo}
</p>
))}
</div>
</div>
))}
</div>
</Card>
{/* Ejercicios relacionados con la sección */}
{currentSeccion.data.ejercicios && currentSeccion.data.ejercicios.length > 0 && (
<Card className="bg-blue-50 border-blue-200">
<h3 className="font-semibold text-blue-900 mb-3">Ejercicios Relacionados</h3>
<p className="text-blue-700 text-sm mb-4">
Practica lo aprendido con estos ejercicios interactivos
</p>
<Button
onClick={() => setActiveTab('Ejercicios')}
variant="outline"
className="border-blue-300 text-blue-700 hover:bg-blue-100"
>
<Play className="w-4 h-4 mr-2" />
Ir a Ejercicios
</Button>
</Card>
)}
</div>
</motion.div>
) : (
<motion.div
key="ejercicios"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6"
>
{activeEjercicio ? (
// Vista de ejercicio activo
<div className="space-y-6">
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a ejercicios
</Button>
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
</div>
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
</div>
) : (
// Lista de ejercicios
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{ejerciciosConfig.map((ejercicio, index) => {
const progreso = getProgresoForEjercicio(ejercicio.id);
const completado = progreso?.completado || false;
return (
<Card
key={ejercicio.id}
className="hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => setActiveEjercicio(ejercicio.id)}
>
<div className="flex items-start gap-4">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
completado ? 'bg-green-100 text-green-600' : 'bg-blue-100 text-blue-600'
}`}
>
{completado ? (
<CheckCircle className="w-6 h-6" />
) : (
<span className="text-xl font-bold">{index + 1}</span>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
{completado && progreso && (
<div className="mt-3 flex items-center gap-2">
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
Completado
</span>
<span className="text-xs text-gray-500">
{progreso.puntuacion} pts
</span>
</div>
)}
</div>
</div>
<Button className="w-full mt-4" size="sm">
<Play className="w-4 h-4 mr-2" />
{completado ? 'Repetir' : 'Comenzar'}
</Button>
</Card>
);
})}
</div>
)}
{/* Mensaje de completado */}
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
<Card className="mt-6 bg-green-50 border-green-200">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<Trophy className="w-6 h-6 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
<p className="text-green-700 text-sm">
Has completado todos los ejercicios de este módulo.
</p>
</div>
</div>
</Card>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
export default Modulo1Page;

View File

@@ -0,0 +1,499 @@
// @ts-nocheck
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/ui/Card';
import { Button } from '../../components/ui/Button';
import { progresoService } from '../../services/api';
import type { Progreso } from '../../types';
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
// Importar contenido del módulo 2
import { default as demandaContent } from '../../content/modulo2/demanda';
import { default as ofertaContent } from '../../content/modulo2/oferta';
import { default as equilibrioContent } from '../../content/modulo2/equilibrio';
// Importar componentes de ejercicios
import { ConstructorCurvas, SimuladorPrecios, IdentificarShocks } from '../../components/exercises/modulo2';
const TABS = ['Contenido', 'Ejercicios'] as const;
type Tab = typeof TABS[number];
interface EjercicioConfig {
id: string;
titulo: string;
descripcion: string;
componente: React.ReactNode;
}
export function Modulo2Page() {
const { numero } = useParams<{ numero: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
const [activeSeccion, setActiveSeccion] = useState<'demanda' | 'oferta' | 'equilibrio'>('demanda');
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
const [progresos, setProgresos] = useState<Progreso[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadProgreso();
}, []);
const loadProgreso = async () => {
try {
const data = await progresoService.getProgreso();
setProgresos(data);
} catch {
// Silenciar error
}
};
const getProgresoForEjercicio = (ejercicioId: string) => {
return progresos.find(
(p) => p.modulo_numero === 2 && p.ejercicio_id === ejercicioId
);
};
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
setLoading(true);
try {
await progresoService.saveProgreso(ejercicioId, puntuacion);
await loadProgreso();
} catch {
// Silenciar error
} finally {
setLoading(false);
}
};
// Configuración de ejercicios
const ejerciciosConfig: EjercicioConfig[] = [
{
id: 'constructor-curvas',
titulo: 'Constructor de Curvas de Oferta y Demanda',
descripcion: 'Construye curvas de oferta y demanda arrastrando puntos para entender sus pendientes y movimientos.',
componente: (
<ConstructorCurvas
ejercicioId="constructor-curvas"
onComplete={(puntuacion) => handleCompleteEjercicio('constructor-curvas', puntuacion)}
/>
),
},
{
id: 'simulador-precios',
titulo: 'Simulador de Precios Intervenidos',
descripcion: 'Ajusta precios máximos y mínimos para observar sus efectos en el mercado: escasez, superávit, y pérdida de bienestar.',
componente: (
<SimuladorPrecios
ejercicioId="simulador-precios"
onComplete={(puntuacion) => handleCompleteEjercicio('simulador-precios', puntuacion)}
/>
),
},
{
id: 'identificar-shocks',
titulo: 'Identificador de Shocks del Mercado',
descripcion: 'Analiza escenarios económicos reales e identifica si afectan la oferta, la demanda, ambas, y cómo cambian precio y cantidad de equilibrio.',
componente: (
<IdentificarShocks
ejercicioId="identificar-shocks"
onComplete={(puntuacion) => handleCompleteEjercicio('identificar-shocks', puntuacion)}
/>
),
},
];
// Estructura de contenido del módulo 2
const seccionesContenido = {
demanda: {
titulo: 'Ley de la Demanda',
contenido: [
{
titulo: demandaContent.definicion.titulo,
texto: demandaContent.definicion.definicion,
elementos: demandaContent.definicion.elementosClave,
},
{
titulo: demandaContent.ley.titulo,
texto: demandaContent.ley.enunciado,
efectos: demandaContent.ley.efectos,
},
{
titulo: 'Factores que Desplazan la Demanda',
texto: 'Los siguientes factores causan desplazamientos de la curva de demanda:',
factores: demandaContent.factores,
},
],
},
oferta: {
titulo: 'Ley de la Oferta',
contenido: [
{
titulo: ofertaContent.definicion.titulo,
texto: ofertaContent.definicion.definicion,
elementos: ofertaContent.definicion.elementosClave,
},
{
titulo: ofertaContent.ley.titulo,
texto: ofertaContent.ley.enunciado,
razones: ofertaContent.ley.razones,
},
{
titulo: 'Factores que Desplazan la Oferta',
texto: 'Los siguientes factores causan desplazamientos de la curva de oferta:',
factores: ofertaContent.factores,
},
],
},
equilibrio: {
titulo: 'Equilibrio de Mercado',
contenido: [
{
titulo: equilibrioContent.definicion.titulo,
texto: equilibrioContent.definicion.definicion,
caracteristicas: equilibrioContent.definicion.caracteristicas,
},
{
titulo: 'Excedentes del Mercado',
texto: 'En el equilibrio se generan beneficios para consumidores y productores:',
excedentes: [
equilibrioContent.excedentes.excedenteConsumidor,
equilibrioContent.excedentes.excedenteProductor,
],
},
{
titulo: 'Controles de Precio',
texto: 'Los gobiernos pueden intervenir el mercado estableciendo precios máximos o mínimos:',
controles: [
equilibrioContent.controles.precioMaximo,
equilibrioContent.controles.precioMinimo,
],
},
],
},
};
const currentSeccion = seccionesContenido[activeSeccion];
// Calcular progreso
const ejerciciosCompletados = ejerciciosConfig.filter(
(e) => getProgresoForEjercicio(e.id)?.completado
).length;
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a Módulos
</Link>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Progreso:</span>
<div className="w-32 bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${porcentajeProgreso}%` }}
/>
</div>
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
</div>
</div>
</div>
</header>
{/* Título del módulo */}
<div className="bg-gradient-to-r from-green-600 to-green-800 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
2
</div>
<div>
<h1 className="text-3xl font-bold">Módulo 2: Oferta, Demanda y Equilibrio</h1>
<p className="text-green-100 mt-1">
Curvas de oferta y demanda, equilibrio de mercado y controles de precio
</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex gap-2 border-b border-gray-200">
{TABS.map((tab) => (
<button
key={tab}
onClick={() => {
setActiveTab(tab);
setActiveEjercicio(null);
}}
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
activeTab === tab
? 'text-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
{tab}
{activeTab === tab && (
<motion.div
layoutId="activeTab2"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
/>
)}
</button>
))}
</div>
{/* Contenido según tab activo */}
<AnimatePresence mode="wait">
{activeTab === 'Contenido' ? (
<motion.div
key="contenido"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
>
{/* Navegación de secciones */}
<div className="lg:col-span-1">
<Card className="sticky top-24">
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
<nav className="space-y-2">
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
<button
key={key}
onClick={() => setActiveSeccion(key)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
activeSeccion === key
? 'bg-green-50 text-green-700 font-medium'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
{seccionesContenido[key].titulo}
<ChevronRight className="w-4 h-4" />
</button>
))}
</nav>
</Card>
</div>
{/* Contenido de la sección */}
<div className="lg:col-span-3 space-y-6">
<Card>
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
<div className="space-y-6">
{currentSeccion.contenido.map((item, index) => (
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
<h3 className="text-lg font-semibold text-gray-800 mb-3">{item.titulo}</h3>
<p className="text-gray-600 mb-4 leading-relaxed">{item.texto}</p>
{/* Mostrar elementos clave si existen */}
{item.elementos && (
<ul className="list-disc list-inside space-y-2 text-gray-600">
{item.elementos.map((el: any, i: number) => (
<li key={i}>
<strong>{el.elemento}:</strong> {el.descripcion}
</li>
))}
</ul>
)}
{/* Mostrar efectos/razones si existen */}
{item.efectos && (
<div className="space-y-3">
{item.efectos.map((efecto: any, i: number) => (
<div key={i} className="bg-gray-50 p-3 rounded-lg">
<h4 className="font-medium text-gray-800">{efecto.nombre}</h4>
<p className="text-sm text-gray-600">{efecto.descripcion}</p>
<p className="text-sm text-blue-600 mt-1">Ejemplo: {efecto.ejemplo}</p>
</div>
))}
</div>
)}
{/* Mostrar razones si existen */}
{item.razones && (
<div className="space-y-3">
{item.razones.map((razon: any, i: number) => (
<div key={i} className="bg-gray-50 p-3 rounded-lg">
<h4 className="font-medium text-gray-800">{razon.nombre}</h4>
<p className="text-sm text-gray-600">{razon.descripcion}</p>
<p className="text-sm text-green-600 mt-1">Ejemplo: {razon.ejemplo}</p>
</div>
))}
</div>
)}
{/* Mostrar factores si existen */}
{item.factores && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{item.factores.map((factor: any, i: number) => (
<div key={i} className="bg-blue-50 p-3 rounded-lg border border-blue-100">
<div className="flex items-center gap-2 mb-1">
<span className="text-lg">{factor.icono}</span>
<h4 className="font-medium text-blue-900">{factor.nombre}</h4>
</div>
<p className="text-sm text-blue-700">{factor.descripcion}</p>
<p className="text-sm text-blue-600 mt-1 italic">{factor.ejemplo}</p>
</div>
))}
</div>
)}
{/* Mostrar características si existen */}
{item.caracteristicas && (
<ul className="list-disc list-inside space-y-2 text-gray-600">
{item.caracteristicas.map((car: any, i: number) => (
<li key={i}>
<strong>{car.caracteristica}:</strong> {car.explicacion}
</li>
))}
</ul>
)}
{/* Mostrar excedentes si existen */}
{item.excedentes && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{item.excedentes.map((exc: any, i: number) => (
<div key={i} className="bg-purple-50 p-3 rounded-lg border border-purple-100">
<h4 className="font-medium text-purple-900">{exc.nombre}</h4>
<p className="text-sm text-purple-700">{exc.definicion}</p>
<p className="text-sm text-purple-600 mt-1">Fórmula: {exc.formula}</p>
</div>
))}
</div>
)}
{/* Mostrar controles si existen */}
{item.controles && (
<div className="space-y-3">
{item.controles.map((control: any, i: number) => (
<div key={i} className="bg-orange-50 p-3 rounded-lg border border-orange-100">
<h4 className="font-medium text-orange-900">{control.nombre}</h4>
<p className="text-sm text-orange-700">{control.definicion}</p>
<p className="text-sm text-orange-600 mt-1">
Condición: {control.condicionEfectivo}
</p>
</div>
))}
</div>
)}
</div>
))}
</div>
</Card>
<Card className="bg-green-50 border-green-200">
<h3 className="font-semibold text-green-900 mb-3">Ejercicios Relacionados</h3>
<p className="text-green-700 text-sm mb-4">
Pon a prueba tus conocimientos con ejercicios interactivos sobre oferta, demanda y equilibrio
</p>
<Button
onClick={() => setActiveTab('Ejercicios')}
variant="outline"
className="border-green-300 text-green-700 hover:bg-green-100"
>
<Play className="w-4 h-4 mr-2" />
Ir a Ejercicios
</Button>
</Card>
</div>
</motion.div>
) : (
<motion.div
key="ejercicios"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6"
>
{activeEjercicio ? (
<div className="space-y-6">
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a ejercicios
</Button>
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
</div>
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{ejerciciosConfig.map((ejercicio, index) => {
const progreso = getProgresoForEjercicio(ejercicio.id);
const completado = progreso?.completado || false;
return (
<Card
key={ejercicio.id}
className="hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => setActiveEjercicio(ejercicio.id)}
>
<div className="flex items-start gap-4">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
completado ? 'bg-green-100 text-green-600' : 'bg-green-100 text-green-600'
}`}
>
{completado ? (
<CheckCircle className="w-6 h-6" />
) : (
<span className="text-xl font-bold">{index + 1}</span>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
{completado && progreso && (
<div className="mt-3 flex items-center gap-2">
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
Completado
</span>
<span className="text-xs text-gray-500">
{progreso.puntuacion} pts
</span>
</div>
)}
</div>
</div>
<Button className="w-full mt-4" size="sm">
<Play className="w-4 h-4 mr-2" />
{completado ? 'Repetir' : 'Comenzar'}
</Button>
</Card>
);
})}
</div>
)}
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
<Card className="mt-6 bg-green-50 border-green-200">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<Trophy className="w-6 h-6 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
<p className="text-green-700 text-sm">
Has completado todos los ejercicios de este módulo.
</p>
</div>
</div>
</Card>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
export default Modulo2Page;

View File

@@ -0,0 +1,610 @@
// @ts-nocheck
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/ui/Card';
import { Button } from '../../components/ui/Button';
import { progresoService } from '../../services/api';
import type { Progreso } from '../../types';
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
// Importar contenido del módulo 3
import { conceptosElasticidad } from '../../content/modulo3/conceptos';
import { tiposElasticidad } from '../../content/modulo3/tipos';
import { clasificacionBienes } from '../../content/modulo3/clasificacion';
import { ejercicios as modulo3Ejercicios } from '../../content/modulo3/ejercicios';
// Importar componentes de ejercicios
import { CalculadoraElasticidad, ClasificadorBienes, EjerciciosExamen } from '../../components/exercises/modulo3';
const TABS = ['Contenido', 'Ejercicios'] as const;
type Tab = typeof TABS[number];
interface EjercicioConfig {
id: string;
titulo: string;
descripcion: string;
componente: React.ReactNode;
}
interface ContenidoItem {
titulo: string;
texto: string;
interpretacion?: string;
formula?: { latex?: string; ecuacion?: string } | string;
interpretaciones?: Array<{
rango: string;
clasificacion: string;
significado?: string;
descripcion?: string;
ejemplo?: string;
}>;
factores?: Array<{
factor?: string;
nombre?: string;
efecto?: string;
descripcion?: string;
explicacion?: string;
}>;
reglas?: Array<{
elasticidad: string;
efectoPrecioArriba: string;
efectoPrecioAbajo: string;
}>;
clasificacion?: Array<{
tipo: string;
descripcion: string;
condicion: string;
subtipos?: Array<{ tipo: string; rango: string }>;
}>;
categorias?: Array<{
tipo: string;
descripcion: string;
condicion: string;
ejemplos?: string[];
}>;
matriz?: Array<{
combinacion: string;
ejemplo: string;
caracteristicas: string;
}>;
determinantes?: string[];
}
export function Modulo3Page() {
const { numero } = useParams<{ numero: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
const [activeSeccion, setActiveSeccion] = useState<'conceptos' | 'tipos' | 'clasificacion'>('conceptos');
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
const [progresos, setProgresos] = useState<Progreso[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadProgreso();
}, []);
const loadProgreso = async () => {
try {
const data = await progresoService.getProgreso();
setProgresos(data);
} catch {
// Silenciar error
}
};
const getProgresoForEjercicio = (ejercicioId: string) => {
return progresos.find(
(p) => p.modulo_numero === 3 && p.ejercicio_id === ejercicioId
);
};
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
setLoading(true);
try {
await progresoService.saveProgreso(ejercicioId, puntuacion);
await loadProgreso();
} catch {
// Silenciar error
} finally {
setLoading(false);
}
};
// Configuración de ejercicios
const ejerciciosConfig: EjercicioConfig[] = [
{
id: 'calculadora-elasticidad',
titulo: 'Calculadora de Elasticidad',
descripcion: 'Calcula paso a paso la elasticidad precio, ingreso y cruzada con ejemplos prácticos.',
componente: (
<CalculadoraElasticidad
ejercicioId="calculadora-elasticidad"
onComplete={(puntuacion) => handleCompleteEjercicio('calculadora-elasticidad', puntuacion)}
/>
),
},
{
id: 'clasificador-bienes',
titulo: 'Clasificador de Bienes',
descripcion: 'Clasifica bienes según su elasticidad ingreso y cruzada. Identifica normales, inferiores, lujos, sustitutos y complementos.',
componente: (
<ClasificadorBienes
ejercicioId="clasificador-bienes"
onComplete={(puntuacion) => handleCompleteEjercicio('clasificador-bienes', puntuacion)}
/>
),
},
{
id: 'ejercicios-examen',
titulo: 'Ejercicios Tipo Examen',
descripcion: 'Resuelve problemas integradores de elasticidad con dificultad de examen.',
componente: (
<EjerciciosExamen
ejercicioId="ejercicios-examen"
onComplete={(puntuacion) => handleCompleteEjercicio('ejercicios-examen', puntuacion)}
/>
),
},
];
// Estructura de contenido del módulo 3
const seccionesContenido: {
conceptos: { titulo: string; contenido: ContenidoItem[] };
tipos: { titulo: string; contenido: ContenidoItem[] };
clasificacion: { titulo: string; contenido: ContenidoItem[] };
} = {
conceptos: {
titulo: 'Conceptos Fundamentales',
contenido: [
{
titulo: conceptosElasticidad.definicionElasticidad.titulo,
texto: conceptosElasticidad.definicionElasticidad.definicion,
interpretacion: conceptosElasticidad.definicionElasticidad.interpretacionIntuitiva,
formula: conceptosElasticidad.definicionElasticidad.formulaGeneral,
},
{
titulo: conceptosElasticidad.elasticidadPrecioDemanda.titulo,
texto: conceptosElasticidad.elasticidadPrecioDemanda.definicion,
interpretaciones: conceptosElasticidad.elasticidadPrecioDemanda.interpretacion,
},
{
titulo: conceptosElasticidad.determinantesElasticidad.titulo,
texto: 'Los siguientes factores determinan la elasticidad de un bien:',
factores: conceptosElasticidad.determinantesElasticidad.factores,
},
{
titulo: conceptosElasticidad.relacionIngresoTotal.titulo,
texto: conceptosElasticidad.relacionIngresoTotal.definicion,
reglas: conceptosElasticidad.relacionIngresoTotal.reglas,
},
],
},
tipos: {
titulo: 'Tipos de Elasticidad',
contenido: [
{
titulo: 'Elasticidad Precio de la Demanda (Ed)',
texto: tiposElasticidad.tipos[0].descripcion,
formula: tiposElasticidad.tipos[0].formula,
determinantes: tiposElasticidad.tipos[0].determinantes,
},
{
titulo: 'Elasticidad Ingreso de la Demanda (Ei)',
texto: tiposElasticidad.tipos[1].descripcion,
formula: tiposElasticidad.tipos[1].formula,
clasificacion: tiposElasticidad.tipos[1].clasificacionBienes,
},
{
titulo: 'Elasticidad Cruzada (Exy)',
texto: tiposElasticidad.tipos[2].descripcion,
formula: tiposElasticidad.tipos[2].formula,
categorias: tiposElasticidad.tipos[2].clasificacionBienes,
},
{
titulo: 'Elasticidad Precio de la Oferta (Es)',
texto: tiposElasticidad.tipos[3].descripcion,
formula: tiposElasticidad.tipos[3].formula,
interpretacion: tiposElasticidad.tipos[3].interpretacion,
},
],
},
clasificacion: {
titulo: 'Clasificación de Bienes',
contenido: [
{
titulo: 'Según Elasticidad Ingreso',
texto: clasificacionBienes.clasificacionPorIngreso.descripcion,
formula: clasificacionBienes.clasificacionPorIngreso.formulaReferencia,
categorias: clasificacionBienes.clasificacionPorIngreso.categorias,
},
{
titulo: 'Según Elasticidad Cruzada',
texto: clasificacionBienes.clasificacionPorElasticidadCruzada.descripcion,
formula: clasificacionBienes.clasificacionPorElasticidadCruzada.formulaReferencia,
categorias: clasificacionBienes.clasificacionPorElasticidadCruzada.categorias,
},
{
titulo: 'Matriz de Clasificación Completa',
texto: clasificacionBienes.matrizClasificacionCompleta.descripcion,
matriz: clasificacionBienes.matrizClasificacionCompleta.matriz,
},
],
},
};
const currentSeccion = seccionesContenido[activeSeccion];
// Calcular progreso
const ejerciciosCompletados = ejerciciosConfig.filter(
(e) => getProgresoForEjercicio(e.id)?.completado
).length;
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a Módulos
</Link>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Progreso:</span>
<div className="w-32 bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${porcentajeProgreso}%` }}
/>
</div>
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
</div>
</div>
</div>
</header>
{/* Título del módulo */}
<div className="bg-gradient-to-r from-purple-600 to-purple-800 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
3
</div>
<div>
<h1 className="text-3xl font-bold">Módulo 3: Elasticidad</h1>
<p className="text-purple-100 mt-1">
Tipos de elasticidad, clasificación de bienes y análisis de sensibilidad
</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex gap-2 border-b border-gray-200">
{TABS.map((tab) => (
<button
key={tab}
onClick={() => {
setActiveTab(tab);
setActiveEjercicio(null);
}}
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
activeTab === tab
? 'text-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
{tab}
{activeTab === tab && (
<motion.div
layoutId="activeTab3"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
/>
)}
</button>
))}
</div>
{/* Contenido según tab activo */}
<AnimatePresence mode="wait">
{activeTab === 'Contenido' ? (
<motion.div
key="contenido"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
>
{/* Navegación de secciones */}
<div className="lg:col-span-1">
<Card className="sticky top-24">
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
<nav className="space-y-2">
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
<button
key={key}
onClick={() => setActiveSeccion(key)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
activeSeccion === key
? 'bg-purple-50 text-purple-700 font-medium'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
{seccionesContenido[key].titulo}
<ChevronRight className="w-4 h-4" />
</button>
))}
</nav>
</Card>
</div>
{/* Contenido de la sección */}
<div className="lg:col-span-3 space-y-6">
<Card>
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
<div className="space-y-6">
{currentSeccion.contenido.map((item, index) => (
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
<h3 className="text-lg font-semibold text-gray-800 mb-3">{item.titulo}</h3>
<p className="text-gray-600 mb-4 leading-relaxed">{item.texto}</p>
{/* Mostrar interpretación si existe */}
{item.interpretacion && (
<div className="bg-blue-50 p-4 rounded-lg mb-4">
<p className="text-blue-800 text-sm">{item.interpretacion}</p>
</div>
)}
{/* Mostrar fórmula si existe */}
{item.formula && (
<div className="bg-gray-100 p-4 rounded-lg mb-4">
<pre className="text-sm text-gray-800 overflow-x-auto">
{item.formula.latex || item.formula.ecuacion || item.formula}
</pre>
</div>
)}
{/* Mostrar interpretaciones de elasticidad */}
{item.interpretaciones && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{item.interpretaciones.map((interp: any, i: number) => (
<div key={i} className={`p-3 rounded-lg border ${
interp.rango === '|Ed| > 1' || interp.rango === 'Es > 1'
? 'bg-green-50 border-green-200'
: interp.rango === '|Ed| < 1' || interp.rango === 'Es < 1'
? 'bg-yellow-50 border-yellow-200'
: 'bg-blue-50 border-blue-200'
}`}>
<h4 className="font-medium text-gray-800">{interp.clasificacion}</h4>
<p className="text-sm text-gray-600">{interp.significado || interp.descripcion}</p>
{interp.ejemplo && (
<p className="text-sm text-gray-500 mt-1">Ejemplo: {interp.ejemplo}</p>
)}
</div>
))}
</div>
)}
{/* Mostrar factores determinantes */}
{item.factores && (
<div className="space-y-2">
{item.factores.map((factor: any, i: number) => (
<div key={i} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<h4 className="font-medium text-gray-800">{factor.factor || factor.nombre}</h4>
<p className="text-sm text-gray-600">{factor.efecto || factor.descripcion}</p>
{factor.explicacion && (
<p className="text-sm text-gray-500 mt-1">{factor.explicacion}</p>
)}
</div>
</div>
))}
</div>
)}
{/* Mostrar reglas de ingreso total */}
{item.reglas && (
<div className="space-y-3">
{item.reglas.map((regla: any, i: number) => (
<div key={i} className="bg-purple-50 p-3 rounded-lg border border-purple-200">
<h4 className="font-medium text-purple-900">{regla.elasticidad}</h4>
<div className="text-sm text-purple-700 mt-1 space-y-1">
<p>Precio : {regla.efectoPrecioArriba}</p>
<p>Precio : {regla.efectoPrecioAbajo}</p>
</div>
</div>
))}
</div>
)}
{/* Mostrar clasificación de bienes por ingreso */}
{item.clasificacion && item.titulo?.includes('Ingreso') && (
<div className="space-y-3">
{item.clasificacion.map((cat: any, i: number) => (
<div key={i} className={`p-3 rounded-lg border ${
cat.condicion?.includes('> 0') && !cat.condicion?.includes('<')
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}>
<h4 className="font-medium text-gray-800">{cat.tipo}</h4>
<p className="text-sm text-gray-600">{cat.descripcion}</p>
<p className="text-sm font-medium mt-1">Condición: {cat.condicion}</p>
{cat.subtipos && (
<div className="mt-2 space-y-1">
{cat.subtipos.map((sub: any, j: number) => (
<div key={j} className="text-sm text-gray-600 pl-3 border-l-2 border-gray-300">
<strong>{sub.tipo}:</strong> {sub.rango}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
{/* Mostrar clasificación por elasticidad cruzada */}
{item.categorias && item.titulo?.includes('Cruzada') && (
<div className="space-y-3">
{item.categorias.map((cat: any, i: number) => (
<div key={i} className={`p-3 rounded-lg border ${
cat.condicion?.includes('> 0')
? 'bg-green-50 border-green-200'
: cat.condicion?.includes('< 0')
? 'bg-red-50 border-red-200'
: 'bg-gray-50 border-gray-200'
}`}>
<h4 className="font-medium text-gray-800">{cat.tipo}</h4>
<p className="text-sm text-gray-600">{cat.descripcion}</p>
<p className="text-sm font-medium mt-1">Exy {cat.condicion}</p>
{cat.ejemplos && (
<p className="text-sm text-gray-500 mt-1">
Ejemplos: {cat.ejemplos.slice(0, 3).join(', ')}
</p>
)}
</div>
))}
</div>
)}
{/* Mostrar matriz de clasificación */}
{item.matriz && (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-medium text-gray-700">Combinación</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Ejemplo</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Características</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{item.matriz.map((fila: any, i: number) => (
<tr key={i}>
<td className="px-3 py-2 font-medium text-gray-800">{fila.combinacion}</td>
<td className="px-3 py-2 text-gray-600">{fila.ejemplo}</td>
<td className="px-3 py-2 text-gray-600">{fila.caracteristicas}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
))}
</div>
</Card>
<Card className="bg-purple-50 border-purple-200">
<h3 className="font-semibold text-purple-900 mb-3">Ejercicios Relacionados</h3>
<p className="text-purple-700 text-sm mb-4">
Practica el cálculo y clasificación de elasticidad con ejercicios interactivos
</p>
<Button
onClick={() => setActiveTab('Ejercicios')}
variant="outline"
className="border-purple-300 text-purple-700 hover:bg-purple-100"
>
<Play className="w-4 h-4 mr-2" />
Ir a Ejercicios
</Button>
</Card>
</div>
</motion.div>
) : (
<motion.div
key="ejercicios"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6"
>
{activeEjercicio ? (
<div className="space-y-6">
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a ejercicios
</Button>
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
</div>
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{ejerciciosConfig.map((ejercicio, index) => {
const progreso = getProgresoForEjercicio(ejercicio.id);
const completado = progreso?.completado || false;
return (
<Card
key={ejercicio.id}
className="hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => setActiveEjercicio(ejercicio.id)}
>
<div className="flex items-start gap-4">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
completado ? 'bg-green-100 text-green-600' : 'bg-purple-100 text-purple-600'
}`}
>
{completado ? (
<CheckCircle className="w-6 h-6" />
) : (
<span className="text-xl font-bold">{index + 1}</span>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
{completado && progreso && (
<div className="mt-3 flex items-center gap-2">
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
Completado
</span>
<span className="text-xs text-gray-500">
{progreso.puntuacion} pts
</span>
</div>
)}
</div>
</div>
<Button className="w-full mt-4" size="sm">
<Play className="w-4 h-4 mr-2" />
{completado ? 'Repetir' : 'Comenzar'}
</Button>
</Card>
);
})}
</div>
)}
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
<Card className="mt-6 bg-green-50 border-green-200">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<Trophy className="w-6 h-6 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
<p className="text-green-700 text-sm">
Has completado todos los ejercicios de este módulo.
</p>
</div>
</div>
</Card>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
export default Modulo3Page;

View File

@@ -0,0 +1,423 @@
// @ts-nocheck
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../components/ui/Card';
import { Button } from '../../components/ui/Button';
import { progresoService } from '../../services/api';
import type { Progreso } from '../../types';
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
// Importar contenido del módulo 4
import { produccion } from '../../content/modulo4/produccion';
import { costos } from '../../content/modulo4/costos';
import { mercado } from '../../content/modulo4/mercado';
// Importar componentes de ejercicios
import { CalculadoraCostos, SimuladorProduccion, VisualizadorExcedentes } from '../../components/exercises/modulo4';
const TABS = ['Contenido', 'Ejercicios'] as const;
type Tab = typeof TABS[number];
interface EjercicioConfig {
id: string;
titulo: string;
descripcion: string;
componente: React.ReactNode;
}
export function Modulo4Page() {
const { numero: _numero } = useParams<{ numero: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
const [activeSeccion, setActiveSeccion] = useState<'produccion' | 'costos' | 'mercado'>('produccion');
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
const [progresos, setProgresos] = useState<Progreso[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadProgreso();
}, []);
const loadProgreso = async () => {
try {
const data = await progresoService.getProgreso();
setProgresos(data);
} catch {
// Silenciar error
}
};
const getProgresoForEjercicio = (ejercicioId: string) => {
return progresos.find(
(p) => p.modulo_numero === 4 && p.ejercicio_id === ejercicioId
);
};
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
setLoading(true);
try {
await progresoService.saveProgreso(ejercicioId, puntuacion);
await loadProgreso();
} catch {
// Silenciar error
} finally {
setLoading(false);
}
};
// Configuración de ejercicios
const ejerciciosConfig: EjercicioConfig[] = [
{
id: 'calculadora-costos',
titulo: 'Calculadora de Costos',
descripcion: 'Ingresa CF, CV para cada nivel de producción y calcula automáticamente todos los costos medios y marginales.',
componente: (
<CalculadoraCostos
ejercicioId="calculadora-costos"
onComplete={() => handleCompleteEjercicio('calculadora-costos', 100)}
/>
),
},
{
id: 'simulador-produccion',
titulo: 'Simulador de Producción',
descripcion: 'Dado un precio de mercado y curva de costos, encuentra la cantidad óptima y determina si debes producir o cerrar.',
componente: (
<SimuladorProduccion
ejercicioId="simulador-produccion"
onComplete={() => handleCompleteEjercicio('simulador-produccion', 100)}
/>
),
},
{
id: 'visualizador-excedentes',
titulo: 'Visualizador de Excedentes',
descripcion: 'Interactúa con el gráfico para ver cómo cambia el excedente del productor al variar el precio y la cantidad.',
componente: (
<VisualizadorExcedentes
ejercicioId="visualizador-excedentes"
onComplete={() => handleCompleteEjercicio('visualizador-excedentes', 100)}
/>
),
},
];
// Estructura de contenido del módulo 4
const seccionesContenido = {
produccion: {
titulo: produccion.titulo,
contenido: produccion.contenido,
},
costos: {
titulo: costos.titulo,
contenido: costos.contenido,
},
mercado: {
titulo: mercado.titulo,
contenido: mercado.contenido,
},
};
const currentSeccion = seccionesContenido[activeSeccion];
// Calcular progreso
const ejerciciosCompletados = ejerciciosConfig.filter(
(e) => getProgresoForEjercicio(e.id)?.completado
).length;
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
// Función auxiliar para renderizar contenido markdown-like
const renderContenido = (texto: string) => {
const partes = texto.split(/(\*\*.*?\*\*|\$.*?\$|`.*?`|\n\n)/);
return partes.map((parte, index) => {
if (parte.startsWith('**') && parte.endsWith('**')) {
return <strong key={index}>{parte.slice(2, -2)}</strong>;
}
if (parte.startsWith('$') && parte.endsWith('$')) {
return <code key={index} className="bg-gray-100 px-1 rounded">{parte.slice(1, -1)}</code>;
}
if (parte.startsWith('`') && parte.endsWith('`')) {
return <code key={index} className="bg-gray-100 px-1 rounded text-red-600">{parte.slice(1, -1)}</code>;
}
if (parte === '\n\n') {
return <br key={index} />;
}
return <span key={index}>{parte}</span>;
});
};
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a Módulos
</Link>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Progreso:</span>
<div className="w-32 bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${porcentajeProgreso}%` }}
/>
</div>
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
</div>
</div>
</div>
</header>
{/* Título del módulo */}
<div className="bg-gradient-to-r from-orange-600 to-orange-800 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
4
</div>
<div>
<h1 className="text-3xl font-bold">Módulo 4: Teoría del Productor</h1>
<p className="text-orange-100 mt-1">
Producción, costos y competencia perfecta
</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex gap-2 border-b border-gray-200">
{TABS.map((tab) => (
<button
key={tab}
onClick={() => {
setActiveTab(tab);
setActiveEjercicio(null);
}}
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
activeTab === tab
? 'text-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
{tab}
{activeTab === tab && (
<motion.div
layoutId="activeTab4"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
/>
)}
</button>
))}
</div>
{/* Contenido según tab activo */}
<AnimatePresence mode="wait">
{activeTab === 'Contenido' ? (
<motion.div
key="contenido"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
>
{/* Navegación de secciones */}
<div className="lg:col-span-1">
<Card className="sticky top-24">
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
<nav className="space-y-2">
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
<button
key={key}
onClick={() => setActiveSeccion(key)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
activeSeccion === key
? 'bg-orange-50 text-orange-700 font-medium'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
{seccionesContenido[key].titulo}
<ChevronRight className="w-4 h-4" />
</button>
))}
</nav>
</Card>
</div>
{/* Contenido de la sección */}
<div className="lg:col-span-3 space-y-6">
<Card>
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
<div className="space-y-6">
{currentSeccion.contenido.map((seccion, index) => (
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
<h3 className="text-lg font-semibold text-gray-800 mb-3">{seccion.titulo}</h3>
<div className="prose prose-orange max-w-none">
{seccion.contenido.split('\n\n').map((parrafo, pIndex) => {
// Detectar si es una tabla
if (parrafo.includes('|') && parrafo.includes('---')) {
const lineas = parrafo.split('\n').filter(l => l.trim() && !l.includes('---'));
if (lineas.length >= 2) {
return (
<div key={pIndex} className="overflow-x-auto my-4">
<table className="min-w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-50">
{lineas[0].split('|').filter(Boolean).map((cell, cIndex) => (
<th key={cIndex} className="px-3 py-2 text-left font-medium border">
{cell.trim()}
</th>
))}
</tr>
</thead>
<tbody>
{lineas.slice(1).map((linea, lIndex) => (
<tr key={lIndex} className="border-b">
{linea.split('|').filter(Boolean).map((cell, cIndex) => (
<td key={cIndex} className="px-3 py-2 border">
{cell.trim()}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
}
// Detectar si es código
if (parrafo.startsWith('```')) {
return (
<pre key={pIndex} className="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto my-4">
<code>{parrafo.replace(/```/g, '').trim()}</code>
</pre>
);
}
return (
<p key={pIndex} className="text-gray-600 mb-4 leading-relaxed whitespace-pre-line">
{renderContenido(parrafo)}
</p>
);
})}
</div>
</div>
))}
</div>
</Card>
<Card className="bg-orange-50 border-orange-200">
<h3 className="font-semibold text-orange-900 mb-3">Ejercicios Relacionados</h3>
<p className="text-orange-700 text-sm mb-4">
Practica con simuladores interactivos de costos, producción y excedentes
</p>
<Button
onClick={() => setActiveTab('Ejercicios')}
variant="outline"
className="border-orange-300 text-orange-700 hover:bg-orange-100"
>
<Play className="w-4 h-4 mr-2" />
Ir a Ejercicios
</Button>
</Card>
</div>
</motion.div>
) : (
<motion.div
key="ejercicios"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-6"
>
{activeEjercicio ? (
<div className="space-y-6">
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a ejercicios
</Button>
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
</div>
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{ejerciciosConfig.map((ejercicio, index) => {
const progreso = getProgresoForEjercicio(ejercicio.id);
const completado = progreso?.completado || false;
return (
<Card
key={ejercicio.id}
className="hover:shadow-lg transition-shadow cursor-pointer"
onClick={() => setActiveEjercicio(ejercicio.id)}
>
<div className="flex items-start gap-4">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
completado ? 'bg-green-100 text-green-600' : 'bg-orange-100 text-orange-600'
}`}
>
{completado ? (
<CheckCircle className="w-6 h-6" />
) : (
<span className="text-xl font-bold">{index + 1}</span>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
{completado && progreso && (
<div className="mt-3 flex items-center gap-2">
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
Completado
</span>
<span className="text-xs text-gray-500">
{progreso.puntuacion} pts
</span>
</div>
)}
</div>
</div>
<Button className="w-full mt-4" size="sm">
<Play className="w-4 h-4 mr-2" />
{completado ? 'Repetir' : 'Comenzar'}
</Button>
</Card>
);
})}
</div>
)}
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
<Card className="mt-6 bg-green-50 border-green-200">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<Trophy className="w-6 h-6 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
<p className="text-green-700 text-sm">
Has completado todos los ejercicios de este módulo.
</p>
</div>
</div>
</Card>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
export default Modulo4Page;

View File

@@ -0,0 +1,4 @@
export { Modulo1Page } from './Modulo1Page';
export { Modulo2Page } from './Modulo2Page';
export { Modulo3Page } from './Modulo3Page';
export { Modulo4Page } from './Modulo4Page';

View File

@@ -1,5 +1,5 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import type { LoginRequest, LoginResponse, Usuario, Progreso, Modulo } from '../types';
import type { LoginRequest, LoginResponse, Usuario, Progreso, Modulo, ProgresoModulo, Badge, ResumenProgreso } from '../types';
const API_BASE_URL = '/api';
@@ -96,8 +96,8 @@ export const progresoService = {
return response.data;
},
async saveProgreso(progreso: Progreso): Promise<Progreso> {
const response = await api.post<Progreso>('/progreso', progreso);
async saveProgreso(data: { modulo_numero: number; ejercicio_id: string; puntuacion: number; completado?: boolean }): Promise<Progreso> {
const response = await api.post<Progreso>('/progreso', data);
return response.data;
},
@@ -105,16 +105,36 @@ export const progresoService = {
const response = await api.get<Progreso[]>(`/admin/usuarios/${userId}/progreso`);
return response.data;
},
async getResumen(): Promise<ResumenProgreso> {
const response = await api.get<ResumenProgreso>('/progreso/resumen');
return response.data;
},
async getResumenProgreso(): Promise<ResumenProgreso> {
const response = await api.get<ResumenProgreso>('/progreso/resumen');
return response.data;
},
async getProgresoModulos(): Promise<ProgresoModulo[]> {
const response = await api.get<ProgresoModulo[]>('/progreso/modulos');
return response.data;
},
async getBadges(): Promise<Badge[]> {
const response = await api.get<Badge[]>('/progreso/badges');
return response.data;
},
};
export const moduloService = {
async getModulos(): Promise<Modulo[]> {
const response = await api.get<Modulo[]>('/modulos');
const response = await api.get<Modulo[]>('/contenido/modulos');
return response.data;
},
async getModulo(numero: number): Promise<Modulo> {
const response = await api.get<Modulo>(`/modulos/${numero}`);
const response = await api.get<Modulo>(`/contenido/modulos/${numero}`);
return response.data;
},
};

View File

@@ -66,7 +66,10 @@ export const useAuthStore = create<AuthState>()(
}),
{
name: 'auth-storage',
partialize: (state) => ({ isAuthenticated: state.isAuthenticated }),
partialize: (state) => ({
isAuthenticated: state.isAuthenticated,
usuario: state.usuario
}),
}
)
);

View File

@@ -0,0 +1,134 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { ProgresoEjercicio, ProgresoModulo, Badge, NivelUsuario } from '../types';
import { progresoService } from '../services/api';
interface ProgresoState {
progresoModulos: ProgresoModulo[];
badges: Badge[];
totalPuntuacion: number;
nivel: NivelUsuario;
isLoading: boolean;
error: string | null;
// Actions
loadProgreso: () => Promise<void>;
saveProgreso: (ejercicioId: string, puntuacion: number, moduloNumero: number) => Promise<void>;
getProgresoModulo: (moduloNumero: number) => ProgresoModulo | undefined;
getEjercicioCompletado: (ejercicioId: string, moduloNumero: number) => boolean;
getPuntuacionEjercicio: (ejercicioId: string, moduloNumero: number) => number;
getPorcentajeModulo: (moduloNumero: number, totalEjercicios: number) => number;
clearError: () => void;
}
export const useProgresoStore = create<ProgresoState>()(
persist(
(set, get) => ({
progresoModulos: [],
badges: [],
totalPuntuacion: 0,
nivel: 'Novato',
isLoading: false,
error: null,
loadProgreso: async () => {
set({ isLoading: true, error: null });
try {
const resumen = await progresoService.getResumen();
set({
progresoModulos: resumen.moduloProgreso,
badges: resumen.badges,
totalPuntuacion: resumen.totalPuntuacion,
nivel: resumen.nivel,
isLoading: false,
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Error al cargar progreso';
set({ error: message, isLoading: false });
}
},
saveProgreso: async (ejercicioId: string, puntuacion: number, moduloNumero: number) => {
set({ isLoading: true, error: null });
try {
await progresoService.saveProgreso({
modulo_numero: moduloNumero,
ejercicio_id: ejercicioId,
puntuacion,
completado: true,
});
// Update local state
const { progresoModulos } = get();
const moduloIndex = progresoModulos.findIndex(m => m.moduloNumero === moduloNumero);
if (moduloIndex >= 0) {
const nuevoEjercicio: ProgresoEjercicio = {
ejercicioId,
completado: true,
puntuacion,
fecha: new Date().toISOString(),
};
const moduloActualizado = { ...progresoModulos[moduloIndex] };
const ejercicioExistente = moduloActualizado.ejercicios.findIndex(
e => e.ejercicioId === ejercicioId
);
if (ejercicioExistente >= 0) {
moduloActualizado.ejercicios[ejercicioExistente] = nuevoEjercicio;
} else {
moduloActualizado.ejercicios.push(nuevoEjercicio);
}
const nuevosProgresos = [...progresoModulos];
nuevosProgresos[moduloIndex] = moduloActualizado;
set({ progresoModulos: nuevosProgresos, isLoading: false });
}
// Reload full progress to get updated badges and level
await get().loadProgreso();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Error al guardar progreso';
set({ error: message, isLoading: false });
}
},
getProgresoModulo: (moduloNumero: number) => {
return get().progresoModulos.find(m => m.moduloNumero === moduloNumero);
},
getEjercicioCompletado: (ejercicioId: string, moduloNumero: number) => {
const modulo = get().getProgresoModulo(moduloNumero);
if (!modulo) return false;
const ejercicio = modulo.ejercicios.find(e => e.ejercicioId === ejercicioId);
return ejercicio?.completado || false;
},
getPuntuacionEjercicio: (ejercicioId: string, moduloNumero: number) => {
const modulo = get().getProgresoModulo(moduloNumero);
if (!modulo) return 0;
const ejercicio = modulo.ejercicios.find(e => e.ejercicioId === ejercicioId);
return ejercicio?.puntuacion || 0;
},
getPorcentajeModulo: (moduloNumero: number, totalEjercicios: number) => {
const modulo = get().getProgresoModulo(moduloNumero);
if (!modulo || totalEjercicios === 0) return 0;
const completados = modulo.ejercicios.filter(e => e.completado).length;
return Math.round((completados / totalEjercicios) * 100);
},
clearError: () => set({ error: null }),
}),
{
name: 'progreso-storage',
partialize: (state) => ({
progresoModulos: state.progresoModulos,
totalPuntuacion: state.totalPuntuacion,
nivel: state.nivel,
}),
}
)
);

View File

@@ -0,0 +1,266 @@
import { create } from 'zustand';
import { progresoService } from '../services/api';
import type { Progreso, Badge, NivelUsuario } from '../types';
export interface EjercicioProgreso {
ejercicioId: string;
completado: boolean;
puntuacion: number;
fechaCompletado?: string;
intentos: number;
}
export interface ModuloProgreso {
moduloId: string;
ejercicios: Record<string, EjercicioProgreso>;
completado: boolean;
}
interface ProgressState {
modulos: Record<string, ModuloProgreso>;
puntuacionTotal: number;
badges: Badge[];
nivel: NivelUsuario;
isLoading: boolean;
error: string | null;
// Acciones
loadProgreso: () => Promise<void>;
saveProgreso: (moduloId: string, ejercicioId: string, puntuacion: number) => Promise<void>;
getProgresoEjercicio: (moduloId: string, ejercicioId: string) => EjercicioProgreso | undefined;
getProgresoModulo: (moduloId: string) => ModuloProgreso | undefined;
resetProgreso: (moduloId?: string) => void;
calcularPorcentajeModulo: (moduloId: string, totalEjercicios: number) => number;
getBadgesDesbloqueados: () => Badge[];
getBadgesBloqueados: () => Badge[];
getProgreso: (moduloId: string, ejercicioId: string) => EjercicioProgreso | undefined;
}
function calcularNivel(puntuacion: number): NivelUsuario {
if (puntuacion >= 2000) return 'Maestro';
if (puntuacion >= 1000) return 'Experto';
if (puntuacion >= 300) return 'Aprendiz';
return 'Novato';
}
function transformarProgresoAPI(progresoAPI: Progreso[]): Record<string, ModuloProgreso> {
const modulos: Record<string, ModuloProgreso> = {};
progresoAPI.forEach((p) => {
const moduloId = `modulo${p.modulo_numero}`;
if (!modulos[moduloId]) {
modulos[moduloId] = {
moduloId,
ejercicios: {},
completado: false,
};
}
modulos[moduloId].ejercicios[p.ejercicio_id] = {
ejercicioId: p.ejercicio_id,
completado: p.completado,
puntuacion: p.puntuacion,
fechaCompletado: new Date().toISOString(),
intentos: 1,
};
});
return modulos;
}
export const useProgressStore = create<ProgressState>()(
(set, get) => ({
modulos: {},
puntuacionTotal: 0,
badges: [],
nivel: 'Novato',
isLoading: false,
error: null,
loadProgreso: async () => {
set({ isLoading: true, error: null });
try {
const [progresoData, resumenData] = await Promise.all([
progresoService.getProgreso(),
progresoService.getResumenProgreso(),
]);
const modulos = transformarProgresoAPI(progresoData);
set({
modulos,
puntuacionTotal: resumenData?.totalPuntuacion ?? 0,
badges: resumenData?.badges ?? [],
nivel: resumenData?.nivel ?? 'Novato',
isLoading: false,
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Error al cargar el progreso',
isLoading: false,
});
console.error('Error loading progreso:', error);
}
},
saveProgreso: async (moduloId: string, ejercicioId: string, puntuacion: number) => {
const moduloNumero = parseInt(moduloId.replace('modulo', ''), 10);
set({ isLoading: true, error: null });
try {
// Guardar en la API
await progresoService.saveProgreso({
modulo_numero: moduloNumero,
ejercicio_id: ejercicioId,
puntuacion,
completado: true,
});
// Actualizar estado local
set((state) => {
const modulo = state.modulos[moduloId] || {
moduloId,
ejercicios: {},
completado: false,
};
const ejercicioExistente = modulo.ejercicios[ejercicioId];
const esMejorPuntuacion = !ejercicioExistente || puntuacion > ejercicioExistente.puntuacion;
const nuevoEjercicio: EjercicioProgreso = {
ejercicioId,
completado: true,
puntuacion: esMejorPuntuacion ? puntuacion : ejercicioExistente.puntuacion,
fechaCompletado: new Date().toISOString(),
intentos: (ejercicioExistente?.intentos || 0) + 1,
};
const nuevosEjercicios = {
...modulo.ejercicios,
[ejercicioId]: nuevoEjercicio,
};
const moduloActualizado: ModuloProgreso = {
...modulo,
ejercicios: nuevosEjercicios,
};
// Recalcular puntuación total
let nuevaPuntuacionTotal = 0;
Object.values(state.modulos).forEach((mod) => {
if (mod.moduloId !== moduloId) {
Object.values(mod.ejercicios).forEach((ej) => {
if (ej.completado) {
nuevaPuntuacionTotal += ej.puntuacion;
}
});
}
});
// Agregar ejercicios del módulo actual
Object.values(nuevosEjercicios).forEach((ej) => {
if (ej.completado) {
nuevaPuntuacionTotal += ej.puntuacion;
}
});
const nuevoNivel = calcularNivel(nuevaPuntuacionTotal);
return {
modulos: {
...state.modulos,
[moduloId]: moduloActualizado,
},
puntuacionTotal: nuevaPuntuacionTotal,
nivel: nuevoNivel,
isLoading: false,
};
});
// Recargar resumen para obtener badges actualizados
const resumen = await progresoService.getResumenProgreso();
set({
badges: resumen?.badges ?? [],
nivel: resumen?.nivel ?? 'Novato',
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Error al guardar el progreso',
isLoading: false,
});
console.error('Error saving progreso:', error);
throw error;
}
},
getProgresoEjercicio: (moduloId: string, ejercicioId: string) => {
const state = get();
return state.modulos[moduloId]?.ejercicios[ejercicioId];
},
getProgresoModulo: (moduloId: string) => {
const state = get();
return state.modulos[moduloId];
},
resetProgreso: (moduloId?: string) => {
if (moduloId) {
set((state) => {
const { [moduloId]: _, ...restoModulos } = state.modulos;
// Recalcular puntuación
let nuevaPuntuacionTotal = 0;
Object.values(restoModulos).forEach((mod) => {
Object.values(mod.ejercicios).forEach((ej) => {
if (ej.completado) {
nuevaPuntuacionTotal += ej.puntuacion;
}
});
});
return {
modulos: restoModulos,
puntuacionTotal: nuevaPuntuacionTotal,
nivel: calcularNivel(nuevaPuntuacionTotal),
};
});
} else {
set({
modulos: {},
puntuacionTotal: 0,
nivel: 'Novato',
});
}
},
calcularPorcentajeModulo: (moduloId: string, totalEjercicios: number): number => {
const state = get();
const modulo = state.modulos[moduloId];
if (!modulo || totalEjercicios === 0) return 0;
const ejerciciosCompletados = Object.values(modulo.ejercicios).filter(
(ej) => ej.completado
).length;
return Math.round((ejerciciosCompletados / totalEjercicios) * 100);
},
getBadgesDesbloqueados: () => {
return get().badges.filter(b => b.desbloqueado);
},
getBadgesBloqueados: () => {
return get().badges.filter(b => !b.desbloqueado);
},
getProgreso: (moduloId: string, ejercicioId: string) => {
const state = get();
return state.modulos[moduloId]?.ejercicios[ejercicioId];
},
})
);
export default useProgressStore;

View File

@@ -14,11 +14,52 @@ export interface Progreso {
puntuacion: number;
}
export interface ProgresoEjercicio {
ejercicioId: string;
completado: boolean;
puntuacion: number;
fecha: string;
}
export interface ProgresoModulo {
moduloNumero: number;
ejercicios: ProgresoEjercicio[];
porcentaje: number;
}
export interface BadgeCondicion {
tipo: 'ejercicios_completados' | 'puntuacion_total' | 'modulo_completado';
valor: number;
moduloId?: string;
}
export interface Badge {
id: string;
titulo: string;
descripcion: string;
icono: string;
desbloqueado: boolean;
requisito: string;
condicion?: BadgeCondicion;
fechaDesbloqueo?: string;
}
export type NivelUsuario = 'Novato' | 'Aprendiz' | 'Experto' | 'Maestro';
export interface ConfiguracionEjercicio {
dificultad: 'facil' | 'medio' | 'dificil';
tiempoMaximo?: number;
intentosMaximos?: number;
mostrarPistas: boolean;
}
export interface Ejercicio {
id: string;
titulo: string;
descripcion: string;
tipo: 'quiz' | 'simulador' | 'ejercicio';
configuracion?: ConfiguracionEjercicio;
moduloNumero: number;
}
export interface Modulo {
@@ -36,6 +77,16 @@ export interface ModuloProgreso {
totalEjercicios: number;
}
export interface ResumenProgreso {
totalPuntuacion: number;
ejerciciosCompletados: number;
totalEjercicios: number;
porcentajeGlobal: number;
badges: Badge[];
nivel: NivelUsuario;
moduloProgreso: ProgresoModulo[];
}
export interface LoginRequest {
email?: string;
username?: string;